Click here to Skip to main content
15,867,308 members
Articles / Web Development / ASP.NET

Skinny controller in ASP.NET MVC 4

Rate me:
Please Sign up or sign in to vote.
4.75/5 (7 votes)
26 Nov 2012CPOL3 min read 23.5K   22   13
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:

C#
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:

C#
public interface IEnsureNotNull
{
    void EnsureAllInjectInstanceNotNull();
}

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

C#
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:

C#
[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)


Written By
Architect NashTech
Vietnam Vietnam
Thang has a great passion in .NET, JavaScript, and Node. You can visit Thang's blog at https://weblogs.asp.net/thangchung to read about his thoughts on software development.

Comments and Discussions

 
QuestionGreat article Pin
Komil17-Dec-12 8:39
Komil17-Dec-12 8:39 
AnswerRe: Great article Pin
thangchung17-Dec-12 17:49
professionalthangchung17-Dec-12 17:49 
GeneralMy vote of 1 Pin
jgauffin3-Dec-12 6:20
jgauffin3-Dec-12 6:20 
GeneralRe: My vote of 1 Pin
Sympletech4-Dec-12 5:36
Sympletech4-Dec-12 5:36 
GeneralRe: My vote of 1 Pin
thangchung17-Dec-12 17:47
professionalthangchung17-Dec-12 17:47 
GeneralRe: My vote of 1 Pin
jimmyzimms6-Mar-13 8:54
jimmyzimms6-Mar-13 8:54 
GeneralRe: My vote of 1 Pin
thangchung17-Dec-12 17:42
professionalthangchung17-Dec-12 17:42 
GeneralRe: My vote of 1 Pin
Quí Nguyễn NT18-Dec-12 6:17
Quí Nguyễn NT18-Dec-12 6:17 
Hi thangchung,

Don't know what actually you mentioned as layout business here but if that is business in UI, we should let the View handle it.
GeneralRe: My vote of 1 Pin
thangchung18-Dec-12 20:36
professionalthangchung18-Dec-12 20:36 
QuestionLooks interesting Pin
Michał Zalewski28-Nov-12 6:52
Michał Zalewski28-Nov-12 6:52 
AnswerRe: Looks interesting Pin
thangchung28-Nov-12 22:01
professionalthangchung28-Nov-12 22:01 
GeneralMy vote of 5 Pin
hoang1421426-Nov-12 7:11
hoang1421426-Nov-12 7:11 
GeneralRe: My vote of 5 Pin
thangchung28-Nov-12 22:03
professionalthangchung28-Nov-12 22:03 

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

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