If you are programming in ASP.NET 5.0 MVC, you will certainly have seen the class ModelState
pop up a few times. Ever wondered what that is, or what the property ModelState.IsValid
does, and why it's there? So did I.
In this post, we're going to explain what the ModelState
is, and what it is used for. We'll also show how to use it to validate our POSTed inputs, and do simple custom validation. Let's go!
What is the ModelState?
In short, the ModelState
is a collection of name and value pairs that are submitted to the server during a POST. It also contains error messages about each name-value pair, if any are found.
ModelState
is a property of a Controller
instance, and can be accessed from any class that inherits from Microsoft.AspNetCore.Mvc.Controller.
The ModelState
has two purposes: to store and submit POSTed name-value pairs, and to store the validation errors associated with each value.
All right, enough of the boring explanation. It's code time!
The Sample Project
As always, there's a sample project on GitHub that you can use to follow along with this post. Check it out here:
Setup
Let's start writing the code we need to demonstrate how the ModelState
works in ASP.NET 5 MVC. We will begin by creating a straightforward view model, AddMovieVM
:
namespace ModelStateCoreDemo.ViewModels
{
public class AddMovieVM
{
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public int RuntimeMinutes { get; set; }
}
}
We will also create a corresponding Add.cshtml
view in the folder Views/Movie:
@model ModelStateCoreDemo.ViewModels.AddMovieVM
<h2>Add Movie</h2>
<form asp-action="AddPost" asp-controller="Movie" method="post">
<div>
<div>
<label asp-for="Title"></label>
<input type="text" asp-for="Title" />
</div>
<div>
<label asp-for="Description"></label>
<input type="text" asp-for="Description" />
</div>
<div>
<label asp-for="ReleaseDate"></label>
<input type="date" asp-for="ReleaseDate" />
</div>
<div>
<label asp-for="RuntimeMinutes"></label>
<input type="number" asp-for="RuntimeMinutes" />
</div>
<div>
<input type="submit" value="Save" />
</div>
</div>
</form>
If you are not familiar with the asp-for
property, check out my earlier post on Tag Helpers (they work the same way in ASP.NET 5 as in ASP.NET Core):
Lastly, we create a MovieController
class with two actions:
using Microsoft.AspNetCore.Mvc;
using ModelStateCoreDemo.ViewModels;
public class MovieController : Controller
{
[HttpGet("movies/add")]
public IActionResult Add()
{
AddMovieVM model = new AddMovieVM();
return View(model);
}
[HttpPost("movies/add/post")]
public IActionResult AddPost(AddMovieVM model)
{
if(!ModelState.IsValid)
{
return View("Add", model);
}
return RedirectToAction("Index");
}
}
When we submit our Add form to the POST action, all of the values we entered on the view will show up in the correct properties in the AddMovieVM
instance. But how did they get there?
Peeking Into the ModelState
Let's take a peek at the rendered HTML for the Add page:
When this page is POSTed to the server, all values in <input>
tags will be submitted as name-value pairs.
At the point when ASP.NET 5 MVC receives a POST action, it takes all of the name-value pairs and adds them as individual instances of ModelStateEntry
to an instance of ModelStateDictionary
. Both ModelStateEntry and ModelStateDictionary have pages in Microsoft's documentation.
In Visual Studio, we can use the Locals window to show what exactly this looks like:
We can see from the Values
property of the ModelState
that there are three name-value pairs: one each for Title
, ReleaseDate
, and RuntimeMinutes
.
Each of the items in the results for the Values
is of type ModelStateEntry
. But what exactly is this?
What's In the ModelStateEntry?
Here's what those same input values look like, taken from the same debugger session.
You can see that each ModelStateEntry
contains properties like RawValue
, which holds the raw value that was submitted, and Key
, which holds the name of said value. ASP.NET 5 MVC creates all of these instances for us automatically when we submit a POST action that has associated data. ASP.NET 5 MVC is therefore making what was a complicated set of data into easier-to-use instances.
There are two important functions of the ModelState
that we still need to discuss: errors and validation.
Validation Errors in the ModelState
Let's make a quick modification to our AddMovieVM
class so that we are now implementing validation on its fields.
public class AddMovieVM
{
[Required(ErrorMessage = "The Title cannot be blank.")]
[DisplayName("Title: ")]
public string Title { get; set; }
[DisplayName("Release Date: ")]
public DateTime ReleaseDate { get; set; }
[Required(ErrorMessage = "The Runtime Minutes cannot be blank.")]
[Range(1, int.MaxValue,
ErrorMessage = "The Runtime Minutes must be greater than 0.")]
[DisplayName("Runtime Minutes: ")]
public int RuntimeMinutes { get; set; }
}
We have added validation to these fields using the [Required]
and [Range]
validation attributes, as well as specifying the name to use in <label>
tags with the [DisplayName]
attribute. If the Title
, the Description
, or the RuntimeMinutes
field fails validation, we need to show an error message.
To do that, we must make some changes to our view; we will need to add a <span>
tag with the property asp-validation-for
set for each input:
@model ModelStateCoreDemo.ViewModels.AddMovieVM
<h2>Add Movie</h2>
<form asp-action="AddPost" asp-controller="Movie" method="post">
<div>
<div>
<label asp-for="Title"></label>
<input type="text" asp-for="Title"/>
<span asp-validation-for="Title"></span>
</div>
<div>
<label asp-for="ReleaseDate"></label>
<input type="date" asp-for="ReleaseDate" />
<span asp-validation-for="ReleaseDate"></span>
</div>
<div>
<label asp-for="RuntimeMinutes"></label>
<input type="number" asp-for="RuntimeMinutes" />
<span asp-validation-for="RuntimeMinutes"></span>
</div>
<div>
<input type="submit" value="Save"/>
</div>
</div>
</form>
The asp-validation-for
property will render the validation errors message in the specified <span>
element, with some default CSS already applied.
Let's take a look at what happens to our collection of ModelStateEntry
instances when an invalid object is submitted to the server:
Note that the IsValid
property of the ModelState
is now false
, and the ValidationState
property of the invalid ModelStateEntry
instances is now Invalid
.
When ASP.NET 5 MVC creates the model state for the submitted values, it also iterates through each property in the view model and validates said property using the attributes associated to that property. In certain cases, attributes are implicitly evaluated (in our particular case, ReleaseDate
has an implicit [Required]
attribute because its type is DateTime
and therefore cannot be blank).
If any errors are found, they are added to the Errors
property of each ModelStateEntry
. There is also a property ErrorCount
on the ModelState
that shows all errors found during the validation. If errors exist for a given POSTed value, the ValidationState
of that property becomes Invalid
.
In this case, because at least one of the ModelStateEntry
instances has an error, the IsValid
property of ModelState
is now false
.
What all of this means is merely this: we are using ASP.NET 5 MVC the way it was intended to be used. The ModelState
stores submitted values, calculates the errors associated with each, and allows our controllers to check for said errors as well as the IsValid
flag. In many scenarios, this is all we need, and all of it happens behind the scenes!
Custom Validation
There are times when we need more complex validation than ASP.NET 5 provides natively.
Let's pretend that we now need a new field, Description
, for each movie. Let's also pretend that in addition to not allowing Description
to be blank, it also cannot be exactly the same as the Title
.
First, we can add the Description
field to the view model:
public class AddMovieVM
{
[Required(ErrorMessage = "The Title cannot be blank.")]
[DisplayName("Title: ")]
public string Title { get; set; }
[Required(ErrorMessage = "The Description cannot be blank.")]
[DisplayName("Description: ")]
public string Description { get; set; }
[DisplayName("Release Date: ")]
public DateTime ReleaseDate { get; set; }
[Required(ErrorMessage = "The Runtime Minutes cannot be blank.")]
[Range(1, int.MaxValue,
ErrorMessage = "The Runtime Minutes must be greater than 0.")]
[DisplayName("Runtime Minutes: ")]
public int RuntimeMinutes { get; set; }
}
We can also add it to the view:
@model ModelStateCoreDemo.ViewModels.AddMovieVM
<h2>Add Movie</h2>
<form asp-action="AddPost" asp-controller="Movie" method="post">
<div>
<div>
<label asp-for="Title"></label>
<input type="text" asp-for="Title" />
<span asp-validation-for="Title"></span>
</div>
<div>
<label asp-for="Description"></label>
<input type="text" asp-for="Description" />
<span asp-validation-for="Description"></span>
</div>
<div>
<label asp-for="ReleaseDate"></label>
<input type="date" asp-for="ReleaseDate" />
<span asp-validation-for="ReleaseDate"></span>
</div>
<div>
<label asp-for="RuntimeMinutes"></label>
<input type="number" asp-for="RuntimeMinutes" />
<span asp-validation-for="RuntimeMinutes"></span>
</div>
<div>
<input type="submit" value="Save" />
</div>
</div>
</form>
When the form is POSTed, we can do custom validation in our controllers. To check that the Description
does not exactly match the Title
, we can modify the POST action on our MovieController
class like so:
public class MovieController : Controller
{
//... Other methods
[HttpPost("movies/add/post")]
public IActionResult AddPost(AddMovieVM model)
{
if(model.Title == model.Description)
{
ModelState.AddModelError("description",
"The Description cannot exactly match the Title.");
}
if(!ModelState.IsValid)
{
return View("Add", model);
}
return RedirectToAction("Index");
}
}
Which will show the error next to the input for Description
:
Summary
In ASP.NET 5 MVC, the ModelState
property of a controller represents the submitted values, and validation errors in those values if such errors exist, during a POST action.
During the POST, the values submitted can be validated, and the validation process uses attributes defined by .NET like [Required]
and [Range]
. We can do simple custom validation server-side by modifying our controllers.
On views, the asp-validation-for
property on a <span>
element will show an error message for a specific property of the view model.
I wrote a similar post a few years back about how ModelState
behaved in ASP.NET Framework. You can read that post here:
Finally, if this post helped you, would you consider buying me a coffee? Your support means a lot to me, and helps fund all of my projects. Thank you!
Happy Coding!