.NET 8 Data Annotations Validation
Introduction
I wrote about entity validation in the past, the reason I'm coming back to it is that there are some changes in .NET 8, that I haven't revisited. Time for a refresher!
.NET has featured a way to validate classes and their properties for a long time, the API is called Data Annotations Validation, and it integrates nicely with ASP.NET Core MVC and other frameworks (not Entity Framework Core, as we've talked recently). It is essentially, but not just, based on a set of attributes and a class that performs validations on a target class and its properties, from the validation attributes it contain. The base class for attribute validation is called, surprise, surprise, ValidationAttribute, and there are already a few attributes that implement some common validations. I will talk about them now.
The ValidationAttribute has two overriden methods IsValid methods, one that takes a ValidationContext object as its parameter and returns a ValidationResult, and a simpler version that has no parameters and just returns a boolean. Why the two, I hear you ask? Well, it depends on which version of the Validate method is called, the one that takes the ValidationContext parameter will call the appropriate IsValid overload. More on this later on.
Please note that some of these attributes also have a special meaning for Entity Framework Core and ASP.NET Core, in regards to defining the entity's database definitions and other metadata, but that's not what we're interested in this post, and we won't be talking about it.
Required
Maybe one of the most used validation attribute is [Required]. It is used to tell Data Annotations that a specific field or property is, well, required, which means that it must have a non-default value. This means, for reference types, that it cannot be null, for strings, that it cannot be also empty or with blanks. Value types are not affected by it.
Example usage:
[Required(AllowEmptyStrings = false)]
public string? Name { get; set; }
The AllowEmptyStrings property does exactly what it says: if set, it does not consider a failure if the string is empty, only if it is null.
Maximum and Minimum String Length
There are a couple of ways by which we can define the minimum and maximum limits for a string property: one is the [MinLength] and [MaxLength] attributes. As you can imagine, they allow you to set, respectively, the minimum and maximum allowed length for a string. Note that they don't do anything else, meaning, if the string is null, they are just ignored. You can apply any or both at the same time:
[MinLength(3)]
[MaxLength(50)]
public string? Name { get; set; }
Another option is the [StringLength] attribute, which combines these two:
[StringLength(50, MinimumLength: 3)]
public string? Name { get; set; }
Probably more convenient than the previous two-attribute alternative. Here, the MinimumLength property is optional, only the maximum is required.
Maximum and Minimum Collection or String Size
There is another attribute that can also be used for defining the minimum and maximum limits for a string, but, also for a collection of any kind: the [Length] attribute. Here's an example for a collection:
[Length(5, 10)]
public List<string> Items { get; set; } = new List<string>();
Both the minimum and the maximum values are required, but you can obviously set the minimum to 0 and/or the maximum to int.MaxValue, for no limits.
Numeric Range
It is also possible to specify the lower and/or higher limits for a numeric value, through the [Range] attribute:
[Range(0, 10, MinimumIsExclusive = true, MaximumIsExclusive = false)]
public int Position { get; set; }
We can control wether or not the lower and higher limits are exclusive or not through the MinimumIsExclusive and MaximumIsExclusive optional properties. This attribute can be applied to any numeric field or property.
Allowed and Disallowed Values
If we want our field or property to only accept/do not accept a set of predefined values, we can use the [AllowedValues] and [DeniedValues]:
[AllowedValues("Red", "Green", "Blue")]
[DeniedValues("Black", "White", "Grey")]
public string? Colour { get; set; }
The values are checked as-is, meaning, for strings, there is no way to compare case-insensitive. These attributes were introduced in .NET 8.
Comparison
What if you want to compare a value of a property with that of another property, as for password confirmations? Enter the [Compare] attribute:
[Required]
public string Password { get; set; }
[Compare(nameof(Password))]
public string Confirmation { get; set; }
Enumeration Values
Sometimes we want to set to a string property a value that must match an enumeration. For that we have [EnumDataType]:
[EnumDataType(typeof(DayOfWeek)]
public string DayOfWeek { get; set; }
Regular Expression
Regular expressions in .NET are quite powerful and there is a way to validate a string against a regular expression by using the [RegularExpression] attribute:
[RegularExpression(@"\w{5,10}")]
public string Password { get; set; }
URL
You could use the regular expression validator for validating a property for a valid URL, but an alternative is to use the [Url] attribute:
[Url]
public string Url { get; set; }
Note: this validator attribute will only check if the string starts by one of the following protocols, all case-insensitive:
- http://
- https://
- ftp://
Email Address
Similar to URL, you could use a regular expression to validate an email address, but there is the [EmailAddress] attribute exactly for that purpose:
[EmailAddress]
public string Email { get; set; }
In case you are wondering, here is the regular expression that is used:
^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$
Credit Card
And for credit card validation we have [CreditCard]:
[CreditCard]
public string CreditCard { get; set; }
This will check if the card is valid according to the standard Luhn algorithm.
Phone Number
In order to validate a phone number, we can use the [Phone] attribute:
[Phone]
public string Mobile { get; set; }
This supports:
- An optional prefix starting with +
- A set of numbers separated by -, .
- An optional numeric extension separated by * following x/ext.
This is the actual regular expression that is used:
^(\+\s?)?((?<!\+.*)\(\+?\d+([\s\-\.]?\d+)?\)|\d+)([\s\-\.]?(\(\d+([\s\-\.]?\d+)?\)|\d+))*(\s?(x|ext\.?)\s?\d+)?$
File Extensions
To check if a string ends in one of a possible file extensions, we have the [FileExtensions] attribute
[FileExtensions(Extensions = "gif,png,jpg,jpeg,tiff,bmp")]
public string ImageUrl { get; set; }
The Extensions property can take a comma-separated list of file extensions. If no extensions are supplied, the default value is "png,jpg,jpeg,gif".
Base64 String
To check if a string is a valid Base64-encoded string, we have the [Base64String]:
[Base64String]
public string? ImageData { get; set; }
This attribute was introduced in .NET 8.
Custom Validation Attributes
Yet another option is to roll out your own validation attribute. It must inherit from ValidationAttribute and implement the IsValid method, like this is doing:
[Serializable]
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class IsEvenAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (IsValid(value))
{
return ValidationResult.Success;
}
return new ValidationResult($"Value {value} is not even", new string[] { validationContext.MemberName! });
}
protected override bool IsValid(object? value)
{
if (IsNumber(value?.GetType()!))
{
var number = (long) Convert.ChangeType(value, TypeCode.Int64)!;
if ((number % 2) == 0)
{
return true;
}
}
return false;
}
private static bool IsNumber(Type? type)
{
if (type == null)
{
return false;
}
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
return true;
case TypeCode.Object:
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return IsNumber(Nullable.GetUnderlyingType(type)!);
}
return false;
}
return false;
}
}
In this example, we're making it applicable just for properties (AttributeTargets.Property), but we could also make one that is applicable to classes (AttributeTargets.Class) or structs (AttributeTargets.Struct). It checks if the target value is of a numeric type, and if so, converts it into a long and checks if it is even. Note that you should override both IsValid methods. Here's how to apply it:
[IsEven]
public long NumberOfWheels { get; set; }
You can also set if your custom validation attribute requires being called with a ValidationContext, this is achieved by returning true or false on the RequiresValidationContext virtual property, which by default returns false.
Custom Validation Using a Method
And now for something completely different: what if you have a method that already performs the validation that you're interested in? You can use it if you apply the [CustomValidation] attribute! Here's an example:
[CustomValidation(typeof(CustomValidator), nameof(CustomValidator.IsEven))]
public long NumberOfWheels { get; set; }
The class that holds the method that will do the validation can either be:
- A static class
- A public non-abstract class with a public parameterless constructor
As for the validation method, it needs to have one of two possible signatures:
- public ValidationResult Validate(object entity, ValidationContext context)
- public bool Validate(object entity)
As you can see, these match the Validate and IsValid methods we talked about early on. The method can be static or instance, as long as it's public and non-abstract. One example, for the same validation shown previously (is even):
public static class CustomValidator
{
public static ValidationResult IsEven(object entity, ValidationContext context)
{
if (IsNumber(entity?.GetType()!))
{
var number = (long) Convert.ChangeType(entity, TypeCode.Int64)!;
if ((number % 2) == 0)
{
return ValidationResult.Success;
}
}
return new ValidationResult($"Value {entity} is not even", new string[] { validationContext.MemberName! });
}
private static bool IsNumber(Type? type)
{
if (type == null)
{
return false;
}
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
return true;
case TypeCode.Object:
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return IsNumber(Nullable.GetUnderlyingType(type)!);
}
return false;
}
return false;
}
}
Returning a ValidationResult object allows for more information than just a boolean, you can also return the member names that were involved in the validation failure, as well as an error message.
Class Self-Validation
And what if you need to perform validations that envolve multiple properties? Well, one possible option is to apply a validation attribute to the class that contains the properties and then check the value that is being validated for the appropriate class and extract the properties to check. Other option is to have the class implement IValidatableObject! This interface is also part of the Data Annotations API and it is used for classes that self-validated. Here is an example:
public class Contract : IValidatableObject
{
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string? Name { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(Name))
{
yield return new ValidationResult("Missing Name", new string[] { nameof(Name) });
}
if (StartDate == null)
{
yield return new ValidationResult("Missing Start Date", new string[] { nameof(StartDate) });
}
if ((EndDate != null) && (StartDate != null) && (EndDate < StartDate))
{
yield return new ValidationResult("End Date before Start Date", new string[] { nameof(EndDate) });
}
}
}
This is a simple example that shows three validations:
- Name is not null or empty
- StartDate is not null
- If StartDate and EndDate are both supplied, EndDate is after StartDate
You can return as many ValidationResult as you want, they will all count as a validation failure.
Performing Validations
Now, for actually performing the validation, we have a few options:
- Validating the whole instance and all of its properties (this includes class self-validation)
- Validating a single property
And then some more:
- Use the existing class and property validation attributes
- Supply our own validation attributes
We can also choose how we want the results:
- Throw a ValidationException when the first validation error is found
- Get a list of all the validation errors (ValidationResult objects returned)
The helper class that performs all the validation operations is, unsurprisingly, Validator. Let's see how we can validate the whole entity and return all the validation errors:
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(entity, new ValidationContext(entity), results, validateAllProperties: true);
TryValidateObject method will never throw an exception, instead, it will populate the results collection with all the ValidationResult objects returned from all the validation attributes that were found for the class and its properties, if the validateAllProperties was set to true, otherwise it will just return the first failed result. TryValidateObject returns a boolean value that says if the object was found to be valid (true) or not (false), but alternatively you can also look at the results collection: if it's empty, then the object is valid.
If instead we want to validate a specific property we use TryValidateProperty:
var isValidProperty = Validator.TryValidateProperty(entity.NumberOfWheels, new ValidationContext(entity) { MemberName = "NumberOfWheels" }, results);
Here, on the ValidationContext, we need to pass the property name that we are validating as the MemberName.
If we prefer to have a ValidationException thrown at the first validation error, just use ValidateObject, for the whole object:
Validator.ValidateObject(entity, new ValidationContext(entity), validateAllProperties: true);
Or ValidateProperty, for a single property:
Validator.ValidateProperty(entity.NumberOfWheels, new ValidationContext(entity) { MemberName = "NumberOfWheels" });
The other thing I mentioned was, if you want to specify your own validation attributes, you can do so using the TryValidateValue/ValidateValue methods and just pass any collection of validation attributes:
var isValid = Validator.TryValidateValue(entity.NumberOfWheels, new ValidationContext(entity) { MemberName = "NumberOfWheels" }, results, new [] { new IsEvenAttribute() });
Validator.ValidateValue(entity.NumberOfWheels, new ValidationContext(entity) { MemberName = "NumberOfWheels" }, new [] { new IsEvenAttribute() });
And that's it for validation!
Addenda: Error Messages
A final word: all of the default attributes provide their own default error messages, but you can override them in one of two ways:
- By providing a static error message
- Or by providing the name of a type and property that provide the error message at runtime
For the first option, the ErrorMessage property is what we use, here's a quick example:
[Required(ErrorMessage = "The {0} field is mandatory!")]
public string? Name { get; set; }
As you can see, we can put {0} placeholders on the string, and they will be replaced by the actual property name. Actually, each validation attribute can support other placeholders, which are always optional, for example:
[StringLength(50, MinimumLength: 3, ErrorMessage = "The size of the {0} field must be between {2} and {1}")]
public string? Name { get; set; }
For [StringLength], the MinimumLength will go in placeholder {2} and MaximumLength in {1}.
The other option is to use ErrorMessageResourceType and ErrorMessageResourceName. These are used typically with resource files, and so both must be set simultaneously. One example:
[Required(ErrorMessageResourceType = typeof(Resource), ErrorMessageResourceName = "RequiredName")]
public string? Name { get; set; }
Yet another option is to override the FormatErrorMessage method. It takes as its name parameter the name of the property where the validation attribute is being called (from the ValidationContext's MemberName), which will be empty if it's a whole class, and we just need to return an appropriate error message:
public override string ReturnErrorMessage(string name)
{
return $"Missing {name}!";
}
Conclusion
As you can see, the Data Validations API is quite powerful, especially if you add class self-validation, use custom validation methods and/or implement your own custom validation attributes. As always, I'd like to hear your thoughts on this, any comments, questions, corrections, are always welcome! Happy validating!