Model validation in ASP.NET Core MVC

By Rachel Appel

Introduction to model validation

Before an app stores data in a database, the app must validate the data. Data must be checked for potential security threats, verified that it's appropriately formatted by type and size, and it must conform to your rules. Validation is necessary although it can be redundant and tedious to implement. In MVC, validation happens on both the client and server.

Fortunately, .NET has abstracted validation into validation attributes. These attributes contain validation code, thereby reducing the amount of code you must write.

In ASP.NET Core 2.2 and later, the ASP.NET Core runtime short-circuits (skips) validation if it can determine that a given model graph doesn't require validation. Skipping validation can provide significant performance improvements when validating models that cannot or do not have any associated validators. The skipped validation includes objects such as collections of primitives (byte[], string[], Dictionary<string, string>, etc.), or complex object graphs without any validators.

View or download sample from GitHub.

Validation Attributes

Validation attributes are a way to configure model validation so it's similar conceptually to validation on fields in database tables. This includes constraints such as assigning data types or required fields. Other types of validation include applying patterns to data to enforce business rules, such as a credit card, phone number, or email address. Validation attributes make enforcing these requirements much simpler and easier to use.

Validation attributes are specified at the property level:

public string MyProperty { get; set; }

Below is an annotated Movie model from an app that stores information about movies and TV shows. Most of the properties are required and several string properties have length requirements. Additionally, there's a numeric range restriction in place for the Price property from 0 to $999.99, along with a custom validation attribute.

public class Movie
    public int Id { get; set; }

    public string Title { get; set; }

    public DateTime ReleaseDate { get; set; }

    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

Simply reading through the model reveals the rules about data for this app, making it easier to maintain the code. Below are several popular built-in validation attributes:

  • [CreditCard]: Validates the property has a credit card format.

  • [Compare]: Validates two properties in a model match.

  • [EmailAddress]: Validates the property has an email format.

  • [Phone]: Validates the property has a telephone format.

  • [Range]: Validates the property value falls within the given range.

  • [RegularExpression]: Validates that the data matches the specified regular expression.

  • [Required]: Makes a property required.

  • [StringLength]: Validates that a string property has at most the given maximum length.

  • [Url]: Validates the property has a URL format.

MVC supports any attribute that derives from ValidationAttribute for validation purposes. Many useful validation attributes can be found in the System.ComponentModel.DataAnnotations namespace.

There may be instances where you need more features than built-in attributes provide. For those times, you can create custom validation attributes by deriving from ValidationAttribute or changing your model to implement IValidatableObject.

Notes on the use of the Required attribute

Non-nullable value types (such as decimal, int, float, and DateTime) are inherently required and don't need the Required attribute. The app performs no server-side validation checks for non-nullable types that are marked Required.

MVC model binding, which isn't concerned with validation and validation attributes, rejects a form field submission containing a missing value or whitespace for a non-nullable type. In the absence of a BindRequired attribute on the target property, model binding ignores missing data for non-nullable types, where the form field is absent from the incoming form data.

The BindRequired attribute (also see Model Binding in ASP.NET Core) is useful to ensure form data is complete. When applied to a property, the model binding system requires a value for that property. When applied to a type, the model binding system requires values for all of the properties of that type.

When you use a Nullable<T> type (for example, decimal? or System.Nullable<decimal>) and mark it Required, a server-side validation check is performed as if the property were a standard nullable type (for example, a string).

Client-side validation requires a value for a form field that corresponds to a model property that you've marked Required and for a non-nullable type property that you haven't marked Required. Required can be used to control the client-side validation error message.

Top-level node validation

Top-level nodes include:

  • Action parameters
  • Controller properties
  • Page handler parameters
  • Page model properties

Model-bound top-level nodes are validated in addition to validating model properties. In the following example from the sample app, the VerifyPhone method uses the RegularExpressionAttribute to validate the user data in the Phone field of a form:

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
    if (!ModelState.IsValid)
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");

    return Json(true);

Top-level nodes can use BindRequiredAttribute with validation attributes. In the following example from the sample app, the CheckAge method specifies that the age parameter must be bound from the query string when the form is submitted:

public IActionResult CheckAge(
    [BindRequired, FromQuery] int age)

In the Check Age page (CheckAge.cshtml), there are two forms. The first form submits an Age value of 99 as a query string: https://localhost:5001/Users/CheckAge?Age=99.

When a properly formatted age parameter from the query string is submitted, the form validates.

The second form on the Check Age page submits the Age value in the body of the request, and validation fails. Binding fails because the age parameter must come from a query string.

Validation is enabled by default and controlled by the AllowValidatingTopLevelNodes property of MvcOptions. To disable top-level node validation, set AllowValidatingTopLevelNodes to false in MVC options (Startup.ConfigureServices):

services.AddMvc(options => 
        options.MaxModelValidationErrors = 50;
        options.AllowValidatingTopLevelNodes = false;

Model State

Model state represents validation errors in submitted HTML form values.

MVC will continue validating fields until it reaches the maximum number of errors (200 by default). You can configure this number with the following code in Startup.ConfigureServices:

services.AddMvc(options => options.MaxModelValidationErrors = 50)

Handle Model State errors

Model validation occurs before the execution of a controller action. It's the action's responsibility to inspect ModelState.IsValid and react appropriately. In many cases, the appropriate reaction is to return an error response, ideally detailing the reason why model validation failed.

When ModelState.IsValid evaluates to false in web API controllers using the [ApiController] attribute, an automatic HTTP 400 response containing issue details is returned. For more information, see Automatic HTTP 400 responses.

Some apps will choose to follow a standard convention for dealing with model validation errors, in which case a filter may be an appropriate place to implement such a policy. You should test how your actions behave with valid and invalid model states.

Manual validation

After model binding and validation are complete, you may want to repeat parts of it. For example, a user may have entered text in a field expecting an integer, or you may need to compute a value for a model's property.

You may need to run validation manually. To do so, call the TryValidateModel method, as shown here:


Custom validation

Validation attributes work for most validation needs. However, some validation rules are specific to your business. Your rules might not be common data validation techniques such as ensuring a field is required or that it conforms to a range of values. For these scenarios, custom validation attributes are a great solution. Creating your own custom validation attributes in MVC is easy. Just inherit from the ValidationAttribute, and override the IsValid method. The IsValid method accepts two parameters, the first is an object named value and the second is a ValidationContext object named validationContext. Value refers to the actual value from the field that your custom validator is validating.

In the following sample, a business rule states that users may not set the genre to Classic for a movie released after 1960. The [ClassicMovie] attribute checks the genre first, and if it's a classic, then it checks the release date to see that it's later than 1960. If it's released after 1960, validation fails. The attribute accepts an integer parameter representing the year that you can use to validate data. You can capture the value of the parameter in the attribute's constructor, as shown here:

public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator
    private int _year;

    public ClassicMovieAttribute(int year)
        _year = year;

    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext)
        Movie movie = (Movie)validationContext.ObjectInstance;

        if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year)
            return new ValidationResult(GetErrorMessage());

        return ValidationResult.Success;

The movie variable above represents a Movie object that contains the data from the form submission to validate. In this case, the validation code checks the date and genre in the IsValid method of the ClassicMovieAttribute class as per the rules. Upon successful validation ,IsValid returns a ValidationResult.Success code. When validation fails, a ValidationResult with an error message is returned:

private string GetErrorMessage()
    return $"Classic movies must have a release year earlier than {_year}.";

When a user modifies the Genre field and submits the form, the IsValid method of the ClassicMovieAttribute will verify whether the movie is a classic. Like any built-in attribute, apply the ClassicMovieAttribute to a property such as ReleaseDate to ensure validation happens, as shown in the previous code sample. Since the example works only with Movie types, a better option is to use IValidatableObject as shown in the following paragraph.

Alternatively, this same code could be placed in the model by implementing the Validate method on the IValidatableObject interface. While custom validation attributes work well for validating individual properties, implementing IValidatableObject can be used to implement class-level validation as seen here.

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        yield return new ValidationResult(
            $"Classic movies must have a release year earlier than {_classicYear}.",
            new[] { "ReleaseDate" });

Client side validation

Client side validation is a great convenience for users. It saves time they would otherwise spend waiting for a round trip to the server. In business terms, even a few fractions of seconds multiplied hundreds of times each day adds up to be a lot of time, expense, and frustration. Straightforward and immediate validation enables users to work more efficiently and produce better quality input and output.

You must have a view with the proper JavaScript script references in place for client-side validation to work as you see here.

<script src=""></script>
<script src=""></script>
<script src=""></script>

The jQuery Unobtrusive Validation script is a custom Microsoft front-end library that builds on the popular jQuery Validate plugin. Without jQuery Unobtrusive Validation, you would have to code the same validation logic in two places: once in the server side validation attributes on model properties, and then again in client side scripts (the examples for jQuery Validate's validate() method shows how complex this could become). Instead, MVC's Tag Helpers and HTML helpers are able to use the validation attributes and type metadata from model properties to render HTML 5 data- attributes in the form elements that need validation. MVC generates the data- attributes for both built-in and custom attributes. jQuery Unobtrusive Validation then parses the data- attributes and passes the logic to jQuery Validate, effectively "copying" the server side validation logic to the client. You can display validation errors on the client using the relevant tag helpers as shown here:

<div class="form-group">
    <label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="ReleaseDate" class="form-control" />
        <span asp-validation-for="ReleaseDate" class="text-danger"></span>

The tag helpers above render the HTML below. Notice that the data- attributes in the HTML output correspond to the validation attributes for the ReleaseDate property. The data-val-required attribute below contains an error message to display if the user doesn't fill in the release date field. jQuery Unobtrusive Validation passes this value to the jQuery Validate required() method, which then displays that message in the accompanying <span> element.

<form action="/Movies/Create" method="post">
    <div class="form-horizontal">
        <div class="text-danger"></div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
            <div class="col-md-10">
                <input class="form-control" type="datetime"
                data-val="true" data-val-required="The ReleaseDate field is required."
                id="ReleaseDate" name="ReleaseDate" value="" />
                <span class="text-danger field-validation-valid"
                data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>

Client-side validation prevents submission until the form is valid. The Submit button runs JavaScript that either submits the form or displays error messages.

MVC determines type attribute values based on the .NET data type of a property, possibly overridden using [DataType] attributes. The base [DataType] attribute does no real server-side validation. Browsers choose their own error messages and display those errors as they wish, however the jQuery Validation Unobtrusive package can override the messages and display them consistently with others. This happens most obviously when users apply [DataType] subclasses such as [EmailAddress].

Add Validation to Dynamic Forms

Because jQuery Unobtrusive Validation passes validation logic and parameters to jQuery Validate when the page first loads, dynamically generated forms won't automatically exhibit validation. Instead, you must tell jQuery Unobtrusive Validation to parse the dynamic form immediately after creating it. For example, the code below shows how you might set up client side validation on a form added via AJAX.

    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];

The $.validator.unobtrusive.parse() method accepts a jQuery selector for its one argument. This method tells jQuery Unobtrusive Validation to parse the data- attributes of forms within that selector. The values of those attributes are then passed to the jQuery Validate plugin so that the form exhibits the desired client side validation rules.

Add Validation to Dynamic Controls

You can also update the validation rules on a form when individual controls, such as <input/>s and <select/>s, are dynamically generated. You cannot pass selectors for these elements to the parse() method directly because the surrounding form has already been parsed and won't update. Instead, you first remove the existing validation data, then reparse the entire form, as shown below:

    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validate
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation


You may create client side logic for your custom attribute, and unobtrusive validation which creates an adapter to jquery validation will execute it on the client for you automatically as part of validation. The first step is to control what data- attributes are added by implementing the IClientModelValidator interface as shown here:

public void AddValidation(ClientModelValidationContext context)
    if (context == null)
        throw new ArgumentNullException(nameof(context));

    MergeAttribute(context.Attributes, "data-val", "true");
    MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

    var year = _year.ToString(CultureInfo.InvariantCulture);
    MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);

Attributes that implement this interface can add HTML attributes to generated fields. Examining the output for the ReleaseDate element reveals HTML that's similar to the previous example, except now there's a data-val-classicmovie attribute that was defined in the AddValidation method of IClientModelValidator.

<input class="form-control" type="datetime"
    data-val-classicmovie="Classic movies must have a release year earlier than 1960."
    data-val-required="The ReleaseDate field is required."
    id="ReleaseDate" name="ReleaseDate" value="" />

Unobtrusive validation uses the data in the data- attributes to display error messages. However, jQuery doesn't know about rules or messages until you add them to jQuery's validator object. This is shown in the following example, which adds a custom classicmovie client validation method to the jQuery validator object. For an explanation of the unobtrusive.adapters.add method, see Unobtrusive Client Validation in ASP.NET MVC.

    function (value, element, params) {
        // Get element value. Classic genre has value '0'.
        var genre = $(params[0]).val(),
            year = params[1],
            date = new Date(value);
        if (genre && genre.length > 0 && genre[0] === '0') {
            // Since this is a classic movie, invalid if release date is after given year.
            return date.getFullYear() <= year;

        return true;

    function (options) {
        var element = $(options.form).find('select#Genre')[0];
        options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
        options.messages['classicmovie'] = options.message;

With the preceding code, the classicmovie method performs client-side validation on the movie release date. The error message displays if the method returns false.

Remote validation

Remote validation is a great feature to use when you need to validate data on the client against data on the server. For example, your app may need to verify whether an email or user name is already in use, and it must query a large amount of data to do so. Downloading large sets of data for validating one or a few fields consumes too many resources. It may also expose sensitive information. An alternative is to make a round-trip request to validate a field.

You can implement remote validation in a two step process. First, you must annotate your model with the [Remote] attribute. The [Remote] attribute accepts multiple overloads you can use to direct client side JavaScript to the appropriate code to call. The example below points to the VerifyEmail action method of the Users controller.

[Remote(action: "VerifyEmail", controller: "Users")]
public string Email { get; set; }

The second step is putting the validation code in the corresponding action method as defined in the [Remote] attribute. According to the jQuery Validate remote method documentation, the server response must be a JSON string that's either:

  • "true" for valid elements.
  • "false", undefined, or null for invalid elements, using the default error message.

If the server response is a string (for example, "That name is already taken, try peter123 instead"), the string is displayed as a custom error message in place of the default string.

The definition of the VerifyEmail method follows these rules, as shown below. It returns a validation error message if the email is taken, or true if the email is free, and wraps the result in a JsonResult object. The client side can then use the returned value to proceed or display the error if needed.

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
    if (!_userRepository.VerifyEmail(email))
        return Json($"Email {email} is already in use.");

    return Json(true);

Now when users enter an email, JavaScript in the view makes a remote call to see if that email has been taken and, if so, displays the error message. Otherwise, the user can submit the form as usual.

The AdditionalFields property of the [Remote] attribute is useful for validating combinations of fields against data on the server. For example, if the User model from above had two additional properties called FirstName and LastName, you might want to verify that no existing users already have that pair of names. You define the new properties as shown in the following code:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
public string FirstName { get; set; }
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
public string LastName { get; set; }

AdditionalFields could've been set explicitly to the strings "FirstName" and "LastName", but using the nameof operator like this simplifies later refactoring. The action method to perform the validation must then accept two arguments, one for the value of FirstName and one for the value of LastName.

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyName(string firstName, string lastName)
    if (!_userRepository.VerifyName(firstName, lastName))
        return Json(data: $"A user named {firstName} {lastName} already exists.");

    return Json(data: true);

Now when users enter a first and last name, JavaScript:

  • Makes a remote call to see if that pair of names has been taken.
  • If the pair has been taken, an error message is displayed.
  • If not taken, the user can submit the form.

If you need to validate two or more additional fields with the [Remote] attribute, you provide them as a comma-delimited list. For example, to add a MiddleName property to the model, set the [Remote] attribute as shown in the following code:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }

AdditionalFields, like all attribute arguments, must be a constant expression. Therefore, you must not use an interpolated string or call Join to initialize AdditionalFields. For every additional field that you add to the [Remote] attribute, you must add another argument to the corresponding controller action method.