csharp-dotnet · v2026.09.12 · 2026-09-12 · sha256 f593caea0f497252
csharp-dotnet v2026.09.12A
Immutable. This exact content never changes and is served at /api/v1/blob/f593caea0f497252.
---
name: csharp-dotnet
description: "Develops C# and .NET applications with ASP.NET Core, Blazor and Entity Framework."
license: MIT (upstream attributions in NOTICE.md)
metadata:
author: HyperSkills
version: "2026.09.12"
category: framework
---
# csharp-dotnet
## Scope
Covers application and library code on modern .NET: the C# language, the SDK build
(MSBuild, NuGet, Central Package Management), ASP.NET Core services and APIs, Blazor,
.NET MAUI, the testing stack, runtime and build diagnostics, and moving a codebase across
target frameworks. .NET 10 is the current LTS and the default target here; C# 14 is its
language version, and .NET 11 with C# 15 is the next STS.
Not covered: reviewing a diff for general correctness and risk — use the `code-review`
skill. Root-causing a reproducible local failure — use the `debugging` skill. Test-first
methodology and what deserves a test at all — use the `test-driven-development` skill.
Visual and layout design of a web UI — use the `frontend-design` skill. Unity C# and its
engine APIs — use the `unity` skill; Godot's C# bindings — use the `godot` skill. Azure
resource orchestration (Bicep, azd, ARM, Azure SDK clients) — use the `azure` skill. Out
of scope entirely: .NET Aspire orchestration, F# and Visual Basic, database schema
design, and Windows Forms/WPF beyond the upgrade checklist.
Paths below are relative to this skill's directory.
## Core rules
1. Read `global.json`, the target framework, `Directory.Build.props`/`.targets` and
`Directory.Packages.props` before changing a project. Every rule below depends on which
SDK, TFM and package policy the repository is actually on.
2. Adopt what the project already uses — minimal APIs or controllers, xUnit or MSTest,
constructor injection or `Add*` extension methods. A second convention beside an
existing one gives the same concept two answers.
3. `Directory.Build.props` is imported before the project file and `Directory.Build.targets`
after the SDK targets. Defaults go in `.props`; anything reading an SDK-computed value
goes in `.targets`.
4. A `PropertyGroup` conditioned on `$(TargetFramework)` inside a `.props` file never
matches for a single-targeting project — the property is not set yet, so the condition
fails silently. Move it to `.targets`. `ItemGroup` and `Target` conditions are unaffected.
5. Give shared defaults `Condition="'$(Prop)' == ''"`. Without it the value cannot be
overridden, and an unconditional assignment in two files is a silent last-write-wins.
6. Append to list-valued properties: `<DefineConstants>$(DefineConstants);FEATURE_X</DefineConstants>`.
A bare assignment drops every constant set upstream, including the SDK's own.
7. Quote both sides of every MSBuild comparison. `$(X)==Release` misevaluates or fails to
parse the moment `X` is empty.
8. Under Central Package Management a `PackageReference` must not carry `Version` — that is
NU1008. Put the version in `Directory.Packages.props` as `PackageVersion`, and use
`VersionOverride` only where one project genuinely needs a different version.
9. Analyzer and build-tool packages need `PrivateAssets="all"`, or become
`GlobalPackageReference` in `Directory.Packages.props`. Otherwise they flow transitively
to everyone who consumes the library.
10. Do not restate SDK defaults or list `Compile` items in an SDK-style project. Redundant
properties hide the real customisations and pin old behaviour when defaults change.
11. Add or remove a single package with `dotnet add package` / `dotnet remove package`; edit
the XML directly for bulk alignment, conditional versions and `VersionOverride`, which
the CLI cannot express. Either way, prove the result with `dotnet restore` and a build.
12. Custom targets declare `Inputs`/`Outputs` and register generated files in `FileWrites`,
and every build question is answered from a binary log (`-bl`) or a preprocessed project
(`-pp`) rather than from reading the files and guessing.
13. Turn on `<Nullable>enable</Nullable>` and let control flow prove non-nullness — a guard
clause or pattern match creates a null-safe region. Reach for the null-forgiving operator
only when a real external invariant cannot be expressed, and use the
`System.Diagnostics.CodeAnalysis`
attributes (`NotNullWhen`, `MemberNotNull`, `NotNullIfNotNull`, `DoesNotReturn`) for
contracts the signature cannot carry.
14. A singleton must never capture a scoped service. The captured instance is promoted to
singleton lifetime, which is a correctness bug (stale state, concurrent use), not a
performance one. Scope validation catches it, but it is on by default only in the
Development environment — set `ValidateScopes` and `ValidateOnBuild` explicitly.
15. A `DbContext` is scoped and is not thread-safe. Never hold one in a singleton or use one
from two concurrent operations; take `IServiceScopeFactory` or `IDbContextFactory<T>`
and create one per unit of work.
16. Conventional middleware is instantiated once, so a scoped dependency must arrive as a
parameter of `InvokeAsync`, not through the constructor. Constructor injection of a
scoped service throws at runtime; the alternative is factory-based middleware.
17. Never call `BuildServiceProvider()` while configuring services. It builds a second
container with its own copies of every singleton. Use the options pattern, or the
registration overload that hands you an `IServiceProvider`.
18. Never block on asynchronous work with `.Result`, `.Wait()` or `GetAwaiter().GetResult()`.
It deadlocks wherever a synchronization context is captured and starves the thread pool
where one is not — which is how an ASP.NET Core app stops responding under load.
19. `async void` is for event handlers only. Its exceptions are raised on the synchronization
context and cannot be caught by the caller, so failures disappear.
20. A `ValueTask` may be awaited exactly once and never after the operation completes.
Awaiting one twice is undefined behaviour; store the result, or call `AsTask()`.
21. Flow `CancellationToken` into every async call you make. A token that stops at the method
signature makes shutdown and request abort silently ineffective.
22. `ConfigureAwait(false)` belongs in library code that might run under a UI or legacy
ASP.NET synchronization context. ASP.NET Core has no synchronization context, so adding
it there fixes nothing — the blocking call in rule 18 is the actual defect.
23. Get `HttpClient` from `IHttpClientFactory` or a typed client. A `new HttpClient()` per
call exhausts sockets, and a long-lived static one never sees DNS changes.
24. Return `TypedResults` and an explicit `Results<Ok<T>, NotFound>`-style union from minimal
API handlers, and surface failures as `ProblemDetails`. Untyped `Results.Ok(...)` leaves
the generated OpenAPI document with no response schema.
25. Judge performance and memory only from a Release build in its real deployment form, with
a deterministic reproduction and an equivalent before/after measurement. A single average
from a Debug build cannot support any claim.
## Workflows
### add-feature
- [ ] Read the build files (rule 1) and one existing feature of the same shape to copy its
layout, DI style and naming.
- [ ] Define the contract first: request/response `sealed record` types, the interface the
feature depends on, and the failure cases. Never expose EF Core entities on the wire.
- [ ] Register services in an `Add{Feature}` extension method returning `IServiceCollection`,
so the same wiring is reusable from `WebApplicationFactory` in tests.
- [ ] Pick the narrowest lifetime that works, then re-check rule 14 for every singleton you
added.
- [ ] Write the handler last: async all the way, `CancellationToken` threaded through,
`TypedResults` on the way out.
- [ ] Add packages per rule 11; if the repository uses CPM, the project file gets no version.
- [ ] Add tests per `add-tests`.
- [ ] **Gate:** `dotnet build -warnaserror` and `dotnet test` for the affected projects pass,
and the new endpoint or entry point is exercised by at least one test.
### fix-build
- [ ] Reproduce with a clean build (`dotnet build --no-incremental`) and capture a binary log:
`dotnet build -bl:build.binlog`. Read the log, not the console tail.
- [ ] Classify first: restore/NuGet error (NU*), evaluation problem (a property or item is
not what you expect), compile error, or a custom target misbehaving.
- [ ] For an evaluation problem run `dotnet msbuild -pp:preprocessed.xml <project>` and find
which file wrote the final value — rules 3 to 7 are the usual causes.
- [ ] For NU1008/NU1010 and version drift, check CPM state: is
`ManagePackageVersionsCentrally` on, does every `PackageReference` lack `Version`, is
there a stray version in an imported `.props`.
- [ ] For "it rebuilds when nothing changed", look for custom targets with no `Inputs`/
`Outputs`, outputs written outside the declared `Outputs`, and volatile values
(timestamps, GUIDs) in a path.
- [ ] **Gate:** two consecutive builds — the second reports the targets as skipped/up to date,
and `dotnet restore` is clean.
### review
- [ ] Establish the diff scope and read its tests first; they state what the author believes
the code does.
- [ ] Walk the Core rules in order — they are ordered by how often each one is the real defect.
- [ ] Check the DI diff separately: new singletons against rule 14, new middleware against
rule 16, any `BuildServiceProvider` against rule 17.
- [ ] Check every `await`-free `Task` return, every `.Result`, every `async void`, and every
`CancellationToken` parameter that is accepted but never passed on.
- [ ] Check the build diff separately: versions outside the central file, new analyzer
packages without `PrivateAssets`, properties added unconditionally.
- [ ] Report with the Output format below.
- [ ] **Gate:** every finding carries `path:line`, one line of reasoning, and a concrete fix.
### diagnose-runtime-issue
- [ ] Establish the symptom and the violated target in numbers, then a deterministic
reproduction on a Release build in its real deployment form (rule 25).
- [ ] Record the environment before collecting anything: commit, SDK and runtime versions,
OS, architecture, container limits, and the exact commands used.
- [ ] Start with counters (`dotnet-counters monitor`): thread-pool queue length and thread
count point at blocking (rule 18); Gen 2/LOH growth points at a leak; high time-in-GC
points at allocation rate.
- [ ] Then take the artefact the symptom calls for — `dotnet-trace collect` for CPU and
wall-clock time, `dotnet-dump collect` for a hang or a leak, and analyse the dump with
`dotnet-dump analyze` (`dumpheap -stat`, `gcroot`).
- [ ] Change one thing, then re-measure the same workload the same way. Report before and
after with the spread, not a single average.
- [ ] **Gate:** the report names the dominant cause with the evidence that identifies it, and
the after-measurement comes from an equivalent run.
### add-tests
- [ ] Use the framework already in the repository. For a new test project, default to
xUnit v3 on Microsoft.Testing.Platform.
- [ ] Put pure logic in unit tests, and test the composed application through
`WebApplicationFactory<TEntryPoint>`, replacing only the boundary the test needs.
- [ ] Replace ambient state with an injectable abstraction — `TimeProvider` for time,
`System.IO.Abstractions` for the file system — instead of asserting around it.
- [ ] Prefer a hand-written fake at an interface boundary; add a mocking library only when
there is no interface you can implement.
- [ ] Assert observable behaviour: the response the caller sees, the row that was written,
the message that was published. Not the calls made on a mock.
- [ ] **Gate:** `dotnet test` passes, and each new test fails when the behaviour it covers is
broken.
### upgrade-target-framework
- [ ] Record the current TFMs, SDK, and package versions, and get a clean build with zero new
warnings on the current target first. That is the baseline every later failure is
compared against.
- [ ] Move one major version at a time. Change `TargetFramework`, then bring
`Microsoft.Extensions.*`, `Microsoft.AspNetCore.*` and `Microsoft.EntityFrameworkCore.*`
to the matching major version, in `Directory.Packages.props` if CPM is on.
- [ ] Run `dotnet restore` and read the new NuGet diagnostics before building: pruned
framework-provided packages (NU1510) should be removed rather than pinned.
- [ ] Build, then work through errors by area — compiler/language, core libraries, ASP.NET
Core, EF Core, serialization, cryptography, containers — committing per area.
- [ ] Then look for behavioural changes, which the compiler cannot show you: they surface as
failing tests or changed output, not as build errors.
- [ ] Update the Dockerfile, CI SDK version and `global.json` last, in one commit.
- [ ] **Gate:** clean build with no new warnings, full test suite green, and the app starts
and serves one real request on the new runtime.
## Topic router
| Topic | Read when | File |
|---|---|---|
| C# language | Nullability, records, pattern matching, exceptions, disposal, Span, generics | `references/csharp-language.md` |
| Async and concurrency | `async`/`await`, `ValueTask`, cancellation, Channels, parallelism, thread pool | `references/async-concurrency.md` |
| Build, MSBuild and NuGet | Project files, `Directory.Build.*`, CPM, restore errors, incremental build, binlogs | `references/build-msbuild-nuget.md` |
| ASP.NET Core | Minimal APIs, controllers, DI, middleware, options, auth, OpenAPI, EF Core on the request path | `references/aspnetcore.md` |
| Blazor | Render modes, prerendering, component authoring, JS interop, disposal | `references/blazor.md` |
| .NET MAUI | App lifecycle, bindings, Shell navigation, `CollectionView`, platform code | `references/maui.md` |
| Testing | Choosing a framework or test level, MTP, integration tests, testability | `references/testing.md` |
| Diagnostics and performance | Slow, leaking, hanging or allocation-heavy code in a real environment | `references/diagnostics.md` |
| Upgrades | Moving target frameworks, adopting nullable, trimming and Native AOT | `references/upgrade.md` |
## Output format
For `review` (and any other report on existing code), group findings by file, ordered by
severity, with no preamble:
```
src/Orders.Api/Program.cs
L11 blocking - OrderCache is a singleton holding the scoped OrdersDbContext. The context
is promoted to singleton lifetime, so it serves stale data and throws on concurrent
use.
before: builder.Services.AddSingleton<IOrderCache, OrderCache>(); // ctor takes OrdersDbContext
after: OrderCache takes IDbContextFactory<OrdersDbContext> and creates one per call.
L44 blocking - http.GetStringAsync(...).Result blocks a request thread; under load this
starves the thread pool. Make the handler async and await it.
L82 important - TenantMiddleware takes the scoped IOrderRepository in its constructor.
Move it to a parameter of InvokeAsync.
src/Orders.Api/Orders.Api.csproj
✓ pass
```
Severities: `blocking` (wrong behaviour, data loss, crash, build break, removed API),
`important` (leaks, lifetime and thread-safety hazards, missing cancellation, silently
dropped configuration), `minor` (naming, redundant defaults, dead code). End with a one-line
verdict: ship, ship after blocking fixes, or rework.
## Environment
The .NET SDK, from `global.json` when the repository pins one. Commands used above:
```bash
dotnet --info # SDK and runtimes actually installed
dotnet restore # NuGet errors surface here, not in build
dotnet build --no-incremental -bl:b.binlog # clean build plus a binary log to read
dotnet msbuild -pp:pp.xml <project> # fully expanded project: who set which property
dotnet test # builds by default; add --no-build to reuse output
dotnet format --verify-no-changes # only where the repository already uses it
```
Diagnostics need the global tools, installed on demand and independent of the app:
```bash
dotnet tool install -g dotnet-counters # live runtime counters
dotnet tool install -g dotnet-trace # CPU and event traces
dotnet tool install -g dotnet-dump # dump capture and post-mortem analysis
```
MAUI additionally needs the platform workloads (`dotnet workload install maui`), and iOS and
Mac Catalyst targets additionally require macOS with Xcode.