Post/Redirect/Get or PRG in short is a common pattern used amongst many web applications, that was designed to prevent duplicate submissions of the forms. Not using such pattern may result e.g. in multiple transactions by POSTing the same form twice, which is something that we definitely do not want to see in our applications. Although, it’s quite easy to be implemented in it’s purest form, it’s a little bit more tricky if we want to save the input data provided by the user (let’s say the form has a lot of fields, and regular redirect would reset it to its initial state since it renders a brand new view). In this post, I’ll present how to add such filters to the MVC application that will both save the input data and also the display the validation errors from the ModelState object.





At first, let’s start with the way it was being used in the previous versions of the ASP.NET MVC. This is really quite old solution (which has over 7 years now), but works well. It is described here at #13 Use PRG Pattern for Data Modification, so let me just copy & paste the source code:

public abstract class ModelStateTempDataTransfer : ActionFilterAttribute { protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName; } public class ExportModelStateToTempData : ModelStateTempDataTransfer { public override void OnActionExecuted(ActionExecutedContext filterContext) { if (!filterContext.Controller.ViewData.ModelState.IsValid) { if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) { filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState; } } base.OnActionExecuted(filterContext); } } public class ImportModelStateFromTempData : ModelStateTempDataTransfer { public override void OnActionExecuted(ActionExecutedContext filterContext) { var modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary; if (modelState != null) { if (filterContext.Result is ViewResult) { filterContext.Controller.ViewData.ModelState.Merge(modelState); } else { filterContext.Controller.TempData.Remove(Key); } } base.OnActionExecuted(filterContext); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public abstract class ModelStateTempDataTransfer : ActionFilterAttribute { protected static readonly string Key = typeof ( ModelStateTempDataTransfer ) . FullName ; } public class ExportModelStateToTempData : ModelStateTempDataTransfer { public override void OnActionExecuted ( ActionExecutedContext filterContext ) { if ( ! filterContext . Controller . ViewData . ModelState . IsValid ) { if ( ( filterContext . Result is RedirectResult ) || ( filterContext . Result is RedirectToRouteResult ) ) { filterContext . Controller . TempData [ Key ] = filterContext . Controller . ViewData . ModelState ; } } base . OnActionExecuted ( filterContext ) ; } } public class ImportModelStateFromTempData : ModelStateTempDataTransfer { public override void OnActionExecuted ( ActionExecutedContext filterContext ) { var modelState = filterContext . Controller . TempData [ Key ] as ModelStateDictionary ; if ( modelState != null ) { if ( filterContext . Result is ViewResult ) { filterContext . Controller . ViewData . ModelState . Merge ( modelState ) ; } else { filterContext . Controller . TempData . Remove ( Key ) ; } } base . OnActionExecuted ( filterContext ) ; } }

And then you can decorate the controller actions with these filters:

[HttpGet] [ImportModelStateFromTempData] [Route("users")] public ActionResult Create() { return View(); } [HttpPost] [ExportModelStateToTempData] [Route("users")] public ActionResult Create(UserViewModel viewModel) { if (!ModelState.IsValid) return RedirectToAction("Create"); //Create user etc. } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [ HttpGet ] [ ImportModelStateFromTempData ] [ Route ( "users" ) ] public ActionResult Create ( ) { return View ( ) ; } [ HttpPost ] [ ExportModelStateToTempData ] [ Route ( "users" ) ] public ActionResult Create ( UserViewModel viewModel ) { if ( ! ModelState . IsValid ) return RedirectToAction ( "Create" ) ; //Create user etc. }

I thought it will be pretty much the same in the new version of ASP.NET 5 and MVC 6, yet seems that I was wrong. I’m working on the web panel for the Warden project, and since I’ve decided it will be a cross-platform tool, it was quite natural to make use of the new ASP.NET 5.

So let me show you, step by step, how to achieve the same functionality as in the example above:

Install the Microsoft.AspNet.Session and Newtonsoft.Json NuGet packages.

In the Startup class add the following: services.AddSession() inside the ConfigureServices() and app.UseSession() within the Configure() method.

class add the following: services.AddSession() inside the ConfigureServices() and app.UseSession() within the Configure() method. Great, now we can make use of the TempData, but beware it’s just the beginning.

Proceed by creating two extension methods for the JSON serialization – you don’t need to do this, but it cleans up the overall code a little bit:

public static class Extensions { public static string ToJson<T>(this T value) { return JsonConvert.SerializeObject(value); } public static T FromJson<T>(this string value) { return JsonConvert.DeserializeObject<T>(value); } } 1 2 3 4 5 6 7 8 9 10 11 12 public static class Extensions { public static string ToJson < T > ( this T value ) { return JsonConvert . SerializeObject ( value ) ; } public static T FromJson < T > ( this string value ) { return JsonConvert . DeserializeObject < T > ( value ) ; } }

Finally, let’s move on to the implementation of these filters. I’ll paste the code first and then describe what’s going on:

public abstract class ModelStateTempDataTransfer : ActionFilterAttribute { protected static readonly string ModelEntriesKey = typeof(ModelStateTempDataTransfer).FullName; protected internal class Entry { public object RawValue { get; set; } public string AttemptedValue { get; set; } public ModelValidationState State { get; set; } public IEnumerable<string> Errors { get; set; } public Entry() { } public Entry(ModelStateEntry entry) { RawValue = entry.RawValue; AttemptedValue = entry.AttemptedValue; State = entry.ValidationState; Errors = entry.Errors?.Select(x => x.ErrorMessage) ?? Enumerable.Empty<string>(); } } } public class ExportModelStateToTempData : ModelStateTempDataTransfer { public override void OnActionExecuted(ActionExecutedContext filterContext) { var tempData = filterContext.HttpContext.RequestServices.GetService<ITempDataDictionary>(); if (filterContext.ModelState.IsValid) return; if (IsInvalidResult(filterContext)) return; var values = filterContext.ModelState.ToList() .Select(item => new KeyValuePair<string, Entry>(item.Key, new Entry(item.Value))) .ToDictionary(x => x.Key, x => x.Value); tempData[ModelEntriesKey] = values.ToJson(); base.OnActionExecuted(filterContext); } private static bool IsInvalidResult(ActionExecutedContext context) => !(context.Result is RedirectToActionResult) && !(context.Result is RedirectResult) && !(context.Result is RedirectToRouteResult); } public class ImportModelStateFromTempData : ModelStateTempDataTransfer { public override void OnActionExecuted(ActionExecutedContext filterContext) { var tempData = filterContext.HttpContext.RequestServices.GetService<ITempDataDictionary>(); if (!tempData.ContainsKey(ModelEntriesKey)) return; var modelEntries = tempData[ModelEntriesKey].ToString().FromJson<Dictionary<string, Entry>>(); if (modelEntries == null) return; foreach (var modelEntry in modelEntries) { filterContext.ModelState.Add(modelEntry.Key, new ModelStateEntry { AttemptedValue = modelEntry.Value.AttemptedValue, RawValue = modelEntry.Value.RawValue, ValidationState = modelEntry.Value.State }); foreach (var error in modelEntry.Value.Errors) { filterContext.ModelState.AddModelError(modelEntry.Key, error); } } tempData.Remove(ModelEntriesKey); base.OnActionExecuted(filterContext); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 public abstract class ModelStateTempDataTransfer : ActionFilterAttribute { protected static readonly string ModelEntriesKey = typeof ( ModelStateTempDataTransfer ) . FullName ; protected internal class Entry { public object RawValue { get ; set ; } public string AttemptedValue { get ; set ; } public ModelValidationState State { get ; set ; } public IEnumerable < string > Errors { get ; set ; } public Entry ( ) { } public Entry ( ModelStateEntry entry ) { RawValue = entry . RawValue ; AttemptedValue = entry . AttemptedValue ; State = entry . ValidationState ; Errors = entry . Errors ? . Select ( x = > x . ErrorMessage ) ? ? Enumerable . Empty < string > ( ) ; } } } public class ExportModelStateToTempData : ModelStateTempDataTransfer { public override void OnActionExecuted ( ActionExecutedContext filterContext ) { var tempData = filterContext . HttpContext . RequestServices . GetService < ITempDataDictionary > ( ) ; if ( filterContext . ModelState . IsValid ) return ; if ( IsInvalidResult ( filterContext ) ) return ; var values = filterContext . ModelState . ToList ( ) . Select ( item = > new KeyValuePair < string , Entry > ( item . Key , new Entry ( item . Value ) ) ) . ToDictionary ( x = > x . Key , x = > x . Value ) ; tempData [ ModelEntriesKey ] = values . ToJson ( ) ; base . OnActionExecuted ( filterContext ) ; } private static bool IsInvalidResult ( ActionExecutedContext context ) = > ! ( context . Result is RedirectToActionResult ) && ! ( context . Result is RedirectResult ) && ! ( context . Result is RedirectToRouteResult ) ; } public class ImportModelStateFromTempData : ModelStateTempDataTransfer { public override void OnActionExecuted ( ActionExecutedContext filterContext ) { var tempData = filterContext . HttpContext . RequestServices . GetService < ITempDataDictionary > ( ) ; if ( ! tempData . ContainsKey ( ModelEntriesKey ) ) return ; var modelEntries = tempData [ ModelEntriesKey ] . ToString ( ) . FromJson < Dictionary < string , Entry >> ( ) ; if ( modelEntries == null ) return ; foreach ( var modelEntry in modelEntries ) { filterContext . ModelState . Add ( modelEntry . Key , new ModelStateEntry { AttemptedValue = modelEntry . Value . AttemptedValue , RawValue = modelEntry . Value . RawValue , ValidationState = modelEntry . Value . State } ) ; foreach ( var error in modelEntry . Value . Errors ) { filterContext . ModelState . AddModelError ( modelEntry . Key , error ) ; } } tempData . Remove ( ModelEntriesKey ) ; base . OnActionExecuted ( filterContext ) ; } }

Even though it does the same thing, that stuff got more complicated while compared to the previous version. At first, you may notice that I’m using JSON to serialize the object stored in the TempData (which works with the session under the hood).

I’ve tried to serialize the whole ModelState in order to be able to invoke the Merge() method in the ImportModelStateFromTempData but without luck – some classes do not have the default constructors, therefore, I had to create the Entry class.

Eventually, you can see the loop that goes through all of the properties and sets the values and errors (if there are any).

And that’s all – it does work the same way as the previous implementation.

Feel free to tweak this code as I’m still getting to know the new ASP.NET 5 so I guess that some things could’ve been done better.

Anyway, I’ve achieved the goal and have managed to get the PRG pattern working in an old fashioned way while using the newest version of the framework.