DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

C# Architecture Mastery — Refactoring Legacy ASP.NET Core Apps Toward Clean Architecture (Part 10)

 C# Architecture Mastery — Refactoring Legacy ASP.NET Core Apps Toward Clean Architecture (Part 10)

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)