Understanding Modelstate in ASP.NET 5 MVC - Windows ASP.NET Core Hosting 2024 | Review and ComparisonWindows ASP.NET Core Hosting 2024 | Review and Comparison

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

https://github.com/exceptionnotfound/ModelStateAspNet5Demo

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> 

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:

<h2>Add Movie</h2>

<form method="post" action="/movies/add">
    <div>
        <div>
            <label for="Title">Title</label>
            <input type="text" 
                   id="Title" 
                   name="Title" 
                   value="">
        </div>
        <div>
            <label for="ReleaseDate">ReleaseDate</label>
            <input type="date" 
                   id="ReleaseDate" 
                   name="ReleaseDate" 
                   value="0001-01-01">
        </div>
        <div>
            <label for="RuntimeMinutes">RuntimeMinutes</label>
            <input type="number" 
                   id="RuntimeMinutes" 
                   name="RuntimeMinutes" 
                   value="0">
        </div>
        <div>
            <input type="submit" value="Save">
        </div>
    </div>
</form> 

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 TitleReleaseDate, 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.