Skip to main content

Command Palette

Search for a command to run...

The 14-Point ASP.NET Core API Code Review Checklist for .NET Teams

Published
15 min read
The 14-Point ASP.NET Core API Code Review Checklist for .NET Teams

Code reviews on ASP.NET Core APIs tend to follow a predictable pattern: reviewers catch the obvious issues — naming, formatting, missing tests — but let the subtle, production-breaking ones slide. A scoped service injected into a singleton, an async void hiding somewhere in an event handler, a DbContext shared across concurrent requests — these are the problems that surface at 2 AM, not during the pull request. An ASP.NET Core API code review checklist built for .NET teams forces the conversation onto those items before they reach production.

If you want to see these patterns implemented correctly in a complete, production-grade codebase, the full source code with edge-case handling and working tests is available on Patreon — everything wired together so you can see how each layer connects.

Understanding how validation, error handling, rate limiting, and authentication fit together in a single production API is what the ASP.NET Core Web API: Zero to Production course covers end-to-end — with a real codebase you can run and adapt from day one.

ASP.NET Core Web API: Zero to Production


How to Use This Checklist

Run through each item for every pull request that touches API behaviour, service registration, data access, or cross-cutting concerns. Items marked 🔴 Critical have caused production incidents across teams; treat them as blockers. Items marked 🟡 Important are high-value catches that prevent subtle bugs or maintenance debt. Items marked 🟢 Good Practice improve long-term quality and should be flagged as suggestions rather than blockers.


1. Dependency Injection: Are Service Lifetimes Correct?

🔴 Critical

The single most common source of runtime exceptions in ASP.NET Core APIs is a lifetime mismatch — typically a scoped service (like DbContext) registered or injected into a singleton. At runtime, the singleton is created once and holds a reference to the scoped service for the entire application lifetime, which means request isolation breaks completely.

What to check:

  • No scoped service is injected into a singleton service or singleton middleware

  • HttpContext is not accessed directly inside a singleton — use IHttpContextAccessor if needed, and only with care

  • DbContext is registered as scoped (the default), not singleton or transient

  • Background services (BackgroundService, IHostedService) that need scoped dependencies use IServiceScopeFactory to create a scope per operation — they never inject scoped services directly via constructor

What a failing pattern looks like: A CacheWarmingService : BackgroundService that has MyDbContext injected directly in the constructor. This compiles fine and will explode at startup with an InvalidOperationException.


2. Async/Await: No Fire-and-Forget Hiding in Plain Sight

🔴 Critical

async void methods are one of the most dangerous patterns in C# because unhandled exceptions inside them crash the process — they cannot be awaited, so the exception has nowhere to go. They belong only in event handlers where the signature is forced on you.

What to check:

  • No async void methods outside of UI event handlers (there are essentially none in an API project)

  • No .Result or .Wait() on Task or ValueTask — both block a thread and risk deadlock in some synchronisation contexts

  • Every Task-returning method is awaited, not discarded with _ = SomeTask()

  • CancellationToken is accepted and passed downstream in long-running operations — especially to EF Core queries and HttpClient calls

  • No Task.Run wrapping synchronous code to fake async


3. Global Error Handling: Is the Exception Pipeline Correct?

🟡 Important

The canonical approach from .NET 8+ is IExceptionHandler — a typed, injected handler that maps known exceptions to structured Problem Details responses. Review whether the team is using this correctly, or still relying on a patchwork of try/catch blocks scattered through controllers.

What to check:

  • UseExceptionHandler() is registered early in the middleware pipeline (before routing, authentication, authorisation)

  • Problem Details format (RFC 7807) is the response shape for all 4xx and 5xx errors — no custom ad-hoc error objects

  • 500 responses never expose exception.Message, StackTrace, or inner exception details to the caller

  • IExceptionHandler maps typed domain exceptions (e.g., NotFoundException, ConflictException) to appropriate status codes — no catch (Exception ex) blobs that swallow context

  • Log level is Warning for 4xx, Error for 5xx — both with structured context, never string interpolation

The decision between IExceptionHandler, middleware, and exception filters is covered in depth at ASP.NET Core Global Exception Handling: IExceptionHandler vs Middleware vs Filters.


4. Request Validation: FluentValidation Done Correctly

🟡 Important

Validation bugs are subtle — they don't always throw exceptions; they let bad data through silently. A validator that appears to work in unit tests can fail in integration if the registration is wrong or the auto-validation pipeline is not wired up.

What to check:

  • Validators are registered with AddFluentValidationAutoValidation() so they fire automatically on model binding — no manual validator.Validate(model) scattered in controllers

  • Cross-field validation rules (e.g., "end date must be after start date") use When() guards or Custom() to avoid redundant error messages

  • Async validators (MustAsync) are used for database-backed rules (e.g., uniqueness checks) — not synchronous validators that make blocking DB calls internally

  • The validator class itself has unit test coverage using TestValidate() — not just integration tests

  • Input DTOs are separate from domain entities — validation lives on the DTO layer, not on the entity


5. EF Core: Query Patterns That Kill Performance at Scale

🟡 Important

EF Core makes it easy to write queries that look fine in development and become catastrophic under load. The patterns below are the ones that slip through reviews most often.

What to check:

  • All read-only queries use AsNoTracking() — the change tracker adds overhead with no benefit for queries that do not update

  • No N+1 queries: navigation properties loaded inside loops without Include() or explicit projection are a classic failure mode

  • FindAsync() is used for single-record lookups by primary key; FirstOrDefaultAsync() with a predicate for anything more complex — never SingleOrDefaultAsync() on a full-scan

  • Filtering happens at the database level (Where() before ToListAsync()) — not in-memory after materialisation

  • CancellationToken is passed to every EF Core query

For a focused treatment of EF Core query pitfalls, the EF Core Performance Tuning Checklist for High-Traffic APIs is worth adding to the review queue.


6. Authentication and Authorisation: No Accidentally Open Endpoints

🔴 Critical

Missing [Authorize] on a write endpoint is not a configuration oversight — it is a security incident waiting to happen. The pattern of applying a global authorisation policy and then explicitly opting out with [AllowAnonymous] is safer than the inverse, but it is not the default in most codebases.

What to check:

  • A global DefaultPolicy is configured so all endpoints require authentication unless explicitly opted out

  • [AllowAnonymous] is intentional and documented — not a forgotten debug shortcut

  • Role-based and policy-based authorisation are used consistently; no ad-hoc User.IsInRole() checks buried in service layers

  • JWT configuration has ClockSkew = TimeSpan.Zero — the default 5-minute skew allows tokens to remain valid past their stated expiry

  • Token validation parameters include ValidateIssuer, ValidateAudience, and ValidateLifetime — all true

  • Sensitive endpoints (password change, admin actions, financial operations) have an additional authorisation policy, not just [Authorize]


7. Rate Limiting: Protection That Actually Activates

🟡 Important

Rate limiting middleware exists in the pipeline but is only effective when policies are applied and the OnRejected handler returns the correct status code and headers. Review teams often add the middleware but forget the [EnableRateLimiting] attribute on the relevant endpoints.

What to check:

  • Rate limiting policies are applied to public-facing and unauthenticated endpoints at minimum

  • OnRejected returns HTTP 429 with a Retry-After header and a Problem Details body — not a default 503

  • Policies are partitioned by user identity or client IP — a global fixed-window policy that penalises all users equally is not appropriate for multi-tenant APIs

  • [DisableRateLimiting] is not applied to endpoints that handle write operations or authentication

The ASP.NET Core 10 Rate Limiting for SaaS: Enterprise Policy Guide covers the policy design decisions in detail.


8. Logging: Structured, Contextual, and at the Right Level

🟡 Important

Logging is the team's primary diagnostic tool in production. Unstructured logs, wrong log levels, and missing correlation context make incidents significantly harder to diagnose.

What to check:

  • All log calls use named placeholders (_logger.LogInformation("Processing order {OrderId}", orderId)) — never string interpolation or $"..." — Serilog and other structured providers cannot extract properties from interpolated strings

  • Log levels are semantically correct: Debug for trace-level internals, Information for significant state changes, Warning for recoverable problems and 4xx responses, Error for 5xx failures and unhandled exceptions

  • UseSerilogRequestLogging() (or equivalent) is in the pipeline so HTTP request logs carry correlation IDs automatically

  • Sensitive data (passwords, tokens, PII, card numbers) is never logged — not even partially

  • No Console.WriteLine or Debug.WriteLine left over from development


9. Caching: No Stale Data, No Cache Stampede

🟡 Important

Caching bugs are particularly insidious because they are intermittent and environment-dependent. A cache that works perfectly with a single instance behaves unpredictably when scaled out unless the implementation is correct.

What to check:

  • IMemoryCache is used only for data that is genuinely local to one instance (e.g., configuration) — shared data uses IDistributedCache (Redis) or HybridCache (.NET 9+)

  • Cache keys include all dimensions that affect the result — missing a query parameter from the cache key causes data bleed between users

  • Expiry policies are set explicitly: both AbsoluteExpiration and SlidingExpiration where appropriate — no entries that never expire

  • Cache invalidation is explicit on writes — no assumption that expiry alone is sufficient for write-heavy paths

  • HybridCache is preferred for new code on .NET 9+ because it provides built-in stampede protection via the GetOrCreateAsync pattern


10. Middleware Order: Pipeline Correctness

🔴 Critical

Middleware in ASP.NET Core executes in registration order, and that order has real semantics. Exception handling must come before routing; authentication must come before authorisation; rate limiting should sit before expensive processing. Getting this wrong produces subtle bugs that are hard to reproduce in isolation.

What to check:

  • UseExceptionHandler() or UseDeveloperExceptionPage() is first in the pipeline

  • UseHttpsRedirection() is placed before UseStaticFiles() and UseRouting()

  • UseAuthentication() always precedes UseAuthorization() — they cannot be swapped

  • Custom middleware that reads request state (headers, body) is registered after UseRouting() but before endpoint execution

  • No custom middleware duplicates what built-in middleware already provides — UseResponseCompression() is not re-implemented manually

For a deeper look at middleware ordering mistakes and their consequences, see 7 Common ASP.NET Core Middleware Mistakes and How to Fix Them.


11. HTTP Client Usage: No Direct new HttpClient()

🟡 Important

Instantiating HttpClient directly is one of the most common anti-patterns in .NET codebases. It exhausts socket connections over time due to the way HttpClient handles DNS and connection pooling — the problem does not appear in development but causes intermittent 503 errors in production under sustained load.

What to check:

  • All HttpClient usage goes through IHttpClientFactory — either typed clients, named clients, or the basic factory

  • HttpClient instances are never created with new HttpClient() outside of test code

  • Resilience policies (retry, circuit breaker, timeout) are applied using Polly via AddStandardResilienceHandler() — not manual try/catch retry loops

  • Base addresses and timeouts are configured at registration time, not scattered across call sites


12. Cancellation Token Propagation

🟡 Important

A CancellationToken parameter that is accepted but never passed downstream is a common review miss. The token exists to propagate cancellation signals — if it is not threaded through, long-running operations continue even after the client has disconnected or the request has timed out.

What to check:

  • Controllers and action methods accept CancellationToken from the framework and pass it through to service layer calls

  • Service layer methods accept and propagate CancellationToken to EF Core queries, HttpClient calls, and channel reads/writes

  • CancellationToken.None is not used as a substitute when a real token is available — this is a suppression, not a default

  • Background jobs that process items from a queue check stoppingToken.IsCancellationRequested in the processing loop


13. Configuration and Secrets: No Credentials in Code

🔴 Critical

Hardcoded connection strings, API keys, or passwords in appsettings.json or source code are a recurring security finding. The configuration hierarchy in .NET exists precisely to prevent this.

What to check:

  • No secrets, connection strings, or API keys appear in appsettings.json committed to source control

  • Environment-specific configuration (production database strings, third-party API keys) comes from environment variables or a secrets manager (User Secrets for development, Azure Key Vault or similar for production)

  • The Options pattern (IOptions<T>, IOptionsSnapshot<T>) is used to bind configuration sections to typed classes — no Configuration["SomeKey"] scattered across services

  • Sensitive configuration keys are not logged at startup — a common pattern is to log all configuration for debugging and accidentally expose credentials


14. Response Shaping: Consistent, Versioned, and Leak-Free

🟡 Important

API responses are a contract. Exposing internal domain types directly, returning different shapes from different endpoints, or leaking stack traces in error responses all create compatibility issues and security risks.

What to check:

  • Response types are dedicated DTOs, not domain entities returned directly — returning an Entity directly leaks internal structure and creates tight coupling between API contract and data model

  • All endpoints declare response types explicitly using ProducesResponseType attributes — or, for Minimal APIs, using TypedResults — so OpenAPI documentation is accurate

  • No dynamic return types or object return types on non-trivial endpoints

  • API versioning is applied to breaking changes — not a new endpoint named GetV2 with no versioning strategy

  • Error responses across all endpoints use the same Problem Details shape — mixing { error: "..." } with { message: "...", code: "..." } creates inconsistent client handling


☕ Prefer a one-time tip? Buy us a coffee — every bit helps keep the content coming!


Frequently Asked Questions

What is the most critical item on an ASP.NET Core API code review checklist?

Service lifetime mismatches — particularly injecting a scoped service such as DbContext into a singleton — are the most commonly missed and most reliably catastrophic. They compile cleanly, pass basic integration tests, and then produce InvalidOperationException errors in production, often under load. Every code review on a service registration file should treat this as the first thing to check.

How often should an ASP.NET Core API code review checklist be updated?

At minimum, review the checklist when a new major version of .NET or ASP.NET Core is released. New versions often introduce new patterns (e.g., IExceptionHandler in .NET 8, HybridCache in .NET 9) that supersede older approaches. Teams should also update the checklist after any production incident — if a bug slipped through review, the checklist should be extended to catch it next time.

Should the code review checklist be enforced by tooling or by reviewers?

Both. Static analysis tools (Roslyn analyzers, SonarQube, .editorconfig rules) can catch a subset of these issues automatically — particularly async void, missing ConfigureAwait, or suppressed CancellationToken. However, architectural and design-level items — such as whether a service lifetime is semantically correct, whether a caching key is complete, or whether an endpoint should require additional authorisation — require human judgment. The checklist bridges the gap between what tooling catches and what only an experienced reviewer notices.

Is it necessary to use AsNoTracking() on all read queries in ASP.NET Core?

Yes, for any query whose results are not going to be saved back to the database in the same request. The EF Core change tracker adds memory and CPU overhead for every entity it tracks, and that overhead scales with the number of entities returned. For APIs that serve read-heavy endpoints, omitting AsNoTracking() on queries that return large result sets is one of the most impactful performance regressions a team can introduce.

What is the correct order of authentication and authorisation middleware in ASP.NET Core?

UseAuthentication() must always precede UseAuthorization(). Authentication establishes the identity of the caller by validating credentials (JWT, cookie, API key). Authorisation then evaluates whether that established identity has permission to access the requested resource. Reversing the order means authorisation runs against an unauthenticated (anonymous) principal, which causes all protected endpoints to return 401, even for valid credentials.

How should a team handle CancellationToken in code reviews?

Review whether the token is both accepted and propagated. A method signature that accepts a CancellationToken but passes CancellationToken.None to its downstream calls provides no actual cancellation support — it is cosmetically compliant but behaviourally inert. Reviewers should trace the token from the controller action through to the EF Core query, the HttpClient call, or the channel read to confirm the full chain is cancellation-aware.

What is the right way to handle secrets in an ASP.NET Core API for production?

In production, secrets should never live in appsettings.json or environment variables that are committed to source control. The recommended hierarchy is: appsettings.json for non-sensitive defaults, User Secrets for development-time overrides, and a secrets manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) for production values. The Options pattern should be used to bind configuration to typed classes so access is centralised and typed, not scattered string-key lookups.

More from this blog

C

Coding Droplets

198 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.