.NET / C# Coding Standards
The full reference for writing C# at Finperiti: naming, layout, types, nullability, async, collections, errors, DI, logging, performance, and security. This is the most detailed page in the handbook on purpose. Follow it and all our C# reads as if one careful engineer wrote it. An .editorconfig and analyzers enforce the mechanical parts; this page is the shared reasoning. See also Gotchas: C#/.NET and Coding Standards & Style.
We target modern .NET (currently .NET 10) and the latest C#, with nullable reference types enabled and warnings taken seriously. Use current language features and the standard library by default. Match this guide and the surrounding code rather than personal taste. Conventions follow Microsoft's published C# guidance except where noted here.
When two rules seem to conflict, choose what is clearest for the next reader. When this guide does not say, match the existing codebase.
Naming
- DoPascalCase for types, methods, properties, events, enums, namespaces, and public constants.
- DocamelCase for locals and parameters;
_camelCasefor private instance fields;s_camelCaseonly if you must have private static fields (prefer not to). - DoPrefix interfaces with
I(IClock), suffix async methods withAsync, and suffix custom exceptions withException. - DoUse the domain's words and whole words. Booleans should read as questions or statements (
isActive,hasConsent,canApprove) (see Domain Modelling & Boundaries). - DoUse only well-known abbreviations (
Id,Url,Http,Db). Write two-letter acronyms in upper case (IO) and longer ones in PascalCase (Html). - DoName generic type parameters meaningfully when it helps (
TKey,TResult), or use plainTfor a single obvious one. - AvoidType-encoded names (
strName,lstItems), single-letter names (except simple loop or lambda variables), and filler names (data,temp,obj,manager,helper).
Files, projects & layout
- DoOne top-level type per file, file named after the type; use file-scoped namespaces.
- DoOrder members the same way every time: constants, fields, constructors, properties, then methods. Within each group, put public members first.
- DoKeep
usingdirectives tidy: remove unused ones and sort them. Prefer imports over fully-qualified names. - DoOrganise projects and folders by feature or domain area, not by technical layer alone, so related code lives together (see Separation of Concerns).
- DoKeep files and methods small. If a file is hard to navigate, or a method needs scrolling, split it.
Formatting & whitespace
- AlwaysLet the formatter (through
.editorconfig) control whitespace, braces, and indentation. Do not hand-format or argue about it in review (see Code Review). - DoUse Allman braces (the opening brace on its own line) as .NET expects. Always put braces around control blocks; never use a brace-less
ifbody. - DoKeep lines fairly short, write one statement per line, and use blank lines to separate logical groups.
- DoUse expression-bodied members for true one-liners; otherwise use a normal body.
- DoPrefer early returns and guard clauses over deep nesting. Keep the main path un-indented (see Coding Standards & Style).
Variables, types & var
- DoUse
varwhen the type is clear from the right-hand side (var list = new List<Customer>()). Use the explicit type when it helps readability or the right-hand side is unclear. - DoDeclare variables at first use, in the narrowest scope. Prefer
constorreadonlyfor values that do not change. - DoUse target-typed
newwhere the type is already stated (Customer c = new()), and collection expressions ([..]) where they read clearly. - DoUse the right numeric type:
decimalfor money,intorlongfor counts and ids. Never usedoubleorfloatfor currency (see Money & Currency). - DoUse
DateTimeOffsetor UTCDateTimefor time. Be explicit about the kind and never depend on the server's local time (see Gotchas: C#/.NET).
double total = 0.1 + 0.2; // total is 0.30000000000000004
Binary floating point cannot hold these values exactly, so sums drift by tiny amounts. Over many rows the error is real money.
decimal total = 0.1m + 0.2m; // total is exactly 0.3
decimal represents currency values exactly, so totals add up.
Types, immutability & modern C#
- DoPrefer immutable types:
recordorrecord structfor data,initorreadonlyproperties, and immutable or read-only collections where practical (see Concurrency). - DoUse
records for DTOs and value-like data (value equality). Use classes for entities that have identity and behaviour. Usestructs only for small immutable values. - DoMake invalid states impossible to represent: use required members, make types non-null by default, and use enums or typed wrappers instead of bare primitives (see Data Modelling & Persistence).
- DoUse pattern matching and
switchexpressions for clear branching that covers every case. Use primary constructors and collection expressions where they reduce noise. - DoPrefer composition over inheritance. Keep inheritance shallow, and seal classes that are not designed to be extended.
- AvoidPrimitive obsession (passing
stringorGuideverywhere), large mutable structs, and deep inheritance hierarchies.
public record Money(decimal Amount, string Currency); // value type
var decision = customer switch { // switch expression
{ Risk: RiskBand.High } => Decision.Escalate,
{ ScreeningComplete: false } => Decision.BlockAndEscalate, // fail closed
_ => Decision.Proceed
};
A record for value data, readable pattern matching that covers every case, and a clear safe default. This is modern C# used to express intent and the fail-closed rule.
Nullability
- AlwaysKeep nullable reference types enabled and treat nullable warnings as bugs to fix, not noise to hide.
- DoShow nullability in signatures so it documents intent. Validate at boundaries so internal code can assume values are non-null (see Trust Boundaries).
- DoReturn empty collections, not null. Use
?.and??where they fit, and useis nulloris not nullfor clarity. - DoUse
requiredmembers and constructor parameters to guarantee a value is set, instead of declaring it nullable and assigning later. - AvoidThe null-forgiving
!operator, except where you can prove the value is non-null. It silences the compiler exactly where bugs hide.
Methods, parameters & APIs
- DoKeep methods short and focused on one job. If you cannot name it in one clear verb phrase, it does too much (see Separation of Concerns).
- DoPrefer few parameters. Group related ones into a type or record. Avoid boolean flag parameters; split the method or use an enum instead.
- DoValidate arguments at public boundaries (for example
ArgumentNullException.ThrowIfNull). Return meaningful types, notnullfor "not found" where an option or Result reads better. - DoAccept the least specific interface that works (
IEnumerable<T>orIReadOnlyList<T>) and return the most useful read-only shape. - DoMake public APIs deliberate and stable. Keep internals
internalorprivate(see Backward Compatibility).
Async & concurrency
- AlwaysUse async all the way. Await async calls up to the entry point. Never block with
.Result,.Wait(), or.GetAwaiter().GetResult()on a request path; this causes deadlocks and thread-pool starvation. - DoUse async I/O for database, HTTP, and file access. Accept a
CancellationToken, pass it through the async chain, and honour it. - DoUse
await Task.WhenAll(...)for independent concurrent work. UseConfigureAwait(false)in library code. - DoAvoid shared mutable state. If you cannot avoid it, protect it correctly, and prefer the database or atomic operations to coordinate (see Concurrency).
- NeverUse
async voidexcept for event handlers; exceptions escape unseen and crash the process. Do not fire-and-forget tasks without handling failures. - Do notCapture a loop variable or a scoped service in a closure or lambda without care. You may capture the last value or a disposed object.
public IActionResult Get(string id) {
var conn = new SqlConnection(cs); // not disposed
var c = _repo.LoadAsync(id).Result; // blocks the thread
if (c == null) throw new Exception("no"); // generic exception
return Ok(c); // serialises whole entity
}
This blocks on async code (deadlock and pool starvation), leaves the connection undisposed (a pool leak), throws a non-specific exception, and returns the whole entity. Several standards broken in five lines.
public async Task<IActionResult> Get(Guid id, CancellationToken ct) {
var c = await _repo.LoadAsync(id, ct);
if (c is null) return NotFound();
return Ok(CustomerResponse.From(c)); // explicit DTO
}
// _repo uses 'await using' on its connection
Async all the way with cancellation, a clear not-found result, a deliberate response shape, and resources disposed in the repository.
public IActionResult Get() {
var data = _repo.LoadAsync().Result; // blocks the request thread
return Ok(data);
}
.Result blocks the thread while it waits for the async work. Under load, this deadlocks or exhausts the thread pool. Make the action async and await it.
public async Task Get(CancellationToken ct) {
var data = await _repo.LoadAsync(ct);
return Ok(data);
}
The thread is freed while awaiting, cancellation flows through, and there is no deadlock risk.
Collections, LINQ & resources
- DoPick the right collection (
List,Dictionary,HashSet, arrays) and return read-only interfaces from APIs. - DoRemember that LINQ runs lazily. Do not run the same query many times. Call
ToListonce when you need to reuse the results. - DoKeep LINQ readable. On hot paths or large sets, prefer streaming and avoid needless allocations (see Performance & Resource Use).
- AlwaysDispose every
IDisposablewithusingorawait using(connections, streams, commands) so resources are released even when an exception is thrown (see Gotchas: C#/.NET). - DoReuse
HttpClientthroughIHttpClientFactory. Never create a new one per call; that exhausts sockets.
public async Task Fetch(string url) {
using var client = new HttpClient(); // a fresh client every call
return await client.GetStringAsync(url);
}
Each disposed client leaves a socket in TIME_WAIT. Under load the host runs out of ports and calls start failing.
public MyService(IHttpClientFactory factory) => _factory = factory;
public async Task Fetch(string url) {
var client = _factory.CreateClient();
return await client.GetStringAsync(url);
}
The factory pools and reuses connections, so sockets are not exhausted.
Exceptions & error handling
- DoThrow specific, meaningful exception types. Keep the original cause when wrapping (
throw new X("context", inner)) (see Error Handling). - DoCatch only what you can handle, at a level that can act on it. Let other exceptions propagate. Use
finallyorusingfor cleanup. - DoDo not use exceptions for normal control flow. Use a Result or try-pattern for expected failures.
- DoFail closed on errors that affect security or compliance: deny or escalate rather than ignore (see Designing for Failure).
- NeverIgnore exceptions silently, catch
Exceptionbroadly just to be safe, or leak stack traces, SQL, or secrets to external callers (see Error Handling).
DI, logging & structure
- DoUse constructor dependency injection, depend on interfaces, and choose lifetimes on purpose (singleton, scoped, transient). Avoid the service-locator pattern and mutable static state (see Inversion of Control).
- DoKeep business logic and security decisions in the application or domain layer, not in controllers or inline SQL (see Separation of Concerns).
- DoUse structured logging with message templates (
logger.LogInformation("... {CustomerId}", id)) and the right log level. Include correlation and tenant context (see Observability & Logging Hygiene). - NeverLog secrets, tokens, or full PII, and never put sensitive data into log strings with string interpolation (see Observability & Logging Hygiene).
Performance & security
- DoWrite clear code first. Optimise hot paths based on profiling, not guesses (see Performance & Resource Use).
- DoWatch allocations on hot paths: avoid needless LINQ, boxing, and string concatenation, and consider
Span<T>orStringBuilderwhere it matters. Do not micro-optimise cold code. - DoParameterise all SQL through Dapper and include the tenant predicate (see SQL / T-SQL Coding Standards, Multi-Tenancy).
- DoUse the standard crypto libraries correctly and keep secrets in the vault (see Cryptography & Key Management, Secrets Management).
- Do notCompare strings with
==when culture matters. Use explicitStringComparison.Ordinalfor identifiers and security-sensitive comparisons. - AvoidUsing
Guid.NewGuid()for database keys. It is a random UUIDv4 that fragments clustered indexes. UseGuid.CreateVersion7()(time-ordered UUIDv7) for keys, and keepNewGuid()for tokens or correlation ids that are not stored (see Database Design). - AvoidEarly micro-optimisation that harms readability for gains you have not measured.
Self-review checklist
- AskDo names, casing, layout, and member order match this guide and the surrounding code? Would the analyzer pass clean?
- AskIs it async all the way with cancellation, and is every IDisposable in a using?
- AskDoes any closure capture a loop variable or a scoped/disposed object?
- AskAre nullable warnings fixed properly (not suppressed), and are invalid states hard to represent?
- AskIs money decimal, are dates UTC, are exceptions specific with the cause kept, and is nothing sensitive logged or leaked?
- AskIs SQL parameterised and tenant-scoped, and is the logic in the right layer (not the controller)?