- all nine files: trailing "Related" sections removed, their unique links inlined where the topic actually arises (FFI boundary -> c-cpp, embedded frontend -> js-typescript, build scripts -> bash, etc.) - README conventions: rule 2 mandated a Related Skills section, which now contradicts practice — replaced with inline-linking and one-home-per-fact rules. The <repo>/<rel path> rule was appended after the wrong section last commit; moved into the list where it belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.5 KiB
6.5 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 futureNullReferenceException. - Never blanket-disable an analyzer. When a rule is genuinely wrong for a piece of code,
scope the suppression to it —
#pragma warning disable CA1822around the smallest region that needs it (with a matchingrestore), or a targeted[SuppressMessage]attribute — and always with aJustification/ comment saying why. An.editorconfigentry turns the rule off for every file it covers; use it only when the rule is wrong codebase-wide.
2. Async
async/awaitthroughout; never.Resultor.Wait()— both deadlock in any context with a synchronization context, and turn an exception into anAggregateException.async Task, neverasync void, except for genuine event handlers.ConfigureAwait(false)in library code; not needed in app code on modern hosts.- Accept a
CancellationTokenon 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 usingdeclarations for everythingIDisposable/IAsyncDisposable. Disposal is not optional for streams, HTTP handlers, DB contexts, or native handles.- One static
HttpClient(orIHttpClientFactory) — a per-requestHttpClientexhausts 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,
varfor obvious types, target-typednew(). recordfor immutable value types;readonly structfor small value types.- Pattern matching and switch expressions over
if/elseladders. IReadOnlyList<T>/IReadOnlyDictionary<K,V>in public signatures returning collections.internalby default;publiconly for genuine API surface.- File and function size limits from the general rules apply.
dotnet formatbefore 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 aTrimmerRootDescriptor, 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.csand<Host>SdkShims.cs. Never scatter#ifdirectives 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);
FluentAssertionsfor 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
cdin the same command string. - Prefer the project's build script over hand-typed
dotnetcalls; it carries-warnaserror, the correct action ordering, and the asset completeness checks — seebash.mdfor 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.