.NET Backend Standards
The rules live in committed config, not in this page. Humans and AI agents read the exact same enforced rules.
Who this applies to: new repos comply on day one. An existing repo complies at its next LTS bump — the gap is a ticket, not an exception.
Every .NET repo ships these four files
Section titled “Every .NET repo ships these four files”| File | Pins | Enforcement |
|---|---|---|
global.json |
SDK version + rollForward |
Same compiler and analyzers for every developer and CI agent |
Directory.Build.props |
TFM, Nullable, TreatWarningsAsErrors, AnalysisLevel, AnalysisMode |
Build breaks |
Directory.Packages.props |
Every package version, once (ManagePackageVersionsCentrally) |
No Version= in any .csproj |
.editorconfig |
Style, formatting, and per-rule analyzer severity | dotnet format --verify-no-changes in the PR pipeline |
- LTS only. New services target the current LTS. We don’t ship STS releases to clients — the support window expires mid-contract.
- Pin
AnalysisLevelto the target framework, neverlatest. An SDK patch must not break an unrelated PR. - A repo-wide
NoWarnneeds Backend Lead sign-off. A per-file#pragmais the escape hatch.
Service shape
Section titled “Service shape”flowchart LR Ep["Api — Endpoints/<Feature>/<UseCase><br/>endpoint · request · validator · query|command"] --> Dom["Domain<br/>pure — no EF, no web framework"] Ep --> Inf["Infrastructure<br/>EF Core · integrations"] Inf --> Dom
- FastEndpoints vertical slices. One folder per use case, holding one endpoint plus its request, response, validator, and its single query or command.
- No MVC controllers, no MediatR, no anaemic pass-through service layer. The endpoint is the handler. Extract a service only when behaviour is genuinely shared or independently testable.
- Litmus: if a file still makes sense with the HTTP framework removed, it does not belong under
Endpoints/.
Cross-cutting defaults
Section titled “Cross-cutting defaults”| Concern | Default |
|---|---|
| Errors | One IExceptionHandler + AddProblemDetails(). Exception text goes to the log, never to the client — shape in API Design |
| Validation | FluentValidation at the boundary, one validator file per slice. Domain invariants throw a typed exception. No Result<T> monad |
| Config | AddOptions<T>().Bind(…).ValidateDataAnnotations().ValidateOnStart() — a bad config fails the boot instead of defaulting |
| Secrets | user-secrets locally, Key Vault–backed variable group when deployed. Never in appsettings*.json |
| Auth | Server-side, per endpoint, permission-based. A client-side guard is UX, never protection |
| Logging | Serilog, with a correlation id enriched on every request |
| Telemetry | OpenTelemetry traces + metrics over OTLP |
| Health | Readiness and liveness endpoints in every environment — restricted to the platform (auth or internal port) outside Development |
Data & migrations
Section titled “Data & migrations”- Never edit an applied migration — add a new one. Forward-only against a client database.
- A migration PR is blocked until the branch has merged every migration already on the target branch. Enforced by a migration-ordering check in the PR pipeline.
- Migrations are added through the repo’s committed script, so the
--contextand startup project can’t drift. - One writable
DbContextper database. A context over data we don’t own is read-only in code, not by convention. - No ambient transaction registered in DI. Never
ReadUncommitted.EnableRetryOnFailure()on every SQL Server context.
Testing
Section titled “Testing”- xUnit v3. Async tests pass
TestContext.Current.CancellationToken— that is what makes the CancellationToken rule enforceable rather than aspirational. WebApplicationFactory<Program>for the API surface. Container-dependent tests carry[Trait("Category", "RequiresDocker")]so the suite still runs green on a machine without Docker.- EF InMemory is for composition tests only. It is not a relational provider, so it cannot validate SQL translation, constraints, or concurrency.
- One test builds the production service graph with
ValidateOnBuild+ValidateScopes. Captive dependencies and unresolvable services fail in CI, not at 2am.
See Testing strategy for the coverage gate and the CI lanes.
Conventions (the non-negotiables)
Section titled “Conventions (the non-negotiables)”- Async all the way — no
.Result,.Wait(), orasync void. PassCancellationTokenthrough the call chain. - DI over statics — constructor injection only. No service locator.
- Nullable on — model absence explicitly. No null-forgiving
!without a comment saying why.
See also: Examples · API Design · Branching & PRs · Pipelines & Environments.