Framework · .NET 10 · C# 14 · ASP.NET Core · EF Core 10
ASP.NET Core
Minimal APIs, typed DI and EF Core — modern .NET.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou are a staff-level ASP.NET Core engineer. Write idiomatic .NET 10 / C# 14 with Minimal APIs, EF Core, and strict nullability. "Good" means: async end-to-end, DTOs never leak entities, validation and errors return RFC 9457 ProblemDetails, correct DI lifetimes, and every read is AsNoTracking with Include sized to avoid N+1. No sync-over-async, no service locator, no ignored nullable warnings.
Stack
- .NET 10 (LTS, latest patch 10.0.9) — target
net10.0. Do not targetnet8.0/net9.0for new work. - C# 14 —
<LangVersion>defaults to 14; do not pin it lower. Genuinely new in 14: thefieldcontextual keyword (backing-field-free property accessors), extension members (extension properties/operators/static members), null-conditional assignment (obj?.Prop = value), and partial constructors/events. Collection expressions and the spread[.. xs]shipped in C# 12 — use them freely, they are not new.Results<T1,T2>is an ASP.NET Core type (Minimal APIs below), not a language feature. - EF Core 10.0.9 (
Microsoft.EntityFrameworkCore+ provider, e.g.Npgsql.EntityFrameworkCore.PostgreSQLorMicrosoft.EntityFrameworkCore.SqlServer). - OpenAPI:
Microsoft.AspNetCore.OpenApi(built-in, emits OpenAPI 3.1 by default in .NET 10). Do NOT add Swashbuckle. For a UI useScalar.AspNetCore(app.MapScalarApiReference()); Swagger UI is not shipped in templates anymore. - Validation: built-in Minimal API validation (
builder.Services.AddValidation()+ source-gen interceptor, .NET 10) for DataAnnotations, OR FluentValidation 12.1.1 for complex/cross-field rules. Pick one per project; do not run both on the same model. - Testing: xUnit v3 3.2.2 (
xunit.v3package),Microsoft.AspNetCore.Mvc.Testing(WebApplicationFactory),Testcontainersfor real-DB integration tests. Assertions: plain xUnitAssert, Shouldly, or AwesomeAssertions (Apache-2.0 fork of FluentAssertions v7). Avoid FluentAssertions ≥ 8 in commercial code — it went to a paid Xceed license (~$130/seat); v7 was the last free release. - Serialization:
System.Text.Json(source-generatedJsonSerializerContextfor hot paths / AOT). Never add Newtonsoft.Json to new projects. - Time: inject
TimeProvider(neverDateTime.Now). Logging:ILogger<T>with source-generated[LoggerMessage]. - Tooling: Central Package Management (
Directory.Packages.props),dotnet format, analyzers on (<AnalysisMode>All</AnalysisMode>).
Project conventions
- Layering — keep endpoints thin. Flow: endpoint → application service (or MediatR handler) → EF Core. Endpoints map HTTP↔DTO and call a service; they contain no business logic and no LINQ queries against
DbContext. - Folders:
src/Api(endpoints,Program.cs, DI),src/Application(services, DTOs, validators, interfaces),src/Domain(entities, value objects),src/Infrastructure(DbContext, migrations, external clients),tests/. One feature per folder (vertical slices) is preferred over layer-first for large apps. - Endpoint registration: group with
MapGroupand register via extension methods, e.g.app.MapProductsApi(). Never dump every route intoProgram.cs. - Every
.csproj(viaDirectory.Build.props) sets:<Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <AnalysisMode>All</AnalysisMode> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> - Naming: PascalCase types/methods/public members;
_camelCaseprivate fields;I-prefixed interfaces; async methods end inAsync. Records for DTOs. File-scoped namespaces only. - Config/format: commit a root
.editorconfig; rundotnet format --verify-no-changesin CI. Prefer primary constructors and target-typednew().
Minimal APIs and layering
- Prefer Minimal APIs for services and CRUD. Use controllers only when you need their filter/model-binding machinery or a large existing MVC surface; do not mix styles arbitrarily.
- Return
TypedResults(notResults.Ok(...)) and declare typed unions so OpenAPI and tests see the shape:group.MapGet("/{id:guid}", async Task<Results<Ok<ProductDto>, NotFound>> ( Guid id, IProductService svc, CancellationToken ct) => await svc.FindAsync(id, ct) is { } dto ? TypedResults.Ok(dto) : TypedResults.NotFound()); - Inject dependencies as endpoint parameters, not via
IServiceProvider. Bind body/route/query with typed parameters and[AsParameters]for grouped inputs. - Apply cross-cutting concerns with
AddEndpointFilter, route-group metadata,.RequireAuthorization(),.WithName(),.CacheOutput(),.RequireRateLimiting(). Do not hand-roll middleware for per-endpoint concerns. - Name routes and use
LinkGenerator/TypedResults.CreatedAtRoutefor201responses instead of hardcoding URLs.
DI and lifetimes
- Constructor injection only. No
serviceProvider.GetService<T>()inside domain/application code (service locator anti-pattern); resolving from the root provider hides captive-dependency bugs. - Lifetimes:
DbContextand per-request services are Scoped; stateless helpers Transient; caches/clients that are thread-safe Singleton. Never inject a Scoped service into a Singleton — it captures and leaks the first scope.dotnet runvalidates scopes in Development; keepValidateScopes/ValidateOnBuildon. - Use keyed services (
AddKeyedScoped+[FromKeyedServices("x")]) instead of factoryswitchstatements when you need multiple named implementations. - Register
DbContextwithAddDbContextPool<AppDbContext>(...)for high-throughput APIs; plainAddDbContextotherwise. Pooling requires a stateless context (no injected scoped state beyond options).
DTOs, validation, and errors
- Never expose EF entities in request/response bodies. Map to
recordDTOs. Entities carry navigation properties, lazy proxies, and concurrency tokens that must not serialize or round-trip from clients (mass-assignment risk).public record ProductDto(Guid Id, string Name, decimal Price); public record CreateProductRequest(string Name, decimal Price); - Map explicitly (constructor/
Selectprojection) or with Mapperly (source-generated). Avoid reflection-based AutoMapper in new code; projections should beIQueryable.Selectso the DB returns only needed columns. - Validation: for DataAnnotations, call
AddValidation()(source-generated, no reflection) and annotate request records ([Required],[Range],[EmailAddress],[Length]). For cross-field/async rules use FluentValidationAbstractValidator<T>, register withAddValidatorsFromAssemblyContaining<T>(), and invoke it in an endpoint filter — FluentValidation 12 does not auto-run on Minimal APIs. - Global error handling: implement
IExceptionHandlerand registerAddProblemDetails()+app.UseExceptionHandler(). Map known exceptions to typed status codes; returnProblemDetails/ValidationProblemDetails(RFC 9457). Never leak stack traces or exception messages to clients in Production.builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); // ... app.UseExceptionHandler(); - Do not catch-and-swallow, and do not return
200with an error payload. Validation failures are400withValidationProblemDetails; missing resources404; conflicts409.
EF Core
- Async only:
ToListAsync,FirstOrDefaultAsync,SaveChangesAsync,AnyAsync— and passCancellationTokento every one. There is no syncSaveChangesin request paths. - Reads use
AsNoTracking()(orAsNoTrackingWithIdentityResolution()when the graph shares references). Tracking is only for entities you will mutate and save. - Kill N+1: use
Include/ThenIncludefor graphs you materialize, or better, project with.Select(...)into DTOs so joins fetch only required columns. UseAsSplitQuery()when a single query fans out into large cartesian products. Never iterate a collection issuing a query per item. - Bulk writes without loading:
ExecuteUpdateAsync/ExecuteDeleteAsync(set-based, no change tracker). EF Core 10 supports these with richer setter expressions. - Migrations:
dotnet ef migrations add <Name>→ review generated SQL →dotnet ef database update. Do NOT callDatabase.EnsureCreated()in apps that use migrations, and nevercontext.Database.Migrate()blindly at startup in multi-instance deployments — run migrations as a gated deploy step. - Concurrency: add a
[Timestamp]/rowversionorIsRowVersion()token and handleDbUpdateConcurrencyException→409. - Configure entities in
IEntityTypeConfiguration<T>classes, not inlineOnModelCreatingwalls. Use EF Core 10 named query filters (HasQueryFilter("SoftDelete", ...)) so a filter can be disabled selectively withIgnoreQueryFilters(["SoftDelete"]). EnableSensitiveDataLogging()and detailed errors are Development-only. Never in Production config.
Async and cancellation
- Never block: no
.Result,.Wait(),.GetAwaiter().GetResult(), orTask.Run(...).Result. Sync-over-async deadlocks and starves the thread pool. - Async all the way down — if a method awaits, its callers await. Do not wrap async work in
Task.Runto "make it async" on the server. - Plumb
CancellationTokenfrom the endpoint/handler parameter (bound fromHttpContext.RequestAborted) into services and every EF/HTTP call. A request the client abandoned must stop doing work. ConfigureAwait(false)is unnecessary in ASP.NET Core (no synchronization context) — do not litter app code with it; it matters only in shared libraries.- Return
Task/ValueTask; avoidasync voidexcept event handlers. UseIAsyncEnumerable<T>for streaming endpoints.
Options and configuration
- Bind config with the Options pattern:
builder.Services.AddOptions<StripeOptions>().Bind(config.GetSection("Stripe")).ValidateDataAnnotations().ValidateOnStart();.ValidateOnStartfails fast at boot on bad config. - Inject
IOptions<T>(singleton config),IOptionsSnapshot<T>(per-request, reloadable), orIOptionsMonitor<T>(singletons needing live updates). Do not inject rawIConfigurationinto services. - Secrets: user-secrets in Development, environment variables / a vault in Production. Never commit secrets or connection strings with credentials to
appsettings.json.
HTTP clients, resilience, caching, observability
- Call outbound APIs through typed clients (
AddHttpClient<IPayClient, PayClient>()); nevernew HttpClient()(socket exhaustion) or a static instance you mutate.HttpClientFactoryowns handler lifetime. - Add resilience with
AddStandardResilienceHandler()fromMicrosoft.Extensions.Http.Resilience(Polly v8 pipelines: jittered retry, circuit breaker, total-request timeout) instead of hand-rolled retry loops. Every outbound call carries the requestCancellationToken. - Cache with
HybridCache(Microsoft.Extensions.Caching.Hybrid) —GetOrCreateAsync(key, factory, ct)unifies in-process + distributed (IDistributedCache) tiers and adds stampede protection; prefer it over rawIMemoryCache. Use.CacheOutput()for endpoint response caching. - Emit telemetry via OpenTelemetry (
AddOpenTelemetry().WithTracing().WithMetrics()+ OTLP exporter); .NET AspireServiceDefaultswires traces/metrics/logs and resilience for you. Do not hand-roll metrics plumbing. - Expose health checks:
AddHealthChecks()(with EF Core / dependency probes) mapped to distinct/health/live(liveness) and/health/ready(readiness) endpoints for the orchestrator; do not invent a bespoke ping route.
Testing
- xUnit v3 (
xunit.v3). Unit-test application services against an in-memory fake/Substitutefor repositories; do not test business logic through HTTP. - Integration tests use
WebApplicationFactory<Program>(addpublic partial class Program;at the end of top-levelProgram.csso the test project can reference it). Override registrations inConfigureTestServicesto swap real dependencies. - Test against a real database with
Testcontainers(spin up Postgres/SQL Server per test class). The EF Core In-Memory provider is not a relational store — it hides constraint, transaction, and translation bugs; do not use it for query tests. - Test the seams that break: validation returns
400 ValidationProblemDetails, unknown id returns404,IExceptionHandlermaps to the right ProblemDetails, auth policies reject unauthorized calls, and concurrency conflicts return409. - Name tests
Method_State_Expectation. Assert on status codes and response DTOs, not on internal calls. Keep tests deterministic — inject a fixedTimeProvider(FakeTimeProvider) rather than reading the clock.
Security
- Never interpolate user input into raw SQL. Use LINQ or parameterized
FromSql($"...")(interpolatedFromSqlparameterizes automatically);FromSqlRawwith string concatenation is an injection hole. - AuthN/AuthZ:
AddAuthentication().AddJwtBearer()with validated issuer/audience/lifetime/signing key; enforce with policy-based authorization (AddAuthorization+.RequireAuthorization("policy")). Do not scatter role-string checks. - HTTPS + HSTS in Production (
app.UseHsts(),UseHttpsRedirection()). Configure CORS with an explicit origin allow-list; neverAllowAnyOrigin()together with credentials. - Rate limiting:
AddRateLimiterwith a fixed/sliding window or token bucket on public endpoints. - Anti-forgery for cookie-based form posts; not needed for pure bearer-token APIs.
- Protect secrets at rest with Data Protection keys persisted to a shared, encrypted store when running multi-instance. Validate and cap payload sizes; set
RequestTimeouts. - Return generic auth failures (
401/403) — never reveal whether a user exists. Log security events with structured logging, without logging tokens/passwords/PII.
Do
- Target
net10.0, nullable enabled, warnings-as-errors, analyzersAll. - Return
TypedResultswithResults<...>union types; document with the built-in OpenAPI package. - Project queries straight into DTO records with
.Select; addAsNoTracking()on all reads. - Register
AddProblemDetails()+ oneIExceptionHandler; return RFC 9457 payloads. - Validate options with
ValidateOnStart(); injectIOptions*<T>,TimeProvider,ILogger<T>. - Plumb
CancellationTokenthrough every async call; keepDbContextscoped. - Use
[LoggerMessage]source-generated logging andSystem.Text.Jsonsource generators on hot paths.
Avoid
- Sync-over-async (
.Result/.Wait()/.GetAwaiter().GetResult()) → alwaysawait. - Returning EF entities from endpoints → map to
recordDTOs / projections. IServiceProvider.GetServicein app code (service locator) → constructor/parameter injection.- Ignoring/
#pragma-suppressing nullable warnings → model nullability correctly;requiredmembers andArgumentNullException.ThrowIfNull. - Loading entities then looping to save →
ExecuteUpdateAsync/ExecuteDeleteAsync. - N+1 from lazy loading →
Includeor projection; keep lazy-loading proxies off. - Newtonsoft.Json / Swashbuckle / AutoMapper by reflex →
System.Text.Json/Microsoft.AspNetCore.OpenApi+ Scalar / Mapperly or manual mapping. DateTime.Now/EnsureCreated()in migration projects /AllowAnyOrigin()with credentials /FromSqlRawwith concatenation →TimeProvider/ migrations / explicit CORS origins / parameterized SQL.- EF In-Memory provider for query tests → Testcontainers with the real engine.
new HttpClient()per call / hand-rolled retry loops → typedAddHttpClient<T>+AddStandardResilienceHandler().
When you code
- Make small, focused diffs scoped to one feature/slice. Do not restructure folders or bump dependencies unless asked.
- After changes, run
dotnet build(warnings are errors),dotnet format --verify-no-changes, anddotnet test. A change is not done until these pass. - When you touch the model, generate a migration (
dotnet ef migrations add) and include it; state the SQL it produces if it is non-trivial. - Match the existing pattern in the file (Minimal API vs controllers, mapping style, validation library). Do not introduce a second way to do the same thing.
- Ask before: changing target framework or major package versions, adding a new dependency, altering the DB schema/data destructively, changing auth/authorization policy, or introducing a new architectural layer. Otherwise proceed and report what you ran.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on .NET 10 · C# 14 · ASP.NET Core · EF Core 10.