3 ways to keep your asp.net mvc controllers thin

May 23, 2016 · 5 minute read

You’ve been told that you should keep your asp.net controllers thin, that there shouldn’t be any logic in them, that testing controllers is pointless.

But as your application becomes more complicated, so do your controllers. Now some pesky logic has infiltrated them and they’re looking suspiciously fat. Everyone’s advice is ringing in your ears, you know it’s probably bad to have logic in your controllers but there it is.

Now that you do have logic-heavy controllers, testing them is vital as it’s in this logic that bugs like to hide.

However, if you remove the logic, the need to heavily test your controllers goes away and you can focus instead on testing your business logic, without getting hung up on testing the controller itself.

Here are three ways to reduce the logic living in your controllers.

1. Remove data access and business logic

Underpinning all of these steps is a core understanding that a controller lives on the edge of your application and is there to orchestrate, take incoming requests, pass them onto your core business logic and return the appropriate response.

Anything more and it’s doing too much.

For example, we’ve all seen (and indeed written) data access code directly in the controller. Something like this.

public class OrderController : DefaultController
{
    private StoreContext db = new StoreContext();

    public ActionResult Index(Guid accountId)
    {
        var orders = db.Orders
            .OrderByDescending(x=>x.OrderDate)
            .Where(x=>x.AccountId == request.AccountId)
            .ToList();
        return View(orders);
    }
}

But if you instead push your data access code and business logic into another class (OrdersService) you can keep your controller focused on making the call and handling the result.

public class OrderController : Controller
{
    public ActionResult Index(ListOrdersRequest request)
    {
        OrderList orders = OrdersService.ForAccount(request.AccountId);
        return View(orders);
    }
}

No matter how complicated this logic now becomes, the controller will remain simple and concise.

2. Use action filters to handle repetitive logic

There are some things that many (or all) of your controller actions might need to do.

Take model validation. You want to check that valid parameters have been passed to your controller action.

By using a filter, you can take this code out of your controller action and put it on any action you want using an attribute.

[ValidateModelState]
[HttpPost]
public ActionResult Create(CreateUserRequest request)
{
    Users.Save(request.Email);
    return RedirectToAction(nameof(List), "User");
}

In case you’re wondering nameof is new in C# 6 and saves you from using a magic string for the action parameter to RedirectToAction(). If you’re not using C# 6 you’ll need to specify “List” instead e.g. RedirectToAction("List").

The CreateUserRequest class is marked up with DataAnnotations to control what is and isn’t valid.

public class CreateUserRequest
{
    [Required, EmailAddress]
    public string Email { get; set; }
}

And here’s a simple implementation of the validation filter…

public class ValidateModelState: ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!filterContext.Controller.ViewData.ModelState.IsValid)
        {
            filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
    }
}

In this case it simply returns a Bad Request HTTP status code if the model fails validation.

3. Map your view models using AutoMapper

So now you’ve removed core business logic (retrieval of orders) and request validation. What about all the mapping code you inevitably wind up writing to transform your domain objects into view models?

In all but the simplest applications you’re going to need to perform some transformation on the business objects returned from your service layer. Perhaps you don’t need all the fields for your view, or perhaps you want to mash up the results from several different services.

Jimmy Bogard’s Automapper can help you seriously thin out your controller actions.

Here’s what it looks like in usage.

public class OrderController : Controller
{
    public ActionResult Index(ListOrdersRequest request)
    {
        OrderList orders = OrdersService.ForAccount(request.AccountId);
        return View(Mapper.Map<OrderListViewModel>(orders));
    }
}

To make this work, you’ll need to grab AutoMapper from NuGet.

Out of the box AutoMapper will attempt to automatically match up source and destination values. It is really good for flattening complex domain objects into much simpler models (for example view models).

Here’s the configuration to make our example work.

public class AutoMapping
{
    public static void Initialise()
    {
        Mapper.Initialize(cfg =>
        {
            cfg.CreateMap<Order, OrderListViewModel.OrderViewModel>();
            cfg.CreateMap<OrderList, OrderListViewModel>();                
        });
    }
}

This can be called in global.asax like so…

protected void Application_Start()
{
    AutoMapping.Initialise();          
}

In case you’re wondering what our OrderListViewModel looks like…

public class OrderListViewModel
{
    public List<OrderViewModel> Orders { get; set; } = new List<OrderViewModel>();

    public class OrderViewModel
    {
        public string OrderDate { get; set; }
    }
}

And our original domain object (OrderList) can contain many more properties, only the ones our view model requires will be mapped across.

public class OrderList
{
    public List<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public DateTime OrderDate { get; set; }
    public User OrderedBy { get; set; }
    public List<OrderLine> Lines { get; set; }
}

Update:

As Stuart pointed out in the comments. You don’t have to use the static methods of Mapper. If you wish, you can register IMapper with your dependency injection framework of choice and then inject this dependency into your controller instead.

In summary

By keeping your controllers thin you can:

  • Re-use common code across multiple controllers and actions
  • Swap out components as and when you need
  • Focus your testing efforts on the business logic that really matters

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.