Click here to Skip to main content
Click here to Skip to main content
Go to top

Skinny controller in ASP.NET MVC 4

, 26 Nov 2012
Rate this:
Please Sign up or sign in to vote.
Applying best practices for ASP.NET web applications.

The original article can be found at my blog

The complete source code: MSDN Code

Background

You need to know at least C# and some backgrounds about ASP.NET MVC. Beside that you need to know some basic patterns (that good if you can reference to Gang of Fours book).

Introduction

The last month, I published an article about how to make a controller more skinny. And this article also made some people very curious. Two months later, I finished a sample on MSDN code, and now I am really glad to show all of you the complete version of this topic. Now I want to share this for all of you in this community.

Rails community always inspires a lot of good ideas. I really love this community. One of these is "Fat models and skinny controllers". I have spent a lot of time on ASP.NET MVC, and really I did some mistakes, because I made the controller so fat. Such a controller is really dirty and very hard to maintain in future. It violates seriously SRP principle and KISS as well. But how can we achieve that in ASP.NET MVC? That answer was really clear after I read "Manning ASP.NET MVC 4 in Action". It is simple that we can separate it into ActionResult, and try to implement logic and persistence data inside this. In the last two years, I have read this from Jimmy Bogard blog, but in that time I never had a consideration about it. That's enough talking now.

I just published a sample on ASP.NET MVC 4, implemented on Visual Studio 2012. I used EF framework here for implementing the persistence layer, and also used two free templates from the internet to make the UI for this sample.

In this sample, I try to implement a simple magazine website that manages articles, categories, and news. It is not finished at all at this time, but no problems, because I just need to show you how to make the controller skinny. And I wanna hear more about your ideas.

The first thing, I am abstracting the base ActionResult class like this:

public abstract class MyActionResult : ActionResult, IEnsureNotNull
{
    public abstract void EnsureAllInjectInstanceNotNull();
}

public abstract class ActionResultBase<TController> : MyActionResult where TController : Controller
{
    protected readonly Expression<Func<TController, ActionResult>> ViewNameExpression;
    protected readonly IExConfigurationManager ConfigurationManager;

    protected ActionResultBase (Expression<Func<TController, ActionResult>> expr)
        : this(DependencyResolver.Current.GetService<IExConfigurationManager>(), expr)
    {
    }

    protected ActionResultBase(
        IExConfigurationManager configurationManager,
        Expression<Func<TController, ActionResult>> expr)
    {
        Guard.ArgumentNotNull(expr, "ViewNameExpression");
        Guard.ArgumentNotNull(configurationManager, "ConfigurationManager");

        ViewNameExpression = expr;
        ConfigurationManager = configurationManager;
    }

    protected ViewResult GetViewResult<TViewModel>(TViewModel viewModel)
    {
        var m = (MethodCallExpression)ViewNameExpression.Body;
        if (m.Method.ReturnType != typeof(ActionResult))
        {
            throw new ArgumentException("ControllerAction method '" + 
                  m.Method.Name + "' does not return type ActionResult");
        }
 
        var result = new ViewResult
        {
            ViewName = m.Method.Name
        };

        result.ViewData.Model = viewModel;

        return result;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        EnsureAllInjectInstanceNotNull();
    }
}

I also have an interface for validation of all inject objects. This interface makes sure all inject objects that I inject using Autofac container are not null. The implementation of this as below:

public interface IEnsureNotNull
{
    void EnsureAllInjectInstanceNotNull();
}

Afterwards, I am just simple implementing the HomePageViewModelActionResult class like this

public class HomePageViewModelActionResult<TController> : ActionResultBase<TController> where TController : Controller
{
    #region variables & ctors

    private readonly ICategoryRepository _categoryRepository;
    private readonly IItemRepository _itemRepository;

    private readonly int _numOfPage;

    public HomePageViewModelActionResult(Expression<Func<TController, ActionResult>> viewNameExpression)
        : this(viewNameExpression,
               DependencyResolver.Current.GetService<ICategoryRepository>(),
               DependencyResolver.Current.GetService<IItemRepository>())
    {
    }

    public HomePageViewModelActionResult(
        Expression<Func<TController, ActionResult>> viewNameExpression,
        ICategoryRepository categoryRepository,
        IItemRepository itemRepository)
        : base(viewNameExpression)
    {
        _categoryRepository = categoryRepository;
        _itemRepository = itemRepository;

        _numOfPage = ConfigurationManager.GetAppConfigBy("NumOfPage").ToInteger();
    }

    #endregion

    #region implementation

    public override void ExecuteResult(ControllerContext context)
    {
        base.ExecuteResult(context);

        var cats = _categoryRepository.GetCategories();

        var mainViewModel = new HomePageViewModel();
        var headerViewModel = new HeaderViewModel();
        var footerViewModel = new FooterViewModel();
        var mainPageViewModel = new MainPageViewModel();

        headerViewModel.SiteTitle = "Magazine Website";
        if (cats != null && cats.Any())
        {
            headerViewModel.Categories = cats.ToList();
            footerViewModel.Categories = cats.ToList();
        }

        mainPageViewModel.LeftColumn = BindingDataForMainPageLeftColumnViewModel();
        mainPageViewModel.RightColumn = BindingDataForMainPageRightColumnViewModel();

        mainViewModel.Header = headerViewModel;
        mainViewModel.DashBoard = new DashboardViewModel();
        mainViewModel.Footer = footerViewModel;
        mainViewModel.MainPage = mainPageViewModel;

        GetViewResult(mainViewModel).ExecuteResult(context);
    }

    public override void EnsureAllInjectInstanceNotNull()
    {
        Guard.ArgumentNotNull(_categoryRepository, "CategoryRepository");
        Guard.ArgumentNotNull(_itemRepository, "ItemRepository");
        Guard.ArgumentMustMoreThanZero(_numOfPage, "NumOfPage");
    }

    #endregion

    #region private functions

    private MainPageRightColumnViewModel BindingDataForMainPageRightColumnViewModel()
    {
        var mainPageRightCol = new MainPageRightColumnViewModel();

        mainPageRightCol.LatestNews = _itemRepository.GetNewestItem(_numOfPage).ToList();
        mainPageRightCol.MostViews = _itemRepository.GetMostViews(_numOfPage).ToList();

        return mainPageRightCol;
    }

    private MainPageLeftColumnViewModel BindingDataForMainPageLeftColumnViewModel()
    {
        var mainPageLeftCol = new MainPageLeftColumnViewModel();

        var items = _itemRepository.GetNewestItem(_numOfPage);

        if (items != null && items.Any())
        {
            var firstItem = items.First();

            if (firstItem == null)
                throw new NoNullAllowedException("First Item".ToNotNullErrorMessage());

            if (firstItem.ItemContent == null)
                throw new NoNullAllowedException("First ItemContent".ToNotNullErrorMessage());
 
            mainPageLeftCol.FirstItem = firstItem;

            if (items.Count() > 1)
            {
                mainPageLeftCol.RemainItems = items.Where(x => x.ItemContent != null && 
                                x.Id != mainPageLeftCol.FirstItem.Id).ToList();
            }
        }

        return mainPageLeftCol;
    }

    #endregion
}

Final step, I get into HomeController and add some code like this:

[Authorize]
public class HomeController : BaseController
{
    [AllowAnonymous]
    public ActionResult Index()
    {
        return new HomePageViewModelActionResult<HomeController>(x=>x.Index());
    }

    [AllowAnonymous]
    public ActionResult Details(int id)
    {
        return new DetailsViewModelActionResult<HomeController>(x => x.Details(id), id);
    }

    [AllowAnonymous]
    public ActionResult Category(int id)
    {
        return new CategoryViewModelActionResult<HomeController>(x => x.Category(id), id);
    }
}

As you can see, the code in the controller is really skinny, and all the logic I move to the custom ActionResult class. Some people said, it just moves the code out of the controller and puts it to another class, so it is still hard to maintain. Looks like it just moves the complicate code from one place to another. But if you have a look and think about it in details, you have to find out if you have code for processing all logic that is related to HttpContext or something like this. You can do it on the Controller, and try to delegate another logic (such as processing business requirement, persistence data,...) to the custom ActionResult class.

Points of Interest

This is really exciting because we can separate complex code to ActionResult and try to do the business logic there.

This makes the controller so skinny, that mean we can do unit testing very easily on the controller. And on the ActionResult code this focuses only on the application business. We can do unit testing easily on both of them.

History

1.0 draft version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

thangchung
Software Developer (Senior) Harvey Nash Viet Nam
Vietnam Vietnam
I am working at Viet Nam. I have a chance to work on .NET in 2004 and love it until now. I mainly focus on ASP.NET MVC. I love it and find out a lot of thing interesting from it.
Follow on   Twitter   Google+

Comments and Discussions

 
QuestionGreat article PinmemberKomil17-Dec-12 8:39 
AnswerRe: Great article Pinmemberthangchung17-Dec-12 17:49 
GeneralMy vote of 1 [modified] Pinmemberjgauffin3-Dec-12 6:20 
GeneralRe: My vote of 1 PinmemberSympletech4-Dec-12 5:36 
GeneralRe: My vote of 1 Pinmemberthangchung17-Dec-12 17:47 
GeneralRe: My vote of 1 Pinmemberjimmyzimms6-Mar-13 8:54 
GeneralRe: My vote of 1 Pinmemberthangchung17-Dec-12 17:42 
GeneralRe: My vote of 1 Pinmemberquiit18-Dec-12 6:17 
GeneralRe: My vote of 1 Pinmemberthangchung18-Dec-12 20:36 
QuestionLooks interesting PinmemberMichał Zalewski28-Nov-12 6:52 
AnswerRe: Looks interesting Pinmemberthangchung28-Nov-12 22:01 
GeneralMy vote of 5 Pinmemberhoang1421426-Nov-12 7:11 
GeneralRe: My vote of 5 Pinmemberthangchung28-Nov-12 22:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140916.1 | Last Updated 26 Nov 2012
Article Copyright 2012 by thangchung
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid