Tidying up POSTs in ASP.NET MVC

Original author: Jimmy Bogard
  • Transfer
The topic describes one of the professional techniques for developing ASP.NET MVC applications, which can significantly reduce the number of repeated code in the POST handlers of form actions. Despite the fact that I learned about it even during the first edition of ASP.NET MVC In Action and the first mvcConf , the style of presentation of Jimmy Bogard seemed very simple to me and I decided to publish a free translation for those who still do not use this approach in practice .


Many people ask why AutoMapper has so few built-in features for reverse mapping ( DTOs -> persistent object models). The fact is that existing opportunities greatly limit domain models, forcing them to be anemic, so we just found another way to deal with complexity in our POST requests.

Look at the medium / large ASP.NET MVC site, complexity or size, and you will notice that some patterns appear in the implementation. You will see a big difference between what your GET and POST actions look like. This is expected because GETs are Queries, and POSTs are Commands (if you implemented them correctly, this is exactly the case). You will not necessarily see a 1: 1 ratio for form tags and POST actions, as the form can also be used to send requests (for example, a search form).

For GET actions, in my opinion, the problem has already been resolved. GET actions create a ViewModel and send it to the view, and use any number of optimizations / abstractions (AutoMapper, model binding, projection on conventions, etc.).

POSTs are a completely different beast. The vectors of complexity in changing information and the receipt / validation of commands are completely orthogonal to GETs, which makes us throw away all our previous decisions. Usually we see something like this:
[HttpPost]
public ActionResult Edit(ConferenceEditModel form)
{
    if (!ModelState.IsValid)
    {
        return View(form);
    }

    var conf = _repository.GetById(form.Id);

    conf.ChangeName(form.Name);

    foreach (var attendeeEditModel in form.Attendees)
    {
        var attendee = conf.GetAttendee(attendeeEditModel.Id);

        attendee.ChangeName(attendeeEditModel.FirstName, attendeeEditModel.LastName);
        attendee.Email = attendeeEditModel.Email;
    }

    return this.RedirectToAction(c => c.Index(null), "Default");
}

* This source code was highlighted with Source Code Highlighter.

What we see again and again and again again is a pattern similar to:
[HttpPost]
public ActionResult Edit(SomeEditModel form)
{
    if (IsNotValid)
    {
        return ShowAView(form);
    }

    DoActualWork();

    return RedirectToSuccessPage();
}

* This source code was highlighted with Source Code Highlighter.

Where everything that is marked in red changes from a POST action to a POST action.

So why should we worry about these actions? Why not form a common execution path according to what we see here? Here are a few reasons we are faced with:
  • POST actions require dependencies other than GETs and a dichotomy between these species leads to swelling of the controllers;
  • The desire to make changes / improvements for ALL POST actions is centralized, such as adding logging, validation, authorization, event notifications, etc;
  • The problems are thoroughly mixed. PERFORMANCE of work is mixed with MANAGEMENT of how this work should be done. Sometimes it's awful.

As a workaround, we used a combination of techniques:
  • Own action result for controlling the general flow of execution;
  • Separation of the “execution of work” and the general flow of execution.

We do not always want to create these abstractions, but it can be useful for managing the complexity of POSTs. To get started, let's create an action result.

Determining the overall flow of execution


Before going too far along the path to creating the result of an action, let's look at the general template above. Some things must be defined in a controller action, but others may be random. For example, a “DoActualWork” block can be determined based on the form received. We will never have 2 different ways to handle the action of a form, so let's define an interface for handling this form:
public interface IFormHandler
{
    void Handle(T form);
}

* This source code was highlighted with Source Code Highlighter.

It's simple enough, a class that represents " Action (T) " or an implementation of the Command pattern. In fact, if you are familiar with messages, it looks just like a message handler. A form is a message and the handler knows what to do with such a message.

The abstraction above represents what we need to do for the DoActualWork block, and the rest can be dragged into the general result of the action:
public class FormActionResult : ActionResult
{
    public ViewResult Failure { get; private set; }
    public ActionResult Success { get; private set; }
    public T Form { get; private set; }

    public FormActionResult(T form, ActionResult success, ViewResult failure)
    {
        Form = form;
        Success = success;
        Failure = failure;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (!context.Controller.ViewData.ModelState.IsValid)
        {
            Failure.ExecuteResult(context);

            return;
        }

        var handler = ObjectFactory.GetInstance>();

        handler.Handle(Form);

        Success.ExecuteResult(context);
    }
}

* This source code was highlighted with Source Code Highlighter.

We examined the main execution pipeline, and found pieces that change. It is noteworthy that these are ActionResult for execution in case of a successful outcome and ActionResult for execution in the case of a failed outcome. The specific form handler for execution is already defined based on the type of forms, so we use any popular IoC container to find the specific form handler for execution ( StructureMap in my case). Let's tell StructureMap to find IFormHandler implementations based on implementations, this is just one line of code:
Scan(scanner =>
{
    scanner.TheCallingAssembly();
    scanner.ConnectImplementationsToTypesClosing(typeof(IFormHandler<>));
});

* This source code was highlighted with Source Code Highlighter.

Now drag the “DoActualWork” block into the class, which only deals with processing the form, and not tracking the traffic UI:
public class ConferenceEditModelFormHandler
    : IFormHandler
{
    private readonly IConferenceRepository _repository;

    public ConferenceEditModelFormHandler(
        IConferenceRepository repository)
    {
        _repository = repository;
    }

    public void Handle(ConferenceEditModel form)
    {
        Conference conf = _repository.GetById(form.Id);

        conf.ChangeName(form.Name);

        foreach (var attendeeEditModel in GetAttendeeForms(form))
        {
            Attendee attendee = conf.GetAttendee(attendeeEditModel.Id);

            attendee.ChangeName(attendeeEditModel.FirstName,
                                attendeeEditModel.LastName);
            attendee.Email = attendeeEditModel.Email;
        }
    }

    private ConferenceEditModel.AttendeeEditModel[] GetAttendeeForms(ConferenceEditModel form)
    {
        return form.Attendees ??
              new ConferenceEditModel.AttendeeEditModel[0];
    }
}

* This source code was highlighted with Source Code Highlighter.

Now this class is designed only for successful form processing. Namely, returning to my domain object and changing it accordingly. Because I have a behavioral model of the domain, you will not see the possibility of "reverse mapping". This is done on purpose.

What is really interesting is how we isolated all these problems and that they no longer depend on the specific ASP.NET action result. At the moment, we have effectively separated the problems of performing work from direct work.

Applied to our controller


Now that we have developed our action result, the last problem remains - applying this action result to our controller action. Like most people, we often insert a layer in the hierarchy of controller classes to be able to use helper methods in all of our controllers. In this class, we will add a helper method to build our custom action result:
public class DefaultController : Controller
{
    protected FormActionResult Form(
        TForm form,
        ActionResult success)
    {
        var failure = View(form);

        return new FormActionResult(form, success, failure);
    }

* This source code was highlighted with Source Code Highlighter.

It simply wraps some default paths that we often define. For example, a processing error almost always shows the view from which we just arrived. Finally, we can change our original POST controller action:
public class ConferenceController : DefaultController
{
    [HttpPost]
    public ActionResult Edit(ConferenceEditModel form)
    {
        var successResult =
            this.RedirectToAction(c => c.Index(null), "Default");

        return Form(form, successResult);
    }

* This source code was highlighted with Source Code Highlighter.

We reduced the action of the controller so that it really was a description of what we are doing, and the way we do it, we have moved to a lower level. This is a classic example of the application of OO composition in practice, we have combined the various ways to execute the POST form in the result of the action and implemented the form handler. In fact, we did not reduce the code that we are forced to write, it just moved, and it became a little easier for us to reason.

Another interesting side effect is that we are now creating unit / integration tests for the form handler, but not for the controller action. And what is there to check? We have no incentive to write a test, as there is too little logic.

When observing the use of large-scale patterns, it is important to investigate the association of routes (routes) in the first place. This allows us to be a little more flexible in putting parts together than in the case of inherited routes.

Although this is a somewhat complex example, in the next article we will look at how more complex POST actions can look when our validation goes beyond simple elements and our POST handlers become even more complicated.

Also popular now: