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:

terminal
$ 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 idSeverityCatches
csharp/async-voidBLOCKER`async void` anywhere except event handlers — exceptions escape the caller and crash the process; return `Task`.
csharp/sync-over-asyncBLOCKERSync-over-async: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` on async calls — deadlock and thread-pool starvation; async all the way.
csharp/httpclient-per-requestBLOCKER`HttpClient` instantiated per request — socket exhaustion; use `IHttpClientFactory`.
csharp/captive-dependenciesBLOCKERCaptive dependencies: scoped services (DbContext) injected into singletons — cross-request state bleed; inject `IServiceScopeFactory` or restructure.
csharp/sql-string-concatenationBLOCKERString-concatenated SQL with external input — parameterized queries / EF LINQ only.
csharp/swallowed-exceptionsBLOCKERSwallowed exceptions: `catch { }` or catch-log-continue treating failure as success.
csharp/missing-cancellation-tokenBLOCKERMissing `CancellationToken` propagation on new async endpoints and outbound calls.
csharp/ef-n-plus-oneHIGHEF Core N+1: navigation properties accessed in loops — `.Include`/projection; and unbounded queries without paging.
csharp/multiple-enumerationHIGH`IEnumerable` multiple enumeration of LINQ-to-entities queries — materialize once with `ToListAsync`.
csharp/datetime-nowHIGHDateTime.Now in new code — `DateTimeOffset.UtcNow` / `TimeProvider`.
csharp/entity-in-controller-responseHIGHEntities returned from controllers — DTOs/records only; entities leak lazy-nav and internal fields.
csharp/missing-model-validationHIGHMissing model validation at the boundary (`[ApiController]` + data annotations or FluentValidation).
csharp/lock-over-asyncHIGH`lock` over async operations (can't await inside lock) — `SemaphoreSlim`.
csharp/fire-and-forget-task-runHIGHFire-and-forget `Task.Run` without exception observation — use hosted services / background queues.
csharp/exception-detail-in-responseHIGHException details in API responses — ProblemDetails mapping.
csharp/records-for-dtosSUGGESTIONRecords for immutable DTOs; `required`/init-only properties over constructors with many args.
csharp/nullable-reference-typesSUGGESTIONNullable reference types enabled and respected in new projects — no `!` dammit-operator without comment.
csharp/structured-loggingSUGGESTION`ILogger<T>` structured logging with message templates, not string interpolation.
csharp/api-style-consistencySUGGESTIONMinimal 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:

a finding from this file
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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

standards/stacks/csharp.md · 30 lines · 2.6 KB
# 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.