Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Framework · .NET 10 · C# 14 · ASP.NET Core · EF Core 10

ASP.NET Core

Minimal APIs, typed DI and EF Core — modern .NET.

csharpdotnetaspnetefcore

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You 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 target net8.0/net9.0 for new work.
  • C# 14<LangVersion> defaults to 14; do not pin it lower. Genuinely new in 14: the field contextual 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.PostgreSQL or Microsoft.EntityFrameworkCore.SqlServer).
  • OpenAPI: Microsoft.AspNetCore.OpenApi (built-in, emits OpenAPI 3.1 by default in .NET 10). Do NOT add Swashbuckle. For a UI use Scalar.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.v3 package), Microsoft.AspNetCore.Mvc.Testing (WebApplicationFactory), Testcontainers for real-DB integration tests. Assertions: plain xUnit Assert, 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-generated JsonSerializerContext for hot paths / AOT). Never add Newtonsoft.Json to new projects.
  • Time: inject TimeProvider (never DateTime.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 MapGroup and register via extension methods, e.g. app.MapProductsApi(). Never dump every route into Program.cs.
  • Every .csproj (via Directory.Build.props) sets:
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisMode>All</AnalysisMode>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    
  • Naming: PascalCase types/methods/public members; _camelCase private fields; I-prefixed interfaces; async methods end in Async. Records for DTOs. File-scoped namespaces only.
  • Config/format: commit a root .editorconfig; run dotnet format --verify-no-changes in CI. Prefer primary constructors and target-typed new().

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 (not Results.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.CreatedAtRoute for 201 responses 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: DbContext and 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 run validates scopes in Development; keep ValidateScopes/ValidateOnBuild on.
  • Use keyed services (AddKeyedScoped + [FromKeyedServices("x")]) instead of factory switch statements when you need multiple named implementations.
  • Register DbContext with AddDbContextPool<AppDbContext>(...) for high-throughput APIs; plain AddDbContext otherwise. 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 record DTOs. 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/Select projection) or with Mapperly (source-generated). Avoid reflection-based AutoMapper in new code; projections should be IQueryable.Select so 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 FluentValidation AbstractValidator<T>, register with AddValidatorsFromAssemblyContaining<T>(), and invoke it in an endpoint filter — FluentValidation 12 does not auto-run on Minimal APIs.
  • Global error handling: implement IExceptionHandler and register AddProblemDetails() + app.UseExceptionHandler(). Map known exceptions to typed status codes; return ProblemDetails/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 200 with an error payload. Validation failures are 400 with ValidationProblemDetails; missing resources 404; conflicts 409.

EF Core

  • Async only: ToListAsync, FirstOrDefaultAsync, SaveChangesAsync, AnyAsync — and pass CancellationToken to every one. There is no sync SaveChanges in request paths.
  • Reads use AsNoTracking() (or AsNoTrackingWithIdentityResolution() when the graph shares references). Tracking is only for entities you will mutate and save.
  • Kill N+1: use Include/ThenInclude for graphs you materialize, or better, project with .Select(...) into DTOs so joins fetch only required columns. Use AsSplitQuery() 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 call Database.EnsureCreated() in apps that use migrations, and never context.Database.Migrate() blindly at startup in multi-instance deployments — run migrations as a gated deploy step.
  • Concurrency: add a [Timestamp]/rowversion or IsRowVersion() token and handle DbUpdateConcurrencyException409.
  • Configure entities in IEntityTypeConfiguration<T> classes, not inline OnModelCreating walls. Use EF Core 10 named query filters (HasQueryFilter("SoftDelete", ...)) so a filter can be disabled selectively with IgnoreQueryFilters(["SoftDelete"]).
  • EnableSensitiveDataLogging() and detailed errors are Development-only. Never in Production config.

Async and cancellation

  • Never block: no .Result, .Wait(), .GetAwaiter().GetResult(), or Task.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.Run to "make it async" on the server.
  • Plumb CancellationToken from the endpoint/handler parameter (bound from HttpContext.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; avoid async void except event handlers. Use IAsyncEnumerable<T> for streaming endpoints.

Options and configuration

  • Bind config with the Options pattern: builder.Services.AddOptions<StripeOptions>().Bind(config.GetSection("Stripe")).ValidateDataAnnotations().ValidateOnStart();. ValidateOnStart fails fast at boot on bad config.
  • Inject IOptions<T> (singleton config), IOptionsSnapshot<T> (per-request, reloadable), or IOptionsMonitor<T> (singletons needing live updates). Do not inject raw IConfiguration into 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>()); never new HttpClient() (socket exhaustion) or a static instance you mutate. HttpClientFactory owns handler lifetime.
  • Add resilience with AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience (Polly v8 pipelines: jittered retry, circuit breaker, total-request timeout) instead of hand-rolled retry loops. Every outbound call carries the request CancellationToken.
  • Cache with HybridCache (Microsoft.Extensions.Caching.Hybrid) — GetOrCreateAsync(key, factory, ct) unifies in-process + distributed (IDistributedCache) tiers and adds stampede protection; prefer it over raw IMemoryCache. Use .CacheOutput() for endpoint response caching.
  • Emit telemetry via OpenTelemetry (AddOpenTelemetry().WithTracing().WithMetrics() + OTLP exporter); .NET Aspire ServiceDefaults wires 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/Substitute for repositories; do not test business logic through HTTP.
  • Integration tests use WebApplicationFactory<Program> (add public partial class Program; at the end of top-level Program.cs so the test project can reference it). Override registrations in ConfigureTestServices to 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 returns 404, IExceptionHandler maps to the right ProblemDetails, auth policies reject unauthorized calls, and concurrency conflicts return 409.
  • Name tests Method_State_Expectation. Assert on status codes and response DTOs, not on internal calls. Keep tests deterministic — inject a fixed TimeProvider (FakeTimeProvider) rather than reading the clock.

Security

  • Never interpolate user input into raw SQL. Use LINQ or parameterized FromSql($"...") (interpolated FromSql parameterizes automatically); FromSqlRaw with 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; never AllowAnyOrigin() together with credentials.
  • Rate limiting: AddRateLimiter with 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, analyzers All.
  • Return TypedResults with Results<...> union types; document with the built-in OpenAPI package.
  • Project queries straight into DTO records with .Select; add AsNoTracking() on all reads.
  • Register AddProblemDetails() + one IExceptionHandler; return RFC 9457 payloads.
  • Validate options with ValidateOnStart(); inject IOptions*<T>, TimeProvider, ILogger<T>.
  • Plumb CancellationToken through every async call; keep DbContext scoped.
  • Use [LoggerMessage] source-generated logging and System.Text.Json source generators on hot paths.

Avoid

  • Sync-over-async (.Result/.Wait()/.GetAwaiter().GetResult()) → always await.
  • Returning EF entities from endpoints → map to record DTOs / projections.
  • IServiceProvider.GetService in app code (service locator) → constructor/parameter injection.
  • Ignoring/#pragma-suppressing nullable warnings → model nullability correctly; required members and ArgumentNullException.ThrowIfNull.
  • Loading entities then looping to saveExecuteUpdateAsync/ExecuteDeleteAsync.
  • N+1 from lazy loadingInclude or projection; keep lazy-loading proxies off.
  • Newtonsoft.Json / Swashbuckle / AutoMapper by reflexSystem.Text.Json / Microsoft.AspNetCore.OpenApi + Scalar / Mapperly or manual mapping.
  • DateTime.Now / EnsureCreated() in migration projects / AllowAnyOrigin() with credentials / FromSqlRaw with concatenationTimeProvider / 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 → typed AddHttpClient<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, and dotnet 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.

Back to top ↑