Files
skills-public/code-languages/csharp-dotnet.md
T
BluscreamandClaude Opus 5 57968904bf feat(skills): add coding-dev.md and per-language rules under code-languages/
Generic coding rules and preferences distilled from existing agent rule
files, project AGENTS.md/.rules, and saved memories:

- strictness maximal; suppressions must be reported to the user
- never filter build/lint/test output through grep/tail/head
- reuse > fork > self-implement; generic code becomes its own package
- 1000-line file / 100-line function limits, split by concern
- regression test every fixed bug; never test against real user data
- events over polling; scope discipline; honest reporting

Per-language: rust, js-typescript, bash, python, go, csharp-dotnet, c-cpp.
Each extends coding-dev.md rather than repeating it.

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

152 lines
6.2 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`](../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. If a rule is genuinely wrong for the codebase, configure
it once in `.editorconfig` with a comment.
---
## 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.
- **Never stop, deploy to, or restart an application while the user has it running**, unless
they have said to.
---
## 9. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`bash.md`](./bash.md) — for the release pipeline scripts around a .NET build.