ASP.NET MVC Controller Best Practices – Skinny Controllers

No blog post can cover ASP.NET MVC controller best practices in one go. So this is the first post in a series about ASP.NET MVC controller best practices.

In this post we’re looking at why your ASP.NET MVC controllers should be ‘skinny’.

The code used in this post can be downloaded here.

I’ve heard Jeffrey Palermo say if you can’t see a ASP.NET MVC action method on a screen without having to scroll, you have a problem. In most cases the problem is the controller has multiple responsibilities.

The code in this post is adapted from a code sample I saw in an ASP.NET MVC book.

The controller looks like this

using System;
using System.Linq;
using System.Web.Mvc;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;

namespace TestingMvcControllers.Controllers
{
    public class AnimalsController : Controller
    {
        public int PageSize = 4;
        private readonly IAnimalsRepository _animalsRepository;

        public AnimalsController(IAnimalsRepository animalsRepository)
        {
            _animalsRepository = animalsRepository;
        }

        public ViewResult List(string category, int page)
        {
            var animalsInCategory = (category == null)
                                        ? _animalsRepository.Animals
                                        : _animalsRepository.Animals.Where(x => x.Category == category);

            int numberOfAnimals = animalsInCategory.Count();
            ViewData["TotalPages"] = (int)Math.Ceiling((double)numberOfAnimals / PageSize);
            ViewData["CurrentPage"] = page;
            ViewData["CurrentCategory"] = category;

            return View(animalsInCategory
                            .Skip((page - 1) * PageSize)
                            .Take(PageSize)
                            .ToList()
                );
        }
    }
}

This approach will work, but what if you wanted to implement paging for another user interface? You would not be able to reuse the paging functionality in the controller and have to repeat yourself. This goes against the ‘Don’t Repeat Yourself (DRY) principle.

At this point some of you maybe be saying you ain’t gonna need it (YAGNI). Maybe… but let’s have a look at impact paging logic in the controller has when it comes to the unit testing the ASP.NET MVC controller.

using System.Collections.Generic;
using NUnit.Framework;
using Rhino.Mocks;
using TestingMvcControllers.Controllers;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;

namespace TestingMvcControllers.Tests
{
    [TestFixture]
    public class AnimalsControllerTests
    {
        private IAnimalsRepository _animalsRepository;
        private AnimalsController _controller;

        [SetUp]
        public void SetUp()
        {
            _animalsRepository = MockRepository.GenerateStub<IAnimalsRepository>();
            _controller = new AnimalsController(_animalsRepository);
        }

        [Test]
        public void List_Presents_Correct_Page_Of_Animals()
        {
            // Arrange
            _controller.PageSize = 3;

            // Act
            _animalsRepository.Stub(a => a.Animals).Return(AnimalsStub.Animals);
            var result = _controller.List(null, 2);

            // Assert
            Assert.IsNotNull(result, "Didn't render view");
            var Animals = result.ViewData.Model as IList<Animal>;
            Assert.AreEqual(2, Animals.Count, "Got wrong number of animals");
            Assert.AreEqual(2, (int)result.ViewData["CurrentPage"], "Page number was wrong");
            Assert.AreEqual(2, (int)result.ViewData["TotalPages"], "Page count was wrong");
            Assert.AreEqual("Iguana", Animals[0].Name);
            Assert.AreEqual("Loin", Animals[1].Name);
        }

        [Test]
        public void List_Includes_All_Animals_When_Category_Is_Null()
        {
            // Arrange            
            _controller.PageSize = 10;            

            // Act
            _animalsRepository.Stub(a => a.Animals).Return(AnimalsStub.Animals);
            var result = _controller.List(null, 1);

            // Assert
            Assert.IsNotNull(result, "Didn't render view");
            var Animals = (IList<Animal>)result.ViewData.Model;
            Assert.AreEqual(5, Animals.Count, "Should have got 5 animals");
        }
        
        [Test]
        public void List_Filters_By_Category_When_Requested()
        {
            // Arrnage
            _controller.PageSize = 10;

            // Act
            _animalsRepository.Stub(a => a.Animals).Return(AnimalsStub.Animals);
            var result = _controller.List("Reptile", 1);

            // Assert
            Assert.IsNotNull(result, "Didn't render view");
            var Animals = (IList<Animal>)result.ViewData.Model;
            Assert.AreEqual(3, Animals.Count, "Should have got 3 animals");
            Assert.AreEqual("Crocodile", Animals[0].Name);
            Assert.AreEqual("Snake", Animals[1].Name);
            Assert.AreEqual("Iguana", Animals[2].Name);
            Assert.AreEqual("Reptile", result.ViewData["CurrentCategory"]);
        }
    }
}

Because the controller is also responsible for paging, you have to write unit tests for paging logic on top of the unit tests you should be writing for the controller. This means you will either ending up not testing enough and/or too much and find the tests a nightmare to maintain in the future. See my post about How to Unit Test ASP.NET MVC Controllers.

How I would refactor

I would move the paging functionality into a new layer. Yes I know adding another layer is always the solution to any problem in software development ;)

The animal service layer looks like this

using System;
using System.Linq;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;

namespace TestingMvcControllers.Services
{
    public class AnimalsService : IAnimalsService
    {
        private readonly IAnimalsRepository _animalsRepository;
        public int PageSize = 4;

        public AnimalsService(IAnimalsRepository animalsRepository)
        {
            _animalsRepository = animalsRepository;
        }

        public AnimalsDto GetAnimals(string category, int page)
        {
            AnimalsDto animalsDto = new AnimalsDto();

            var animalsInCategory =  (category == null)
                            ? _animalsRepository.Animals
                            : _animalsRepository.Animals.Where(x => x.Category == category);

            int numberOfAnimals = animalsInCategory.Count();
            animalsDto.CurrentCategory = category;
            animalsDto.CurrentPage = page;
            animalsDto.TotalPages = (int)Math.Ceiling((double)numberOfAnimals / PageSize);
            animalsDto.Animals = animalsInCategory
                .Skip((page - 1)*PageSize)
                .Take(PageSize)
                .ToList();

            return animalsDto;
        }
    }
}

Notice how a ‘Dto’ object is returned from the service. While this should be done using an AutoMapper, it’s not the focus of this test. The total number of pages can now be returned to the view as part of the model, rather than as a dictionary entry in ViewData.

The controller now looks clean and simple

using System.Web.Mvc;
using TestingMvcControllers.Interfaces;

namespace TestingMvcControllers.Controllers
{
    public class BetterAnimalsController : Controller
    {        
        private readonly IAnimalsService _animalsService;

        public BetterAnimalsController(IAnimalsService animalsService)
        {
            _animalsService = animalsService;
        }

        public ViewResult List(string category, int page)
        {
            var animalsDto = _animalsService.GetAnimals(category, page);
            return View("List",animalsDto);
        }
    }
}

The unit tests for the controller are about testing the controller and nothing more!

using MvcContrib.TestHelper;
using NUnit.Framework;
using Rhino.Mocks;
using TestingMvcControllers.Controllers;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;

namespace TestingMvcControllers.Tests
{
    [TestFixture]
    public class BetterAnimalsControllerTests
    {
        private IAnimalsService _animalsService;
        private BetterAnimalsController _controller;

        [SetUp]
        public void SetUp()
        {
            _animalsService = MockRepository.GenerateStub<IAnimalsService>();
            _controller = new BetterAnimalsController(_animalsService);
        }

        [Test]
        public void The_List_Action_Returns_AnimalsDto_To_The_View()
        {
            // Arrange
            _animalsService.Stub(a => a.GetAnimals(null,0))
                .IgnoreArguments()
                .Return(new AnimalsDto());

            // Act
            var result = _controller.List(null, 0);

            // Assert            
            result.AssertViewRendered().WithViewData<AnimalsDto>();
        }

        [Test]
        public void The_List_Action_Returns_List_View()
        {
            // Arrange
            _animalsService.Stub(a => a.GetAnimals(null, 0))
                .IgnoreArguments()
                .Return(new AnimalsDto());

            // Act
            var result = _controller.List(null, 0);

            // Assert            
            result.AssertViewRendered().ForView("List");
        }
    }
}

Jag Reehal’s Final Thought on ‘Skinny ASP.NET MVC Controllers’

There is of course more than one way to skin a cat. This solution could have been refactored in many different ways.

The refactoring done in the post means:

  • The ASP.NET MVC controller is skinnier with less responsibility which means it’s easier to read and maintain
  • The controller unit tests can focus on the ‘subject under test’
  • The paging functionality can be used by multiple user interfaces

Comments

  • http://www.marcdormey.com marcdormey

    This is a perfect example of “Command/Query Separation”.

    1. Find a domain entity
    2. Execute one method on that entity
    3. Commit the transaction

    Keeps the controller and presentation clean.
    Read more http://dylanbeattie.blogspot.com/2009/09/altnet-beers-commandquery-separation.html

  • http://bobcravens.com Bob Cravens

    Thanks for refactoring the data access details out of the controller. Although LINQ to SQL / Entity Framework makes our lives easy, it is often overused in controllers (presentation layer). It is too easy to use .skip / .take for paging in the controller. This has maintainability issues. Much better to have the LINQ code refactored into a service (like you have presented).

  • http://gratdevel.blogspot.com Neil Kerkin

    I’m all for skinny controllers (http://stackoverflow.com/questions/975680/asp-net-mvc-actions-separation-of-concerns-single-responsibility-principle) but you haven’t removed any complexity from your application. i.e. you now have to test repeated logic (paging) in each of your services rather than on your controllers.

    Why not show one further refactoring and abstract out the paging logic e.g. PagedList? I understand this isn’t a “How to implement paging” article but i feel it would improve the example.

  • http://www.arrangeactassert.com Jag Reehal

    Hi Neil thanks for the comment.

    I agree that in a real world ASP.NET MVC website you should abstract the paging logic so you don’t end up repeating yourself.

    The aim of the post is to show why you should keep your controllers skinny and how you could go about refactoring a fat controller. Any further refactoring would take the focus away from this.

  • http://gratdevel.blogspot.com Neil Kerkin

    Hi Jag,
    my point is that if someone follows your current approach they only get a few of the benefits of skinny controllers e.g. SoC and simplified tests(which you haven’t show for GetAnimals).

    You specifically mention Paging and DRY but this logic will be repeated in every IService class. Hence duplicate logic and tests.

    Effectively you haven’t removed the smell, just moved it to another layer.

    Don’t get me wrong, i think this is a good article. Just needs that extra step to show the full benefits of skinny controllers.

    Maybe a follow-up post?

  • http://thoughtresults.com/ Saeed Neamati

    What you said here seemed completely logical to me. I mean, the essence of Single Responsibility Principal can also be applied to methods, which are controller actions in this context. However, I think if you provide some links, you backup your argument more solidly fella!
    Thanks ;)