C# / .NET Standard¶
v1.0 — 2026-06-06 — applies to all .NET solutions.
Framework & project setup¶
- Always target the latest LTS version of .NET (currently .NET 10). Targeting an STS release is allowed only by exception: document in the repo's CLAUDE.md/README exactly which features required the STS, and plan the hop to the next LTS.
- Every solution has a root
Directory.Build.props(see template):Nullable=enable,ImplicitUsings=enable,TreatWarningsAsErrors=true,EnforceCodeStyleInBuild=true,AnalysisLevel=latest-recommended. - Use Central Package Management (
Directory.Packages.props) in multi-project solutions; noVersionattributes in csproj files. - Style is enforced by
.editorconfig+ build, not by this document. The template editorconfig is canonical.
Style (delta from Microsoft defaults)¶
- DECISION: Tabs for indentation, CRLF line endings (matches ExpertGroup core; Microsoft's documented default is 4 spaces — we deviate deliberately).
- Line length: max 180 characters (fits a 27" 2.5K display). Wrap only when a line exceeds it — do NOT put one parameter per line by default; pack parameters and continue on the next line when needed:
⏳ PENDING (action item, ratified 2026-06): build-enforce this via a custom Roslyn analyzer — ExpertGroup.Analyzers, rule
EG0001"line exceeds max length", warning severity (becomes build-breaking throughTreatWarningsAsErrorsin app projects; test projects stay relaxed). The analyzer reads the limit from.editorconfigmax_line_length(via AnalyzerConfigOptions), so the template'smax_line_length = 180is the single source of truth. Ship as a NuGet package on the internal feed, referenced once inDirectory.Build.props. Until it ships, the limit is review-enforced. The package is also the future home for other org-specific rules (e.g. "resolver must call IActorIdentityGuard").
// Yes
internal class AuthService(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, ITokenService tokenService,
IDistributedCache cache, IConfiguration configuration) : IAuthService
// No — one-parameter-per-line when it all fits in far fewer lines
internal class AuthService(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
...
var when the type is apparent (warning-enforced).
- PascalCase types/methods/properties; camelCase private fields (no leading underscore); camelCase locals/parameters; I prefix interfaces.
- Pattern matching over type checks (x is not null); expression-bodied properties/accessors yes, methods/constructors no.
- No #region. No commented-out code. Comments explain why, not what.
Nullability¶
Nullable=enableeverywhere; nullable warnings are errors.- Avoid
!(null-forgiving) except at well-justified boundaries (e.g. EF navigation init) — comment the justification.
Async¶
- Async all the way: never
.Result,.Wait(), orGetAwaiter().GetResult()on async code. - Suffix async methods with
Async(exception: GraphQL resolvers and controller actions may omit it — existing codebase convention). - Public async APIs accept a
CancellationToken. Pass tokens through; don't swallow them withCancellationToken.Noneunless deliberate. ConfigureAwait(false)in shared libraries (ExpertGroup.Core.*); not required in ASP.NET Core app code.async voidonly for event handlers.
Dependency injection¶
- Constructor injection (or
[Service]parameter injection in HotChocolate resolvers). No service locator, no static state. - Register services via extension methods (
AddExpertGroupXyz()) following the ExpertGroup.Core ecosystem-discovery pattern.
Domain model — entity design (DDD-lite)¶
Added from expert review (item 23: Ardalis):
- Private setters on entity properties; state changes go through methods that enforce invariants, not property assignment from outside.
- Constructors/factory methods enforce invariants — an entity that compiles is an entity in a valid state; no "create empty then fill" pattern.
- Encapsulated collections: expose
IReadOnlyCollection<T>backed by a private list; mutation viaAddX()/RemoveX()methods on the entity. - Behavior over anemic property bags: logic that operates on an entity's state lives on the entity, not scattered across services.
- EF Core maps all of this fine (private setters, backing fields) — anemic entities are a habit, not a constraint.
Errors & logging¶
- Throw specific exceptions; use the ExpertGroup.Core exception-translation layers at API boundaries.
- Structured logging (
ILogger<T>with message templates) — never string interpolation in log calls. - Never log secrets, tokens, or personal data.
Sources: MS C# conventions · Code analysis · CPM · Async guidance · ConfigureAwait FAQ