The topic describes one of the professional techniques of developing
ASP.NET MVC applications, which can significantly reduce the number of duplicate code in POST action handlers for forms. In spite of the fact that I found out about it at the time of the first edition of
ASP.NET MVC In Action and the first
mvcConf , the presentation style of
Jimmy Bogard seemed to me very simple and I decided to publish a free translation for those who do not use this approach in practice. .
Many people ask why
AutoMapper has so few built-in capabilities for reverse mapping (
DTO s -> persistent object models). The fact is that the existing capabilities severely limit the domain models, forcing them to be anemic, so we just found another way to deal with the complexity in our POST requests.
Look at the medium / large ASP.NET MVC site, at the complexity or size, and you will notice that there are some patterns in the implementation. You will see a big difference between what your GET and POST actions look like. This is expected because GETs are requests (Queries), and POSTs are commands (Commands) (if you implemented them correctly, then that's it). You will not necessarily see a 1: 1 ratio for form tags and POST actions, since The form can also be used to send requests (for example, a search form).
')
For GET action, in my opinion, the problem has already been solved. GET actions create a ViewModel and send it to the view and use any number of optimizations / abstractions (AutoMapper, model binding, projections on conventions, etc.).
POSTs are a completely different beast. The vectors of difficulty in changing information and the receipt / validation of commands are completely orthogonal to GETs, which causes us to 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 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 POST action to POST action.
So why should we worry about these actions? Why not form a common way of doing what we see here? Here are a few reasons we encountered:
- POST actions require dependencies other than GETs, and the dichotomy between these types leads to controller swelling (controllers);
- The desire to make changes / improvements for ALL POST actions is centralized, as an example, adding logging, validation, authorization, event notifications, etc;
- The problems are thoroughly mixed. PERFORMANCE of work is mixed with MANAGEMENT in how this work should be done. Sometimes it's terrible.
As a workaround, we used a combination of techniques:
- Own result of action (action result) for management of the general flow of execution;
- Separation of “job performance” and the overall flow of execution.
We do not always want to create these abstractions, but this can be useful for managing the complexity of POSTs. First, let's create the result of the action.
Determining the overall flow of execution
Before going too far along the path of creating an action result, let's look at the general pattern above. Some things need to be defined in the controller action, but others may be random. For example, the “DoActualWork” block can be defined 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 to handle this form:
public interface IFormHandler<T>
{
void Handle(T form);
}
* This source code was highlighted with Source Code Highlighter .
Everything is quite simple, a class that represents "
Action (T) " or the implementation of the pattern Command. 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 overall result of the action:
public class FormActionResult<T> : 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<IFormHandler<T>>();
handler.Handle(Form);
Success.ExecuteResult(context);
}
}
* This source code was highlighted with Source Code Highlighter .
We reviewed the main execution pipeline (execution pipeline), and found pieces that vary. It is noteworthy that this is an
ActionResult to execute in case of a successful outcome, and an ActionResult is in case of an unsuccessful outcome. The specific form handler to execute is already determined based on the type of form, so we use any popular
IoC container to find a specific form handler to execute (
StructureMap in my case). Say StructureMap to find implementations of IFormHandler based on implementations 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 that deals only with form processing, and not with tracking UI traffic:
public class ConferenceEditModelFormHandler
: IFormHandler<ConferenceEditModel>
{
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 intentional.
What is really interesting is how we isolated all these problems and that they are no longer dependent on a particular ASP.NET result of the action. At the moment, we have effectively separated the problems of doing work from direct work.
With reference to our controller
Now that we have developed our result of the action, the last problem remains - applying this result of the action to our controller action. Like most people, we often insert a layer in the class hierarchy of controllers to be able to apply helper methods in all of our controllers. In this class, we will add a helper method for constructing our custom action result:
public class DefaultController : Controller
{
protected FormActionResult<TForm> Form<TForm>(
TForm form,
ActionResult success)
{
var failure = View(form);
return new FormActionResult<TForm>(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 came. 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 have reduced the action of the controller so that it is really a description of what we do, and how we do it, we moved to the level below. This is a classic example of applying OO
composition in practice, we have combined various ways of performing POST forms into the result of an action and implemented a form handler. In fact, we didn’t reduce the code that we are forced to write, it just moved, and it became a little easier for us to talk.
Another interesting side effect is that we are now creating modular / integration tests for the form handler, but not for the controller action. And what is there to check it? We have no incentive to write a test, because there is too little logic.
Observing the use of large-scale patterns, it is important to investigate first the aggregation of routes (routes). This allows us to be a little more flexible in folding the pieces together than in the case of inherited routes.
Although this is a somewhat complicated 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 difficult.