It’s not too difficult to perform model state validation for a JSON API because you only need to return validation errors, especially when using the ApiController
attribute. It’s still a little difficult for those who are still using Razor and server-side rendering, though.
It’s difficult because after posting a form, you have to return the right View
. Writing this kind of code in each Action
is the simplest way to accomplish the task.
if (ModelState.IsValid) { return RenderTheGetForm(); }
It accomplishes the task, but there is typically a lot of extra, tiresome code.
Instead, by moving this code into an Action Filter
attribute that can be used similarly to the ApiController attribute, we can keep the Controllers lean. So I made the decision to write one and share it.
This is how it goes. You drop the AutoValidateModel
attribute on the action that uses a View Model and validation after installing the AutomaticModelStateValidation
package.
[Route("/[controller]")] public class SubmissionController : Controller { [HttpGet] public ActionResult NewSubmission() { // Load data for the blank form return View(new NewSubmissionViewModel(...)); } [HttpPost()] [AutoValidateModel(nameof(NewSubmission))] public RedirectToActionResult SaveSubmission(SaveSubmissionViewModel model) { // Save submission to database return RedirectToAction(nameof(ViewSubmission), new { Id = 1 }); } [HttpGet] public ActionResult ViewSubmission(int id) { // Load submission from database return View(new ViewSubmissionViewModel(...)); } }
The ModelState.IsValid
check is carried out by the AutoValidateModel
attribute on the SaveSubmission
Action. The specified fallback Action is invoked and the ModelState
from the prior Action
is merged in if the model is invalid. Otherwise, everything proceeds as usual.
This means that with just one line of code, you can render the invalid Form along with validation messages and the appropriate HTTP status code!
Deep Diving
One of the features of this attribute is the ability to programmatically call the fallback action without making a second trip to the client. By temporarily storing the ModelState and rerouting to the prior action, I’ve previously accomplished similar functionality. But this time, I completely skipped that step by utilizing AspNetCore's
advancements.
The ActionFilterAttribute
class is implemented primarily by the AutoValidateModelAttribute
. The next step is to choose which check controller action to execute after first determining whether the ModelState
is invalid.
var controllerName = SansController(controller ?? context.Controller.GetType().Name);
The controller’s Type
name will be used as a fallback if it isn’t explicitly specified. I also remove the suffix “Controller
” in this case.
The following step involves getting the IActionDescriptorCollectionProvider
and locating the appropriate ActionDescriptor.
var controllerActionDescriptor = actionDescriptorCollectionProvider .ActionDescriptors.Items .OfType<ControllerActionDescriptor>() .FirstOrDefault(x => x.ControllerName == controllerName && x.ActionName == action);
The next thing I’ll need is an ActionContext
so I can call the ActionDescriptor
. Additionally, I transmit the previous ModelState
at this point to ensure that the errors in the new Action
‘s validation are preserved.
var actionContext = new ActionContext(context.HttpContext, context.RouteData, controllerActionDescriptor, context.ModelState);
The last major piece is getting an IActionInvokerFactory
to create a ControllerActionInvoker
and then invoking it.
var actionInvokerFactory = GetService<IActionInvokerFactory>(); var invoker = actionInvokerFactory.CreateInvoker(actionContext); await invoker.InvokeAsync();
RouteData
if no View
is explicitly specified. If I don’t update the RouteData to correspond, the incorrect view will be used because I’m invoking a second Action within a single Request. So, before invoking the ControllerActionInvoker
, I also set the action name to the name of the fallback action.if (context.RouteData.Values.ContainsKey(ActionNameKey)) { context.RouteData.Values[ActionNameKey] = controllerActionDescriptor.ActionName; }
Yury Sobolev is Full Stack Software Developer by passion and profession working on Microsoft ASP.NET Core. Also he has hands-on experience on working with Angular, Backbone, React, ASP.NET Core Web API, Restful Web Services, WCF, SQL Server.