.NET / ASP.NET Core Development
When to use
- Scaffolding or extending ASP.NET Core services (REST APIs, gRPC, Blazor, background workers)
- Designing the middleware pipeline, filters, or DI container in .NET 8+
- Mapping domain objects to the database via Entity Framework Core 8+
- Implementing authentication (JWT Bearer, cookie, OIDC) with ASP.NET Core Identity or custom schemes
- Adding health checks, OpenTelemetry, and structured logging with
ILogger - Optimising startup performance with NativeAOT or
PublishReadyToRun - Writing tests: xUnit, integration tests with
WebApplicationFactory, Testcontainers
Workflow
- Scaffold with
dotnet newtemplates —webapi(minimal API, .NET 8 default),mvc,worker,grpc. Use--use-minimal-apisflag for new REST services; MVC is preferred for apps with many controllers or complex filters. - Choose the project structure:
src/ MyApp.Api/ ← Entry point, middleware, endpoint definitions MyApp.Application/ ← Use cases, commands/queries (CQRS with MediatR optional) MyApp.Domain/ ← Entities, value objects, domain events MyApp.Infrastructure/ ← EF Core DbContext, migrations, external clients tests/ MyApp.UnitTests/ MyApp.IntegrationTests/ - Register services at startup in
Program.csusingbuilder.Services.Add*. Group registrations by feature using extension methods (services.AddOrderingFeature()). Never scatterAddSingletonacross the codebase. - Define the data contracts with C# records (immutable DTOs). Add data annotations or FluentValidation rules. Use
IValidator<T>from FluentValidation for complex rules; never validate in controllers. - Map the data model with EF Core code-first. Configure via
IEntityTypeConfiguration<T>— do not use data annotation attributes on domain entities. Migrations:dotnet ef migrations add, neverEnsureCreatedin production. - Secure the application:
- JWT Bearer: register
AddAuthentication().AddJwtBearer(...)and validateIssuer,Audience,SigningKeyfrom options. - Use
[Authorize]attribute orRequireAuthorization()on minimal API groups. - ASP.NET Core Identity for user management; use
UserManager<TUser>— never raw SQL for password operations. - CORS: explicit allowlist via
builder.Services.AddCors(options => options.AddPolicy(...))— neverAllowAnyOriginin production.
- JWT Bearer: register
- Add health checks:
builder.Services.AddHealthChecks()with database, upstream HTTP, and queue checks. Expose/health/readyand/health/liveseparately for Kubernetes probes. - Structured logging: use
ILogger<T>(injected); configure Serilog orMicrosoft.Extensions.Loggingwith JSON sink for production. NeverConsole.WriteLineorDebug.WriteLinein production code. - Enable OpenTelemetry via
OpenTelemetry.Extensions.Hosting— traces, metrics, and logs to OTLP endpoint. UseActivitySourcefor custom spans. - Native AOT considerations (if publishing AOT): avoid reflection, dynamic code gen,
XmlSerializer,BinaryFormatter. Use source generators (System.Text.Jsonsource gen). Rundotnet publish -r linux-x64 -p:PublishAot=trueto validate early. - Write tests:
- Unit: xUnit with
FluentAssertions; mock withNSubstituteorMoq. - Integration:
WebApplicationFactory<Program>with in-memory or Testcontainers Postgres for EF Core. - Avoid
[InlineData]for complex inputs — use[MemberData]or[ClassData]with typed inputs.
- Unit: xUnit with
- Audit against
.claude/checklists/security.mdand.claude/checklists/production.md.
Standards
Dependency Injection
- Only constructor injection. No
ServiceLocator, no staticIServiceProvideraccess. - Lifetime mismatches are bugs: never inject
ScopedintoSingleton. UseIServiceScopeFactoryin background services to create a scope per work item. - Register options with
builder.Services.Configure<MyOptions>(config.GetSection("MyOptions"))and injectIOptions<T>(singleton) orIOptionsSnapshot<T>(per-request) — neverIConfigurationdirectly in business logic. - Validate options at startup: call
.ValidateDataAnnotations().ValidateOnStart()afterConfigure<T>.
EF Core
AsNoTracking()on all read-only queries — significant performance gain for query-heavy endpoints.- Never
Include()navigation properties speculatively; load only what the use case needs. - For bulk operations use
ExecuteUpdateAsync()/ExecuteDeleteAsync()(EF Core 7+) — avoids loading entities into memory. - Migrations are immutable once applied to a shared environment — never edit, always add a new migration.
DbContextisScoped— never inject it into aSingletonservice.- Connection resiliency:
options.EnableRetryOnFailure()for SQL Server /Npgsql.EnableRetryOnFailure()for PostgreSQL.
Minimal APIs vs MVC
- Minimal APIs: prefer for microservices, small surface areas, AOT targets, or when OpenAPI customisation is via extension methods.
- MVC controllers: prefer for large APIs with many filters, complex model binding, or existing teams with MVC conventions.
- Do not mix both in the same project without a clear boundary.
Error handling
- Register
app.UseExceptionHandler("/error")orIProblemDetailsService(AddProblemDetails()) for RFC 7807 responses. - Return
TypedResults.Problem(...)from minimal API endpoints — not raw500strings. - Never return stack traces to clients; log them with correlation IDs.
Security
- Enable
UseHttpsRedirectionandUseHstsfor browser-facing apps. AntiForgerymiddleware for Blazor/MVC apps that use cookies.- Secrets via
dotnet user-secrets(local dev) and environment variables / Azure Key Vault / AWS Secrets Manager (all other environments) — never inappsettings.jsoncommitted to VCS. Content-Security-Policy,X-Frame-Options,X-Content-Type-Optionsheaders via middleware orNWebsec.
Do not
- Do not use
HttpContext.Request.Formwithout[FromForm]and anti-forgery validation in MVC. - Do not suppress nullable warnings with
!(null-forgiving operator) in production code; fix the nullability. - Do not block on
.Resultor.Wait()in async code — alwaysawait. - Do not use
EnsureCreated()ormigrate: truein production startup code. - Do not
new DbContext(options)manually — always inject from DI.
Common mistakes to avoid
| Mistake | Fix |
|---|---|
DbContext disposed before async result materialised |
Always await EF Core queries; never return IQueryable<T> from repository methods |
Scoped service in a hosted background IHostedService |
Use IServiceScopeFactory.CreateScope() per work iteration |
Missing CancellationToken propagation |
Accept and pass CancellationToken from controller action to service and down to EF Core / HttpClient calls |
appsettings.json with real secrets |
Use dotnet user-secrets add locally; env vars in CI/prod |
Integration tests sharing WebApplicationFactory across test classes |
Use IClassFixture<WebApplicationFactory<Program>> for sharing; reset DB state per test class |
| N+1 from lazy navigation loading (disabled in EF Core by default) | Use Include / ThenInclude or split queries (AsSplitQuery()) for collection includes |
| AOT failure at publish time | Run dotnet publish -p:PublishAot=true in CI; fix trim/reflection warnings before they accumulate |
Output format
- New service:
Program.cswith middleware pipeline,appsettings.jsonskeleton, and dependency registration extension methods. - Entity + migration:
IEntityTypeConfiguration<T>class and correspondingdotnet ef migrationsSQL preview. - Test class: xUnit with
WebApplicationFactory, FluentAssertions assertions, and Testcontainers setup. - Options class:
record MyOptionswith data annotations and startup validation.
Output artifacts go to docs/specs/ for design decisions; code files alongside source.
Related checklists
- .claude/checklists/security.md
- .claude/checklists/performance.md
- .claude/checklists/qa.md
- .claude/checklists/production.md
Related agents
- .claude/agents/core/solution-architect.md
- .claude/agents/engineering/backend-engineer.md
- .claude/agents/engineering/database-architect.md
- .claude/agents/quality/security-auditor.md