Legacy code is a challenge every software team faces. Old systems often slow down delivery, introduce technical debt, and make maintenance difficult. As senior developers and architects, our goal is to modernize these systems while preserving business logic—making them maintainable, performant, and scalable.
Architects: AI won’t replace you—it will elevate your architectural decisions.
Sr. Devs: Treat AI as a refactoring assistant, not just an autocomplete tool.
In this article, I share practical examples of how to refactor legacy C# code for clarity, performance, and modern best practices.
Example 1: Simplifying Loops with LINQ
Legacy code often relies on manual loops and conditional checks:
Before:
public List<Order> GetOrders(int customerId)
{
var orders = new List<Order>();
foreach (var o in _dbContext.Orders)
{
if (o.CustomerId == customerId && o.IsActive)
{
orders.Add(o);
}
}
return orders;
}
Refactored:
public async Task<List<Order>> GetActiveOrdersAsync(int customerId) =>
await _dbContext.Orders
.Where(o => o.CustomerId == customerId && o.IsActive)
.ToListAsync();
Benefits:
- Async-ready for non-blocking database operations
- Clearer intent with fewer lines of code
- Easier to maintain and extend
Example 2: Declarative Filtering
Nested loops for filtering collections can be simplified using LINQ:
Before:
public List<int> GetEvenNumbers(List<int> numbers)
{
var evens = new List<int>();
foreach (var n in numbers)
{
if (n % 2 == 0)
{
evens.Add(n);
}
}
return evens;
}
After:
public List<int> GetEvenNumbers(List<int> numbers) =>
numbers.Where(n => n % 2 == 0).ToList();
Cleaner, more readable, and easier to maintain.
Example 3: Modern String Handling
String concatenation can be error-prone and harder to read.
Before:
public string GetFullName(string first, string last)
{
return first + " " + last;
}
After:
public string GetFullName(string first, string last) =>
$"{first} {last}";
Benefits:
- Modern, readable syntax
- Less risk of subtle bugs
Example 4: Pattern Matching for Switch Statements
Traditional switch statements can be verbose. C# pattern matching simplifies them:
Before:
public string GetStatus(int code)
{
switch (code)
{
case 200: return "OK";
case 404: return "Not Found";
case 500: return "Error";
default: return "Unknown";
}
}
After:
public string GetStatus(int code) =>
code switch
{
200 => "OK",
404 => "Not Found",
500 => "Error",
_ => "Unknown"
};
More concise, modern, and easier to extend.
Example 5: Proper Logging Practices
Many legacy systems still use Console.WriteLine
for error handling, which is not suitable for production:
Before:
public void Process()
{
try
{
DoWork();
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
After:
public void Process()
{
try
{
DoWork();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while processing");
}
}
Benefits:
- Centralized, structured logging
- Easier debugging and monitoring
Example 6: Converting Blocking Calls to Async
Blocking HTTP calls is common in older codebases. Modern async patterns improve responsiveness:
Before:
public string GetData()
{
var client = new HttpClient();
var response = client.GetAsync("https://api.example.com").Result;
return response.Content.ReadAsStringAsync().Result;
}
After:
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
var response = await client.GetAsync("https://api.example.com");
return await response.Content.ReadAsStringAsync();
}
Benefits:
- Non-blocking I/O
- Scales better under load
- Follows modern .NET async/await best practices
Key Takeaways for Architects and Senior Developers
- Refactoring improves maintainability: Modern syntax, async/await, LINQ, and pattern matching reduce complexity.
- Small changes make a big difference: Cleaner code reduces bugs, improves readability, and simplifies onboarding.
- Logging and observability matter: Replace ad-hoc
Console.WriteLine
with structured logging. - Async-first design is critical: Avoid blocking calls in modern applications.
Modernizing legacy code is not just about “making it work”—it’s about making it sustainable, maintainable, and scalable. With the right practices, even decades-old systems can evolve without rewriting them from scratch.