C# (.NET)
.NET services.
What this is
.NET services. Async correctness is the dominant theme in the BLOCKER tier — async void outside event handlers, sync-over-async blocking that deadlocks the thread pool, missing cancellation tokens, HttpClient instantiated per request instead of via IHttpClientFactory. The HIGH tier is mostly EF Core and LINQ pitfalls: N+1 navigation property access, multiple enumeration of the same query, entities leaking out of controllers instead of DTOs.
How to onboard it
A repository picks up this stack by onboarding on a profile that includes it. redline init detects the profile from what is in the repository, so in most cases this is automatic:
$ npx redlinegate init # detects the profile $ npx redlinegate init --profile service-dotnet # or name one
One profile pulls these rules in: service-dotnet.
Once onboarded, your files match this stack when they fit any of these globs:
**/*.cs
How to use it — 19 rules
Nothing to run. Once your profile includes C# (.NET), redline init renders these rules into your repository's AI tooling and the reviewer applies them on every pull request. When a review comment cites one of these ids, this table is where to look up what it catches and why.
| Rule id | Severity | Catches |
|---|---|---|
csharp/async-void | BLOCKER | `async void` anywhere except event handlers — exceptions escape the caller and crash the process; return `Task`. |
csharp/sync-over-async | BLOCKER | Sync-over-async: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` on async calls — deadlock and thread-pool starvation; async all the way. |
csharp/httpclient-per-request | BLOCKER | `HttpClient` instantiated per request — socket exhaustion; use `IHttpClientFactory`. |
csharp/captive-dependencies | BLOCKER | Captive dependencies: scoped services (DbContext) injected into singletons — cross-request state bleed; inject `IServiceScopeFactory` or restructure. |
csharp/sql-string-concatenation | BLOCKER | String-concatenated SQL with external input — parameterized queries / EF LINQ only. |
csharp/swallowed-exceptions | BLOCKER | Swallowed exceptions: `catch { }` or catch-log-continue treating failure as success. |
csharp/missing-cancellation-token | BLOCKER | Missing `CancellationToken` propagation on new async endpoints and outbound calls. |
csharp/ef-n-plus-one | HIGH | EF Core N+1: navigation properties accessed in loops — `.Include`/projection; and unbounded queries without paging. |
csharp/multiple-enumeration | HIGH | `IEnumerable` multiple enumeration of LINQ-to-entities queries — materialize once with `ToListAsync`. |
csharp/datetime-now | HIGH | DateTime.Now in new code — `DateTimeOffset.UtcNow` / `TimeProvider`. |
csharp/entity-in-controller-response | HIGH | Entities returned from controllers — DTOs/records only; entities leak lazy-nav and internal fields. |
csharp/missing-model-validation | HIGH | Missing model validation at the boundary (`[ApiController]` + data annotations or FluentValidation). |
csharp/lock-over-async | HIGH | `lock` over async operations (can't await inside lock) — `SemaphoreSlim`. |
csharp/fire-and-forget-task-run | HIGH | Fire-and-forget `Task.Run` without exception observation — use hosted services / background queues. |
csharp/exception-detail-in-response | HIGH | Exception details in API responses — ProblemDetails mapping. |
csharp/records-for-dtos | SUGGESTION | Records for immutable DTOs; `required`/init-only properties over constructors with many args. |
csharp/nullable-reference-types | SUGGESTION | Nullable reference types enabled and respected in new projects — no `!` dammit-operator without comment. |
csharp/structured-logging | SUGGESTION | `ILogger<T>` structured logging with message templates, not string interpolation. |
csharp/api-style-consistency | SUGGESTION | Minimal APIs vs controllers — follow whichever the service already uses. |
Expected output
A finding from this file, and every finding Redline produces, opens with a machine-readable first line — severity, then the rule id in brackets, then the problem in one line:
Redline/BLOCKER [csharp/async-void]: <one-line problem>Ids are aggregated per rule, which is how the organisation finds out which rules earn their place and which only generate noise — so a finding without a valid id cannot be measured and counts as untagged. Of the 19 rules here, 7 BLOCKER, 8 HIGH and 4 SUGGESTION. Only a BLOCKER must not merge; a SUGGESTION may be dismissed without justification, and is never upgraded to get attention.
If a rule here fires constantly on code your team has deliberately decided to allow, that is the signal to raise with the standards owner — the rule id is what makes that conversation measurable — not to argue it away comment by comment.
How to edit it
Rules are edited in standards/stacks/csharp.md and nowhere else. The rendered copies in AGENTS.md, .github/copilot-instructions.md and .github/instructions/ are generated and are overwritten by the next render. A change here propagates to every onboarded repository as a pull request, so treat it as a production change.
- Edit the markdownstandards/ is the only place a human edits a rule. Everything under AGENTS.md, .github/copilot-instructions.md and .github/instructions/ is rendered from it and is overwritten by the next render.
- node scripts/assign-rule-ids.mjsAssigns a permanent <stack>/<slug> id to any new rule bullet and rewrites the file in place. Do not invent an id by hand. CI runs the same script with --check and fails if a rule is missing one.
- node scripts/render-self.mjsRe-renders this repository's own artifacts from the edited source. CI runs it with --check, so stale checked-in output fails the build.
- Bump standards/manifest.json → versionRequired in the same pull request as the rule change. Sync pull requests quote the version, so a repository's rendered artifacts always name where they came from.
- Add a CHANGELOG.md entryAlso in the same pull request. A standards change with no measurement is an opinion — record the seed score alongside it.
- node scripts/validate.mjsThe bundle self-check CI runs: manifest integrity, well-formed rule ids, the severity output contract surviving your edit, glob portability.
Rule ids are permanent. Rewording a rule is fine and keeps its id; renaming or removing an id orphans every historical telemetry record that cited it.
The full file
# C# (.NET) Review Rules
## BLOCKER — request changes
- `csharp/async-void` — **`async void`** anywhere except event handlers — exceptions escape the caller and crash the process; return `Task`.
- `csharp/sync-over-async` — **Sync-over-async**: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` on async calls — deadlock and thread-pool starvation; async all the way.
- `csharp/httpclient-per-request` — **`HttpClient` instantiated per request** — socket exhaustion; use `IHttpClientFactory`.
- `csharp/captive-dependencies` — **Captive dependencies**: scoped services (DbContext) injected into singletons — cross-request state bleed; inject `IServiceScopeFactory` or restructure.
- `csharp/sql-string-concatenation` — **String-concatenated SQL with external input** — parameterized queries / EF LINQ only.
- `csharp/swallowed-exceptions` — **Swallowed exceptions**: `catch { }` or catch-log-continue treating failure as success.
- `csharp/missing-cancellation-token` — **Missing `CancellationToken` propagation** on new async endpoints and outbound calls.
## HIGH
- `csharp/ef-n-plus-one` — EF Core N+1: navigation properties accessed in loops — `.Include`/projection; and unbounded queries without paging.
- `csharp/multiple-enumeration` — `IEnumerable` multiple enumeration of LINQ-to-entities queries — materialize once with `ToListAsync`.
- `csharp/datetime-now` — DateTime.Now in new code — `DateTimeOffset.UtcNow` / `TimeProvider`.
- `csharp/entity-in-controller-response` — Entities returned from controllers — DTOs/records only; entities leak lazy-nav and internal fields.
- `csharp/missing-model-validation` — Missing model validation at the boundary (`[ApiController]` + data annotations or FluentValidation).
- `csharp/lock-over-async` — `lock` over async operations (can't await inside lock) — `SemaphoreSlim`.
- `csharp/fire-and-forget-task-run` — Fire-and-forget `Task.Run` without exception observation — use hosted services / background queues.
- `csharp/exception-detail-in-response` — Exception details in API responses — ProblemDetails mapping.
## SUGGESTION
- `csharp/records-for-dtos` — Records for immutable DTOs; `required`/init-only properties over constructors with many args.
- `csharp/nullable-reference-types` — Nullable reference types enabled and respected in new projects — no `!` dammit-operator without comment.
- `csharp/structured-logging` — `ILogger<T>` structured logging with message templates, not string interpolation.
- `csharp/api-style-consistency` — Minimal APIs vs controllers — follow whichever the service already uses.