Wednesday, March 10, 2010

Maintainable MVC: Post-Redirect-Get pattern

This article is part of the Maintainable MVC Series.



It keeps amazing me that every time I see some example MVC code from Scott Guthrie, Phil Haack or one of our other MVC heroes, it keeps looking like this:



[csharp highlight="10"]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(BlogItem blogItem)
{
if (ModelState.IsValid)
{
...
return RedirectToAction("OtherAction");
}

return View(blogItem);
}
[/csharp]

The big problem with this code is that it returns a view (on the highlighted line), while the method handles a POST. It seems like a very bad practice, because it disrupts the natural flow of your website. It makes it impossible to make use of your browser history (the back button) without running into a 'this page is expired' warning. Or your user could post the same data multiple times.



It is an annoyance which will definitely cost your website some visitors! So no more return View in a post method!




The simple pattern that makes these warnings something of the past is the Post-Redirect-Get pattern (PRG); it just states that a post should always be followed by a browser redirect.



[csharp]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(BlogItem blogItem)
{
if (ModelState.IsValid)
{
...
return RedirectToAction("OtherAction");
}

return RedirectToAction("Create");
}
[/csharp]

If it is this simple, why do the MVC gurus return a view in their example? Well, to keep your form filled with the input the user just entered and to show the validation errors, after a redirect, takes a little extra effort. By default, MVC doesn't remember your ModelState from one request to the next.



To make sure the ModelState is ported to the next request we use the ModelStateToTempDataAttribute from the MvcContrib project. If a controller is decorated with this attribute you no longer have to worry about showing validation errors in a form when using the PRG pattern.



ModelStateToTempDataAttribute



[csharp]
public class ModelStateToTempDataAttribute : ActionFilterAttribute
{
public const string TempDataKey = "__MvcContrib_ValidationFailures__";

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
ModelStateDictionary modelState = filterContext.Controller.ViewData.ModelState;

ControllerBase controller = filterContext.Controller;

if(filterContext.Result is ViewResult)
{
// If there are failures in tempdata, copy them to the modelstate
CopyTempDataToModelState(controller.ViewData.ModelState,
controller.TempData);
return;
}

// If we're redirecting and there are errors, put them in tempdata instead
// (so they can later be copied back to modelstate)
if((filterContext.Result is RedirectToRouteResult
|| filterContext.Result is RedirectResult) && !modelState.IsValid)
{
CopyModelStateToTempData(controller.ViewData.ModelState,
controller.TempData);
}
}

private void CopyTempDataToModelState(ModelStateDictionary modelState,
TempDataDictionary tempData)
{
if(!tempData.ContainsKey(TempDataKey))
{
return;
}

ModelStateDictionary fromTempData = tempData[TempDataKey]
as ModelStateDictionary;
if(fromTempData == null)
{
return;
}

foreach(KeyValuePair<string,ModelState> pair in fromTempData)
{
if (modelState.ContainsKey(pair.Key))
{
modelState[pair.Key].Value = pair.Value.Value;

foreach(ModelError error in pair.Value.Errors)
{
modelState[pair.Key].Errors.Add(error);
}
}
else
{
modelState.Add(pair.Key, pair.Value);
}
}
}

private static void CopyModelStateToTempData(ModelStateDictionary modelState,
TempDataDictionary tempData)
{
tempData[TempDataKey] = modelState;
}
}
[/csharp]

The attribute only saves ModelState to TempData if there are validation errors. And if the next action returns a view, the ModelState is retrieved from TempData. TempData itself is a wrapper around Session-State in which objects are only present until the next request.



All we have to do now is to make use of this attribute is to apply it to each controller where needed:



[csharp]
[ModelStateToTempData]
public HomeController : Controller
{
[/csharp]

I've read some comments from people who want to have more control and split the attribute in one writing to TempData and the other retrieving it. But this leads to a proliferation of attributes throughout your code. I can't think of single moment I ever had need for finer-grained control than just applying the one attribute to the controller as a whole.



The drawback



Of course there is one caveat for the use of this attribute. It saves data in TempData, which is saved in Session-State. If the sites you build run on multiple machines behind a load balancer you can't handle subsequent requests by different machines, unless they share their Session-State.



For the smaller sites we choose to set the load balancer in sticky mode, so a user will be coupled to the same server during his/her session. In this case, having multiple servers doesn't really increase availability, in that a user will still hit the same server after it has crashed.



For storing Session-State MSDN shows us we have the following options:




  • InProc mode, which stores session state in memory on the Web server. This is the default.

  • StateServer mode, which stores session state in a separate process called the ASP.NET state service. This ensures that session state is preserved if the Web application is restarted and also makes session state available to multiple Web servers in a Web farm.

  • SQLServer mode stores session state in a SQL Server database. This ensures that session state is preserved if the Web application is restarted and also makes session state available to multiple Web servers in a Web farm.

  • Custom mode, which enables you to specify a custom storage provider.

  • Off mode, which disables session state.



If we don't take the sticky road and need to share Session-State between multiple servers we'll have to go for the StateServer or SQLServer option. Both necessitate that the items stored in session are serializable.



More on how we encapsulate Session-State in a later part.

10 comments:

  1. Thanks for this, great series!

    ReplyDelete
  2. I someone have slow connection then it is possible to send the same form multiple times (just before browser renders other view).

    ReplyDelete
  3. While most modern browsers have a double-click protection for submitting forms, indeed some older browser don't. In handling your form post you should check if you didn't already perform the action. However this problem is independent of Post/Redirect/Get.

    The pattern does prevent repeating the form post, once you are redirected and use the back button.

    ReplyDelete
  4. http://www.stevefenton.co.uk/Content/Blog/Date/201104/Blog/ASP-NET-MVC-Post-Redirect-Get-Pattern/

    Here it says that on an error, the PRG pattern does not require you to return a GET action. You're only required to return a GET when any data has changed.

    ReplyDelete
  5. Nice article. I probably fall into the category of people that like the fact that you never see the 'are you sure you want to re-submit?' warning.

    However not doing a redirect on invalid state, as the article mentions, is very useful for avoiding use of tempdata and sessionstate, and thus being able to scale out more easily.

    To make returning a view from your POST action as easy as possible make sure you create your view model in a separate mapper for maximum reuse. I so often see a lot of logic repeated in both controller actions, while this is totally unnecessary. Always keep DRY (don't repeat yourself) in mind.

    ReplyDelete
  6. Hello Jorrit - it's great to see people discussing the Post-Redirect-Get pattern.

    The PRG pattern is a valuable way of handling your user experience after a form post, even when you aren't using the ASP.NET MVC framework.

    There are quite a few articles online that blindly redirect whether or not the model state is valid, but this is not necessary. The cleanest and simplest way to handle an invalid post is to re-display the view. This then negates the need to pass around data as it already available directly.

    Although one reason to use the post-redirect-get pattern is to avoid the "Are you sure you wish to re-submit this form" warning, another reason is that if the user mistakenly confirms a second submission you have to consider the impact of the second submission on your data. If the original post was invalid, a re-submission will not pose a risk to your data integrity.

    Keep up the great work on your blog.

    ReplyDelete
  7. Samuel TremblayFriday, March 23, 2012

    Totally agree with Steve Fenton.

    ReplyDelete
  8. Reliability comes with cost, because SQL Server is also slowest mode (about 20% slower).

    You can experience different errors and problems when try to enable sessions on SQL Server. In many cases, error is related to insufficient rights.
    It's a great pleasure reading your post.It's full of information I am looking for more about different options?

    ReplyDelete
  9. Agree with Steve as well. The problem with the above technique is if something goes wrong, user stopping redirect, connection hickup, etc. such that the second request is not made and temp data are not consumed. User could then see these errors in a completely fresh form or even a form on another page since they all share the same tempData key. If the ModelState had hidden Id fields used to identify the object being edited, then the Id of a totally different type of object is being interpreted on a different page. So the user goes to Edit their account, gets some bad data, the modify the form to correct the data, but the Id in the hidden field is still wrong, hence they are submitting an edit for a different item then they intended.

    I personally would generate guid, use that as the key, and pass that guid along in the redirect. If StateGuid exists in querystring then retrieve via that id. This ties the TempData with the context of the current PRG flow.

    ReplyDelete