ASP.NET MVC Controllers: Push the Envelope!

This post takes the standard ASP.NET MVC controller and uses a minimal amount of code to provide maximum efficiency.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
Cricket on arm of man in a plane

In ASP.NET MVC, the controller is the aggregator and does two things: it collects data and returns models to views. That's it. So the sooner you get your data, the sooner you can get a rendered view.

One of the rules I learned from using ASP.NET MVC during the ASP.NET MVC 2-ish days was to do as little as possible in the controllers and pass the workload to the system. The whole idea behind ASP.NET MVC controllers is to do as much as possible in as few lines as possible, right? It would make sense that the faster you get your code, the faster the view displays (actually, that's another post altogether). ;-)

What if I told you that most of your controllers SHOULD have one line of code?

When I first started using MVC, I realized that there needed to be an additional separation of code instead of shoving everything into the controller. Most of the data in the controller just didn't feel right. This is where your Patterns and Practices should kick in.

Ok, let's get started. Let's create a FAQController (not a FATController) for your site. If you've been in the ASP.NET MVC space for a while, this may look familiar to you:

Controllers\FaqController.cs

using System.Web.Mvc;
using ThinController.Models;
namespace ThinController.Controllers
{
    public class FaqController : Controller
    {
        // Something quick for demo purposes.
        private readonly FaqRepository _repository = new FaqRepository();
        
        // GET: FAQ
        public ActionResult Index()
        {
            var viewModel = new FaqViewModel
            {
                PageTitle = "FAQ List",
                MetaDescription = "This is the Frequently Asked Questions Page.",
                MetaKeywords = "FAQ, New Questions, Old Questions, FAQ List",
                FaqList = _repository.GetAll()
            };
            return View(viewModel);
        }
    }
}

Pretty simple ASP.NET MVC controller. Of course, you can use ViewBag or ViewData in its place, but I prefer the ViewModel approach as I explained in a previous post.

This approach technically uses two lines, but this is an easy controller. I've seen some controllers that were pages long and I'm sure some developers out there have controllers that are much larger than this simple example.

Time to Refactor

What do we know about ASP.NET MVC Controllers? They accept the input and return models, right? So based on our example above, we need to create a factory that builds our ViewModel for us and returns it to the view.

But how do we do that? I know a lot of developers are screaming DI (Dependency Injection). While I agree, I want to make this as simple as possible, so for now, we'll use reflection.

First, let's set up our factory.

Classes\DefaultViewModelFactory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ThinController.Interfaces;
namespace ThinController.Classes
{
    public class DefaultViewModelFactoryIViewModelFactory
    {
        public TViewModel GetViewModel<TController, TViewModel>(TController controller)
        {
            var viewModels = GetSystemTypes(controller,
 typeof (TViewModel));             var modelBuilders = GetSystemTypes(controller, 
typeof(IViewModelBuilder<TController, TViewModel>));             var typeName = typeof(TViewModel).Name;             var model = (from t in viewModels                 where typeof(TViewModel).IsAssignableFrom(t)
&& t.Name.Equals(typeName)                 let strategy = (TViewModel)Activator.CreateInstance(t)                 select strategy).FirstOrDefault();             var modelBuilder = (from t in modelBuilders                 where typeof(IViewModelBuilder<TController, 
TViewModel>).IsAssignableFrom(t)                 let strategy = 
(IViewModelBuilder<TController, 
TViewModel>)Activator.CreateInstance(t)                 select strategy).FirstOrDefault();             // Redirect and assist developers in adding their own modelbuilder/viewmodel             if (modelBuilder == null)                 throw new Exception(                     String.Format(                         "Could not find a ModelBuilder with a {0} Controller/{1} "+
"ViewModel pairing. Please create one."
,                         typeof (TController).Name, typeof (TViewModel).Name));                          return modelBuilder.Build(controller, model);         }         private IEnumerable<Type> GetSystemTypes<TController>(
TController controller, Type type)         {             // First, get the types of the executing assembly             var currentAssemblyTypes = Assembly                 .GetExecutingAssembly()                 .GetTypes()                 .Where(e => type.IsAssignableFrom(e))                 .ToList();             // Then the calling assembly             currentAssemblyTypes.AddRange(                 Assembly                     .GetCallingAssembly()                     .GetTypes()                     .Where(e => type.IsAssignableFrom(e))                     .ToList()                 );             return currentAssemblyTypes.Distinct();         }     } }

Interfaces\IViewModelFactory.cs

namespace ThinController.Interfaces
{
    public interface IViewModelFactory
    {
        TViewModel GetViewModel<TController, TViewModel>(TController controller);
    }
}

This was definitely a mess and needed updated. Please refer to this post for updated code: Go to UPDATE: Using Dependency Injection on ViewModelBuilder

The ViewModelFactory class accepts two types to create the ViewModel: the controller and the ViewModel to build. The reason to build your factory this way is two-fold:

  1. This abstracts the building of your ViewModel into a separate class away from our controller making things easier and smaller to test.
  2. While coding the controller, the interface for the ViewModelBuilder nudges you into a standard coding practice for your controllers. There will ALWAYS be a Controller/ViewModel combination. You can't get around that and the interface will make you aware of it during design-time.

Build it and they will...uhh...call it?

Now that we have our factory in place, let's work on the builder itself.

Interfaces\IViewModelBuilder.cs

namespace ThinController.Interfaces
{
    public interface IViewModelBuilder<TController, TViewModel>
    {
        TViewModel Build(TController controller, TViewModel viewModel);
    }
}

ViewModelBuilder\FaqViewModelBuilder.cs

using ThinController.Controllers;
using ThinController.Interfaces;
using ThinController.Models;
using ThinController.Repository;
namespace ThinController.ViewModelBuilder
{
    public class FaqViewModelBuilder : IViewModelBuilder<FaqControllerFaqViewModel>
    {
        public FaqViewModel Build(FaqController controller, FaqViewModel viewModel)
        {
            viewModel.PageTitle = "FAQ List";
            viewModel.MetaDescription = "This is the Frequently Asked Questions Page.";
            viewModel.MetaKeywords = "FAQ, New Questions, Old Questions, FAQ List";
            
            // Strictly demo purposes. :-)
            var repository = new FaqRepository();
            viewModel.FaqList = repository.GetAll();
            return viewModel;
        }
    }
}

Just the way I like code; small and easy to read. :-)

The general rule here is that each ViewModel will have its own builder. If there are common routines that all builders share, add a method to the interface or create an abstract ViewModelBuilder class to minimize the amount of common code. Remember DRY.

As you can see, this makes our code extremely easy to test.

Back to the Controller

Now that we have a foundation, we need to modify our controller to use the DefaultViewModelFactory.

Controllers\FaqController.cs

using System.Web.Mvc;
using ThinController.Classes;
using ThinController.Models;
namespace ThinController.Controllers
{
    public class FaqController : Controller
    {
        // Removed
        // private readonly FaqRepository _repository = new FaqRepository();
        private DefaultViewModelFactory _factory = new DefaultViewModelFactory();
        // GET: FAQ
        public ActionResult Index()
        {
            return View(_factory.GetViewModel<FaqControllerFaqViewModel>(this));
        }
    }
}

Phew! We've modified our controller to use the DefaultViewModelFactory...and Voila! One line per method/page.

Here are a couple of exercises for my readers:

  • For those out there screaming DI (Dependency Injection) for the DefaultViewModelFactory, I wanted to keep this as simple as possible for the readers. Yes, that is a good candidate for DI.
  • Create an additional method for passing a parameter into the DefaultViewModelFactory.
  • Take the same approach for a POST method (I'll probably do a future post on this as well).

Conclusion

In this post, I explained how to get the maximum efficiency by breaking up the controller into manageable, testable, and extendable components.

As you can see, I am always trying to push the envelope and find better ways to write ASP.NET MVC code.

If you have any questions, post a comment. I always love to hear from the audience! :-)

Deliverable: Download the code that accompanies this post (ThinController.zip)

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus