Skip to main content

C# Code Review Checklist

Introduction

Code reviews are a crucial part of the software development process. They help identify bugs, maintain code quality standards, and share knowledge among team members. A thorough code review can catch issues before they reach production and foster a culture of continuous improvement.

This guide provides a comprehensive checklist specifically tailored for reviewing C# code. Whether you're a beginner conducting your first code review or an experienced developer looking to standardize your review process, this checklist will help ensure you're considering the most important aspects of code quality.

Why Code Reviews Matter

Before diving into the checklist, let's understand why code reviews are essential:

  • Bug Detection: Many bugs can be caught before testing
  • Knowledge Sharing: Team members learn from each other's expertise
  • Code Consistency: Ensures codebase follows the same patterns and standards
  • Mentorship: Provides opportunities for junior developers to learn
  • Documentation: Ensures code changes are well-documented

The C# Code Review Checklist

1. Code Readability and Style

Naming Conventions

  • Variables, methods, and classes have meaningful names that indicate their purpose
  • Follow PascalCase for class names, method names, and properties
  • Use camelCase for local variables and parameters
  • Use UPPERCASE for constants

Example:

csharp
// Poor naming
public class x
{
public void y(int z)
{
int a = z * 2;
}
}

// Good naming
public class UserManager
{
private const int MAX_LOGIN_ATTEMPTS = 3;

public bool AuthenticateUser(string username)
{
int attemptCount = 0;
// Authentication logic
return true;
}
}

Code Formatting

  • Consistent indentation (usually 4 spaces in C#)
  • Appropriate use of blank lines to separate logical sections
  • Consistent brace style (typically K&R style in C#)
  • Lines aren't too long (usually 80-120 characters)

Comments and Documentation

  • XML documentation for public APIs
  • Comments explain "why" rather than "what" when necessary
  • No commented-out code

Example of good documentation:

csharp
/// <summary>
/// Authenticates a user against the database
/// </summary>
/// <param name="username">The username to authenticate</param>
/// <param name="password">The user's password</param>
/// <returns>True if authentication succeeds, false otherwise</returns>
/// <exception cref="ArgumentNullException">Thrown when username or password is null</exception>
public bool AuthenticateUser(string username, string password)
{
// Implementation
}

2. Code Structure and Organization

Classes and Methods

  • Single Responsibility Principle: Classes and methods should do one thing
  • Methods are reasonably sized (not too long)
  • Classes have appropriate access modifiers
  • Constructors initialize objects to a valid state

SOLID Principles

  • Single Responsibility: Each class has one reason to change
  • Open/Closed: Open for extension, closed for modification
  • Liskov Substitution: Subtypes must be substitutable for their base types
  • Interface Segregation: Many specific interfaces over one general interface
  • Dependency Inversion: Depend on abstractions, not implementations

Example of Dependency Inversion:

csharp
// Poor dependency management
public class UserService
{
private readonly SqlDatabase _database; // Concrete dependency

public UserService()
{
_database = new SqlDatabase();
}
}

// Better with Dependency Inversion
public class UserService
{
private readonly IDatabase _database; // Abstract dependency

public UserService(IDatabase database)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
}
}

3. C# Language Features

Modern C# Usage

  • Using appropriate C# features for the targeted framework version
  • Proper use of nullable reference types (C# 8.0+)
  • Using pattern matching when appropriate
  • Using expression-bodied members for simple methods
csharp
// Traditional property
public string FullName
{
get { return $"{FirstName} {LastName}"; }
}

// Expression-bodied property (C# 6.0+)
public string FullName => $"{FirstName} {LastName}";

// Pattern matching (C# 7.0+)
public void ProcessShape(object shape)
{
switch (shape)
{
case Circle circle when circle.Radius > 0:
Console.WriteLine($"Circle with radius {circle.Radius}");
break;
case Rectangle rectangle:
Console.WriteLine($"Rectangle with width {rectangle.Width}");
break;
case null:
throw new ArgumentNullException(nameof(shape));
default:
Console.WriteLine("Unknown shape");
break;
}
}

LINQ Usage

  • Appropriate use of LINQ for data manipulation
  • Avoiding unnecessary multiple enumerations
  • Consider performance implications of LINQ queries
csharp
// Potentially inefficient - evaluates query twice
var people = GetPeople();
var count = people.Count();
if (count > 0)
{
var firstPerson = people.First();
}

// More efficient
var people = GetPeople().ToList(); // Materialize once
var count = people.Count;
if (count > 0)
{
var firstPerson = people[0];
}

4. Error Handling

Exception Management

  • Exceptions are caught at appropriate levels
  • Specific exceptions are caught before general ones
  • Exceptions include meaningful messages
  • Exception details are logged
csharp
// Poor exception handling
try
{
// Method that could throw many types of exceptions
ProcessData(data);
}
catch (Exception ex)
{
// Catches everything with no specific handling
Console.WriteLine("Error");
}

// Better exception handling
try
{
ProcessData(data);
}
catch (FileNotFoundException ex)
{
_logger.LogError($"Configuration file not found: {ex.Message}");
// Specific recovery action
}
catch (JsonException ex)
{
_logger.LogError($"Invalid configuration format: {ex.Message}");
// Different recovery action
}
catch (Exception ex)
{
_logger.LogError($"Unexpected error during processing: {ex.Message}");
throw; // Rethrow if truly unexpected
}

Null Checking

  • Proper null checks for parameters
  • Use of the null-conditional operator (?.) and null-coalescing operator (??) when appropriate
  • With C# 8.0+, appropriate use of nullable reference types
csharp
// Traditional null checking
public string GetUserDisplayName(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}

if (user.Profile == null)
{
return user.Username;
}

return user.Profile.DisplayName ?? user.Username;
}

// Modern null checking (C# 6.0+)
public string GetUserDisplayName(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}

return user.Profile?.DisplayName ?? user.Username;
}

// With nullable reference types (C# 8.0+)
public string GetUserDisplayName(User user)
{
ArgumentNullException.ThrowIfNull(user);
return user.Profile?.DisplayName ?? user.Username;
}

5. Performance Considerations

Resource Management

  • IDisposable objects are properly disposed (using statements)
  • Connections, files, and other resources are closed properly
  • Large objects are properly managed
csharp
// Poor resource management
public void ReadFile(string path)
{
StreamReader reader = new StreamReader(path);
var content = reader.ReadToEnd();
// reader is never closed/disposed
}

// Good resource management
public void ReadFile(string path)
{
using (StreamReader reader = new StreamReader(path))
{
var content = reader.ReadToEnd();
} // reader is automatically disposed here
}

// C# 8.0+ using declaration
public void ReadFile(string path)
{
using StreamReader reader = new StreamReader(path);
var content = reader.ReadToEnd();
// reader is disposed at the end of the method
}

Optimizations

  • Avoiding premature optimization
  • Appropriate algorithm choices
  • Efficient string operations (StringBuilder for multiple concatenations)
  • Avoiding excessive object creation
csharp
// Inefficient string concatenation
public string BuildReport(string[] lines)
{
string report = "";
foreach (var line in lines)
{
report += line + Environment.NewLine; // Creates a new string each iteration
}
return report;
}

// More efficient with StringBuilder
public string BuildReport(string[] lines)
{
StringBuilder report = new StringBuilder();
foreach (var line in lines)
{
report.AppendLine(line);
}
return report.ToString();
}

6. Security Considerations

Input Validation

  • All user inputs are validated
  • Potential injection attacks are prevented
  • Sensitive data is not exposed
csharp
// Vulnerable to SQL injection
public User GetUser(string username)
{
string query = $"SELECT * FROM Users WHERE Username = '{username}'";
// Execute query directly - vulnerable!
}

// Better approach with parameterization
public User GetUser(string username)
{
string query = "SELECT * FROM Users WHERE Username = @username";
// Execute with parameter
using (var command = new SqlCommand(query, connection))
{
command.Parameters.Add("@username", SqlDbType.NVarChar).Value = username;
// Execute safely
}
}

Authorization

  • Proper authorization checks before performing sensitive operations
  • Principle of least privilege is followed
  • No hardcoded credentials

7. Testability

Unit Testing Considerations

  • Code is organized to be testable
  • Dependencies are injectable
  • Complex conditions are testable
csharp
// Hard to test
public class OrderProcessor
{
public bool ProcessOrder(Order order)
{
var paymentGateway = new PaymentGateway();
var inventoryService = new InventoryService();

// Process with concrete implementations
}
}

// Testable with dependency injection
public class OrderProcessor
{
private readonly IPaymentGateway _paymentGateway;
private readonly IInventoryService _inventoryService;

public OrderProcessor(IPaymentGateway paymentGateway, IInventoryService inventoryService)
{
_paymentGateway = paymentGateway;
_inventoryService = inventoryService;
}

public bool ProcessOrder(Order order)
{
// Process with injected dependencies that can be mocked in tests
}
}

Real-World Code Review Example

Let's look at a complete example of before and after code review changes:

Before Review

csharp
public class DataHandler
{
public void Process()
{
try
{
var conn = new SqlConnection("connectionstring");
conn.Open();
var cmd = new SqlCommand("SELECT * FROM Users WHERE Username='" + Request.QueryString["user"] + "'", conn);
var reader = cmd.ExecuteReader();

string result = "";
while (reader.Read())
{
result = result + reader["Name"] + ",";
}

if (result != "")
DisplayResults(result);
}
catch (Exception e)
{
Console.WriteLine("Error");
}
}

void DisplayResults(string r)
{
// Display logic
}
}

After Review

csharp
public class UserDataHandler
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<UserDataHandler> _logger;

public UserDataHandler(IDbConnectionFactory connectionFactory, ILogger<UserDataHandler> logger)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<IEnumerable<string>> GetUserNamesAsync(string username)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Username cannot be empty", nameof(username));
}

List<string> results = new List<string>();

try
{
using (var connection = _connectionFactory.CreateConnection())
{
await connection.OpenAsync();

using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT Name FROM Users WHERE Username = @Username";
var parameter = command.CreateParameter();
parameter.ParameterName = "@Username";
parameter.Value = username;
command.Parameters.Add(parameter);

using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
results.Add(reader["Name"].ToString());
}
}
}
}

return results;
}
catch (SqlException ex)
{
_logger.LogError(ex, "Database error occurred while retrieving user data for {Username}", username);
throw new DataAccessException("Error retrieving user data", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred while processing user data for {Username}", username);
throw;
}
}

public void DisplayResults(IEnumerable<string> names)
{
// Display logic
}
}

Improvements Made:

  1. Naming: Changed class and method names to be more descriptive
  2. Dependencies: Added dependency injection for testability
  3. Security: Fixed SQL injection vulnerability with parameterized query
  4. Resource Management: Proper disposal of connections with using statements
  5. Error Handling: Better exception handling with specific catches and logging
  6. Performance: More efficient data collection with List<T> instead of string concatenation
  7. Async: Updated to use async methods for better scalability
  8. Input Validation: Added checking for empty usernames
  9. Responsibility: Split data access and display logic more clearly

Common Code Review Pitfalls to Avoid

As a reviewer, be careful to:

  1. Focus on the code, not the person: Frame feedback constructively
  2. Avoid nitpicking: Focus on important issues rather than personal style preferences
  3. Balance positives and negatives: Point out good practices too
  4. Set clear expectations: Have documented standards to refer to
  5. Consider the context: Some technical debt might be acceptable in certain situations

Code Review Tools for C#

Several tools can help automate parts of the code review process:

  • StyleCop: Analyzes C# code for style guideline conformance
  • SonarQube: Detects code smells, bugs, and security vulnerabilities
  • ReSharper: Provides suggestions for code improvements
  • Visual Studio Code Analysis: Built into Visual Studio
  • GitHub Actions: Can automate running code quality checks

Summary

A thorough code review is invaluable for maintaining high-quality C# code. This checklist covers the essential aspects to examine:

  • Code readability and style
  • Code structure and organization
  • C# language features
  • Error handling
  • Performance considerations
  • Security considerations
  • Testability

Remember that code reviews should be a positive experience that helps the entire team grow and improve. The goal is not to find fault but to ensure the best possible code reaches production.

By consistently applying this code review checklist, you'll gradually establish higher standards across your codebase and build a stronger development culture.

Additional Resources

Exercises

  1. Review Practice: Find an open-source C# project on GitHub and review a pull request using this checklist
  2. Self-Review: Apply this checklist to code you wrote more than 3 months ago
  3. Team Standards: Work with your team to create a customized version of this checklist that fits your specific project needs
  4. Automation: Set up at least one static analysis tool to run automatically on your codebase


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)