---
name: ".NET"
description: "Always-on rules for writing C# and ASP.NET Core services, project files and configuration."
applyTo: "**/*.cs,**/*.csproj,**/appsettings*.json"
---

# .NET rules

Target the current LTS unless `global.json` or `<TargetFramework>` pins otherwise; check before using newer APIs.

## Endpoints and structure

- Keep endpoints thin: bind, validate, call one service or handler, map the result. No EF queries or business rules in `Program.cs` or controllers.
- For new APIs, prefer minimal APIs with `MapGroup` per feature and `TypedResults`; stay with controllers where the codebase already uses them.
- Return errors as `ProblemDetails` (`AddProblemDetails()`, `Results.Problem`); never return exception messages or stack traces to clients.
- Validate every request DTO at the boundary (FluentValidation or data annotations) and reject unknown or over-long input.
- Put `RequireAuthorization()` or `[Authorize]` with a named policy on every endpoint; add `AllowAnonymous` explicitly and rarely. Check resource ownership, not just authentication, to prevent IDOR.

## Dependency injection and configuration

- Match lifetimes: `DbContext` is scoped; never inject scoped services into singletons. Use `IServiceScopeFactory` in background services.
- Use constructor injection; never call `IServiceProvider.GetService` in business code.
- Bind configuration to typed options with `.ValidateDataAnnotations().ValidateOnStart()`. Bad config fails at startup, not in production traffic.
- Never put secrets in `appsettings*.json` or source. Use User Secrets locally and Azure Key Vault or pipeline secret variables in deployed environments; prefer managed identity over passwords.
- Use `appsettings.{Environment}.json` only for non-secret differences.

## Async and resilience

- Go async all the way; never `.Result`, `.Wait()` or `.GetAwaiter().GetResult()` on request paths. It causes thread-pool starvation.
- Accept a `CancellationToken` and pass it to every I/O call (EF Core, HttpClient, streams).
- No `async void` except event handlers; no fire-and-forget tasks from requests. Use a queue or `BackgroundService`.
- Create HTTP clients through `IHttpClientFactory` (typed clients) with timeouts and the standard resilience handler; never `new HttpClient()` per call.

## Data access

- Use `AsNoTracking()` and project to DTOs with `Select` for reads; do not return entities from APIs.
- Avoid N+1 queries: load related data with `Include` or projection, not queries in a loop.
- Never build SQL by string concatenation; use LINQ, parameters or `FromSql` with interpolation (parameterized).
- Apply schema changes through reviewed EF migrations using expand/contract; never `EnsureCreated` or auto-migrate in production.
- Use `decimal` for money and UTC `DateTimeOffset` for time.

## Code quality

- Enable `<Nullable>enable</Nullable>` and treat nullable warnings as errors; do not silence them with `!`.
- Use structured logging with message templates (`logger.LogInformation("Order {OrderId} created", id)`), not string interpolation; never log tokens, passwords or PII.
- Catch specific exceptions only; no empty `catch` blocks.
- Keep package versions central (`Directory.Packages.props`) and do not add packages without a clear need; watch `dotnet list package --vulnerable`.
- Cover new behaviour with xUnit tests; use `WebApplicationFactory` for endpoint tests and Testcontainers over the EF in-memory provider.

Go deeper: for larger tasks use the dotnet-development, dotnet-architecture, dotnet-testing, dotnet-performance and api-design skills, and backend-code-review before opening a pull request.
