Refactoring Legacy Code in C#: A Step-by-Step Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Refactoring Legacy C# Code is a Necessity



Every .NET developer sooner or later encounters legacy code. This is not necessarily a 20-year-old project on .NET Framework 1.1 — often code written a year ago in a hurry, without tests and violating SOLID principles, becomes "legacy." Working with such code turns into a nightmare: any change breaks functionality in unexpected places, and understanding the logic is only possible through debugging.



Refactoring is not rewriting from scratch. It is improving the internal structure of the code without changing its external behavior. In this article, we will analyze practical techniques for refactoring legacy C# code that can be applied immediately, minimizing risks.



1. Preparation: The Golden Rule of Refactoring



Before changing code, make sure you have a "safety net" — a set of automated tests. Without them, refactoring turns into guesswork.



Creating Characterization Tests

If there are no tests, write characterization tests (also known as golden master or snapshot tests). The idea is simple: run the method with different input data, record the result, and then verify that the result has not changed after refactoring.



// Example of a characterization test for a legacy method[Test]public void CalculateTotal_ShouldReturnSameResult_AfterRefactoring(){    // Arrange    var order = new Order { Items = new List<Item> { new Item { Price = 100, Quantity = 2 } } };    var expected = 200; // Pre-known result

// Act var result = LegacyOrderCalculator.CalculateTotal(order);

// Assert Assert.AreEqual(expected, result);}


Tip: Use libraries like Verify or ApprovalTests for automatic snapshot testing.



2. Technique #1: Extract Method



The simplest and most effective technique. When you see a method 200 lines long with comments like "// discount calculation here" — it is an ideal candidate.



Example: Large Method



public decimal ProcessOrder(Order order){    decimal total = 0;    foreach (var item in order.Items)    {        total += item.Price * item.Quantity;    }

// Discount calculation if (order.Customer.IsPremium) { total *= 0.9m; // 10% discount } else if (order.TotalAmount > 1000) { total *= 0.95m; // 5% discount }

// Applying tax total *= 1.2m; // 20% tax

return total;}


After Refactoring



public decimal ProcessOrder(Order order){    decimal subtotal = CalculateSubtotal(order.Items);    decimal afterDiscount = ApplyDiscount(subtotal, order.Customer);    return ApplyTax(afterDiscount);}

private decimal CalculateSubtotal(IEnumerable<Item> items){ return items.Sum(item => item.Price * item.Quantity);}

private decimal ApplyDiscount(decimal total, Customer customer){ if (customer.IsPremium) return total * 0.9m; if (total > 1000) return total * 0.95m; return total;}

private decimal ApplyTax(decimal amount){ return amount * 1.2m;}


Benefits:

  • Each method has a single responsibility
  • The code becomes readable and testable
  • The discount logic can be reused in other places


3. Technique #2: Replace Conditional Logic with Polymorphism



Legacy code often contains long chains of if-else or switch statements. This violates the Open/Closed Principle (OCP).



Example: Switch by Document Type



public decimal CalculateShippingCost(Order order){    switch (order.ShippingType)    {        case "Standard":            return 5;        case "Express":            return 15;        case "Overnight                

Blogs

Book Recommendations