C# Conventions
Personal C# style, structure, and runtime conventions in one place: how code is shaped (naming, layout, syntax) and how it behaves (async, I/O, exceptions, logging, DI). Style is enforced by
(Allman braces, 120-char line limit, file-scoped namespaces) and
EnforceCodeStyleInBuild=true
.
Specialized concerns route elsewhere: performance / memory layout ->
; concurrency primitives (
,
,
) ->
csharp-concurrency-patterns
; the
router maps the rest (EF, serialization, DI registration, config binding, DDD, packaging, tests).
Style and Structure
File structure
- Max 300 lines per file. Split by extracting cohesive groups of methods into new classes.
- Max 120 chars per line in files. Count visible characters before committing. Markdown, JSON, config files exempt.
- Partial classes only for generated code (EF migrations, designer files) or extending a generated class.
- One top-level type per file. File name must match the type name exactly.
- File-scoped namespaces:
namespace MyApp.Services;
- never the braced form.
Naming
- for private instance fields and fields.
- for public members, types, events, and every regardless of accessibility. A is ; a inside a method body is also .
- for non- local variables and method parameters.
- Prefix interfaces with : , not .
- Suffix async methods with : .
- No abbreviations unless universally accepted (, , , ).
- Boolean members read as a statement: , , - not , , .
Naming intent - apply four tests to every name:
- Domain-aligned - use vocabulary from the project domain. Avoid , , , , , or vague verbs like / when a domain-specific term exists.
- Intent-revealing - the name explains what the member does without reading the implementation.
- DDD-consistent - value objects model concepts, not primitives. Don't suffix entity types with or . Do suffix repositories and services.
- Free of misleading names - a method named must persist; a method must not also mutate state.
Class member ordering
Enforced by
. Order: private constants/statics, private readonly, private fields, protected/public properties, constructors, public/protected/private methods. Public properties before the constructor.
Constructor parameter ordering
Private readonly fields, constructor parameters, and constructor body assignments must follow the same order.
Group order:
- / - always first.
- Other interfaces.
- Classes (including sealed records, delegates such as , concrete service types).
- Structs (value types).
Within each group, order by scope, broadest first. Required before optional - all defaulted params trail required ones.
Blank lines
Enforced by
/ formatter. The non-mechanical rule: one blank line before control-transfer statements (
,
,
, etc.) when preceded by another statement - so the exit visually separates from preceding logic.
Methods
- Max 20 lines per method body. Refactor if exceeded.
- Max 3 parameters. Use a parameter object (record or class) for more.
- Methods do one thing. If 'and' appears in a method name, split it.
- No or parameters - return a tuple or result object instead.
- Every case body wrapped in its own block - even when one statement, even when no variable is declared. Brace any half-braced switch you edit. Example:
csharp
switch (x)
{
case A:
{
DoA();
break;
}
case B:
{
var y = Compute();
Use(y);
return;
}
default:
{
return;
}
}
Per-case braces give each case its own scope (no accidental variable leak); blank line before
/
when preceded by another statement; no blank line when the transfer is the only statement after
(the
above).
Types and variables
- Use only when the type is obvious from the right-hand side: OK;
var result = GetResult();
not OK.
- Nullable reference types enabled project-wide; treat nullable warnings as errors.
- Prefer for immutable DTOs, value-like data, and types defined by their values. Prefer when the type has identity, mutable state, inheritance, or behavior beyond data.
- Value objects: model as small immutable types - typically - validate in the constructor (trust everywhere after), and expose explicit conversions / factory methods only, never an (it silently defeats the type safety it exists to provide). Add a when the value object must bind from configuration.
- Member signatures expose the narrowest useful shape: accept / / (or on hot paths), and return a read-only collection type (, ); return a / array only when the caller is meant to mutate it.
- No magic numbers or magic strings - use named constants or enums.
- Enums: explicit underlying values for any enum persisted to a database or sent over the wire. Use only when bitwise combination is intended.
- Expression-bodied members only for single-expression getters and trivial methods.
- No public mutable fields - use properties.
- String comparison: always specify for non-linguistic comparisons (identifiers, keys, file paths),
StringComparison.OrdinalIgnoreCase
for case-insensitive. Never rely on culture-default comparison.
Visibility and sealing
- Default to the lowest visibility that works: for class members, for assembly-scoped types, only for cross-assembly API.
- Mark new classes unless inheritance is part of the design. Sealed classes enable JIT devirtualization and signal intent.
- Mark methods or only when overriding is genuinely required. Prefer composition over inheritance.
- Static classes only for pure utilities (no state, no I/O, no DI dependencies). For anything else, use a regular class with DI.
- Static fields only for true constants or thread-safe caches. Mutable static state is forbidden.
Modern C# syntax preferences (11+/12+/13)
- Primary constructors - banned. Explicit ctor + assignment keeps member-ordering and field convention visible.
- Collection expressions , spread - default for literal construction. Use only when items are added conditionally. Do not write in new code.
- - default for new internal APIs (zero-alloc). Use only when the caller already owns an array.
- Raw strings / - use for multi-line literals and for any string containing . Drop for new code unless single-line and short.
- members (C# 11) - use for properties that must be set during initialization but cannot be enforced by a constructor (e.g. records or DTOs with many properties).
- keyword in accessors (C# 13) - allowed only for trivial guards. Anything richer keeps an explicit backing field.
- (C# 13) - use for new lock objects. Do not retrofit existing sites.
- Switch expression vs switch statement - prefer switch expression for value-returning code. Use switch statement only when arms have side effects (DI registration, channel writes, logging).
Performance concerns (sealing, readonly structs,
/
/
, collection choice) belong with the
skill.
Forbidden patterns
- No blocks.
- No for non-utility classes.
- No commented-out code - delete it.
- No without an associated ticket reference.
- No reflection in business or hot-path code; use source generators or compile-time alternatives. No object-mapping libraries (AutoMapper / Mapster / ExpressMapper) - write explicit mapping methods (compile-time checked, debuggable, refactor-safe). Reflection is acceptable only in serialization, the DI container, ORM / EF, test infrastructure, or one-time bootstrap - never for DTO / domain mapping. When you must reach a private member (serializer, test helper), use (.NET 8), not .
When a convention here drives a package change - adding, removing, or swapping one (e.g. dropping a banned mapper, replacing Newtonsoft with System.Text.Json) - the install itself follows
: use the
CLI, never hand-edit
.
- No - use + pattern matching or a typed interface.
- No top-level statements outside .
Documentation
- Every public API surface has XML doc comments covering parameters, return values, thrown exceptions, and remarks for non-obvious behavior.
Runtime and Behavior
Behavior, I/O, and composition rules.
DateTime and timezones
- Store and pass , not , for any value crossing process or DB boundaries.
- All persisted timestamps in UTC. Convert to local only at the presentation boundary.
- Never call or directly in business logic. Inject an / abstraction (or on .NET 8+).
- Never call for measurements - use .
Async
- Async all the way. No , , or
.GetAwaiter().GetResult()
in async code paths.
- Return , not , for async methods (except event handlers).
- Always pass and forward for I/O-bound or long-running operations.
- No except event handlers; mark those clearly.
- Use in library code; ignore it in ASP.NET Core application code (no sync context).
- Use / only on hot paths where synchronous completion is common and benchmarks show allocation pressure. Default to .
- Return for streaming results (paged DB reads, long-running enumerations). Annotate the parameter with .
Dispose pattern
- Use declarations () over blocks where scope allows.
- Implement for types holding async resources. Implement both and when both sync and async disposal paths are realistic.
- Never call on injected dependencies - the DI container owns their lifetime.
- Use the full Dispose pattern (
protected virtual Dispose(bool disposing)
) only for unmanaged resources or when inheritance is in play. Otherwise a simple is enough.
Exception handling and Result pattern
- Distinguish expected outcomes from exceptional failures. Validation, not-found, and business-rule failures are expected - return a result type rather than throwing. Prefer a domain-specific result (a sealed record with / factory methods and an error-code enum, e.g. ) over a generic / when the operation's failure modes are known.
- Exceptions for unexpected failures only (I/O errors, programming errors, contract violations).
- Catch specific exceptions; never bare in business logic unless logging and re-throwing.
- Do not use exceptions for control flow.
- Re-throw with not (preserves stack trace).
- Validate arguments at the top of public methods. Prefer the static throw-helpers over hand-written guards:
ArgumentNullException.ThrowIfNull(x)
, ArgumentException.ThrowIfNullOrWhiteSpace(s)
, ArgumentOutOfRangeException.ThrowIfNegative
/ (.NET 8).
Logging
- Structured logging via . Use templates with named placeholders:
_logger.LogInformation('Order {OrderId} placed for {UserId}', orderId, userId)
. Never use string interpolation in log calls.
- Log levels: (diagnostic noise), (dev), (business events), (recoverable issue), (operation failed), (system unusable).
- Log exceptions with the exception object as the first arg:
_logger.LogError(ex, 'Failed to {Action}', actionName)
. Never an exception into the message.
- Never log: passwords, tokens, secrets, full payment data, PII beyond what is operationally needed. For healthcare and e-commerce projects, treat full identifiers as PII.
- One log statement per logical event. Avoid log spam in tight loops.
Secrets and configuration sources
- Secrets never live in . Local development uses . Production uses environment variables, Azure Key Vault, AWS Secrets Manager, or platform equivalents.
- Configuration layering: (defaults) ->
appsettings.{Environment}.json
-> environment variables -> command-line args. Later layers override earlier.
Typed options binding (
/
/
) and startup validation (
,
, data-annotation validation) live in
microsoft-extensions-configuration
- consult it for those, do not restate here.
LINQ
- Method syntax (, , ), not query syntax, unless query syntax is materially clearer (e.g. multi-join queries).
- No more than 4-5 chained operators without an intermediate variable with a descriptive name.
- Materialize queries (, ) before returning from a method that owns the DbContext or connection lifetime.
JSON serialization
- is the default. Newtonsoft.Json only for legacy compatibility or features missing from STJ (e.g. polymorphic serialization in older runtimes).
- Configure once and reuse - never construct per call.
- Naming policy:
JsonNamingPolicy.CamelCase
for external APIs unless a contract requires otherwise.
- Use source-generated for hot paths and AOT compatibility.
- Never deserialize untrusted JSON without size and depth limits.
Decoupling and DI lifetimes
- Never call on service-layer or infrastructure types inside a class body - use factories or DI.
- No circular dependencies between namespaces.
- Never inject a shorter-lifetime service into a longer-lifetime one (captive dependency). Use or a factory for cross-lifetime access.