---
name: dotnet-development
description: Write production-grade C# and ASP.NET Core services on modern .NET — minimal APIs vs controllers, DI lifetimes, options and secrets, correct async/await with cancellation, nullable reference types, validation, ProblemDetails errors, EF Core usage, HttpClientFactory with resilience, structured logging, JWT and policy auth, background services and OpenAPI. Use this whenever the user asks to write, refactor, debug or explain C# back-end code; edits Program.cs, a *.csproj, appsettings.json, a DbContext, entity configurations, migrations, controllers, endpoint mappings, middleware, hosted services or HttpClient registrations; hits a deadlock, ObjectDisposedException, "cannot consume scoped service from singleton", null warnings, or asks "how do I do X in ASP.NET Core". Also apply it when reviewing C#/.NET pull requests, even if the user only asks about one method.
metadata:
  technology: .NET
  type: development
---

# .NET Development

Write every service as if it will run under load, behind a load balancer, with requests cancelled mid-flight and configuration injected by the platform. Target the **current LTS** release (.NET 8 and .NET 10 are LTS; odd-numbered releases such as .NET 9 are STS with a shorter support window) unless the project's `global.json` or `<TargetFramework>` pins otherwise — check before using newer APIs.

## 1. Decide the endpoint style

| Situation | Default | Change when |
|---|---|---|
| New HTTP API, small-to-medium surface | **Minimal APIs** with `MapGroup` per feature, `TypedResults` | The team already has a large controller codebase — stay consistent |
| Heavy use of filters, model binding conventions, OData | **Controllers** (`[ApiController]`) | — |
| Native AOT / trimming required | Minimal APIs (MVC controllers are not AOT-compatible) | — |
| Server-rendered UI | Razor Pages / Blazor | Not covered here |

Keep endpoints thin: bind, validate, call one handler/service, map the result. No EF queries or business rules inline in `Program.cs`.

```csharp
var orders = app.MapGroup("/orders").WithTags("Orders").RequireAuthorization();
orders.MapGet("/{id:guid}", async Task<Results<Ok<OrderDto>, NotFound>> (
    Guid id, IOrderQueries queries, CancellationToken ct) =>
    await queries.GetAsync(id, ct) is { } dto ? TypedResults.Ok(dto) : TypedResults.NotFound());
```

## 2. Dependency injection lifetimes

| Lifetime | Use for | Pitfall |
|---|---|---|
| Singleton | Stateless, thread-safe services, caches, compiled lookups | **Captive dependency**: a singleton holding a scoped `DbContext` (shared across requests, not thread-safe) or a transient typed `HttpClient` (never rotates handlers) |
| Scoped | `DbContext`, unit of work, per-request context | Resolving outside a scope (background services) throws or leaks |
| Transient | Lightweight, stateless helpers | Disposable transients resolved from the root container are held until shutdown |

- Enable `ValidateScopes` and `ValidateOnBuild` (on by default in Development) so captive dependencies fail at startup.
- In singletons/background services, create a scope: `using var scope = scopeFactory.CreateScope();`.
- Prefer constructor injection (primary constructors are fine); never `IServiceProvider.GetService` inside business code (service locator). Keyed services (.NET 8+) instead of hand-rolled factories when you need named variants.

## 3. Configuration, options and secrets

- Bind sections to typed options and validate at startup: `services.AddOptions<PaymentOptions>().BindConfiguration("Payment").ValidateDataAnnotations().ValidateOnStart();`
- `IOptions<T>` for static config, `IOptionsSnapshot<T>` (scoped) per request, `IOptionsMonitor<T>` in singletons that must see reloads.
- Secrets never in `appsettings.json` or source: User Secrets locally, environment variables or a vault/secret-store configuration provider in deployed environments. Prefer managed/workload identity over connection-string passwords.
- `appsettings.{Environment}.json` for non-secret differences only.

## 4. Async correctness

- Async all the way down. **Never** `.Result`, `.Wait()` or `.GetAwaiter().GetResult()` on request paths — it blocks thread-pool threads and causes starvation under load.
- Accept and pass `CancellationToken` through every I/O call (EF, HttpClient, streams); endpoints get `HttpContext.RequestAborted` bound automatically.
- ASP.NET Core has no `SynchronizationContext`, so `ConfigureAwait(false)` is unnecessary in app code; **do** use it in reusable libraries that may run in UI/legacy hosts.
- No `async void` except event handlers. Don't fire-and-forget `Task`s from a request — hand work to a queue/background service.
- `ValueTask` only for hot paths that usually complete synchronously; never await a `ValueTask` twice.

## 5. Language features that prevent bugs

- `<Nullable>enable</Nullable>` and `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` (at least for nullable warnings) in `Directory.Build.props`. Don't silence with `!` — fix the flow or model the absence.
- `record` / `record struct` for DTOs, commands and value objects (value equality, immutability); `required` and `init` for mandatory properties.
- Pattern matching and switch expressions for state/type dispatch; exhaustive switches with a `_ => throw new UnreachableException()` arm.
- Use `sealed` by default on classes not designed for inheritance.

## 6. Validation and errors

- Validate at the edge: FluentValidation (explicit validator per request type), DataAnnotations with `[ApiController]`, or the built-in minimal API validation in newer releases — pick one per solution.
- Invariants that must always hold belong in the domain type (constructor/factory), not only in validators.
- Return **RFC 9457 ProblemDetails** for every error: `builder.Services.AddProblemDetails();` + `app.UseExceptionHandler();` + `app.UseStatusCodePages();`. Map known exceptions with an `IExceptionHandler` (.NET 8+).
- Never leak stack traces or exception messages to clients outside Development; log them with a correlation/trace id and return that id in the problem.

## 7. EF Core

| Concern | Rule |
|---|---|
| Lifetime | `AddDbContext` (scoped) or `AddDbContextPool`; `IDbContextFactory<T>` for background/Blazor/parallel work. Never share a context across threads |
| Reads | `AsNoTracking()` + projection with `Select` to a DTO; tracking only when you will modify |
| N+1 | No lazy loading in APIs; `Include` or projection; watch the logged SQL count |
| Bulk changes | `ExecuteUpdateAsync` / `ExecuteDeleteAsync` (EF Core 7+) instead of load-modify-save loops |
| Raw SQL | `FromSql($"... WHERE Id = {id}")` / `SqlQuery<T>` — interpolation is parameterised. `FromSqlRaw` only with explicit parameters, never concatenation |
| Migrations | One migration per change, reviewed SQL (`dotnet ef migrations script --idempotent`), applied by a pipeline step or migration bundle, not `Database.Migrate()` at app start in multi-instance deployments |
| Concurrency | Row version / concurrency token on aggregates edited concurrently; handle `DbUpdateConcurrencyException` |

## 8. Outbound HTTP and resilience

- Always `IHttpClientFactory` — typed clients (`services.AddHttpClient<PaymentsClient>(...)`). Never `new HttpClient()` per call (socket exhaustion) nor one static client without `PooledConnectionLifetime` (stale DNS).
- Add resilience with `Microsoft.Extensions.Http.Resilience` (built on Polly v8): `.AddStandardResilienceHandler()` gives timeout, retry with jitter, circuit breaker. Retry only idempotent operations or those carrying an idempotency key.
- Set explicit timeouts; propagate the `CancellationToken`.

## 9. Logging

- `ILogger<T>` with **message templates**, not interpolation: `logger.LogInformation("Order {OrderId} placed for {CustomerId}", order.Id, customerId);`
- For hot paths use the `[LoggerMessage]` source generator (no allocations when disabled).
- Never log secrets, tokens, full request bodies or PII; use scopes for correlation. Export via OpenTelemetry rather than vendor SDKs in business code.

## 10. Authentication and authorisation

- JWT bearer: `AddAuthentication().AddJwtBearer(...)` validating issuer, audience, lifetime and signing keys (use the authority's metadata; never disable validation to "make it work").
- Authorise with **policies**, not role strings scattered in code: `AddAuthorizationBuilder().AddPolicy("orders:write", p => p.RequireClaim("scope", "orders.write"));`
- Resource-based checks (does this user own order X?) via `IAuthorizationService` or an explicit check in the handler — endpoint attributes alone don't cover it.
- Fallback policy requiring authenticated users; opt out explicitly with `AllowAnonymous`.

## 11. Background work

- `BackgroundService` for long-running loops; honour `stoppingToken`; wrap each iteration in try/catch + logging (an unhandled exception stops the host by default since .NET 6).
- In-process queue via `System.Threading.Channels` for short, loss-tolerant work; a durable broker or job library for anything that must survive restarts.
- Create a DI scope per unit of work; make handlers idempotent.

## 12. OpenAPI

- .NET 9+: built-in `builder.Services.AddOpenApi()` / `app.MapOpenApi()`; earlier: Swashbuckle or NSwag. Describe responses via `TypedResults`/`Produces<T>`; commit the generated document or diff it in CI to catch breaking changes.

## Deliverable format

When writing or changing code, return:
1. **Files changed** with full code for new files and focused diffs for edits (`Program.cs` registrations included).
2. **Why** — one line per non-obvious decision (lifetime, tracking mode, resilience policy).
3. **Migrations/config** — new migration name, new options keys, secrets the operator must provide.
4. **Tests** added or needed (unit + integration) and how to run them (`dotnet test`).

## Anti-patterns to reject

- `.Result` / `.Wait()` / `async void`; missing `CancellationToken` on I/O.
- Scoped services (especially `DbContext`) captured by singletons or static fields.
- `new HttpClient()` per request; retries on non-idempotent POSTs without an idempotency key.
- Secrets in `appsettings.json`, source or logs; disabled token validation.
- Business logic and EF queries inline in endpoints or controllers.
- Lazy loading in APIs, tracking queries for read-only data, string-concatenated SQL.
- Swallowed exceptions, `catch (Exception) { return null; }`, custom error shapes instead of ProblemDetails.
- Interpolated log messages; `!` to silence nullable warnings.
