From dotnet
General C# coding conventions covering latest language features (C# 13), nullable reference types, naming conventions, formatting rules, exception handling, project structure, data access patterns, validation, and structured logging. Apply this skill whenever writing, reviewing, or refactoring C# code -- including when the user works with C# classes, interfaces, records, enums, or any .NET code that should follow modern C# idioms and best practices, even if they do not explicitly ask for "conventions" or "best practices." This does NOT cover async/await patterns (see csharp-async) or XML documentation specifics (see csharp-docs).
npx claudepluginhub atc-net/atc-agentic-toolkit --plugin dotnetThis skill uses the workspace's default tool permissions.
Apply these conventions when writing, reviewing, or refactoring C# code. They reflect modern C# (up to C# 13) and current .NET idioms. For async/await-specific guidance, defer to the **csharp-async** skill. For XML documentation details, defer to **csharp-docs**.
Write modern, high-performance C# code using records, pattern matching, value objects, async/await, Span<T>/Memory<T>, and best-practice API design patterns. Emphasizes functional-style programming with C# 12+ features. Use when writing new C# code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.
Writes modern C# code with records, pattern matching, async/await. Optimizes .NET apps, implements enterprise patterns like SOLID, and ensures testing with xUnit for refactoring and complex solutions.
Writes modern C# code using records, pattern matching, async/await. Optimizes .NET apps, implements enterprise patterns like SOLID, and provides comprehensive testing with xUnit, Moq.
Share bugs, ideas, or general feedback.
Apply these conventions when writing, reviewing, or refactoring C# code. They reflect modern C# (up to C# 13) and current .NET idioms. For async/await-specific guidance, defer to the csharp-async skill. For XML documentation details, defer to csharp-docs.
Prefer the latest language features where they improve clarity and reduce boilerplate. A shorter file that expresses the same intent is easier to review, easier to maintain, and produces fewer merge conflicts.
Use file-scoped namespaces to eliminate one level of indentation across the entire file:
namespace MyApp.Services;
public class OrderService { }
Use primary constructors for classes and structs whose constructor simply captures dependencies or parameters. This avoids repetitive field assignments:
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
public async Task<Order?> GetAsync(int id, CancellationToken ct)
=> await repository.FindByIdAsync(id, ct);
}
Reserve traditional constructors for cases that require validation logic, overloads, or complex initialization.
Use record (or record struct) for immutable data transfer objects, value objects, and similar types where value equality is the natural semantic:
public record OrderSummary(int Id, decimal Total, DateTimeOffset CreatedAt);
Records give you value equality, ToString(), deconstruction, and with expressions for free. Use with to create modified copies of records without mutation:
var updated = original with { Status = OrderStatus.Shipped };
Use collection expressions (C# 12+) for concise initialization:
int[] numbers = [1, 2, 3];
List<string> names = ["Alice", "Bob"];
List<string> empty = []; // prefer [] over new List<string>()
// Spread operator to combine collections
List<string> all = [.. names, .. otherNames];
Use expression-bodied members for single-expression methods, properties, and indexers:
public string FullName => $"{FirstName} {LastName}";
public override string ToString() => FullName;
Use is null and is not null instead of == null and != null. Pattern matching is more idiomatic, avoids accidental operator overload issues, and reads naturally:
if (order is null)
throw new ArgumentNullException(nameof(order));
if (result is not null)
Process(result);
Use switch expressions for multi-branch logic when each arm is a simple mapping:
var discount = customer.Tier switch
{
CustomerTier.Gold => 0.15m,
CustomerTier.Silver => 0.10m,
CustomerTier.Bronze => 0.05m,
_ => 0m,
};
Use init setters for properties that should be set only during object initialization:
public class OrderOptions
{
public required string ConnectionString { get; init; }
public int MaxRetries { get; init; } = 3;
}
Place global usings in a single file (e.g., GlobalUsings.cs or via <Using> in the project file) to reduce repetitive using statements across the codebase:
global using System.Collections.Immutable;
global using Microsoft.Extensions.Logging;
Use default interface implementations to add behavior to interfaces without breaking existing implementors. This is useful for evolving contracts in library code:
public interface INotificationService
{
Task SendAsync(string message, CancellationToken ct);
Task SendBatchAsync(IEnumerable<string> messages, CancellationToken ct)
{
return Task.WhenAll(messages.Select(m => SendAsync(m, ct)));
}
}
Use [GeneratedRegex] on a partial method instead of new Regex(...) or Regex.IsMatch(...) with a string literal. The compiler emits optimized, allocation-free matching code at build time:
public sealed partial class EmailValidator
{
[GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase)]
private static partial Regex EmailPattern();
public bool IsValid(string email)
=> EmailPattern().IsMatch(email);
}
Never pass RegexOptions.Compiled to [GeneratedRegex] — source generation already produces compiled code and the flag is ignored.
Always specify access modifiers explicitly — never rely on C# defaults. This makes intent clear and prevents accidental exposure:
// Good — explicit
public class OrderService { }
private readonly ILogger _logger;
internal static void Reset() { }
// Bad — implicit (compiles but hides intent)
class OrderService { } // implicitly internal
static void Reset() { } // implicitly private
Prefer SemaphoreSlim over the lock keyword. It supports async WaitAsync, avoids thread pool starvation, and works consistently in both sync and async contexts:
private readonly SemaphoreSlim _gate = new(1, 1);
public async Task ProcessAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try
{
// critical section
}
finally
{
_gate.Release();
}
}
Only use lock for trivial synchronous-only paths where simplicity outweighs flexibility. Never use lock inside an async method.
Mark services, handlers, and test classes as sealed to communicate intent and enable devirtualization. Base classes, domain entities, and types designed for inheritance should not be sealed:
public sealed class OrderProcessor(ILogger<OrderProcessor> logger) { }
public sealed class OrderProcessorTests { }
Use var consistently — most projects enforce var everywhere via .editorconfig (csharp_style_var_for_built_in_types = true, csharp_style_var_elsewhere = true). This keeps code concise and reduces noise when types change during refactoring:
var orders = new List<Order>();
var stream = File.OpenRead(path);
var client = CreateClient();
Enable nullable reference types project-wide (<Nullable>enable</Nullable>). The goal is to make nullability part of the type system so the compiler catches null-related bugs at build time.
T? when null is a valid, meaningful state.ArgumentNullException with nameof:public OrderService(IOrderRepository repository)
{
ArgumentNullException.ThrowIfNull(repository);
_repository = repository;
}
string (non-nullable), do not add a redundant null check -- it clutters the code and undermines the annotation system.!) sparingly and only when you can guarantee non-null through external knowledge the compiler cannot see (e.g., a test assertion). Always add a comment explaining why.Consistent naming reduces cognitive load and makes code navigable without documentation.
| Element | Convention | Example |
|---|---|---|
| Namespace | PascalCase, matching folder structure | MyApp.Services.Orders |
| Class, Record, Struct | PascalCase | OrderService, OrderSummary |
| Interface | "I" + PascalCase | IOrderRepository |
| Public method | PascalCase | CalculateTotal |
| Public property | PascalCase | OrderDate |
| Private field | _camelCase (underscore prefix) | _repository |
| Local variable | camelCase | orderCount |
| Parameter | camelCase | customerId |
| Constant | PascalCase | MaxRetryCount |
| Enum member | PascalCase | OrderStatus.Pending |
| Type parameter | "T" + PascalCase | TEntity, TResult |
IsActive, HasPermission, CanExecute.Id, Url, Http).nameof() instead of string literals when referencing member names -- this survives refactoring and produces compile-time errors when names change.Follow the project's .editorconfig when one exists. When establishing new formatting rules, adopt these defaults:
using directives at the top.public class OrderService
{
public void Process()
{
}
}
Exceptions are for exceptional situations, not control flow. Throwing and catching exceptions is expensive and obscures the normal code path.
ArgumentNullException, ArgumentOutOfRangeException, InvalidOperationException, NotSupportedException).nameof:if (quantity <= 0)
throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be positive.");
SwitchCaseDefaultException (from the Atc library ecosystem) or the built-in UnreachableException:_ => throw new SwitchCaseDefaultException(status)
Exception, unless you are at a top-level boundary (API middleware, hosted service entry point).throw;, not throw ex;) or convert to a domain-specific error.null, a bool try-pattern, or a result type instead.Organize solutions to reflect bounded contexts and keep dependencies flowing inward:
src/
MyApp.Domain/ # Entities, value objects, domain interfaces
MyApp.Application/ # Use cases, DTOs, application interfaces
MyApp.Infrastructure/ # EF Core, external service clients, file I/O
MyApp.Api/ # Controllers, middleware, startup
test/
MyApp.Domain.Tests/
MyApp.Application.Tests/
MyApp.Infrastructure.Tests/
MyApp.Api.Tests/
Api -> Application -> Domain; Infrastructure -> Application -> Domain.Prefer Minimal APIs over MVC controllers for new projects. Use extension methods to group endpoints:
var app = builder.Build();
app.MapEndpoints();
app.MapGet("/", () => TypedResults.Text("OK", "text/plain")).ShortCircuit();
Use fluent extension method chains on IServiceCollection to organize DI registration by concern:
services
.ConfigureObservability(builder.Configuration)
.ConfigureSecurity(builder.Configuration)
.ConfigureRequestHandling()
.ConfigureApiVersioning();
Encapsulate data access behind interfaces so domain and application layers remain independent of the underlying store:
ICosmosReader<T>, ICosmosWriter<T>, or generic repository interfaces).IQueryable<T>) to upper layers — materialize data before returning from the repository.ICosmosReader<T> / ICosmosWriter<T> for typed reads and writes against containers.IEntityTypeConfiguration<T>, use AsNoTracking() for read-only queries, and apply server-side pagination.Choose the right validation strategy for the context:
[Required], [StringLength], [Range]).AddProblemDetails() and the ValidationProblemDetails class:builder.Services.AddProblemDetails();
Use structured logging via ILogger<T> (from Microsoft.Extensions.Logging) so log entries are machine-parsable and searchable. The logging abstraction decouples application code from any specific provider (Application Insights, Seq, console, etc.).
[LoggerMessage] attribute) for high-performance, allocation-free log calls. Place logger message methods in a dedicated partial file named {ClassName}.Log.cs (e.g., OrderProcessor.Log.cs) to separate logging concerns from business logic:// OrderProcessor.cs — business logic only
public sealed partial class OrderProcessor(ILogger<OrderProcessor> logger)
{
public async Task ProcessAsync(Order order, CancellationToken ct)
{
LogOrderPlaced(order.Id);
// ... business logic ...
}
}
// OrderProcessor.Log.cs — all [LoggerMessage] methods for this class
public sealed partial class OrderProcessor
{
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "{CallerMethodName}({CallerLineNumber}) - Order {OrderId} placed")]
private partial void LogOrderPlaced(
int orderId,
[CallerMemberName] string callerMethodName = "",
[CallerLineNumber] int callerLineNumber = 0);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Error,
Message = "{CallerMethodName}({CallerLineNumber}) - Order {OrderId} failed")]
private partial void LogOrderFailed(
int orderId,
[CallerMemberName] string callerMethodName = "",
[CallerLineNumber] int callerLineNumber = 0);
}
public static class LoggingEventIdConstants
{
public static class OrderProcessor
{
public const int OrderPlaced = 1001;
public const int OrderFailed = 1002;
}
}
{OrderId}, not {0} or {id}).logger.LogInformation($"Order {id}")) — this defeats structured logging and allocates on every call regardless of log level.Trace/Debug for diagnostics, Information for business events, Warning for recoverable issues, Error/Critical for failures requiring attention.using var activity = DiagnosticSource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("order.items.count", items.Count);
app.MapHealthChecks("/health");
app.MapHealthChecks("/health-extended", HealthCheckOptionsFactory.CreateJson());
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> in Directory.Build.props. Fix all analyzer warnings rather than suppressing them. If suppression is truly necessary, add a comment explaining why and a TODO to revisit.