- 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>
150 lines
6.5 KiB
Markdown
150 lines
6.5 KiB
Markdown
---
|
|
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
|
|
<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`:**
|
|
|
|
```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<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:
|
|
```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.
|