--- name: csharp-dotnet description: >- Use this skill when writing, building, or releasing C# / .NET code — including nullable reference types, warnings-as-errors, analyzer configuration, async/await rules, trimming and single-file publishing, and multi-generation SDK compatibility shims. --- # C# / .NET — Rules & Preferences Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies; this adds .NET specifics. --- ## 1. Project Settings — Strict By Default ```xml enable true true latest-all enable latest ``` Put these in a `Directory.Build.props` at the repository root so every project inherits them and none can quietly drift. **Every build carries `-warnaserror`:** ```bash dotnet build MySolution.sln -warnaserror ``` - **Nullable warnings are real bugs.** Never silence one with `!` (null-forgiving) — narrow the type or fix the contract. `!` is an assertion, and an assertion you cannot prove is a future `NullReferenceException`. - Never blanket-disable an analyzer. When a rule is genuinely wrong for a piece of code, scope the suppression to it — `#pragma warning disable CA1822` around the smallest region that needs it (with a matching `restore`), or a targeted `[SuppressMessage]` attribute — and always with a `Justification` / comment saying why. An `.editorconfig` entry turns the rule off for every file it covers; use it only when the rule is wrong codebase-wide. --- ## 2. Async - **`async`/`await` throughout; never `.Result` or `.Wait()`** — both deadlock in any context with a synchronization context, and turn an exception into an `AggregateException`. - `async Task`, never `async void`, except for genuine event handlers. - `ConfigureAwait(false)` in library code; not needed in app code on modern hosts. - Accept a `CancellationToken` on every async public method and actually honor it. - `IAsyncEnumerable` for streaming rather than materializing a whole collection. - **Events over polling** — subscribe to the framework's event/observable API; don't run a timer loop. (Global rule; it applies fully here.) --- ## 3. Errors, Disposal & Logging - Specific exception types; never `catch (Exception)` without a reason and a log. - **Never an empty catch block.** - `using`/`await using` declarations for everything `IDisposable`/`IAsyncDisposable`. Disposal is not optional for streams, HTTP handlers, DB contexts, or native handles. - **One static `HttpClient` (or `IHttpClientFactory`)** — a per-request `HttpClient` exhausts sockets under load. - Structured logging (Serilog or `ILogger`) with message templates, not string interpolation: `logger.LogInformation("Loaded {Count} items", count)` — the template is what makes the log queryable. - **Never log secrets.** --- ## 4. Style - File-scoped namespaces, `var` for obvious types, target-typed `new()`. - `record` for immutable value types; `readonly struct` for small value types. - Pattern matching and switch expressions over `if`/`else` ladders. - `IReadOnlyList` / `IReadOnlyDictionary` in public signatures returning collections. - `internal` by default; `public` only for genuine API surface. - File and function size limits from the general rules apply. - `dotnet format` before committing. --- ## 5. Publishing — Trimming & Single File > [!IMPORTANT] > **The trimmer only runs on a Release/trimmed/single-file publish.** A Debug build cannot > reproduce the trimmer dropping a type that only reflection reaches. If your release pipeline > ships trimmed binaries, the publish step is the **only** one that exercises the trimmer — > so it must run before release, not after. - Annotate reflection-reached types with `[DynamicallyAccessedMembers]` or a `TrimmerRootDescriptor`, and **smoke-test the published binary**, not just the build. - Enable `` per library deliberately; blanket trimming of a plugin host breaks plugin loading. - **A release is a fixed, complete set of assets.** Define what that set is, and have the release step verify it is complete rather than trusting whatever happens to be in the output directory. A packaging tool that silently skips a step when a dependency is missing will otherwise ship a release with a missing platform. --- ## 6. Multi-Generation SDK Compatibility When targeting several generations of a host SDK whose API changed shape (a class becoming an interface, a type being renamed): - **All compatibility code lives in dedicated shim files** — e.g. `GlobalUsings.cs` and `SdkShims.cs`. Never scatter `#if` directives through feature code. - Select the target with an MSBuild property so the build is explicit about which generation it produced: ```bash dotnet build MyModules.csproj -c Release -p:HostTarget=stable dotnet build MyModules.csproj -c Release -p:HostTarget=beta ``` - Document the differences between generations in the project's agent/contributor file — which types changed, and which direction each shim adapts. - Keep a local package feed for SDK versions not published to the public registry, and script the packing step rather than doing it by hand. --- ## 7. Testing - xUnit (or the project's existing framework); `FluentAssertions` for readable assertions. - **Regression test every fixed bug.** - Inject abstractions for filesystem, clock, and network so they can be faked — do not reach for a static. - **Never test mutations against real user profiles, real config directories, or live services.** Fixtures and injected backends only. - Test the **published** artifact for anything involving trimming, single-file packing, or native dependencies — the assembly that passed unit tests is not the one you shipped. --- ## 8. Toolchain Notes - When the SDK is not installed on the host (immutable OS, mismatched glibc), build inside the project's designated container. Entering one resets the working directory — chain `cd` in the same command string. - Prefer the project's build script over hand-typed `dotnet` calls; it carries `-warnaserror`, the correct action ordering, and the asset completeness checks — see [`bash.md`](skills-public/code-languages/bash.md) for how that script should be structured. - **Never stop, deploy to, or restart an application while the user has it running**, unless they have said to.