Model validation is a crucial aspect of building robust ASP.NET Core applications, ensuring data integrity and providing a smooth user experience. This post explores the various mechanisms for validating data, from built-in attributes and custom validation logic to handling non-nullable reference types and top-level node validation.
1. Model state
Model state represents data binding and validation errors that occur before action execution, which web apps typically handle by redisplaying pages, while ApiController
-decorated Web APIs automatically return a 400 response.
[ApiController]
[Route("api/[controller]")]
public class MoviesController(IDbContextFactory<MovieContext> contextFactory) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateMoviesAsync(Movie movie)
{
// Not required when annotated with [ApiController]
//
// if (!ModelState.IsValid)
// {
// return BadRequest(ModelState);
// }
using var context = contextFactory.CreateDbContext();
context.Movies.Add(movie);
await context.SaveChangesAsync();
return Ok();
}
}
2. Validation
Validation is automatic but can be manually re-run by clearing the validation state and then calling TryValidateModel
.
// To rerun validation, call ModelStateDictionary.ClearValidationState to clear validation specific to the model being validated followed by TryValidateModel:
ModelState.ClearValidationState(nameof(movie));
if (!TryValidateModel(movie, nameof(movie)))
{
return BadRequest(ModelState);
}
// Validate the movie object using data annotations without using controller context
var validatetionContext = new ValidationContext(movie);
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(
instance: movie,
validationContext: validatetionContext,
validationResults: validationResults,
validateAllProperties: true);
if (isValid is false)
{
return BadRequest(validationResults);
}
The IValidatableObject interface provides model-level, custom cross-property self-validation by returning ValidationResult objects, with its Validate method automatically invoked by Validator.TryValidateObject .
|
3. Validation attributes
Validation attributes, both built-in and custom, define validation rules for model properties, ensuring data conforms to specified formats, ranges, and other criteria.
Here are some of the built-in validation attributes:
-
[ValidateNever]
: Indicates that a property or parameter should be excluded from validation. -
[CreditCard]
: Validates that the property has a credit card format. -
[Compare]
: Validates that two properties in a model match. -
[EmailAddress]
: Validates that the property has an email format. -
[Phone]
: Validates that the property has a telephone number format. -
[Range]
: Validates that the property value falls within a specified range. -
[RegularExpression]
: Validates that the property value matches a specified regular expression. -
[Required]
: Validates that the field isn’t null. -
[StringLength]
: Validates that a string property value doesn’t exceed a specified length limit. -
[Url]
: Validates that the property has a URL format. -
[Remote]
: Validates input on the client by calling an action method on the server. See [Remote] attribute for details about this attribute’s behavior.
A complete list of validation attributes can be found in the System.ComponentModel.DataAnnotations
namespace.
4. Error messages
Validation attributes allow custom error messages using String.Format
, which can include placeholders for dynamic content like field names and length limits.
// When applied to a Name property, the error message created by the preceding code would be "Name length must be between 6 and 8.".
[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]
To find out which parameters are passed to String.Format
for a particular attribute’s error message, see the DataAnnotations source code.
// https://github.com/dotnet/runtime/blob/74be414d84c84f353ae2e471e63e431703efd398/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/StringLengthAttribute.cs#L74
public override string FormatErrorMessage(string name)
{
EnsureLegalLengths();
bool useErrorMessageWithMinimum = MinimumLength != 0 && !CustomErrorMessageSet;
string errorMessage = useErrorMessageWithMinimum
? SR.StringLengthAttribute_ValidationErrorIncludingMinimum
: ErrorMessageString;
// it's ok to pass in the minLength even for the error message without a {2} param since string.Format will just
// ignore extra arguments
return string.Format(CultureInfo.CurrentCulture, errorMessage, name, MaximumLength, MinimumLength);
}
5. Non-nullable reference types and [Required] attribute
When Nullable contexts are enabled with <Nullable>enable</Nullable>
, non-nullable reference types are implicitly validated as required, leading to errors for missing or empty string inputs unless the type is made nullable or the implicit behavior is suppressed by configuring SuppressImplicitRequiredAttributeForNonNullableReferenceTypes
in Program.cs
.
builder.Services.AddControllers(
options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);
6. Custom attributes
Custom validation attributes extend validation capabilities beyond built-in options by inheriting ValidationAttribute
and overriding IsValid
to implement custom logic, optionally using ValidationContext
for additional model information.
public class ClassicMovieAttribute : ValidationAttribute
{
public ClassicMovieAttribute(int year)
=> Year = year;
public int Year { get; }
public string GetErrorMessage() =>
$"Classic movies must have a release year no later than {Year}.";
protected override ValidationResult? IsValid(
object? value, ValidationContext validationContext)
{
var movie = (Movie)validationContext.ObjectInstance;
var releaseYear = ((DateTime)value!).Year;
if (movie.Genre == Genre.Classic && releaseYear > Year)
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
}