Files
skills-public/code-languages/csharp-dotnet.md
T
BluscreamandClaude Opus 5 58525056a1 docs: use <repo>/<rel path> links throughout, document the convention
All cross-skill links now spell out the repository: skills-public/foo.md,
skills-private/bar.md. These resolve from the shared parent directory, so one
spelling works from either repo and from code-languages/ subdirectories —
replacing the previous mix of ./, ../ and absolute file:// URLs. Link labels
normalised to match. Convention added to the README conventions list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:44:23 +02:00

6.6 KiB

name, description
name description
csharp-dotnet 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. Everything there applies; this adds .NET specifics.


1. Project Settings — Strict By Default

<PropertyGroup>
  <Nullable>enable</Nullable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  <AnalysisLevel>latest-all</AnalysisLevel>
  <ImplicitUsings>enable</ImplicitUsings>
  <LangVersion>latest</LangVersion>
</PropertyGroup>

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:

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<T> 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<T>) 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<T> / IReadOnlyDictionary<K,V> 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 <IsTrimmable> 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 <Host>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:
    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.
  • Never stop, deploy to, or restart an application while the user has it running, unless they have said to.

  • coding-dev.md — the general rules this extends.
  • bash.md — for the release pipeline scripts around a .NET build.