Problem
How to validate input models in ASP.NET Core MVC.
Solution
We discussed model binding in a previous post, using that sample as a template, add Save()
method in HomeController
:
[HttpPost]
public IActionResult Save(EmployeeInputModel model)
{
if (ModelState.IsValid)
return Ok(model);
return BadRequest(ModelState);
}
Add a model annotated with various validation attributes:
public class EmployeeInputModel
{
public int Id { get; set; }
[Required]
public string EmployeeNo { get; set; }
[StringLength(10)]
[MinLength(3)]
public string Surname { get; set; }
[EmailAddress]
public string Email { get; set; }
[Url]
public string BlogUrl { get; set; }
[DataType(DataType.Date)]
[Display(Name = "Date of Birth")]
public DateTime BirthDate { get; set; }
public Gender Gender { get; set; }
[Range(0, 10000.00)]
public decimal Salary { get; set; }
public bool IsPartTime { get; set; }
public AddressInputModel Address { get; set; }
}
public class AddressInputModel
{
[Required]
[RegularExpression("[A-Za-z0-9].*")]
public string Line { get; set; }
}
Discussion
Data coming from web (forms, HTTP request, etc.) must be validated. No exceptions! Model Validation is the mechanism ASP.NET Core MVC provides for this purpose. Validation triggers after the Model Binding but before the action executes.
Validations can be added by using Validation Attributes. Framework provides a lot of useful validation attributes and we can even create our own too.
Model State
ControllerBase
provides a property ModelState
to work with model validation. MVC will populate this dictionary with validation errors after model binding. You could then verify its IsValid
property in the action method to act accordingly.
ModelState
has a method to add errors to the model at server side (e.g. in Controller
). This is used for validations that require database access and report errors back to the client. Modified Save()
method demonstrates this:
[HttpPost]
public IActionResult Save(EmployeeInputModel model)
{
if (model.Id == 1)
ModelState.AddModelError("Id", "Id already exist");
if (ModelState.IsValid)
return Ok(model);
return BadRequest(ModelState);
}
ModelState
:
Validation Attributes
Framework provided validation attributes exist in System.ComponentModel.DataAnnotations
. In the sample application, I’ve demonstrated the use of commonly used attributes.
An interesting aspect of model validation is that even without any attributes, MVC will do some basic validations based on data types. Below is the result of a request when model has no validation and no data was supplied, note that empty values fail for Integer
, DateTime
and Boolean
properties:
Custom Validation
Custom validation attributes are useful to create reusable and application specific validation logic. There are two ways to implement this (both can be used on the same model):
Inherit ValidationAttribute and Override its IsValid Method
IsValid
takes in a parameter of type ValidationContext
, which has few useful properties:
ObjectInstance
: Returns the model on which the attribute is applied. Casting this to a specific model class gives you access to its properties.
DisplayName
: Returns either the name of model’s property or string
specific in [Display(Name = “…”)]
attribute applied to the property.
ValidationAttribute
class also provides few useful properties and methods:
ErrorMessage
: Returns the error message set on the attribute when declaring it in the model.
FormatErrorMessage
: Returns the generic message, i.e., the field {0}
is invalid, where {0}
is replaced with DisplayName
.
IsValid
method returns ValidationResult
class, either through its static
field Success
or by initializing a new instance (in case of an error).
Below is a custom validation attribute applied on the BirthDate
property of our model:
[DataType(DataType.Date)]
[AgeCheck]
[Display(Name = "Date of Birth")]
public DateTime BirthDate { get; set; }
public class AgeCheck : ValidationAttribute
{
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
var model = validationContext.ObjectInstance as EmployeeInputModel;
if (model == null)
throw new ArgumentException("Attribute not applied on Employee");
if (model.BirthDate > DateTime.Now.Date)
return new ValidationResult(GetErrorMessage(validationContext));
return ValidationResult.Success;
}
private string GetErrorMessage(ValidationContext validationContext)
{
if (!string.IsNullOrEmpty(this.ErrorMessage))
return this.ErrorMessage;
return $"{validationContext.DisplayName} can't be in future";
}
}
Implement IValidatableObject in Your Model
To implement more complex validations that need access to more than one property in your model, IValidatableObject
works best, by implementing its Validate()
method. A key thing to remember is that these validations will run after attribute based validations are successful.
Validate()
method returns IEnumerable
, because this method can validate more than one property of the model providing a validation error for each.
Here is a sample for our example model:
public class EmployeeInputModel : IValidatableObject
{
...
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (!string.IsNullOrEmpty(BlogUrl) &&
BlogUrl.Contains("tahirnaushad.com"))
yield return new ValidationResult(
"URL already taken", new[] { "BlogUrl" });
}
}