Refactoring Legacy C# Code: From A to Z
Working with legacy code is an inevitable reality for any .NET developer. Old C# projects often contain "spaghetti code," magic numbers, huge classes, and a lack of tests. Instead of rewriting everything from scratch (which is expensive and risky), competent refactoring allows you to gradually improve the code, reducing technical debt. In this article, we'll break down a step-by-step strategy for refactoring legacy C# code with concrete examples.
1. Preparation for Refactoring: Safety First
The main rule of refactoring legacy code is don't break what works. Without tests, any change can lead to bugs in production. Therefore, the first stage is creating a "safety net."
1.1. Adding Tests (Characterization Tests)
If there are no tests, they need to be written. Use the Characterization Tests approach. The idea: you run the existing code with different inputs and record the current behavior as a baseline.
// Example for a legacy methodpublic class LegacyCalculator{ public int CalculateTotal(int a, int b) { // Legacy code we don't fully understand return a + b * 2; // Suspicious logic }}
// Characterization test[Test]public void CalculateTotal_WhenCalled_ReturnsCurrentBehavior(){ var calc = new LegacyCalculator(); int result = calc.CalculateTotal(3, 4); // Record the current result as expected Assert.AreEqual(11, result); // 3 + 4*2 = 11}After writing such tests, you can safely refactor, knowing the tests will catch regressions.
1.2. Isolating Dependencies
Legacy code is often tightly coupled to a database, file system, or external services. Use Extract Interface and dependency injection (DI) for isolation.
// Before: tight dependencypublic class OrderProcessor{ private readonly SqlDatabase _db = new SqlDatabase(); public void Process(Order order) { _db.Save(order); }}
// After: interface and DIpublic interface IOrderRepository{ void Save(Order order);}
public class OrderProcessor{ private readonly IOrderRepository _repository; public OrderProcessor(IOrderRepository repository) { _repository = repository; } public void Process(Order order) { _repository.Save(order); }}2. Key Techniques for Refactoring Legacy C# Code
After preparation, you can start improving the code. Here are the key techniques that work with .NET projects.
2.1. Extract Method
Huge methods with 200+ lines are a classic legacy trait. Break them down into small, readable methods.
// Before refactoringpublic void ProcessInvoice(Invoice invoice){ // 50 lines of discount calculation decimal discount = 0; if (invoice.Total > 1000) discount = 0.1m; else if (invoice.Total > 500) discount = 0.05m; // ... much more code // 30 lines of email sending // ... }
// After refactoringpublic void ProcessInvoice(Invoice invoice){ decimal discount = CalculateDiscount(invoice.Total); SendInvoiceEmail(invoice);}
private decimal CalculateDiscount(decimal total){ if (total > 1000) return 0.1m; if (total > 500) return 0.05m; return 0;}
private void SendInvoiceEmail(Invoice invoice){ // Sending logic}2.2. Replace Magic Numbers with Constants
Magic numbers make code unclear. Replace them with constants.
// Badif (user.Role == 3) { /* admin */ }
// Goodpublic enum UserRole{ Guest = 1, User = 2, Admin = 3}
if (user.Role == (int)UserRole.Admin) { /* admin */ }