This is the code-style guide for the repo. The General section applies repo-wide; the .NET section covers the C# code.
Cross-cutting process rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette) live in AGENTS.md and are not repeated here.
These rules apply repo-wide.
Use each tool's official casing in task labels, docs, and prose - .NET (not .Net), CSharpier, ruff, pyright, uv. Don't invent personal variants.
Each language defines a clean-compile verification - the combination of build, formatter, linter, and code-analysis tools that must report clean before a commit. It is exposed as one or more named VS Code tasks; the concrete names live in the .NET section below.
- Run it after every code change. The relevant language's clean-compile must pass before you commit; CI runs the same checks as a backstop.
- The named task definition is the canonical spec - its exact command sequence, arguments, and strictness. You may run it through the VS Code task or by invoking the equivalent native commands directly; either is fine only if the sequence, arguments, and strictness match exactly. No shortcuts and no more-lenient options (for example, never drop
--verify-no-changesor loosen a--severity). - A local commit/pre-commit gate is the repo's choice. CI is the authoritative backstop regardless; a local gate is an additive convenience - this repo wires Husky.Net (with
dotnet husky runas a style step). Keeping a working gate is not drift.
- A new port is not a license to silence diagnostics. Brownfield / just-ported status never justifies relaxing analyzer or linter severities or muting newly surfaced warnings - fix them.
- Suppress only genuine false-positives or deliberate, documented exceptions, always at the narrowest scope that fits, in this order of preference:
- An in-code annotation on the specific symbol, with a justification - the language's attribute/comment form, never a blanket pragma spanning a region.
- The owning project's local config when the exception is project-wide for one project (e.g. a test project's own
.editorconfig/pyproject.toml). - The root / shared config only when the suppression is genuinely applicable to every project in the repo.
- Never blanket-relax a batch of rules project-wide to get a port to build. The mechanics (which attribute, which config key) are in the .NET section.
These apply repo-wide, in every directory:
- Markdown linting: All
.mdfiles must be lint-clean (error and warning free) via the VS Codemarkdownlintextension..markdownlint-cli2.jsoncat the repo root is the single source of truth - the davidansonmarkdownlintextension and a command-linemarkdownlint-cli2run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g.MD013line-length,MD033inline HTML) are intentional - do not "fix" them. Fix violations at the source rather than disabling rules. - Spelling: All spelling must be clean via the CSpell VS Code integration; words must be correctly spelled in US English (the repo-wide convention - see AGENTS.md). Project-specific terms go in the workspace CSpell config.
This is the style guide for any .NET projects in this repo.
CRITICAL: All builds must complete without warnings. The project enforces this through:
-
The
.NET Formatclean-compile task (see Clean-Compile Verification)- The .NET clean-compile is the
.NET FormatVS Code task, which chainsCSharpier Format->.NET Build->dotnet format style --verify-no-changes. These three task definitions live in.vscode/tasks.json. - After any code change it must pass before commit. Run the
.NET Formattask. To run it natively instead, reproduce that task chain from.vscode/tasks.jsonexactly -CSharpier Format, then.NET Build, then thedotnet format style --verify-no-changes --severity=info ...verify - without dropping or loosening any argument (tasks.json is the canonical command spec). Baredotnet formatalone, skipping CSharpier or the build, is not sufficient.
- The .NET clean-compile is the
-
Analyzer configuration
<EnableNETAnalyzers>true</EnableNETAnalyzers>with<AnalysisLevel>latest-all</AnalysisLevel>and<AnalysisMode>All</AnalysisMode>(full analyzer set enabled)<TreatWarningsAsErrors>true</TreatWarningsAsErrors>- any diagnostic surfaced as a warning fails the build, so it must be fixed or deliberately suppressed, not left to accumulate (see Analyzer Diagnostics and Suppressions)
-
CI lint backstop
- CI runs the clean-compile checks on every PR as the authoritative backstop
- Git hooks are optional; a repo may wire a local runner (Husky.Net) for pre-commit enforcement, but CI is the gate that matters
Available VS Code tasks (run them from VS Code's task runner - Terminal -> Run Task - or an agent's task-running tool). The three clean-compile tasks below are the canonical set; add your own convenience tasks (tool updates, dependency upgrades, benchmarks) on top:
.NET Build: Build with diagnostic verbosity (clean-compile)CSharpier Format: Auto-format code with CSharpier (clean-compile).NET Format: Run CSharpier and build, then verify formatting and style with--verify-no-changes(clean-compile; the task to run after edits)
- CSharpier: Primary code formatter
- Invoked by the
CSharpier Formattask /dotnet csharpier format --log-level=debug .
- Invoked by the
- dotnet format: Style verification
- Verify no changes:
dotnet format style --verify-no-changes --severity=info --verbosity=detailed
- Verify no changes:
- Other tools
dotnet-outdated-tool: Dependency update checks- Nerdbank.GitVersioning: Version management
CI is the authoritative lint backstop. Local pre-commit hooks are optional - wire Husky.Net (or another runner) if you want local enforcement.
- Required VS Code extensions: CSharpier, markdownlint, CSpell
- VS Code settings: Use the workspace settings without overrides
Note: Code snippets are illustrative examples only. Replace namespaces/types to match your project.
-
File-scoped namespaces
namespace Example.Project.Library;
-
Nullable reference types: Enabled (
<Nullable>enable</Nullable>)- Use nullable annotations appropriately
- Use
requiredfor mandatory properties
-
Modern C# features: Prefer modern language constructs
- Primary constructors when appropriate
- Top-level statements for console apps
- Pattern matching over traditional checks
- Collection expressions when types loosely match
- Extension methods - the classic
this-parameter form, or anextension(<receiver>) { ... }block on C# 14+ - Implicit object creation when type is apparent
- Range and index operators
-
Expression-bodied members: Use for applicable members
- Methods, properties, accessors, operators, lambdas, local functions
-
varkeyword: Do NOT usevar(always use explicit types)// Correct int count = 42; string name = "test"; // Incorrect var count = 42; var name = "test";
-
Private fields: underscore prefix with camelCase
private readonly HttpClient _httpClient; private int _counter;
-
Static fields:
s_prefix with camelCaseprivate static int s_instanceCount;
-
Constants: PascalCase
private const int MaxRetries = 3;
-
Global usings: Use
GlobalUsings.csfor common namespacesglobal using System; global using System.Net.Http; global using System.Threading.Tasks; global using Serilog;
-
Usings placement: Outside namespace, sorted with
Systemdirectives firstusing System.CommandLine; using System.Runtime.CompilerServices; using Example.Project.Library; namespace Example.Project.Console;
-
Braces: Allman style
public void Method() { if (condition) { // code } }
-
Indentation
- C# files: 4 spaces
- XML/csproj files: 2 spaces
- YAML files: 2 spaces
- JSON files: 4 spaces
-
Line endings
- C#, XML, YAML, JSON, Windows scripts: CRLF
- Linux scripts (
.sh): LF
-
#region: Do not use regions. Prefer logical file/folder/namespace organization. -
Member ordering (StyleCop SA1201): const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types
-
XML documentation
<GenerateDocumentationFile>true</GenerateDocumentationFile>- Missing XML comments for public APIs are suppressed (
.editorconfig) - Must document all public surfaces.
- Single-line summaries, additional details in remarks, document input parameters, return values, exceptions, and add crefs
/// <summary> /// Example of a single line summary. /// </summary> /// <remarks> /// Additional important details about usage. /// Multiple lines if needed. /// </remarks> /// <param name="category"> /// The quote category to request /// </param> /// <param name="cancellationToken"> /// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <see cref="string"/> containing the quote text. /// </returns> /// <exception cref="System.ArgumentException"> /// Thrown when <paramref name="category"/> is not a supported value. /// </exception> public async Task<string> GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {}
Follow the scope hierarchy in Analyzer Diagnostics and Suppressions. .NET mechanics, narrowest first:
-
Never use
#pragma warning disableto silence an analyzer. -
Symbol-scoped: a
[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]attribute with aJustification, on the specific member or type:[System.Diagnostics.CodeAnalysis.SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "https://github.com/dotnet/sdk/issues/51681" )]
-
Project-scoped (e.g. a test project): a
dotnet_diagnostic.<RULE>.severityentry in that project's own.editorconfig, with a comment explaining why. -
Repo-wide: a
dotnet_diagnostic.<RULE>.severityentry in the root.editorconfig, only when the rule is genuinely not applicable to any project. Relaxing a batch ofCA*rules (ordotnet_analyzer_diagnostic.severity) to push a brownfield port through the build is exactly what this forbids.
-
Serilog logging: Use structured logging
logger.Error(exception, "{Function}", function);
-
Library log configuration: Libraries must expose logging configuration
- Provide options or settings to supply an
ILoggerFactoryand/orILogger - Offer a global fallback logger for static usage when needed
- Provide options or settings to supply an
-
CallerMemberName: Use for automatic function name tracking
public bool LogAndPropagate( Exception exception, [CallerMemberName] string function = "unknown" )
-
Logger extensions: Use
Extensions.csfor logger and other extension methodsextension(ILogger logger) { public bool LogAndPropagate(Exception exception, ...) { } }
-
Exceptions: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception
- Guard clauses: Prefer early returns for validation and error handling
- Async all the way: Avoid blocking calls (
.Result,.Wait()); useasync/await - Cancellation tokens: Accept
CancellationTokenas the last parameter and pass it through - ConfigureAwait: In library code, use
ConfigureAwait(false)unless context is required- Do not call
ConfigureAwait(false)in xUnit tests (see xUnit1030)
- Do not call
- Disposables: Use
await usingfor async disposables; preferusingdeclarations - LINQ vs loops: Use LINQ for clarity, loops for hot paths or allocations
- HTTP: Reuse
HttpClientvia factory; avoid per-request instantiation - Collections: Prefer
IReadOnlyList<T>/IReadOnlyCollection<T>for public APIs - Immutability: Prefer immutable records; use init-only setters when records are not suitable; prefer immutable or frozen collections for read-only data
- Exceptions as control flow: Avoid using exceptions for expected flow
- Sealing classes: Seal classes that are not designed for inheritance
- Read-only data: Use immutable or frozen collections for read-only data sets
- Lazy initialization: Use
Lazy<T>for static, thread-safe instantiation (e.g., logger factory, HTTP factory)
-
Framework: xUnit with AwesomeAssertions
[Fact] public void MethodName_Scenario_ExpectedBehavior() { // Arrange int expected = 42; // Act int actual = GetValue(); // Assert actual.Should().Be(expected); }
-
Organization: Arrange-Act-Assert pattern
-
Naming: Descriptive names with underscores
-
Theory tests: Use
[Theory]with[InlineData]
-
Target framework: .NET 10.0 (
<TargetFramework>net10.0</TargetFramework>) -
AOT compatibility
<IsAotCompatible>true</IsAotCompatible><VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>
-
Assembly information
- Use semantic versioning
- Include SourceLink:
<PublishRepositoryUrl>true</PublishRepositoryUrl> - Embed untracked sources:
<EmbedUntrackedSources>true</EmbedUntrackedSources>
-
Internal visibility: Use
InternalsVisibleTofor test and benchmark access (adapt the project names to your repo's test/benchmark projects)<ItemGroup> <InternalsVisibleTo Include="YourBenchmarkProject" /> <InternalsVisibleTo Include="YourTestProject" /> </ItemGroup>
- Code reviews: All changes go through pull requests