Enterprise software tends to remain in production for a long time. It’s particularly true for ASP.NET and ASP.NET Core, which are common in long-lived enterprise systems, especially in Microsoft-centered environments. According to statistics, roughly 40% of enterprises run at least one critical ASP.NET application, and will keep doing that for years to come, so code review needs to account for framework support, security patching, maintainability, and operational risk.
There is no doubt that such longevity is a strength for large businesses with exceedingly complicated infrastructures. However, it comes at a cost, as systems that last a decade accumulate decisions made under old constraints, by developers who have long since moved on. In addition, the threat landscape looked completely different when the system was designed. Therefore, today you likely need a structured code review to find out what your ASP.NET application actually looks like under the surface. Most importantly, you should run one before a security incident, a failed deployment, or a performance crisis uncovers critical system weaknesses.
In this checklist, written by Redwerk developers with 10+ years of experience, we will cover what makes a thorough review of an ASP.NET Core web application. We’ve made sure it’s understandable to both the people who commission reviews and the developers who run them. Therefore, every section explains not just what to check but why it matters for your business in the long run.
Pre-Review Preparation
First of all, you must always remember that a review is only as good as the preparation behind it. Therefore, time spent gathering context upfront determines whether this process yields insight or confusion.
Define scope and success criteria:
- Clarify whether the review covers the full application, a specific service or module, a pre-release candidate, or a legacy codebase being handed to a new team
- Define what success is for you: a prioritized list of security findings, a performance baseline, readiness for a production deployment, or due diligence ahead of an acquisition
- Identify any known problem areas from the team. These could be slow endpoints, recurring exceptions in logs, intermittent deployment failures, or features that nobody wants to touch
Verify the environment and toolchain:
- Confirm that the solution builds cleanly in both Debug and Release configurations, with no warnings treated as errors and no suppressed warnings without documented justification
- Check that the project targets a supported version of .NET. Microsoft’s support lifecycle page is the authoritative reference you should use. Remember, running an end-of-life runtime in production is a compliance and security risk
- Verify that dotnet restore completes without vulnerability warnings. Run dotnet list package –vulnerable and document any flagged NuGet packages before beginning the review
Review documentation and recent history:
- Setup and onboarding should be documented well enough that a new developer can get up and running predictably, without relying on tribal knowledge
- Review the recent commit history to understand what has changed. This matters because a security fix committed last week deserves closer attention than stable code that has not changed in a year
- Check that any open pull requests are rebased against the main branch so the review reflects the current state of the code
Project Structure and Architecture
ASP.NET Core gives you a lot of freedom in how to organize a project. That freedom is valuable when used deliberately and costly when not used at all, so keep the following points in mind.
Separation of concerns:
- Verify that the application follows a consistent layered structure: controllers or minimal API endpoints handle HTTP concerns only; business logic lives in dedicated service classes. Data access should be clearly separated from HTTP concerns. Whether using EF Core directly in services, query objects, or CQRS-style handlers, the chosen approach should be applied consistently across the codebase.
- Controllers should primarily coordinate HTTP concerns and delegate business logic. If actions contain substantial business rules, persistence logic, or orchestration, responsibilities may be misplaced.
Bad practice:
[HttpPost("orders")]
public async Task CreateOrder([FromBody] OrderRequest request)
{
// Validate, calculate tax, update inventory, send email — all in the controller
var tax = request.Total * 0.2m;
var finalTotal = request.Total + tax;
var order = new Order { Total = finalTotal, UserId = request.UserId };
await _db.Orders.AddAsync(order);
await _db.SaveChangesAsync();
await _inventoryService.DecrementAsync(request.Items);
await _emailService.SendConfirmationAsync(request.UserEmail, order.Id);
return Ok(order);
}
Good practice:
[HttpPost("orders")]
public async Task CreateOrder([FromBody] OrderRequest request)
{
var result = await _orderService.CreateAsync(request);
return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
}
Dependency injection:
- Verify that all services are registered in the dependency injection container and that constructor injection is used consistently, because resolving services manually from the container inside methods is a sign that the architecture is fighting its own design
- Check that service lifetimes are appropriate: transient for lightweight, stateless operations. Scoped for services that share state within a request (such as DbContext), and singleton only for services that are genuinely thread-safe and stateless
Project and solution organization:
- Confirm that the solution structure reflects the architectural boundaries. A project named MyApp.Core containing both domain logic and database models has a naming problem that usually signals an organizational problem underneath it
- Verify that circular project references do not exist. They are technically possible in some configurations, but always indicate a dependency design that needs to be untangled
Middleware Pipeline and Request Handling
The spine of every ASP.NET Core application is its middleware pipeline. Therefore, the order in which middleware components are registered directly affects not only its performance and behavior, but also security. A human body cannot function properly with a compromised spine, and it’s the same for an ASP.NET app. Getting that order wrong might silently break authentication, leak exceptions to clients, or add unnecessary processing to every request.
Middleware ordering:
- Verify that UseExceptionHandler or UseDeveloperExceptionPage is registered first in the pipeline, as exception handling middleware must wrap everything else to catch errors from downstream components
- Confirm that production pipelines follow Microsoft’s recommended middleware ordering: exception handling early, HSTS in non-development environments, HTTPS redirection before request handling, then static files if needed, routing, authentication, and authorization. Misordered middleware can lead to incorrect security behavior or unnecessary request processing
- Check that authentication (UseAuthentication) comes before authorization (UseAuthorization). Reversing the order means authorization decisions are made before the user’s identity is established, which is sure to cause issues down the line
- Review the full pipeline for any middleware components registered in the wrong order or registered more than once, as redundant middleware adds latency without benefit
Request validation:
- Verify that model validation is enforced consistently. In controllers marked with [ApiController], ASP.NET Core automatically returns validation errors for invalid models. In MVC controllers, Razor Pages, Minimal APIs, or custom pipelines, confirm that validation is handled through filters, endpoint filters, FluentValidation, or explicit checks where appropriate
- Confirm that request size limits are configured appropriately. The default MultipartBodyLengthLimit and MaxRequestBodySize settings are not always suitable for production workloads and should be set explicitly based on the application’s requirements
Response handling:
- Check that responses include appropriate cache-control headers for content that should or should not be cached by browsers and CDNs
- Verify that response compression is enabled for text-based content types. Brotli and Gzip compression significantly reduce bandwidth for JSON-heavy APIs
Authentication and Authorization
You should be aware that authentication and authorization problems account for a significant share of real-world security incidents in web applications. To get a better understanding of how crucial this is, consider that in October 2025, Microsoft patched CVE-2025-55315, an HTTP request smuggling vulnerability in the Kestrel server, which received the highest CVSS score ever assigned to an ASP.NET Core issue: 9.9 out of 10.
Microsoft described CVE-2025-55315 as an ‘HTTP request smuggling vulnerability in Kestrel that could allow an attacker to hide one request inside another’. To put it simply, depending on deployment and application behavior, this issue could affect authentication and authorization decisions. Another vulnerability it created was opening a path for request manipulation. The incident illustrates why authentication and authorization must be consistently applied across request paths and deployment configurations. Remember that infrastructure vulnerabilities can amplify weaknesses in application-level access control.
Authentication implementation:
- Verify that the application uses ASP.NET Core Identity, Azure Active Directory, or an established OAuth 2.0 / OpenID Connect library rather than a custom authentication implementation, because custom authentication schemes introduce risk without providing meaningful benefit over the mature solutions already available
- Check that JWT configuration explicitly specifies the allowed signing algorithms. Token validation parameters should validate issuer, audience, lifetime, and signing key. If the application restricts allowed signing algorithms, ensure only expected algorithms are accepted and unsigned tokens are rejected
// Explicit token validation — never rely on defaults alone
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = configuration["Jwt:Audience"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
};
});
- Confirm that token refresh logic handles expiry gracefully, and that expired tokens are cleared from client storage. A token that was valid six months ago must not still be granting access
Authorization implementation:
- Verify that authorization is applied at the controller or endpoint level, not only checked within action method bodies. Inline authorization checks are easy to forget and impossible to audit systematically
- Confirm that role-based and policy-based authorization are used consistently and avoid mixing ad-hoc User.IsInRole() checks scattered through the codebase with formal policy definitions
- Check for insecure direct object references: endpoints that accept a record ID in the URL must verify that the authenticated user has permission to access that specific record, not just that they are logged in
Vulnerable: any authenticated user can access any invoice
[HttpGet("invoices/{id}")]
[Authorize]
public async Task GetInvoice(int id)
{
var invoice = await _db.Invoices.FindAsync(id);
return Ok(invoice);
}
Correct: scope the query to the authenticated user’s own data
[HttpGet("invoices/{id}")]
[Authorize]
public async Task GetInvoice(int id)
{
var userId = User.GetUserId();
var invoice = await _db.Invoices
.FirstOrDefaultAsync(i => i.Id == id && i.OwnerId == userId);
return invoice is null ? NotFound() : Ok(invoice);
}
Data Access and Entity Framework Core
The database is where most ASP.NET application performance problems are born. The majority of problems in this area stem from slow queries, untracked data loading, N+1 queries, and synchronous database calls on request threads. These issues are not only debilitating for your product but also rather expensive to fix. Therefore, identifying them is one of the most valuable benefits of a code review.
Query efficiency:
- Look for N+1 query patterns: a query that loads a list of entities and then loops through them to load related data for each one is executing one query to get N records and then N additional queries to load their relationships, which degrades performance significantly as data volume grows.
Bad practice:
// Loads all orders, then queries the database once per order to load the customer
var orders = await _db.Orders.ToListAsync();
foreach (var order in orders)
{
order.Customer = await _db.Customers.FindAsync(order.CustomerId); // N extra queries
}
Good practice:
// Single query with a join — one database round trip
var orders = await _db.Orders
.Select(o => new OrderDto
{
Id = o.Id,
Total = o.Total,
CustomerName = o.Customer.Name
})
.ToListAsync();
- Verify that read-only queries use AsNoTracking(). According to Microsoft’s EF Core documentation, no-tracking queries return results more efficiently because EF Core does not need to maintain change tracking state for objects that will never be updated
- Check that projections use Select() to retrieve only the columns the endpoint actually needs, rather than loading full entity graphs and discarding most of the data
Async database access:
- Confirm that all EF Core calls use their async variants: ToListAsync(), FirstOrDefaultAsync(), SaveChangesAsync(), and so on. Microsoft’s ASP.NET Core best practices documentation is explicit that blocking calls like .Result and .Wait() on asynchronous database operations can starve the thread pool under load
- Verify that DbContext instances are not shared across threads or stored as singletons. DbContext is not thread-safe, so its lifetime should be scoped to a single request
Schema and migration hygiene:
- Confirm that database migrations are committed to version control and that the migration history is linear, as gaps or conflicts in migration history cause deployment failures
- Check that migrations are applied through a controlled deployment process, such as reviewed migration scripts, CI/CD deployment steps, or an approved release procedure. You should avoid relying on manual developer-run migrations or unreviewed automatic production changes. Manual migration steps are the kind of thing that gets forgotten at the worst possible time
- Review indexes to confirm that columns used in WHERE clauses, JOIN conditions, and ORDER BY expressions have appropriate indexes. Missing indexes are the most common cause of queries that run fine in development and fall apart in production
API Design and Contract Hygiene
An ASP.NET Core API is a contract between the server and every client that depends on it. Therefore, breaking that contract, even accidentally, can cause failures that are often invisible during development. However, they would be extremely painful when these issues reach production.
Versioning:
- Verify that the API uses a consistent versioning strategy, whether that is URL path versioning (/api/v1/), query string versioning, or header-based versioning. The specific approach you choose matters less than simply having one and applying it consistently
- Confirm that deprecated API versions are clearly marked and that a migration timeline is communicated to consumers. Removing an endpoint without notice is a fast way to break integrations
Input validation and response shaping:
- Verify that all request models are annotated with data annotation attributes or Fluent Validation rules, and that the validation is enforced globally through a filter rather than checked individually in each action
- Confirm that API responses use a consistent envelope format; inconsistent response shapes require clients to handle each endpoint differently and make error handling brittle
- Check that error responses return appropriate HTTP status codes and meaningful error messages, as returning a 200 OK with an error field in the body is a pattern that breaks HTTP semantics and confuses clients
OpenAPI and documentation:
- Verify that Swagger or a compatible OpenAPI documentation tool is configured and produces accurate, up-to-date documentation. Bear in mind that outdated API docs are almost worse than no docs because they actively mislead consumers
- Confirm that public API endpoints and externally consumed request/response models are documented clearly enough for the generated OpenAPI documentation to be useful. Prioritize public contracts, non-obvious fields, error responses, authentication requirements, and versioning behavior. These feed directly into the generated documentation and require no extra effort once the habit is in place
Security Best Practices
You definitely need to check out the OWASP DotNet Security Cheat Sheet, maintained specifically for ASP.NET applications. However, most importantly, you must treat it as a baseline, not a ceiling of your security practices. The items below stem from the issues we most commonly find during reviews of production ASP.NET codebases.
Injection prevention:
- Review all database queries for string concatenation; any query that builds SQL by appending user input is vulnerable to SQL injection, regardless of how unlikely the exploit path might seem. Parameterized queries and EF Core’s LINQ interface are the correct tools
- Check for command injection risks in any code that passes user-supplied values to Process.Start, shell commands, or dynamic code execution
Cross-site scripting (XSS) and cross-site request forgery (CSRF):
- Confirm that Razor views use the @ encoding syntax consistently and that raw HTML rendering via Html.Raw() is used only where strictly necessary and with fully trusted content
- For cookie-authenticated browser flows, verify that anti-forgery protection is applied to state-changing form submissions and relevant unsafe HTTP methods. For bearer-token APIs, assess CSRF risk separately. CSRF is primarily a concern when browsers automatically attach credentials, such as cookies or Basic authentication
HTTP security headers:
- Confirm that security headers are set via middleware or a custom filter: Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Strict-Transport-Security are the baseline
- Verify that HTTPS is enforced in production and that UseHsts is configured with an appropriate max-age value. HSTS tells browsers to always use HTTPS for your domain, even if the user types HTTP manually
Sensitive data exposure:
- Ensure that production secrets, API keys, signing keys, and privileged connection strings are not committed to version control. Non-sensitive local defaults may live in configuration files, but real secrets should come from environment variables, secret stores, managed identity, or deployment-time configuration. They belong in environment variables, Azure Key Vault, or another secrets management system
- Verify that sensitive data is not logged; a log entry that captures a full request body can inadvertently persist passwords, credit card numbers, or personal data long after the request has been processed
- Confirm that exception details are never sent to clients in production. ASP.NET Core’s UseDeveloperExceptionPage must be restricted to the development environment
Async Patterns and Performance
ASP.NET Core is built for high concurrency by default, which is one of the reasons for its popularity for enterprise-level systems. However, that concurrency is undermined every time a synchronous operation blocks a request thread. A single blocking call in a hot code path can reduce throughput by an order of magnitude under load. That’s always something you must watch out for.
Avoiding synchronous blocking:
- Search for .Result, .Wait(), and .GetAwaiter().GetResult() on Task. In ASP.NET Core, these calls block request threads and can contribute to thread pool starvation, poor throughput, and latency spikes under load. It’s best to prefer async all the way through the call stack
- Confirm that async void is never used in controller actions or service methods. Avoid async void outside event handlers. async void methods cannot be awaited, exceptions are difficult to observe and handle correctly, and callers cannot know when the operation has completed
ThreadPool and IHttpClientFactory:
- Verify that HttpClient is not instantiated directly with new HttpClient() inside methods. Prefer IHttpClientFactory for outbound HTTP calls, especially when clients need named/typed configuration, resilience policies, logging, or controlled handler lifetimes. Also, verify that the code does not create short-lived HttpClient instances per request, which can exhaust sockets under load
- Check for any CPU-intensive operations running on request threads. Workloads such as image processing, PDF generation, or large data exports should be offloaded to background services or message queues rather than blocking the HTTP request thread
Response caching and output caching:
- Identify endpoints that repeatedly serve the same data and verify that response or output caching is implemented for them. An endpoint that hits the database on every request to return data that changes once per hour is wasting resources
- Confirm that cache invalidation logic exists and is correct, as a cache that grows indefinitely or serves stale data after a write is harder to debug than no cache at all
Configuration and Secrets Management
System configuration is the underlying cause of the gap between how an application behaves in development and in production. Therefore, it’s essential that you examine it carefully during code review to identify any issues that can lead to devastating data leaks.
Environment-specific configuration:
- Confirm that all environment-specific values come from appsettings.{Environment}.json files or environment variables, not from hardcoded values in the source code. A connection string pointing to a local database that makes it into a production deployment will cause an outage
- Verify that the application validates the required configuration at startup and loudly fails with a descriptive error if anything is missing. A partially configured application that starts successfully but crashes on first use is harder to diagnose than one that refuses to start
// Fail at startup, not at runtime
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException(
"Required configuration 'ConnectionStrings:DefaultConnection' is missing.");
- Check that appsettings.Development.json files are in .gitignore if they contain any real credentials, even development ones. Developer credentials committed to version control have a long history of appearing in production through copy-paste or misconfiguration. Check out what it could lead to in one of our articles on data leaks
Secrets in production:
- Verify that production secrets are managed through Azure Key Vault, AWS Secrets Manager, environment variables injected at deployment time, or an equivalent mechanism
- Confirm that the application uses managed identities or workload identity federation to access Azure services rather than connection strings with embedded credentials, because managed identities eliminate an entire category of secret rotation and leakage problems
Testing and Code Coverage
A crucial thing to remember is that it’s impossible to verify that the codebase is stable without testing. There is no doubt that untested systems have instabilities and vulnerabilities that would show up at the worst possible moment.
Unit and integration tests:
- Verify that critical business logic, edge cases, and failure paths are covered by automated tests
- Confirm that tests use dependency injection and mocking (Moq, NSubstitute) to isolate the code under test from real databases, external APIs, and file systems. Note that tests that require a live database are integration tests and should be treated differently from unit tests
- Check that WebApplicationFactory<T> is used for integration tests rather than spinning up full deployment environments. It provides a lightweight in-process test host that runs the full middleware pipeline without requiring a deployed server
Test quality:
- Review test names to confirm they describe behavior rather than method names: CreateOrder_WhenInventoryIsInsufficient_ShouldReturnBadRequest is more useful than TestCreateOrder
- Verify that the CI pipeline blocks merges on failing tests and that no tests are disabled with [Ignore] or Skip without a tracked reason
- Confirm that critical paths are tested and that coverage is tracked. Raw percentages alone don’t guarantee quality: a suite of shallow tests can hit 80% coverage while missing the logic that actually matters. For critical systems, enforcing a minimum threshold adds a useful safety net, but the goal is meaningful coverage of business logic, edge cases, and failure paths
Logging, Observability, and Error Handling
Imagine a situation, you get a call at 2 AM because something went wrong in production and the whole system crashed. The level of your panic in this case should be determined by the quality of your logs. The information you get from them often makes the difference between a ten-minute fix and a three-hour investigation. Therefore, enforcing observability and consistent logging is a must-have foundation for any enterprise-grade system, regardless of what programming language or framework it uses.
Structured logging:
- Verify that the application uses ILogger<T> from the built-in Microsoft.Extensions.Logging abstraction rather than static logging libraries or scattered Console.WriteLine calls. The built-in abstraction integrates with Serilog, NLog, and Azure Application Insights through configuration rather than code changes
- Confirm that log entries use structured properties rather than string interpolation; logger.LogInformation(“Processing order {OrderId} for user {UserId}”, orderId, userId) produces a searchable log entry, while logger.LogInformation($”Processing order {orderId} for user {userId}”) produces a plain string that cannot be queried
Global exception handling:
- Verify that a global exception handling middleware is configured and that it returns consistent, sanitized error responses, as different parts of the application should not each handle exceptions differently
- Confirm that unhandled exceptions are logged with enough safe context to reconstruct what happened: request path, correlation ID, authenticated user identifier where appropriate, and stack trace. Avoid logging raw request bodies, credentials, tokens, payment data, or personal data unless explicitly protected and justified
- Check that operational errors (validation failures, not-found resources, business rule violations) return appropriate 4xx responses and are distinguished from unexpected errors that return 5xx responses. Treating every exception as a 500 makes it impossible to separate bugs from expected error conditions
Health checks and monitoring:
- Verify that health check endpoints are configured using ASP.NET Core’s built-in health checks. These should validate that the application can connect to its database, external dependencies, and any critical infrastructure
- Confirm that Application Insights, Datadog, or an equivalent APM tool is configured in production. Structured logs and health checks are necessary, but distributed tracing across service boundaries requires dedicated tooling
Why You Should Trust Redwerk with ASP.NET Code Review
The Stack Overflow Developer Survey confirmed that ASP.NET Core ranked among the top ten most-used web frameworks worldwide, with 19.1% of professional developers actively working with it. This proves that ASP.NET Core is a mature, capable framework, and applications built on it can serve millions of users, run in regulated industries, and stay in production for a decade or more.
However, the checklist above exists because the framework’s maturity does not protect a codebase from the decisions made during development. Things such as a missing authentication check, a blocking database call in a hot path, secrets committed to version control, or a middleware pipeline assembled in the wrong order lead to serious issues that could, potentially, break down the whole system.
Let’s take a look at a practical example of how this system done right looks like: Current, an e-government SaaS we built for welfare agencies across the United States. The platform runs on .NET and Azure, handles sensitive citizen data across multiple US government agencies, and required 100% ADA compliance from day one.
As we were building for that environment, we had to get every item in this checklist correct before a single line of code went into production. Therefore, Redwerk developers and testers ensured that authorization scoped tightly to each agency’s data and HTTPS was enforced throughout. We also implemented structured logging that satisfied audit requirements and health checks that let operations teams verify every dependency without touching the application. The platform is now used by welfare divisions across the country.
Our ASP.NET development team has been working with the Microsoft stack since 2005. Therefore, when we review a codebase, we cover the full picture, including:
- Architecture
- Security
- Data access patterns
- Async correctness
- Configuration hygiene
- Test coverage
When the review is completed, you receive a report that is specific, prioritized by severity, and written so that both decision-makers and developers can act on it. If your ASP.NET application is due for a closer look, our code review service is the place to start. Tell us what you have built and we will tell you what we find.
See how a code review works for yourself: 80+ identified improvements and security risks for a mobile marketplace