
Master C# code review best practices with a practical checklist, real code examples, and tips to catch bugs early. Level up your .NET team today.
Code review best practices separate high-performing C# teams from teams that ship bugs on Friday afternoons. A good pull request review catches defects before they reach production, spreads knowledge across the team, and keeps a codebase consistent as it grows. A bad one is a rubber stamp — or worse, a week-long argument about brace placement while a null reference bug sails through untouched.
In this guide, you'll learn how to do a code review that actually improves .NET code quality: what to look for in C# specifically, how to structure your review process, a practical C# code review checklist, and the common pitfalls that quietly kill team velocity. Whether you're a junior developer reviewing your first pull request or a tech lead standardizing the process for a whole team, these practices apply.
Why Code Review Best Practices Matter for C# Teams
Research consistently shows that code review is one of the cheapest ways to find defects. Studies at Microsoft and Google found that reviews catch a meaningful percentage of bugs before testing even begins — and the earlier a bug is found, the cheaper it is to fix. But the value goes beyond bug-hunting:
- Knowledge sharing: Reviews prevent "bus factor" problems where only one person understands the payment service.
- Consistency: A codebase written by ten people should read like it was written by one.
- Mentorship: Juniors learn idiomatic C# faster from review feedback than from any tutorial.
- Design pressure: Knowing someone will read your code makes you write better code. This effect is real and measurable.
The catch: these benefits only materialize when reviews focus on the right things. Let's define what "the right things" means in C#.
The C# Code Review Checklist: What to Actually Look For
Review in order of impact. Architecture problems are expensive to fix later; naming nits are cheap. Work down this hierarchy.
1. Correctness — Does the Code Do What It Claims?
Read the pull request description first, then verify the code delivers it. In C#, pay special attention to null handling, async correctness, and resource disposal — the three areas where subtle bugs hide most often.
Here's a method that looks reasonable but contains two classic C# bugs a reviewer should catch:
// ❌ Before review: two hidden bugs
public async Task<List<Order>> GetRecentOrdersAsync(int customerId)
{
var client = new HttpClient(); // Bug 1: socket exhaustion under load
var response = await client.GetAsync($"/api/orders/{customerId}");
var json = await response.Content.ReadAsStringAsync();
// Bug 2: no status check — a 404 deserializes to null,
// and callers get a NullReferenceException later
return JsonSerializer.Deserialize<List<Order>>(json);
}
// ✅ After review feedback
public class OrderService
{
private readonly HttpClient _client; // injected via IHttpClientFactory
public OrderService(HttpClient client) => _client = client;
public async Task<IReadOnlyList<Order>> GetRecentOrdersAsync(
int customerId,
CancellationToken cancellationToken = default)
{
var response = await _client.GetAsync(
$"/api/orders/{customerId}", cancellationToken);
response.EnsureSuccessStatusCode();
var orders = await response.Content
.ReadFromJsonAsync<List<Order>>(cancellationToken: cancellationToken);
return orders ?? [];
}
}
Notice why each change matters: IHttpClientFactory prevents socket exhaustion (a production outage waiting to happen), EnsureSuccessStatusCode converts silent failures into loud ones, the CancellationToken lets callers abandon work cleanly, and returning an empty list instead of null protects every caller from a NullReferenceException.
2. Async/Await Correctness
Async bugs are the most common serious defects in modern C# codebases. Every reviewer should scan for these patterns:
// ❌ async void — exceptions crash the process, callers can't await
public async void SaveChanges() { await _db.SaveChangesAsync(); }
// ❌ .Result / .Wait() — deadlock risk and thread starvation
var user = GetUserAsync(id).Result;
// ❌ Fire-and-forget without error handling
_ = ProcessQueueAsync(); // exceptions vanish silently
// ✅ async Task, awaited properly, token flowed through
public async Task SaveChangesAsync(CancellationToken ct = default)
{
await _db.SaveChangesAsync(ct);
}
The rule to enforce in review: async all the way down. Any .Result, .Wait(), or async void (outside of event handlers) deserves a comment asking for justification.
3. Exception Handling and Resource Management
Look for swallowed exceptions and undisposed resources:
// ❌ Swallows everything, including bugs you want to know about
try
{
ProcessPayment(order);
}
catch (Exception)
{
// ignore
}
// ✅ Catch what you can handle, log with context, let the rest propagate
try
{
ProcessPayment(order);
}
catch (PaymentDeclinedException ex)
{
_logger.LogWarning(ex,
"Payment declined for order {OrderId}", order.Id);
return PaymentResult.Declined(ex.Reason);
}
Also verify that anything implementing IDisposable is wrapped in a using statement or registered with the DI container — SqlConnection, Stream, MemoryCache instances created ad hoc, and so on.
4. Performance Where It Matters
Don't demand micro-optimizations everywhere, but do flag patterns that hurt at scale. The classic one in Entity Framework Core codebases is the N+1 query:
// ❌ N+1: one query for customers, then one query PER customer
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
// Lazy loading fires a separate SQL query each iteration
Console.WriteLine(customer.Orders.Count);
}
// ✅ One query with a join, projecting only what you need
var summaries = await _db.Customers
.Select(c => new { c.Name, OrderCount = c.Orders.Count })
.ToListAsync();
Other performance flags worth raising in a pull request review: string concatenation inside loops (suggest StringBuilder), calling .ToList() mid-query and filtering in memory, and repeated LINQ enumeration of an IEnumerable that hits a database or network.
5. Tests, Naming, and Readability
Verify the pull request includes tests for new behavior — especially edge cases like null inputs, empty collections, and cancellation. Then check readability: could a developer unfamiliar with this feature understand the code in six months? If you had to read a method three times, say so. That confusion is data.
How to Do a Code Review: The Process Side
Knowing what to look for is half the battle. The other half is process — and this is where most teams fail.
Keep Pull Requests Small
This is the single highest-leverage code review best practice. Data from multiple studies shows defect detection drops sharply after about 400 lines of changed code. A 2,000-line PR doesn't get reviewed; it gets skimmed and approved. Aim for PRs under 400 lines, and split large features into stacked, independently reviewable changes: schema first, then service layer, then API endpoint.
Review Promptly — Within One Business Day
A PR that sits for three days blocks its author, goes stale against the main branch, and forces expensive context-switching. High-performing teams treat review turnaround as a team SLA: first response within a few hours, full review within one business day. Reviewing 200 lines takes 15 minutes; make it the thing you do between tasks, not the thing you batch for Friday.
Automate Everything a Machine Can Check
Human review time is expensive. Never spend it on things a tool catches for free:
- Formatting: enforce with
dotnet formatand an.editorconfigcommitted to the repo. - Style and common bugs: enable .NET analyzers (
<AnalysisLevel>latest</AnalysisLevel>) and treat warnings as errors in CI. - Null safety: turn on nullable reference types (
<Nullable>enable</Nullable>) so the compiler flags null bugs before any human looks. - Tests and coverage: run in CI; a red build means the review hasn't started yet.
When machines handle style, humans can spend their attention on design, correctness, and clarity — the things machines can't judge.
Write Comments That Land Well
The fastest way to destroy a review culture is comments that feel like attacks. Three habits fix this:
- Comment on the code, not the person. "This method swallows the exception" rather than "you're ignoring errors again."
- Ask questions when unsure. "What happens if
ordersis empty here?" invites explanation instead of demanding a defense. - Label severity. Prefix nits explicitly —
nit: consider renaming to GetActiveUsers— so authors know what blocks approval and what's optional. Many teams adopt conventional comments for exactly this reason.
And say what's good. "Nice use of IAsyncEnumerable here — this will scale much better" costs five seconds and builds the trust that makes critical feedback land.
Common Code Review Pitfalls (and How to Avoid Them)
- The rubber stamp. Approving without reading is worse than no review — it creates false confidence. If you don't have time to review properly, say so and hand it off.
- Bikeshedding. Twelve comments about naming and zero about the missing transaction around two database writes. Review in priority order: correctness, design, tests, then style.
- Scope creep in review. "While you're in this file, could you also refactor…" — no. File an issue. Review the change that was made.
- Perfectionism as a blocker. The standard is "this improves the codebase and has no defects," not "this is exactly how I would have written it." If it's correct, tested, and readable, approve it.
- Reviewing the diff without the context. A change can look fine in isolation and still break an invariant elsewhere. Pull the branch and run it when the change is risky.
A One-Page C# Code Review Checklist
Pin this next to your pull request template:
- ✅ Does the change do what the PR description says — and only that?
- ✅ Null handling: nullable annotations respected, no unchecked deserialization results?
- ✅ Async: no
async void, no.Result/.Wait(),CancellationTokenflowed through? - ✅ Exceptions caught narrowly, logged with context, never silently swallowed?
- ✅
IDisposableresources disposed;HttpClientviaIHttpClientFactory? - ✅ No N+1 queries, in-memory filtering of database queries, or repeated enumeration?
- ✅ Tests cover the new behavior, including edge cases and failure paths?
- ✅ Names reveal intent; a stranger could follow the logic in six months?
- ✅ No secrets, connection strings, or API keys in the diff?
Conclusion: Key Takeaways
Code review best practices come down to a simple principle: spend scarce human attention where machines can't. Automate formatting, style, and null checking with analyzers and .editorconfig; keep pull requests under 400 lines; respond within a business day; and focus your review on correctness, async safety, exception handling, and readability — the areas where C# bugs actually live.
Key takeaways for your team:
- Small PRs get real reviews; big PRs get rubber stamps.
- The three C# bug hotspots to always check: null handling, async/await misuse, and resource disposal.
- Automate style so humans can review design.
- Kind, specific, severity-labeled comments build a culture where review makes everyone better.
- Approve when the code improves the codebase — not when it matches your personal taste.
Start with the one-page checklist above on your very next pull request review. Within a sprint, you'll catch bugs earlier, argue less, and ship with more confidence.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment