Skip to content

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; no Version attributes 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 through TreatWarningsAsErrors in app projects; test projects stay relaxed). The analyzer reads the limit from .editorconfig max_line_length (via AnalyzerConfigOptions), so the template's max_line_length = 180 is the single source of truth. Ship as a NuGet package on the internal feed, referenced once in Directory.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,
    ...
- Primary constructors wherever possible for dependency injection and simple types. - File-scoped namespaces; one type per file; 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=enable everywhere; 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(), or GetAwaiter().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 with CancellationToken.None unless deliberate.
  • ConfigureAwait(false) in shared libraries (ExpertGroup.Core.*); not required in ASP.NET Core app code.
  • async void only 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 via AddX()/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