Coding Standards

.NET / C# Coding Standards

Foundational

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

Files, projects & layout

Formatting & whitespace

Variables, types & var

Floating point for money 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 for 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#

Modern C# expressing intent 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

Methods, parameters & APIs

Async & concurrency

Blocking, leaking, stringly-typed 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.

Async, disposed, typed, shaped 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.

Sync-over-async deadlock 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.

Async all the way 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

A new HttpClient per call 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.

A shared client from the factory 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

DI, logging & structure

Performance & security

Self-review checklist

Why it matters: A large, growing, mostly junior team shares one C# codebase. Every file needs to read consistently and avoid the language's many traps. Full, automated standards make the correct, safe path the easiest one, keep review focused on what matters, and prevent whole classes of bug, such as blocking async, leaked resources, null surprises, money errors, and injection, before they are ever written.