.NET Code Quality
Introduction
Writing code that works is just the first step in software development. Writing high-quality, maintainable code that other developers can understand and extend is what separates professional developers from beginners. In this guide, we'll explore what code quality means in the .NET ecosystem, why it matters, and practical techniques to improve the quality of your codebase.
Code quality encompasses many aspects:
- Readability and maintainability
- Performance and efficiency
- Security and reliability
- Adherence to best practices and standards
- Testability
Let's dive into how you can enhance each of these aspects in your .NET projects.
Why Code Quality Matters
Before we get into the "how," let's understand the "why":
- Reduced Maintenance Costs: Well-written code is easier to maintain and extend.
- Faster Onboarding: New team members can understand high-quality code more quickly.
- Fewer Bugs: Following best practices typically results in fewer defects.
- Better Performance: Quality code often performs better and uses resources more efficiently.
- Enhanced Security: Quality includes writing secure code that protects user data.
Code Organization and Readability
Naming Conventions
Using clear, descriptive names is fundamental to readable code:
// Poor naming
public decimal Calc(decimal a, decimal b)
{
return a * b;
}
// Better naming
public decimal CalculatePrice(decimal unitPrice, decimal quantity)
{
return unitPrice * quantity;
}
Consistent Formatting
Consistent formatting makes code easier to read:
// Inconsistent formatting
public void ProcessOrder(Order order){
if(order.IsValid){
order.Process();
submitToDatabase(order);
}
}
// Consistent formatting
public void ProcessOrder(Order order)
{
if (order.IsValid)
{
order.Process();
SubmitToDatabase(order);
}
}
Proper Comments
Comments should explain "why" not "what":
// Poor comment - explains what the code does, which is already clear
// Increment counter by 1
counter++;
// Better comment - explains why this operation is necessary
// Increment active connections counter to maintain accurate server load statistics
activeConnectionsCounter++;
Code Structure and Design
Single Responsibility Principle
Each class should have a single responsibility:
// Poor design: Class does too many things
public class UserManager
{
public User GetUser(int id) { /* ... */ }
public void SaveUser(User user) { /* ... */ }
public void SendEmail(User user, string message) { /* ... */ }
public void GenerateReport(List<User> users) { /* ... */ }
}
// Better design: Separate responsibilities
public class UserRepository
{
public User GetUser(int id) { /* ... */ }
public void SaveUser(User user) { /* ... */ }
}
public class NotificationService
{
public void SendEmail(User user, string message) { /* ... */ }
}
public class ReportGenerator
{
public void GenerateUserReport(List<User> users) { /* ... */ }
}
Dependency Injection
Use dependency injection to increase testability and reduce coupling:
// Tightly coupled code
public class OrderProcessor
{
private readonly DatabaseService _database = new DatabaseService();
public void Process(Order order)
{
// Processing logic
_database.Save(order);
}
}
// Using dependency injection
public class OrderProcessor
{
private readonly IDatabaseService _database;
public OrderProcessor(IDatabaseService database)
{
_database = database;
}
public void Process(Order order)
{
// Processing logic
_database.Save(order);
}
}
Error Handling
Proper Exception Handling
Handle exceptions appropriately:
// Poor error handling
public void SaveData(string data)
{
File.WriteAllText("data.txt", data);
}
// Better error handling
public void SaveData(string data)
{
try
{
File.WriteAllText("data.txt", data);
}
catch (IOException ex)
{
// Log the specific error
Logger.LogError($"Failed to write to data file: {ex.Message}");
throw new DataSaveException("Could not save data. See inner exception for details.", ex);
}
}
Use Custom Exceptions
Create custom exceptions for domain-specific errors:
public class InsufficientFundsException : Exception
{
public decimal AttemptedAmount { get; }
public decimal AccountBalance { get; }
public InsufficientFundsException(decimal attemptedAmount, decimal accountBalance)
: base($"Attempted to withdraw {attemptedAmount:C} but account only has {accountBalance:C}")
{
AttemptedAmount = attemptedAmount;
AccountBalance = accountBalance;
}
}
// Usage
public void WithdrawFunds(decimal amount)
{
if (amount > _accountBalance)
{
throw new InsufficientFundsException(amount, _accountBalance);
}
// Process withdrawal
}
Testing for Quality
Unit Testing
Unit tests verify that your code works as expected:
[Fact]
public void CalculateTotal_WithValidItems_ReturnsCorrectSum()
{
// Arrange
var calculator = new PriceCalculator();
var items = new List<Item>
{
new Item { Price = 10.0m, Quantity = 2 },
new Item { Price = 5.0m, Quantity = 1 }
};
// Act
decimal total = calculator.CalculateTotal(items);
// Assert
Assert.Equal(25.0m, total);
}
Test Coverage
Aim for high test coverage, especially for critical business logic.
Code Analysis Tools
Static Analysis
Static analysis tools can automatically identify potential issues in your code.
Using StyleCop
StyleCop enforces coding style guidelines:
<!-- Add to your csproj file -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
Using .NET Analyzers
.NET includes built-in code analyzers:
// Code with potential issue
public void ProcessData(string input)
{
if (input == null)
{
Console.WriteLine("Input is null");
// Method continues without returning, potentially causing NullReferenceException
}
var length = input.Length; // Potential NullReferenceException
}
// Code with analyzer fix applied
public void ProcessData(string input)
{
if (input == null)
{
Console.WriteLine("Input is null");
return; // Early return prevents NullReferenceException
}
var length = input.Length;
}
Performance Considerations
Efficient Resource Usage
Be mindful of resource-intensive operations:
// Inefficient - creates a new string in each iteration
string result = "";
for (int i = 0; i < 10000; i++)
{
result += i.ToString();
}
// More efficient - uses StringBuilder
var builder = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
builder.Append(i);
}
string result = builder.ToString();
Async/Await Best Practices
Use async/await properly:
// Improper use - blocking async code
public string GetDataSync()
{
// This blocks the thread, defeating the purpose of async
return GetDataAsync().Result;
}
// Proper use
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com/data");
}
Security Best Practices
Input Validation
Always validate user input:
// Vulnerable to SQL injection
public User GetUserByUsername(string username)
{
var query = $"SELECT * FROM Users WHERE Username = '{username}'";
// Execute query...
}
// Better approach with parameterized query
public User GetUserByUsername(string username)
{
var query = "SELECT * FROM Users WHERE Username = @Username";
var parameters = new { Username = username };
// Execute query with parameters...
}
Secure Data Storage
Never store sensitive data in plaintext:
// Poor practice - storing password in plaintext
public void CreateUser(string username, string password)
{
_database.SaveUser(username, password);
}
// Better practice - hashing password
public void CreateUser(string username, string password)
{
string hashedPassword = HashPassword(password);
_database.SaveUser(username, hashedPassword);
}
private string HashPassword(string password)
{
// Use a proper password hashing library like BCrypt.NET
return BCrypt.HashPassword(password, BCrypt.GenerateSalt());
}
Code Reviews
Code reviews are a vital practice for maintaining code quality. Here are some tips for effective code reviews:
- Focus on the code, not the person: Use language like "this code might be improved" rather than "you did this wrong."
- Check for readability first: If you can't understand the code, it likely needs improvement.
- Look for test coverage: Ensure all new code has appropriate tests.
- Verify error handling: Make sure exceptions are handled properly.
- Use tools to automate simple checks: Let tools catch style and formatting issues.
Practical Example: Refactoring for Quality
Let's see a complete example of refactoring code for better quality:
Before Refactoring
// Original code with quality issues
public class DataHandler
{
public string Get(int id)
{
var conn = new SqlConnection("connection_string_here");
conn.Open();
var cmd = new SqlCommand($"SELECT Data FROM Items WHERE ID = {id}", conn);
var result = cmd.ExecuteScalar()?.ToString() ?? "";
conn.Close();
return result;
}
public void Save(int id, string data)
{
var conn = new SqlConnection("connection_string_here");
conn.Open();
var cmd = new SqlCommand($"UPDATE Items SET Data = '{data}' WHERE ID = {id}", conn);
cmd.ExecuteNonQuery();
conn.Close();
}
}
After Refactoring
// Interface for dependency injection
public interface IDbConnectionFactory
{
IDbConnection CreateConnection();
}
// Implementation of the factory
public class SqlConnectionFactory : IDbConnectionFactory
{
private readonly string _connectionString;
public SqlConnectionFactory(string connectionString)
{
_connectionString = connectionString;
}
public IDbConnection CreateConnection()
{
return new SqlConnection(_connectionString);
}
}
// Repository with better practices
public class ItemRepository
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<ItemRepository> _logger;
public ItemRepository(IDbConnectionFactory connectionFactory, ILogger<ItemRepository> logger)
{
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task<string> GetDataAsync(int id)
{
using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
var query = "SELECT Data FROM Items WHERE ID = @Id";
var parameters = new { Id = id };
try
{
return await connection.QueryFirstOrDefaultAsync<string>(query, parameters);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve data for item {ItemId}", id);
throw new DataAccessException($"Failed to retrieve data for item {id}", ex);
}
}
public async Task SaveDataAsync(int id, string data)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentException("Data cannot be null or empty", nameof(data));
}
using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
var query = "UPDATE Items SET Data = @Data WHERE ID = @Id";
var parameters = new { Id = id, Data = data };
try
{
await connection.ExecuteAsync(query, parameters);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save data for item {ItemId}", id);
throw new DataAccessException($"Failed to save data for item {id}", ex);
}
}
}
// Custom exception
public class DataAccessException : Exception
{
public DataAccessException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Key Improvements
- Dependency Injection: Added interface for database connections
- Parameterized Queries: Prevented SQL injection
- Proper Resource Disposal: Using
using
statements - Async/Await: Non-blocking database operations
- Exception Handling: Proper logging and custom exceptions
- Input Validation: Checking for null/empty parameters
- Single Responsibility: Class focused on data access only
Summary
Improving code quality in .NET applications is an ongoing process that involves attention to:
- Code Organization: Clear naming, consistent formatting, and proper comments
- Architecture: Following SOLID principles and using dependency injection
- Error Handling: Properly handling exceptions and creating custom exceptions when appropriate
- Testing: Writing comprehensive unit tests for your code
- Using Tools: Leveraging analyzers and other tools to catch issues early
- Performance: Being mindful of resource usage and async patterns
- Security: Validating input and properly handling sensitive data
- Code Reviews: Reviewing code regularly to maintain quality
By consistently applying these practices, you'll write more maintainable, robust, and efficient .NET code.
Additional Resources
- Microsoft's .NET Code Quality Documentation
- StyleCop Documentation
- Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin
- Pluralsight Course: Clean Code: Writing Code for Humans
Exercises
- Take a small .NET application and run code analyzers on it. Fix any warnings that appear.
- Refactor a method that has more than one responsibility into multiple methods.
- Add unit tests for a class in your codebase that currently lacks testing.
- Review some code you wrote a few months ago and identify areas for improvement.
- Practice a code review on a colleague's code, focusing on the quality aspects discussed in this guide.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)