Model Validation in ASP.NET Core from Beginning to Expert

Model Validation in ASP.NET Core from Beginning to Expert

Model Validation is a process to ensure the data received from the View is appropriate to bind the Model. If it is not, then appropriate error messages are displayed on the View, and that will help user to rectify the problem.

Let us understand it by applying Model Validation to a Job Application form. In this form there are different types of controls used for entering:

  • 1. User’s name in an Input control.
  • 2. User’s date of birth in an Input control.
  • 3. User’s sex in a radio button.
  • 4. User’s experience in select control.
  • 5. Accept terms in a checkbox.

What is the Need for Model Validation ?

To understand the need for Model Validation you need to create a Job Application form. So first create JobApplication class inside the Models folder with code as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ModelBindingValidation.Models
{
    public class JobApplication
    {
        public string Name { get; set; }

        public DateTime DOB { get; set; }

        public string Sex { get; set; }

        public string Experience { get; set; }

        public bool TermsAccepted { get; set; }
    }
}

The JobApplication class defines various properties on which Model Validation will be applied.

Next add 2 views, first one is the Index View which should be kept inside the Views ➤ Job folder with code as given below:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Job Application";
}
 
<h2>Job Application</h2>
 
<form class="m-1 p-1" method="post">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="DOB"></label>
        <input asp-for="DOB" type="text" asp-format="{0:d}" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Sex"></label>
        <div>
            <input asp-for="Sex" type="radio" value="M" />Male
            <input asp-for="Sex" type="radio" value="F" />Female
        </div>
    </div>
    <div class="form-group">
        <label asp-for="Experience"></label>
        <select asp-for="Experience" class="form-control">
            <option value="Select">Select</option>
            <option value="0">Fresher</option>
            <option value="1">0-1 years</option>
            <option value="2">1-2 years</option>
            <option value="3">2-3 years</option>
            <option value="4">3-4 years</option>
            <option value="5">4-5 years</option>
        </select>
    </div>
    <div class="form-group">
        <input asp-for="TermsAccepted" />
        <label asp-for="TermsAccepted" class="form-check-label">
            I accept the terms & conditions
        </label>
    </div>
    <button type="submit" class="btn btn-primary">Submit Application</button>
</form>

It contains a form where the users will fill their details and submit it to the job agency.

The second view is called Accepted and is kept inside the Views ➤ Job folder with code given below:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Accepted";
}
 
<h2>Accepted</h2>
 
<table class="table table-bordered">
    <tr>
        <th>Your name is:</th>
        <td>@Model.Name</td>
    </tr>
    <tr>
        <th>Your date of birth is:</th>
        <td>@Model.DOB.ToString("d")</td>
    </tr>
    <tr>
        <th>Your sex is:</th>
        <td>@Model.Sex</td>
    </tr>
    <tr>
        <th>Your experience is:</th>
        <td>@Model.Experience</td>
    </tr>
    <tr>
        <th>Have you accepted terms:</th>
        <td>@Model.TermsAccepted</td>
    </tr>
</table>
<a class="btn btn-success" asp-action="Index">Back</a>

This view will show the submitted values on the form.

Now create a controller file called JobController which should contains 2 action methods, these are:

  • 1. HttpGet version of Index – which will shown the Index View and here the users will fill the form.
  • 2. HttpPost version of Index – which will be invoked once the users submit the form. This action gets all the values filled by the user (in it’s argument which is of type JobApplication) through Model Binding technique. It then invokes the Accepted View and passes the submitted values as it’s model.

The code for these action methods are given below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;

namespace ModelBindingValidation.Controllers
{
    public class JobController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Index(JobApplication jobApplication)
        {
            return View("Accepted", jobApplication);
        }
    }
}

Now run your application and go to the URL – /Job, where you will find the Job Application form. The form looks normal but note that there is No Model Validation applied to it.

Fill the values on the controls as given below:

1. Name – Donald
2. DOB – 24-12-1986
3. Sex – leave it untouched
4. Experience – leave it untouched
5. I accept the terms & conditions – leave it untouched

The image below which shows these filled values:

job application

When you click the Submit Application button you will see your filled values, as shown in the image below:

job application filled values

But Wait! There is some flaw. You filled 24-12-1986 for DOB field but it shows 01-01-0001.

This happens because the DOB field has to be entered in MM-DD-YYYY format buy you entered it in DD-MM-YYYY format, and so this value does not bind to the DOB property of the JobApplication argument of the action method, which instead showed you the default value of a DataTime (01-01-0001).

The form also has no way to force the user to choose his/her sex, experience and Terms-Conditions.

To prevent user from entering wrong date for DOB and force him/her to enter all his information in the controls, we use the process of Model Validation.

Model Validation from “AddModelError()” method & “ModelState.IsValid” property

For doing Model Validation you have to know 2 things:

  • 1. AddModelError()
  • 2. ModelState.IsValid

AddModelError() method

The AddModelError() method is used to record a Model Validation error for a specified property.

The syntax of AddModelError() method is:

AddModelError(property, message);

property – it is the property of the model class.

message – a string that specifies the error message.

ModelState.IsValid property

The ModelState.IsValid is a property that returns true if all the model properties are valid and returns false otherwise.

Now I will use both of these method and property for doing Model Validation. So change the HttpPost version of Index Action Method as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;

namespace ModelBindingValidation.Controllers
{
    public class JobController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Index(JobApplication jobApplication)
        {
            if (string.IsNullOrEmpty(jobApplication.Name))
                ModelState.AddModelError(nameof(jobApplication.Name), "Please enter your name");

            if (jobApplication.DOB == Convert.ToDateTime("01-01-0001 00:00:00"))
                ModelState.AddModelError(nameof(jobApplication.DOB), "Please enter your Date of Birth");
            else if (jobApplication.DOB > DateTime.Now)
                ModelState.AddModelError(nameof(jobApplication.DOB), "Date of Birth cannot be in the future");
            else if (jobApplication.DOB < new DateTime(1980, 1, 1))
                ModelState.AddModelError(nameof(jobApplication.DOB), "Date of Birth should not be before 1980");

            if (string.IsNullOrEmpty(jobApplication.Sex))
                ModelState.AddModelError(nameof(jobApplication.Sex), "Please select your sex");

            if (jobApplication.Experience.ToString() == "Select")
                ModelState.AddModelError(nameof(jobApplication.Experience), "Please select your experience");

            if (!jobApplication.TermsAccepted)
                ModelState.AddModelError(nameof(jobApplication.TermsAccepted), "You must accept the Terms");

            if (ModelState.IsValid)
                return View("Accepted", jobApplication);
            else
                return View();
        }
    }
}

In the above highlighted code block I have applied checks on every property of the JobApplication class to ensure that the users provide some valid values for them. Let us discuss each of them one by one.

Name property

For the Name property the code – if (string.IsNullOrEmpty(jobApplication.Name)) checks if the user has not filled the name in the name control, in that case the code – ModelState.AddModelError(nameof(jobApplication.Name), "Please enter your name"); gets executed and Model Validation error is recorded for the Name property.

DOB property

There are 3 checks done for the DOB property to determine the problems, if any:

Check 1 – If user has not filled the DOB or provided some invalid date like “apple” for this field. Then it is detected by the code if (jobApplication.DOB == Convert.ToDateTime("01-01-0001 00:00:00")). The DateTime property has the default value of 01-01-0001 00:00:00 which happens when user has not supplied any value.

Check 2 – If user has provided a future date (from the current day time). It is checked by the code line – else if (jobApplication.DOB > DateTime.Now).

Check 3 – If the user has provided the DOB before the year 1980. It is checked by the code else if (jobApplication.DOB < new DateTime(1980, 1, 1)).

Appropriate messages are provided whenever the checks detect some problem.

Sex property

Two radio buttons are provided for selecting the sex of the user. If the user does not select his/her sex, which is found out by the code – if (string.IsNullOrEmpty(jobApplication.Sex)). In such a case error message is recorded for the Sex property by the code line – ModelState.AddModelError(nameof(jobApplication.Sex), "Please select your experience");.

Experience property

A select control is provided on the view for selecting the Experience. The default selected value is Select, so the code if (jobApplication.Experience.ToString() == "Select") determines if the user has not selected the experience. In such a case an error message is recorded for the Experience property.

TermsAccepted property

If the user has not selected the checkbox control for the TermsAccepted property then error message is recorded. The code if (!jobApplication.TermsAccepted) finds out is the checkbox is unchecked.

Finally after I have validated all the properties with the code line – ModelState.IsValid to see if there are no errors. It will return true if there are no errors, in that case I am returning to the view called Accepted and passing the jobApplication object as model to it.

It ModelState.IsValid returns false then it means there are some model errors so I am simply returning to the same view i.e. the Index View.

The below code does this checking for the ModelState:

if (ModelState.IsValid)
    return View("Accepted", jobApplication);
else
    return View();

You can now check how this works by filling the form in the URL – /Home/Job. You will notice that you are now required to fill and select all the values, also the date value should not be in the future and must not be before the year 1980.

Check the HTML formed of the Controls

Model Validation feature adds the CSS class called input-validation-error to all the controls that have failed the validation checks i.e. whenever some error message is recorded for a property by the method – ModelState.AddModelError().

To check this, open the URL – /Job and click the Submit Application button without filling any values on the form. When you see the validation errors, check the HTML formed for the ‘Name’ control, which will contain this class – input-validation-error as shown below:

<input class="form-control input-validation-error" type="text" id="Name" name="Name" value="">

Note that you will also find the input-validation-error class on other controls that failed the validation checks.

Now I can use this to add red border to controls that have errors by adding some CSS code. So add a style block on the Index View of the Job Controller like shown below:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Job Application";
}

<style>
    .input-validation-error {
        border-color: red;
    }
</style>
 
<h2>Job Application</h2>
 
<form class="m-1 p-1" method="post">
...
</form>

Now you will see the controls having errors highlighted by red border as shown in the image below:

highlight controls having errors

Displaying Model Validation Errors in “asp-validation-summary” Tag Helper

The asp-validation-summary Tag Helper is used to show the validation errors that have been added in the Action method. Add this Tag Helper to a new div control in the View and you will see all the recorded validation errors, see the below code:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Job Application";
}
 
<h2>Job Application</h2>
 
<div asp-validation-summary="All" class="text-danger"></div>

<form class="m-1 p-1" method="post">
    ...
</form>
 
<style>
    .input-validation-error {
        border-color: red;
    }
</style>

The asp-validation-summary Tag Helper is applied on the div so the validation errors will be shown on this element. Note that the text-danger is a Bootstrap class that gives red color to text.

Create Custom Tag Helper in ASP.NET Core and turn custom HTML codes in the views to extremely powerful features.

Now re-run your application and submit the form, given on the URL – /Home/Job once again, without filling any value in any control. You will see all the Model Validation Messages displayed on your browser, as shown by the image below:

model validation messages using asp validation summary

You can give 3 values to the asp-validation-summary Tag Helper, these are given below:

Name Description
All Displays all the recorded validation messages.
ModelOnly Displays only the validation message that are recorded for the entire model. It excludes those validation messages that are recorded for individual properties.
None Does not displays any validation message.

Displaying Individual Validation Messages with “asp-validation-for” Tag Helper

Obviously it is more useful to display the Model Validation Error Messages alongside the controls that contain the problematic data. This you can achieve by using the asp-validation-for tag helper.

For example, to show error messages for the name property, add this tag helper to a new span element like:

<span asp-validation-for="Name"></span>

Now go to the Index View of the Job Controller and add a span control containing the asp-validation-for tag helper, for each control to show there validation errors messages just next to them, as listed in the below given code:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Job Application";
}

<style>
    .input-validation-error {
        border-color: red;
    }
</style>

<h2>Job Application</h2>

<div asp-validation-summary="All" class="text-danger"></div>

<form class="m-1 p-1" method="post">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="DOB"></label>
        <input asp-for="DOB" type="text" asp-format="{0:d}" class="form-control" />
        <span asp-validation-for="DOB" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Sex"></label>
        <div>
            <input asp-for="Sex" type="radio" value="M" />Male
            <input asp-for="Sex" type="radio" value="F" />Female
        </div>
        <span asp-validation-for="Sex" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Experience"></label>
        <select asp-for="Experience" class="form-control">
            <option value="Select">Select</option>
            <option value="0">Fresher</option>
            <option value="1">0-1 years</option>
            <option value="2">1-2 years</option>
            <option value="3">2-3 years</option>
            <option value="4">3-4 years</option>
            <option value="5">4-5 years</option>
        </select>
        <span asp-validation-for="Experience" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input asp-for="TermsAccepted" />
        <label asp-for="TermsAccepted" class="form-check-label">
            I accept the terms & conditions
        </label>
        <span asp-validation-for="TermsAccepted" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Submit Application</button>
</form>

Run your application once more and submit the form without filling any value. This time you will see the individual error messages displayed besides each control. The below image shows these error messages:

model validation errors besides each control

Model-Level Error Messages

Model-Level Error Messages are those that are applicable to the model as a whole and not to individual properties. You can apply then providing empty string (“”) to the property parameter of the AddModelError() method as shown by the below code.

ModelState.AddModelError("", "Some Model-Level error");  

Suppose I do not want a person named Osama Bin Laden to apply for the Job. So for doing this I add a new else if block to check whether the name is ‘Osama Bin Laden’ and if it is then provide a Model-Level validation error message. This is shown in the code below.

[HttpPost]
public IActionResult Index(JobApplication jobApplication)
{
    if (string.IsNullOrEmpty(jobApplication.Name))
        ModelState.AddModelError(nameof(jobApplication.Name), "Please enter your name");
    else if(jobApplication.Name=="Osama Bin Laden")
        ModelState.AddModelError("", "You cannot apply for the Job");
     
    // removed for clarity
}

Also go to the Index View of Job Controller and change the asp-validation-summary to ModelOnly.

...
 
<h2>Job Application</h2>
 
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<form class="m-1 p-1" method="post">
    ...
</form>
 
<style>
    .input-validation-error {
        border-color: red;
    }
</style>

It’s time to check the working by re-running the application and on the form fill Osama Bin Laden for the name control and click the button. This time you will only see the Model-Level error message – “You cannot apply for the Job”.

See the image below:

model level error message

Model Validation from Data Annotations

There is another way to do Model Validations which is by the use of Validation Attributes in the Model class. This process is known as Model Validation from Data Annotation and is very easy, fast and removes code duplication.

Start with the database programming by reading this article – Create Records using ADO.NET in ASP.NET Core Application

The Validation Attributes are described in the table below:

Name Description
[Required(ErrorMessage = “Some Message”)] Ensures that the value is not empty. Can be applied to values who have a default value of null type like int?, float?, string.
[StringLength(max,min)] Ensures that the string’s length is from min to max values (min & max are included). Example [StringLeght(2,5)] allows all string from length 2 to 5.
[Compare(“OtherProperty”)] Ensures that the property to which it is applied and the property it specifies (i.e. OtherProperty) have the same value.
[Range(min,max)] Ensures that the numeric value lies from the min to max values, min and max values are included. Example [Range(2,5)] allows values from 2,3,4 and 5.
[RegularExpression(“pattern”)] Ensures that the value matches the regular expression pattern. Example for allowing proper email addresses.

Now go to the JobApplication class and import the namespace called System.ComponentModel.DataAnnotations, and then add the attributes to the properties like shown in the code below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;

namespace ModelBindingValidation.Models
{
    public class JobApplication
    {
        [Required]
        [Display(Name = "Job applicant name")]
        public string Name { get; set; }

        public DateTime DOB { get; set; }

        [Required(ErrorMessage = "Please select your sex")]
        public string Sex { get; set; }

        [Range(0, 5)]
        public string Experience { get; set; }

        [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms")]
        public bool TermsAccepted { get; set; }
    }
}

In the Name property I applied 2 attributes, which are:

  • 1. Display
  • 2. Required
[Required]
[Display(Name = "Job applicant name")]
public string Name { get; set; }

With the [Display(Name = "Job applicant name")] attribute applied on the property I have changed the display name of the property in the View to Job applicant name. Also note that since the [Required] attribute has no value specified for the property called ErrorMessage therefore the Name property’s value (which is ‘Job applicant name’) of the ‘Display’ attribute is automatically taken as the value for the ErrorMessage property.

There is no need to apply any validation attribute for DOB field which is of type DateTime. This is because the DateTime field has a default value of 01-01-0001 00:00:00 so when user does not fill any value to this field then this default value will be used, thus making the [Required] attribute useless.

So in such a case and Error message – The value ” is invalid will be shown.

However if you still want to apply [Required] to work on this field then make it a nullable type like:

[Required(ErrorMessage = "Please enter your Date of Birth")]
public DateTime? DOB { get; set; }

In the above code lines I have applied the [Required(ErrorMessage = "Please enter your Date of Birth")] attribute so it ensures that the values for the DOB field cannot be empty and should be in proper date format.

In the same way the [Required] attribute is applied for the Sex field.

In the Experience field only the Range attribute is applied. This is because it is sufficient to do the validation for my select control which contains experience from 0 to 5 only.

[Range(0, 5)]
public int Experience { get; set; }

Finally on the TermsAccepted field I have applied the [Range] attribute like:

[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms")]
public bool TermsAccepted { get; set; }

I cannot use the [Required] attribute and force the user to check the checkbox because as this field is of type bool. This means a default value of false will be sent if the checkbox is not checked, and thus making the Required attribute useless.

To work around I have used the [Range] attribute and provided a type of bool as the value for it’s first parameter. I have also set both the min and max values as true and thus forcing the user to check the checkbox.

Now finally you have to remove the previous validation codes from the action method. So change your Index Action of the Job Controller as shown below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;

namespace ModelBindingValidation.Controllers
{
    public class JobController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Index(JobApplication jobApplication)
        {
            if (ModelState.IsValid)
                return View("Accepted", jobApplication);
            else
                return View();
        }
    }
}

You can see I have simplified my code by removing a full loads of validation code from the action method. Now I am only checking for the validity of the ModelState. The validation will work as before which you can see by clicking the Submit Application button without filling anything on the form.

The image below shows it:

model validation from data annotations

You may ask what happened for validation of Osama Bin Laden future date, and DOB only before 1980. The validation for these types cannot be done from the Data Annotations. You will have to do it in the Action method (like done previously) or by making a Custom Model Validation Attribute. I will come to this part in just a moment.

The below code includes these validations in the action method:

[HttpPost]
public IActionResult Index(JobApplication jobApplication)
{
    if (jobApplication.Name == "Osama Bin Laden")
        ModelState.AddModelError(nameof(jobApplication.Name), "You cannot apply for the Job");
 
    if (jobApplication.DOB > DateTime.Now)
        ModelState.AddModelError(nameof(jobApplication.DOB), "Date of Birth cannot be in the future");
    else if (jobApplication.DOB < new DateTime(1980, 1, 1))
        ModelState.AddModelError(nameof(jobApplication.DOB), "Date of Birth should not be before 1980");
 
    if (ModelState.IsValid)
        return View("Accepted", jobApplication);
    else
        return View();
}
Validating Email Address Format

The Data Annotations approach for doing validation makes adding new fields and their validations very simple. Suppose I want to include an email field and ensure the email address is in correct format. For this I will use the [RegularExpression] attribute by providing the pattern for valid email as – ^[a-zA-Z0-9_\\.-][email protected]([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$.

So go to the JobApplication.cs class and add a new email field as shown in highlighted code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;

namespace ModelBindingValidation.Models
{
    public class JobApplication
    {
        [Required]
        [Display(Name = "Job applicant name")]
        public string Name { get; set; }

        public DateTime DOB { get; set; }

        [Required(ErrorMessage = "Please select your sex")]
        public string Sex { get; set; }

        [Range(0, 5)]
        public string Experience { get; set; }

        [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms")]
        public bool TermsAccepted { get; set; }

        [RegularExpression("^[a-zA-Z0-9_\\.-][email protected]([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "E-mail is not valid")]
        public string Email { get; set; }
    }
}

Then go to the view and add:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Job Application";
}

<style>
    .input-validation-error {
        border-color: red;
    }
</style>

<h2>Job Application</h2>

<div asp-validation-summary="All" class="text-danger"></div>

<form class="m-1 p-1" method="post">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="DOB"></label>
        <input asp-for="DOB" type="text" asp-format="{0:d}" class="form-control" />
        <span asp-validation-for="DOB" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" type="text" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Sex"></label>
        <div>
            <input asp-for="Sex" type="radio" value="M" />Male
            <input asp-for="Sex" type="radio" value="F" />Female
        </div>
        <span asp-validation-for="Sex" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Experience"></label>
        <select asp-for="Experience" class="form-control">
            <option value="Select">Select</option>
            <option value="0">Fresher</option>
            <option value="1">0-1 years</option>
            <option value="2">1-2 years</option>
            <option value="3">2-3 years</option>
            <option value="4">3-4 years</option>
            <option value="5">4-5 years</option>
        </select>
        <span asp-validation-for="Experience" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input asp-for="TermsAccepted" />
        <label asp-for="TermsAccepted" class="form-check-label">
            I accept the terms & conditions
        </label>
        <span asp-validation-for="TermsAccepted" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Submit Application</button>
</form>

And this is all you have to do to add a new email field with proper validations. The below image shows the error message when email is not in a proper format.

email validation data annotations

Custom Model Validation Attribute

You can also create your own Custom Model Validation Attribute that does the validation of values in the manner defined by you.

For this you have to create a class and inherit it from 2 classes. These classes are:

  • 1. Attribute
  • 2. IModelValidator

The IModelValidator interface defines Validate() method which you have to implement in your class. In this method you write your custom validation codes.

The skeleton of the Validate() method is given below:

public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context) 
{
    ...
}

Inside this method you receive information about the property (that is to be validated) through an instance of the ModelValidationContext class.

The ModelValidationContext class properties are described in the table below:

Name Description
Model Returns the property value that is to be validated.
Container Returns the object that contains the property.
ActionContext It provides the context data.

So create a new folder called Infrastructure in your project root folder and add a class called CustomDate.cs inside it. Add the following code to this class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace ModelBindingValidation.Infrastructure
{
    public class CustomDate : Attribute, IModelValidator
    {
        public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
        {

            if (Convert.ToDateTime(context.Model) > DateTime.Now)
                return new List<ModelValidationResult> {
                    new ModelValidationResult("", "Date of Birth cannot be in the future")
                };
            else if (Convert.ToDateTime(context.Model) < new DateTime(1980, 1, 1))
                return new List<ModelValidationResult> {
                    new ModelValidationResult("", "Date of Birth should not be before 1980")
                };
            else
                return Enumerable.Empty<ModelValidationResult>();
        }
    }
}

See the Validate() method where I am performing validation for the date (i.e. the value of the DOB field) by checking 2 conditions:

  • 1. If the date is in the future.
  • 2. If the date is before 1980.

If any of the above 2 condition fails then I am creating a new ModelValidationResult object and returning it in a list manner.

The first argument of ModelValidationResult method is the name of the property to which the error is associated. It is specified as the empty string when validating individual properties. The second argument is the Error message in string format.

If both the conditions passes then I am returning empty enemurable object – return Enumerable.Empty().

Now your custom validation attribute is set to be applied to any field. So add this attribute to the DOB field as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using ModelBindingValidation.Infrastructure;

namespace ModelBindingValidation.Models
{
    public class JobApplication
    {
        [Required]
        [Display(Name = "Job applicant name")]
        public string Name { get; set; }

        [CustomDate]
        public DateTime DOB { get; set; }

        [Required(ErrorMessage = "Please select your sex")]
        public string Sex { get; set; }

        [Range(0, 5)]
        public string Experience { get; set; }

        [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms")]
        public bool TermsAccepted { get; set; }

        [RegularExpression("^[a-zA-Z0-9_\\.-][email protected]([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "E-mail is not valid")]
        public string Email { get; set; }
    }
}

And remove your custom validation code for the DOB field from the action method:

[HttpPost]
public IActionResult Index(JobApplication jobApplication)
{
    if (ModelState.IsValid)
        return View("Accepted", jobApplication);
    else
        return View();
}

Now test your DOB validation which will work just fine. The below image shows the error message for date that is before the year 1980:

custom model validation for date

The Custom Validator is very powerful, I can create validation for names preventing some user with a particular name. Let us do this thing now. So create a new class called NameValidate.cs inside the Infrastructure folder and add the following code to it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace ModelBindingValidation.Infrastructure
{
    public class NameValidate : Attribute, IModelValidator
    {
        public string[] NotAllowed { get; set; }
        public string ErrorMessage { get; set; }
        public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
        {

            if (NotAllowed.Contains(context.Model as string))
                return new List<ModelValidationResult> {
                    new ModelValidationResult("", ErrorMessage)
                };
            else
                return Enumerable.Empty<ModelValidationResult>();
        }
    }
}

Important thing to note is that when applying the [NameValidate] attribute on a field then you have to provide the values for the properties to. These are:

  • 1. ErrorMessage – the error message in string format.
  • 2. NotAllowed – a string array for names you like to prevent.

Now simply add the attribute to the name field like this:

[Required]
[Display(Name = "Job applicant name")]
[NameValidate(NotAllowed = new string[] { "Osama Bin Laden", "Saddam Hussain", "Mohammed Gaddafi" }, ErrorMessage = "You cannot apply for the Job")]
public string Name { get; set; }

With this attribute applied, people like “Osama Bin Laden”, “Saddam Hussain”, “Mohammed Gaddafi” will not be able to apply for the job.

The image given below shown the validation error message when the name is Saddam Hussain:

custom model validation for name

Client-Side Validation

Till now the Model Validations which you have applied are all Server-Side Validations. This means when form is submitted the control values go to server where it is validated. The server then sends back the result which is then shown on the View. This take a few milliseconds time to complete.

If you want instant validation results shown to the users then you have to use the Client-Side Validation. These type of validations happens on the browser itself and are instant as there is no round trip to the server. Note that they are also called as unobtrusive client-side validation and is done by jQuery with is a library of JavaScript.

In order to perform Client-Side Validation you need to add 3 files to your application, these are:

1. jQuery
2. jQuery Validation
3. jQuery Validation Unobtrusive

You can simply install client-side packages like the above 3 using LibMan. I have written a separate tutorial on using LibMan here.

Now reference these 3 files in the Index View of the Job Controller like shown below:

@model JobApplication
@{
    Layout = "_Layout";
    ViewData["Title"] = "Job Application";
}

@section scripts {
    <script src="~/lib/jquery/jquery.min.js"></script>
    <script src="~/lib/jquery-validation/jquery.validate.min.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}

<style>
    .input-validation-error {
        border-color: red;
    }
</style>

<h2>Job Application</h2>

<div asp-validation-summary="All" class="text-danger"></div>

<form class="m-1 p-1" method="post">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="DOB"></label>
        <input asp-for="DOB" type="text" asp-format="{0:d}" class="form-control" />
        <span asp-validation-for="DOB" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" type="text" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Sex"></label>
        <div>
            <input asp-for="Sex" type="radio" value="M" />Male
            <input asp-for="Sex" type="radio" value="F" />Female
        </div>
        <span asp-validation-for="Sex" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Experience"></label>
        <select asp-for="Experience" class="form-control">
            <option value="Select">Select</option>
            <option value="0">Fresher</option>
            <option value="1">0-1 years</option>
            <option value="2">1-2 years</option>
            <option value="3">2-3 years</option>
            <option value="4">3-4 years</option>
            <option value="5">4-5 years</option>
        </select>
        <span asp-validation-for="Experience" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input asp-for="TermsAccepted" />
        <label asp-for="TermsAccepted" class="form-check-label">
            I accept the terms & conditions
        </label>
        <span asp-validation-for="TermsAccepted" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Submit Application</button>
</form>

Now re-run your application and check how client-side validations give instant results without making a round trip to the server.

The Client-Side validations cannot work on properties that have [Range] validation attribute dealing with bool. Therefore you can comment out the Range attribute and use a custom validation attribute for your terms checkbox.

See below code:

//[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms")]
public bool TermsAccepted { get; set; }

Also note that the Client-Side Validation will not work for your Custom Model Validation Attribute like the [CustomDate] and [NameValidate]. For these you have to create Custom Client-Side Validator. I have written a complete tutorial on this topic at here.

Remote Validations

Remote Validations are asynchronous validations, although look like Client-Side validations, but are done on the server by AJAX. The Remove Validations are performed in the background and the user doesn’t have to click the submit button and then wait for a new view to be rendered on the browser.

To create a Remote Validation you need to add an action method that returns JsonResult object and has a parameter whose “name is the same” like the “name of the property” it is applied to.

Let me apply a Remove Validation on the DOB property, so add a new action method called ValidDate on the Job controller. It’s code is given below:

public JsonResult ValidateDate(DateTime DOB)
{
    if (DOB > DateTime.Now)
        return Json("Date of Birth cannot be in the future");
    else if (DOB < new DateTime(1980, 1, 1))
        return Json("Date of Birth should not be before 1980");
    else
        return Json(true);
}

The code is checking if the date is in the future or before the year 1980. In that case it is returning error in JSON format. If date is proper then it returns true in Json.

Now comment out the [CustomDate] attribute on the JobApplication.cs and add the [Remote] attribute to it as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using ModelBindingValidation.Infrastructure;
using Microsoft.AspNetCore.Mvc;

namespace ModelBindingValidation.Models
{
    public class JobApplication
    {
        [Required]
        [Display(Name = "Job applicant name")]
        [NameValidate(NotAllowed = new string[] { "Osama Bin Laden", "Saddam Hussain", "Mohammed Gaddafi" }, ErrorMessage = "You cannot apply for the Job")]
        public string Name { get; set; }

        //[CustomDate]
        [Remote("ValidateDate", "Job")]
        public DateTime DOB { get; set; }

        [Required(ErrorMessage = "Please select your sex")]
        public string Sex { get; set; }

        [Range(0, 5)]
        public string Experience { get; set; }

        [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms")]
        public bool TermsAccepted { get; set; }

        [RegularExpression("^[a-zA-Z0-9_\\.-][email protected]([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "E-mail is not valid")]
        public string Email { get; set; }
    }
}

You can check it by typing in the DOB field and will notice the Remote validation will be called on every keypress event.

The below video shows the Remote Validation Error shown on the view.

You can download the source code using the below link:

Download

Conclusion

In this tutorial I explained you all about the Model Binding and Model Validation techniques. I hope you enjoyed learning them.

Share this article -

yogihosting

ABOUT THE AUTHOR

This article has been written by the Technical Staff of YogiHosting. Check out other articles on "ASP.NET Core, jQuery, EF Core, SEO, jQuery, HTML" and more.