C# Architecture Mastery — Refactoring Legacy ASP.NET Core Apps Toward Clean Architecture (Part 10)
Most ASP.NET Core systems don’t start broken.
They become broken.
Legacy systems are rarely the result of bad developers — they are the result of good developers under pressure.
In this Part 10, we’ll cover how senior teams refactor legacy ASP.NET Core applications toward Clean Architecture incrementally, without rewrites, big-bang refactors, or feature freezes.
This is about controlled evolution, not demolition.
1. The First Rule of Legacy Refactoring
Never rewrite what you can reshape.
Rewrites fail because:
- Business logic is poorly understood
- Edge cases are undocumented
- Delivery stops
Clean Architecture refactoring is about creating seams, not perfection.
2. Identify the Pain Points (Not the Layers)
Before touching code, identify:
- Fat controllers
- God services
- DbContext leaks
- Un-testable logic
- Fear of change
These indicate where to start, not what to redesign wholesale.
3. Step 1 — Stabilize with Characterization Tests
Before refactoring, protect behavior.
// Characterization test
[Fact]
public async Task CreateOrder_Current_Behavior_Is_Preserved()
{
var response = await client.PostAsJsonAsync("/orders", request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
These tests:
- Capture current behavior
- Prevent accidental regressions
- Buy refactoring safety
They are not pretty.
They are essential.
4. Step 2 — Thin the Controllers First
Legacy controllers often contain:
- Business rules
- Data access
- Mapping logic
Refactor by extracting use cases, not by redesigning everything.
// Before
public IActionResult Create(OrderDto dto) { /* everything */ }
// After
public IActionResult Create(OrderDto dto)
{
_useCase.Execute(dto);
return Ok();
}
This is the highest ROI refactor you can do.
5. Step 3 — Introduce Application Boundaries
Create an Application layer and move logic there.
Application/
└─ Orders/
└─ CreateOrderUseCase.cs
Controllers become adapters.
Use cases become the center.
6. Step 4 — Contain DbContext Leaks
Do NOT remove EF Core immediately.
Instead:
- Introduce repository interfaces
- Wrap DbContext access
- Move EF usage outward
public interface IOrderRepository
{
Task SaveAsync(Order order);
}
This creates a migration seam.
7. Step 5 — Extract the Domain Gradually
Legacy domains are often anemic.
Start small:
- Value objects
- Invariants
- Behavior-rich entities
You do not need a “perfect” domain.
You need better boundaries than yesterday.
8. Step 6 — Fix Dependency Direction
Refactoring rule:
Dependencies must always point inward.
Move:
- Interfaces inward
- Implementations outward
Use DI to wire the system at the edge.
9. What NOT to Do (Common Refactoring Traps)
Avoid:
- Big-bang rewrites
- Framework purges
- Premature abstractions
- Over-modeling the domain
- Freezing feature work
Refactoring must coexist with delivery.
10. Measuring Progress (Not Perfection)
You’re winning when:
- Tests get easier to write
- Controllers get smaller
- Logic moves inward
- EF Core becomes less visible
- Changes feel safer
Architecture improves gradually.
Final Thoughts
Clean Architecture is not a destination.
It is a direction.
Legacy systems don’t need perfection — they need momentum in the right direction.
Refactor for safety.
Refactor for clarity.
Refactor continuously.
✍️ Written by Cristian Sifuentes — helping teams modernize legacy .NET systems without rewrites, fear, or lost velocity.

Top comments (0)