Course contentsShow
← C#
Lesson 1713 of 1,90815. Software Quality and TestingFree lesson

Extracting Logic from Bloated Controllers

Move complex logic into service classes to enable granular unit testing.

In this lesson, you will learn how to improve testability and maintainability by offloading complex business rules from your controllers into dedicated service classes.

The Bloated Controller Problem

In ASP.NET Core, it is tempting to write business logic directly inside controller actions. While convenient, this creates "bloated controllers" that are nearly impossible to unit test effectively. When your controller is responsible for orchestration, data transformation, validation, and calling external APIs, your test suite must mock the entire HTTP context just to verify a simple calculation.

Moving Logic to Services

To fix this, we apply the Single Responsibility Principle. Your controller should act only as a traffic cop: receiving the request, delegating the work to a service, and returning the appropriate HTTP response.

Consider this bloated action:

[HttpPost]
public IActionResult ProcessOrder(OrderDto dto)
{
    // Bloated: Logic belongs in a service
    var total = dto.Items.Sum(i => i.Price) * 0.9m; 
    var order = new Order { Total = total, Date = DateTime.UtcNow };
    _dbContext.Orders.Add(order);
    _dbContext.SaveChanges();
    _emailService.SendConfirmation(order);
    return Ok();
}

By extracting the core logic into an IOrderService, we can test the calculation and processing flow in isolation without needing to simulate a web request.

// The controller becomes a thin pass-through
[HttpPost]
public IActionResult ProcessOrder(OrderDto dto)
{
    var result = _orderService.PlaceOrder(dto);
    return Ok(result);
}

Why This Enables Granular Testing

When you extract logic into an OrderService, you can create a unit test project that instantiates the service directly. You no longer need to mock HttpContext, ModelState, or ControllerContext. Instead, you mock only the interfaces the service requires (like IRepository or IEmailService). This results in tests that are faster, more readable, and much less brittle, as they are decoupled from the framework’s routing infrastructure.

By moving business logic into testable service layers, you ensure that your code remains resilient to changes in your web framework while maintaining high coverage for your core business rules.

Key Takeaway: Controllers should be thin orchestrators; by moving logic into services, you unlock the ability to write fast, granular, and isolated unit tests for your business domain.