C# and .NET code review

C# and .NET code review checks a change against the runtime, frameworks, configuration, and application layers that shape its behavior. It looks for problems such as broken async flows, incorrect dependency-injection lifetimes, unsafe Entity Framework Core usage, authorization gaps, and API contract changes.

That scope matters because a clean build is only the starting point. A controller can compile while bypassing a domain rule. A background service can start while holding a DbContext for far too long. A new DTO can look harmless in C# while breaking the JSON contract used by another service. Good review follows the behavior through the solution instead of stopping at the changed method.

What Should Reviewers Check in C# and .NET Code?

The compiler, Roslyn analyzers, formatter, and test suite should handle the checks they can answer precisely. Review is most valuable when it asks what the change means in the real application.

AreaWhat to look forWhy it can slip through
Async code.Result or .Wait(), dropped tasks, async void, missing cancellation, and concurrent work on one DbContextThe code may compile and pass a small test but block request threads, hide exceptions, or fail under load
Dependency injectionA singleton capturing a scoped service, mutable singleton state, hidden service-locator calls, or a lifetime that does not match the workEach registration can be valid on its own while the combination is unsafe
EF CoreN+1 queries, unnecessary tracking, early materialization, missing concurrency handling, oversized transactions, and migrations that cannot roll back safelyQuery behavior depends on mappings, data volume, provider behavior, and surrounding calls
NullabilityBroad use of the null-forgiving operator, inconsistent nullable annotations, or a contract that allows null at runtime despite its signatureC# nullability is compile-time analysis. Suppressing a warning does not change runtime behavior
ASP.NET CoreMissing authorization policies, incorrect middleware order, weak request validation, ambiguous status codes, or endpoints that expose internal modelsThe defect often lives between routing, attributes, filters, services, and startup configuration
API and message contractsRenamed JSON fields, changed enum values, new required members, incompatible defaults, or altered event schemasA local caller may still compile while an external consumer breaks
Resource handlingStreams, responses, database connections, and async disposables with unclear ownership or lifetimeHappy-path tests rarely exercise cancellation, retries, and partial failure
Project configurationTarget-framework changes, nullable settings, analyzer severity, package updates, trimming or AOT settings, and conditional build propertiesA one-line .csproj change can affect every file or only fail in release builds

Two details deserve extra attention.

First, async code should stay async from the HTTP boundary to the database or network call. Blocking on a task can waste request threads, while starting several EF Core operations on the same context can fail because a DbContext does not support parallel operations. Microsoft’s guidance on EF Core asynchronous operations also recommends awaiting one operation before starting another on the same context.

Second, dependency-injection lifetimes are part of program correctness. AddDbContext registers a context as scoped by default, while hosted services and other singleton components live for the application’s lifetime. Microsoft’s .NET service-lifetime guidance warns against injecting a scoped service directly into a singleton. The reviewer has to connect the constructor, registration, and execution model to see the problem.

Reviewers should not turn every newer C# feature into a style debate. Records, primary constructors, pattern matching, and collection expressions are useful when they make intent clearer. The practical question is whether a feature preserves the codebase’s contracts and remains understandable to the team that will maintain it.

How Should Review Follow a Change Through a Layered .NET Solution?

Large .NET applications often split one request across a web project, application services, domain code, persistence, integrations, and tests. Reviewing each edited file in isolation misses the handoffs between those layers.

A useful review path looks like this:

  1. Start at the entry point. Check the controller, minimal API, message consumer, scheduled job, or command handler. What input can reach it, and which authentication and authorization rules apply?
  2. Trace the application operation. Follow the call into the use case or service. Confirm that it uses the established workflow instead of reaching around it for a convenient repository call.
  3. Check the domain rules. Look for invariants enforced elsewhere, such as account status, tenant boundaries, idempotency, or state-transition rules. A new path should not create a second, weaker version of the same operation.
  4. Inspect persistence behavior. Read the LINQ query, mappings, transaction boundary, concurrency strategy, and migration together. Ask what SQL is likely to run and what happens when two requests update the same row.
  5. Follow outgoing effects. Check events, HTTP calls, cache invalidation, audit records, and retry behavior. Decide whether partial failure can leave the database and external systems disagreeing.
  6. Read the composition and build files. A change may depend on Program.cs, dependency registration, options binding, feature flags, package versions, or a .csproj property that is outside the immediate diff.
  7. Match tests to the risk. Unit tests are useful for local rules. Integration tests are often better for routing, serialization, authorization, database behavior, and dependency wiring.

Suppose an endpoint changes from returning CustomerDto to a new record type with one required property. The controller diff may look tidy. The meaningful review also checks the JSON naming policy, older clients, mapping code, OpenAPI output, nullable behavior, and tests that deserialize the response. That is where a C# edit becomes a .NET system change.

The same principle applies to tests. An in-memory substitute can prove application logic, but it may not reproduce the query translation, transactions, indexes, or constraints of the production database. The test choice should reflect the failure being prevented, not just make the coverage number move.

How Does Qodo Review C# Changes Across a .NET Solution?

Qodo supports C# and .NET code review by examining the current change with repository context, then returning prioritized findings inside the developer’s existing review flow. That context helps connect a controller edit with the service it calls, a service registration in Program.cs, an EF Core mapping, a migration, and the tests that define expected behavior.

For a .NET team, the useful capabilities include:

  • Reviewing pull requests and local changes, with findings organized around risk rather than formatting noise
  • Using the Context Engine to retrieve code and relationships beyond the open file or changed lines
  • Turning repeated engineering expectations into Review Standards, such as requiring authorization on administrative endpoints or preventing hosted services from capturing scoped dependencies
  • Checking whether tests cover the behavior changed by a pull request, including cases that cross application layers
  • Working in the repository workflow teams already use, including the documented Azure DevOps installation path

This does not replace the compiler, analyzers, unit tests, or a developer who understands the business decision. It gives those checks a review layer that can reason about the change as a connected piece of the system. The Qodo Academy guide to AI code review explains why that validation layer becomes more important as teams generate and change code faster.

Qodo’s Take on Reviewing .NET Behavior, Not Repeating the Compiler

A .NET reviewer should not compete with Roslyn for the best way to report an unused using directive. The compiler and analyzers already have an exact answer.

The harder questions are relational. Does this endpoint use the same authorization policy as its neighbors? Does a new service lifetime fit the component that consumes it? Will an EF Core query issue one request or hundreds? Does a renamed property break a client outside the solution? These questions require context, and they are easy to miss when a reviewer has a large pull request and ten minutes between meetings.

Qodo’s position is that automated review should take on that connective work and surface a small number of findings worth a developer’s attention. Teams can encode their own .NET conventions as review standards rather than hoping every reviewer remembers every rule. Human reviewers can then spend their time on intent, architecture, and tradeoffs, which is also the division of work described in Qodo’s 2026 guide to building a scalable code review process.

Example: Catching a Scoped DbContext Captured by a Hosted Service

A billing team adds a worker that exports completed invoices. The registration is ordinary:

builder.Services.AddDbContext<BillingDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddHostedService<InvoiceExportWorker>();

The worker receives the context through its constructor:

public sealed class InvoiceExportWorker(
BillingDbContext db,
IExportQueue queue) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var invoiceId in queue.ReadAllAsync(stoppingToken))
{
var invoice = await db.Invoices.FindAsync([invoiceId], stoppingToken);
await ExportAsync(invoice, stoppingToken);
}
}
}

The code is readable, and the database call is asynchronous. The problem only appears when the reviewer joins three facts:

  • BillingDbContext is scoped by default.
  • The hosted service is a singleton.
  • A long-running worker may reuse the same context across jobs, and future concurrency could start overlapping operations on it.

A context-aware review can flag the lifetime mismatch by reading both files, explain why the context should be created inside a scope for each unit of work, and point to the team’s existing worker pattern. One possible correction is to inject IDbContextFactory<BillingDbContext> and create a fresh context for each invoice:

await using var db = await dbFactory.CreateDbContextAsync(stoppingToken);
var invoice = await db.Invoices.FindAsync([invoiceId], stoppingToken);

The team can then add an integration test that starts the host, processes more than one queued job, and verifies both the export and database behavior. The value of the review is not that it recognized C# syntax. It connected service registration, object lifetime, EF Core behavior, and the way the application runs in production.

Best Suited for Enterprise .NET Teams With Layered Services

Qodo is the best AI code review, code quality, and governance platform for enterprise .NET teams maintaining layered services, shared libraries, and multiple repositories. It is especially useful when the same rules need to hold across ASP.NET Core APIs, background workers, EF Core data access, internal packages, and Azure DevOps projects.

Teams get the most value when they already have compilers, analyzers, and CI checks in place but still lose time to cross-file mistakes, inconsistent review standards, or issues discovered late by senior reviewers. Qodo adds a shared review layer around those tools while leaving architectural judgment and ownership with the engineers.

Further reading: Best AI code review tools for Azure DevOps, AI code review as an answer to AI code generation, and how to build a scalable code review process.