The Skinniest ASP.NET MVC Controllers You've Ever Seen, Part 2

In this last part, we discuss how to achieve one-line POSTs in your ASP.NET MVC controllers.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
Skinny controllers

In our previous post, we took our FaqController and added more Views to show how skinny your controllers can become when you move your processing out to another layer.

Your controllers are meant to be thin. The mantra is "Thin Controllers, Fat Models." Place all of your business rules inside your models, not in your controllers.

I've seen some "interesting" controllers since the alpha version of MVC where a lot of developers wrote a ton of code (PAGES of code) that should have been in their models (and that includes my own code!).

Controller POST

Since we already covered the GET-ting of MVC pages, let's get right into the POST-ting of your MVC pages.

The controller POST methods have the following characteristics:

  1. The method name that performs the POST has a different parameter signature than the GET whether it receives a ViewModel or a FormCollection.
  2. You need to place an [HttpPost] attribute on the method name that performs the POST logic.

So let's pick up where we left off with our FaqController:

Controllers\FAQController.cs

using System.Web.Mvc;
using ThinController.Classes;
using ThinController.Models;
namespace ThinController.Controllers
{
    public class FaqController : Controller
    {
        private DefaultViewModelFactory _factory = new DefaultViewModelFactory();
        // GET: FAQ
        public ActionResult Index()
        {
            return View(_factory.GetViewModel<FaqController, 
                FaqViewModel>(this));
        }
        public ActionResult Create()
        {
            return View(_factory.GetViewModel<FaqController, 
                CreateFaqViewModel>(this));
        }
        public ActionResult Update(string id)
        {
            return View(_factory.GetViewModel<FaqController, 
                UpdateFaqViewModelstring>(this, id));
        }
    }
}

As mentioned before, with any method in the controller, they can only return type ActionResult and that's it.

Let's look at a simple POST routine in an MVC controller.

[ValidateAntiForgeryToken]
[HttpPost]
public ActionResult Create(CreateFaqViewModel model)
{
    if (ModelState.IsValid)
    {
        faqRepository.Add(model.FaqItem);
        faqRepository.SaveChanges();
        
        return Redirect(Url.FAQUrl());
    }
    
    return View(model);
}

I'm also using Entity Framework, but the concept is still the same. This "boilerplate" code seems to be consistent with almost every postback in the MVC world...well, at least 80-90% of the time.

How can we abstract that out to it's own testable component? We need to create a new kind of ActionResult.

Time to Refactor!

The process in the controller's postback goes like this:

  1. Check to see if the data is valid
  2. If valid, process the object in some way and redirect them to a "success" url
  3. If not valid, either return to the view or send them to another url to alert the user to the problem.

Simple enough? We need to turn this process into a simple extendable component that we can use over and over again (DRY!).

So what do we need to make this an easy abstraction? We need:

  1. The postback data (the model)
  2. How to process the successful (and valid) model
  3. Provide a "success" process.
  4. Provide a "failure" process.

Ok, let's create a new folder called "ActionResults" and add our new CreateUpdate ActionResult to the folder.

Here is what our CreateUpdateResult looks like:

using System;
using System.Web.Mvc;
using ThinController.Interfaces;
namespace ThinController.ActionResults
{
    public class CreateUpdateResult<T> : ActionResult
    {
        private readonly T _model;
        public IGeneralFormHandler<T> Handler { getset; }
        public Func<T, ActionResult> SuccessResult;
        public Func<T, ActionResult> FailureResult;
        public CreateUpdateResult(T model)
        {
            _model = model;
        }
        public CreateUpdateResult(T model, 
            IGeneralFormHandler<T> handler, 
            Func<T, ActionResult> successResult,
            Func<T, ActionResult> failureResult)
        {
            _model = model;
            Handler = handler;
            SuccessResult = successResult;
            FailureResult = failureResult;
        }
        public override void ExecuteResult(ControllerContext context)
        {
            var viewData = context.Controller.ViewData;
            if (viewData.ModelState.IsValid)
            {
                Handler.ProcessForm(context, _model);
                SuccessResult(_model).ExecuteResult(context);
            }
            else
            {
                FailureResult(_model).ExecuteResult(context);
            }
        }
    }
}

If you cut and pasted this, you probably are getting the IGeneralFormHandler error.

The IGeneralFormHandler interface is used for classes that process the form data when the postback occurs.

Let's add that interface to the Interfaces folder as well.

Interfaces\IGeneralFormHandler.cs

using System.Web.Mvc;
namespace ThinController.Interfaces
{
    public interface IGeneralFormHandler<TModel>
    {
        void ProcessForm(ControllerContext context, TModel model);
    }
}

Finally, add one last folder from the root called FormHandlers. This is where all of your form's postback code will reside.

Let's refactor out the Create process from our controller to a CreateFaqFormHandler:

FormHandlers\CreateFaqFormHandler.cs

using System.Web.Mvc;
using ThinController.Classes;
using ThinController.Interfaces;
using ThinController.Models;
using ThinController.Repository;
namespace ThinController.FormHandlers
{
    public class CreateFaqFormHandler : IGeneralFormHandler<CreateFaqViewModel>
    {
        public void ProcessForm(ControllerContext context, CreateFaqViewModel viewModel)
        {
            var unitOfWork = context.GetUnitOfWork<UnitOfWork>();
            unitOfWork.FaqRepository.Add(viewModel.FaqItem);
            unitOfWork.FaqRepository.SaveChanges();
            /* 
             * If you want, you can have your success page display a message to the user.
             *   context.Controller.TempData["Message"] = 
                     viewModel.Faq.Question + " has been successfully saved.";
             * */
        }
    }
}

NOTE: If you are confused with how we got the UnitOfWork, I refer you to this recent post.

Now, since we have our CreateFaqFormHandler, we might as well go ahead and create our UpdateFaqFormHandler.

FormHandlers\UpdateFaqFormHandler.cs

using System.Web.Mvc;
using ThinController.Classes;
using ThinController.Controllers;
using ThinController.Interfaces;
using ThinController.Models;
using ThinController.Repository;
namespace ThinController.FormHandlers
{
    public class UpdateFaqFormHandler : IGeneralFormHandler<UpdateFaqViewModel>
    {
        public void ProcessForm(ControllerContext context, UpdateFaqViewModel viewModel)
        {
            // grab id from either the viewModel or from the route.
            // var id = viewModel.FaqItem.Id
            var id = context.RouteData.Values["id"].ToString();
            
            var unitOfWork = context.GetUnitOfWork<UnitOfWork>();
            var faqItem = unitOfWork.FaqRepository.GetById(id);
            var controller = (BaseController)context.Controller;
            if (controller.TryUpdateModel(faqItem, "FaqItem"))
            {
                unitOfWork.FaqRepository.SaveChanges();
                context.Controller.TempData["Message"] = 
                    viewModel.FaqItem.Question + " has been successfully updated.";
            }
            else
            {
                context.Controller.TempData["Message"] = 
                    viewModel.FaqItem.Question + " was NOT successfully updated.";
            }
        }
    }
}

For those astute enough to see the BaseController class and the TryUpdateModel method, you may be wondering how we accessed that method outside of the controller because the TryUpdateModel method is protected. I inherited the Controller class and called it BaseController and exposed the TryUpdateModel method as a public method.

Controllers\BaseController.cs

using System.Web.Mvc;
namespace ThinController.Controllers
{
    public class BaseController : Controller
    {
        public new bool TryUpdateModel<TModel>(TModel model, 
            string prefix) where TModel : class
        {
            return base.TryUpdateModel(model, prefix);
        }
    }
}

Of course, it is an overloaded method, but I've only needed this particular call to update my models. If you need additional methods, you're more than welcome to enhance it. :-)

Finish It!

The last thing we need to do is complete our controller POSTs which is by far the easiest thing to do now that we have our foundation in place.

Here is the final controller with our one-line POSTs for the Create and Update methods.

Controllers\FaqController.cs

using System.Web.Mvc;
using ThinController.ActionResults;
using ThinController.Classes;
using ThinController.FormHandlers;
using ThinController.Helpers.Url;
using ThinController.Models;
namespace ThinController.Controllers
{
    public class FaqController : BaseController
    {
        private DefaultViewModelFactory _factory = new DefaultViewModelFactory();
        // GET: FAQ
        public ActionResult Index()
        {
            return View(_factory.GetViewModel<FaqController, 
                FaqViewModel>(this));
        }

        public ActionResult Create()         {             return View(_factory.GetViewModel<FaqController,                  CreateFaqViewModel>(this));         }         [ValidateAntiForgeryTokenHttpPost]         public ActionResult Create(CreateFaqViewModel faqViewModel)         {             return new CreateUpdateResult<CreateFaqViewModel>(                 faqViewModel,                 new CreateFaqFormHandler(),                 viewModel => Redirect(Url.FAQUrl()),                 viewModel => View(viewModel)             );         }         public ActionResult Update(string id)         {             return View(_factory.GetViewModel<FaqController,                  UpdateFaqViewModelstring>(this, id));         }         [ValidateAntiForgeryTokenHttpPost]         public ActionResult Update(UpdateFaqViewModel faqViewModel)         {             return new CreateUpdateResult<UpdateFaqViewModel>(                 faqViewModel,                 new UpdateFaqFormHandler(),                 viewModel => Redirect(Url.FAQUrl()),                 viewModel => View(viewModel)             );         }     } }

THAT, my friend, is one skinny controller. The skinniest controller you will ever see (next to your own controllers using this method) ;-)

Conclusion

In the first part of this series, I showed everyone how to create one-line GETs from your controllers and how to pass parameters and use ViewModelBuilders to optimize your MVC pipeline.

In this second post, you now can create efficient form POSTs using modular components and testable units for your future web apps.

Any questions? Don't be shy! Post in the comments below.

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