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 "con...
Installs into .claude/skills of the current project.
Are you the author of Csharp Conventions?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/atc-net-csharp-conventions)
---
name: csharp-conventions
description: >
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).
user-invocable: false
---
# C# Coding Conventions
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**.
## Modern Language Features
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.
### File-Scoped Namespaces
Use file-scoped namespaces to eliminate one level of indentation across the entire file:
```csharp
namespace MyApp.Services;
public class OrderService { }
```
### Primary Constructors
Use primary constructors for classes and structs whose constructor simply captures dependencies or parameters. This avoids repetitive field assignments:
```csharp
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.
### Records and Immutable Data
Use `record` (or `record struct`) for immutable data transfer objects, value objects, and similar types where value equality is the natural semantic:
```csharp
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:
```csharp
var updated = original with { Status = OrderStatus.Shipped };
```
### Collection Expressions
Use collection expressions (C# 12+) for concise initialization:
```csharp
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];
```
### Expression-Bodied Members
Use expression-bodied members for single-expression methods, properties, and indexers:
```csharp
public string FullName => $"{FirstName} {LastName}";
public override string ToString() => FullName;
```
### Pattern Matching
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:
```csharp
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:
```csharp
var discount = customer.Tier switch
{
CustomerTier.Gold => 0.15m,
CustomerTier.Silver => 0.10m,
CustomerTier.Bronze => 0.05m,
_ => 0m,
};
```
### Init-Only Properties
Use `init` setters for properties that should be set only during object initialization:
```csharp
public class OrderOptions
{
public required string ConnectionString { get; init; }
public int MaxRetries { get; init; } = 3;
}
```
### Global Using Directives
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:
```csharp
global using System.Collections.Immutable;
global using Microsoft.Extensions.Logging;
```
### Default Interface Implementations
Use default interface implementations to add behavior to interfaces without breaking existing implementors. This is useful for evolving contracts in library code:
```csharp
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)));
}
}
```
### Source-Generated Regex
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:
```csharp
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.
### Explicit Access Modifiers
Always specify access modifiers explicitly — never rely on C# defaults. This makes intent clear and prevents accidental exposure:
```csharp
// 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
```
### Locking and Thread Safety
Prefer `SemaphoreSlim` over the `lock` keyword. It supports async `WaitAsync`, avoids thread pool starvation, and works consistently in both sync and async contexts:
```csharp
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.
### Sealed Classes
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:
```csharp
public sealed class OrderProcessor(ILogger<OrderProcessor> logger) { }
public sealed class OrderProcessorTests { }
```
### Type Inference with var
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:
```csharp
var orders = new List<Order>();
var stream = File.OpenRead(path);
var client = CreateClient();
```
## Nullable Reference Types
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.
- Declare variables and parameters as non-nullable by default. Only use `T?` when null is a valid, meaningful state.
- Validate nullable inputs at public API entry points (constructors, public methods) and throw `ArgumentNullException` with `nameof`:
```csharp
public OrderService(IOrderRepository repository)
{
ArgumentNullException.ThrowIfNull(repository);
_repository = repository;
}
```
- Trust null annotations on types you consume. If a method returns `string` (non-nullable), do not add a redundant null check -- it clutters the code and undermines the annotation system.
- Use the null-forgiving operator (`!`) 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.
## Naming Conventions
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` |
- Boolean properties and variables should read as questions: `IsActive`, `HasPermission`, `CanExecute`.
- Avoid abbreviations except widely recognized ones (`Id`, `Url`, `Http`).
- Use `nameof()` instead of string literals when referencing member names -- this survives refactoring and produces compile-time errors when names change.
## Formatting
Follow the project's `.editorconfig` when one exists. When establishing new formatting rules, adopt these defaults:
- **File-scoped namespaces** and single-line `using` directives at the top.
- **Newline before opening braces** (Allman style):
```csharp
public class OrderService
{
public void Process()
{
}
}
```
- **Four-space indentation**, no tabs.
- **One class per file**; the file name matches the type name.
- Order members within a class consistently: constants, fields, constructors, properties, public methods, private methods.
- Use trailing commas in multi-line initializers and switch expressions to reduce diff noise.
## Exception Handling
Exceptions are for exceptional situations, not control flow. Throwing and catching exceptions is expensive and obscures the normal code path.
- Throw meaningful exceptions that follow .NET conventions (`ArgumentNullException`, `ArgumentOutOfRangeException`, `InvalidOperationException`, `NotSupportedException`).
- Always include the parameter name via `nameof`:
```csharp
if (quantity <= 0)
throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be positive.");
```
- Use a dedicated exception for code paths that should never execute (e.g., a default case in an exhaustive switch). This signals intent and provides a clear diagnostic if the assumption is violated. Common choices include `SwitchCaseDefaultException` (from the Atc library ecosystem) or the built-in `UnreachableException`:
```csharp
_ => throw new SwitchCaseDefaultException(status)
```
- Catch specific exception types, not bare `Exception`, unless you are at a top-level boundary (API middleware, hosted service entry point).
- Never swallow exceptions silently. Log and rethrow (`throw;`, not `throw ex;`) or convert to a domain-specific error.
- Never use exceptions for expected conditions like "item not found" -- return `null`, a `bool` try-pattern, or a result type instead.
## Project Structure
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/
```
- Namespace structure should mirror the folder structure.
- Keep the domain layer free of framework dependencies (no EF Core attributes, no ASP.NET references).
- Reference inward: `Api -> Application -> Domain`; `Infrastructure -> Application -> Domain`.
### API Style
Prefer **Minimal APIs** over MVC controllers for new projects. Use extension methods to group endpoints:
```csharp
var app = builder.Build();
app.MapEndpoints();
app.MapGet("/", () => TypedResults.Text("OK", "text/plain")).ShortCircuit();
```
### Service Registration
Use fluent extension method chains on `IServiceCollection` to organize DI registration by concern:
```csharp
services
.ConfigureObservability(builder.Configuration)
.ConfigureSecurity(builder.Configuration)
.ConfigureRequestHandling()
.ConfigureApiVersioning();
```
## Data Access
Encapsulate data access behind interfaces so domain and application layers remain independent of the underlying store:
- Define reader/writer interfaces in the domain layer (e.g., `ICosmosReader<T>`, `ICosmosWriter<T>`, or generic repository interfaces).
- Implement those interfaces in the infrastructure layer with the concrete data technology (Cosmos DB, EF Core, etc.).
- Never leak query abstractions (like `IQueryable<T>`) to upper layers — materialize data before returning from the repository.
- For Cosmos DB projects using the Atc library ecosystem, use `ICosmosReader<T>` / `ICosmosWriter<T>` for typed reads and writes against containers.
- For Entity Framework Core projects, configure entities with `IEntityTypeConfiguration<T>`, use `AsNoTracking()` for read-only queries, and apply server-side pagination.
- Keep data access concerns out of the domain layer — no ORM attributes, no database-specific types.
## Validation
Choose the right validation strategy for the context:
- **DataAnnotations** for simple DTO validation in ASP.NET model binding (`[Required]`, `[StringLength]`, `[Range]`).
- **FluentValidation** for complex business rules, cross-property validation, or conditional logic that is awkward to express with attributes.
- Return **RFC 7807 Problem Details** from APIs for validation errors. ASP.NET Core has built-in support via `AddProblemDetails()` and the `ValidationProblemDetails` class:
```csharp
builder.Services.AddProblemDetails();
```
- Validate at the boundary (API controllers, message handlers) and trust validated data in inner layers.
## Structured Logging
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.).
- Use **source-generated logging** (the `[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:
```csharp
// 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 ...
}
}
```
```csharp
// 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);
}
```
- Use centralized **event ID constants** to keep log event IDs organized and unique:
```csharp
public static class LoggingEventIdConstants
{
public static class OrderProcessor
{
public const int OrderPlaced = 1001;
public const int OrderFailed = 1002;
}
}
```
- Use semantic property names in message templates (`{OrderId}`, not `{0}` or `{id}`).
- Never interpolate strings into log messages (`logger.LogInformation($"Order {id}")`) — this defeats structured logging and allocates on every call regardless of log level.
- Log at appropriate levels: `Trace`/`Debug` for diagnostics, `Information` for business events, `Warning` for recoverable issues, `Error`/`Critical` for failures requiring attention.
## Observability
- Use **OpenTelemetry** for distributed tracing. Create activity sources and tag spans with relevant domain data:
```csharp
using var activity = DiagnosticSource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("order.items.count", items.Count);
```
- Expose health check endpoints for orchestration systems:
```csharp
app.MapHealthChecks("/health");
app.MapHealthChecks("/health-extended", HealthCheckOptionsFactory.CreateJson());
```
## General Principles
- **Treat warnings as errors.** Enable `<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.
- **Clarity over cleverness.** Choose clear, descriptive names that make comments unnecessary. If a method needs a comment to explain *what* it does, rename it. Reserve comments for explaining *why* a non-obvious decision was made.
- **Never remove TODO comments.** They represent intentional technical debt or planned work. Only the author or team lead should decide when a TODO is resolved.
- **Handle edge cases.** Consider null inputs, empty collections, boundary values, and concurrent access scenarios.
- **Keep changes focused.** When reviewing or refactoring, make high-confidence suggestions. Avoid speculative changes that alter behavior without clear justification.
- **Prefer composition over inheritance.** Use interfaces, delegation, and dependency injection to compose behavior rather than building deep class hierarchies.