C# /.NET Development
Check the nearest *.csproj, Directory.Build.props, global.json, and CI for TargetFramework(s), LangVersion, nullable context, and analyzer policy before using newer syntax. Without an explicit LangVersion, the C# version follows the TFM (net8.0 defaults to C# 12; net6.0 defaults to C# 10, which lacks primary constructors and collection expressions). Project conventions win over these defaults, and raising LangVersion or the TFM to unlock syntax is a separate decision — ask first, don't do it on your own.
Defaults
- BCL and existing NuGet packages first. Keep the app's existing choices: controllers vs minimal APIs, DI container, ORM, test framework.
- Nullable reference types stay on. Model absence with
?; handle a missing result explicitly (e.g. returnNotFound/Problem) instead of!. Never suppress a warning; fix it at the source. - Async end to end: no
.Result,.Wait(), orGetAwaiter().GetResult(). PassCancellationTokenthrough cancellable boundaries, including EF calls. - Never let a singleton capture a scoped service (e.g. a
DbContext); useIServiceScopeFactoryorIDbContextFactorywhen a singleton needs scoped data. - Validate request DTOs and message payloads at the handler before mapping to domain types; model binding alone is not validation. Return response DTOs from API boundaries, not entities.
- Bind and validate options at startup (
ValidateOnStart); inject typed options instead of reading config ad hoc. - Background services honor the stopping token; replace
Thread.Sleepwith a cancellable wait (Task.Delaywith the token, orPeriodicTimer).
CLIs
- Existing CLI stack first; the BCL is enough for small tools. Add
System.CommandLineor Spectre.Console only when the command surface justifies it. - Keep
Program.csthin: test the command handler, or aRun(args, stdout, stderr)seam, and assert exit code and output withdotnet test --filter, including a non-zero exit and a stderr message on bad input.
References
- testing.md: read when adding or reshaping tests, or when
dotnet testis slow. - linting.md: read when changing
dotnet format, analyzers, or warning policy.
Done when the relevant build/test/lint checks pass on what you changed, or you name each check that did not run and why.