.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:

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.

Adding Validation Attributes in an External Class

Some of you may know about the [MetadataType] attribute. This attribute can be applied to classes when we don't want to place metadata/validator attributes directly in it. For example:

[MetadataType(typeof(Data.DataMetadata))]
public class Data
{
public string Name { get; set; }

public class DataMetadata
{
[Required]
public string Name { get; set; }
}
}

What this means is, information for Data class will come, exclusively or not (you can mix), from the attributes in DataMetadata, for properties with identical names and types.

Now, the problem is: if we try to validate, it won't work:

var foo = new Data();
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(foo, new ValidationContext(foo), results); //true, even if Data.Name wasn't supplied

Alas, there is a way to make it work: we just need to add a metadata provider to the class through TypeDescriptor.AddProviderTransparent and AssociatedMetadataTypeTypeDescriptionProvider:

TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(Data), typeof(Data.DataMetadada)), typeof(Data));

And now it'll work:

var isValid = Validator.TryValidateObject(foo, new ValidationContext(foo), results);  //false, and result is populated with an invalid ValidationResult

It is a known issue, but there are no plans to address it, as far as I know.

If we want to make it dynamic, by looking at some random type and checking if it has the [MetadataType] attribute:

Snippet

var entityType = typeof(Data);
var attr = Attribute.GetCustomAttribute(entityTypetypeof(MetadataTypeAttribute)) as MetadataTypeAttribute;
            
if (attr != null)
{
    TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(entityTypeattr.MetadataClassType), entityType);
}

I know, I know, it's not practical, but as of now, seems to be the only option, if we want to use [MetadataType] for validation attributes defined externally.

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:

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() });

One important aspect is, the validation methods do not perform recursive validation. This is explicitly stated in the documentation.

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!

                             

3 Comments

  • "Thanks for the clear explanation of .NET 8 validation! I’m still new to all this, so the examples really helped me understand. The new attributes like [AllowedValues] are cool, and I’m excited to try them out. Your post made something complicated seem a lot simpler. Can't wait to learn more!"

  • Love this article. Just a head's up to folks, I think TryValidateObject is busted these days. Let me explain.

    This issue described below is with .Net8 and EF Core. (and a small rant about Microsoft's ridiculous #nullable [crap] and how it EF Core8 has it's own issue with #nullable value types.

    Let me set the stage:
    - The project is database first and managed under a database project, .Net8, (now) EFCore8, DataAnnotations 5.??
    - A .bat file uses SQLPackage to update the local db to setup for scaffolding
    - A .bat scaffolds the models. (uses 'dotnet ef scaffold' with --data-annotations.
    - (Up until this issue models were] Easy-peezy free models -- never have to code them.
    - Models validated using `TryValidateObject`
    - Don't use migrations, use the more robust SQLPackage instead -- easy-peezy "State-based" database change management.
    - Been doing this for over a decade.

    I've been using TryValidateObject for a very long time and discovered something very odd and I'm convinced TryValidateObject is busted when it comes to model scaffolding from 'dotnet ef scaffold' with data annotations.

    [In the model below] When the DayPhone property is null, TryValidateObject model now fails validation; meaning that the errors list has that the Day Phone is required-- but the scaffold model does not say that it is.

    I can only assume this is a bug in DataAnnotations TryValidateObject() method because it assuming that DayPhone is required because it's not defined as nullable with the "string?" therefore it must be required, but that's not true because the convention is [Required] is used instead. So which is it Microsoft? Don't think for a moment that I'm activating the #nullable enabled -- the scaffolded model is worse as it defines properties with a defult '= null;' appended to it, just garbage imo.

    BUT, since EF8, they are no longer using the #nullable and the scaffolding treats --nullable and --data-annotations mutually exclusive to the scaffolding making TryValidateObject is a bust, unstable, and unusable.

    <rant> Note: the #nullable feature in .Net6+ is crap and causes so many issues they should have never put it in.</rant>

    [Keyless]
    [Table("IdentityImport", Schema = "etl")]
    public partial class IdentityImport
    {
    [Required]
    [StringLength(100)]
    [Unicode(false)]
    public string EmailAddress { get; set; }

    [Required]
    [StringLength(30)]
    [Unicode(false)]
    public string FirstName { get; set; }

    [Required]
    [StringLength(30)]
    [Unicode(false)]
    public string LastName { get; set; }

    [StringLength(15)]
    [Unicode(false)]
    public string DayPhone { get; set; }
    }

    Has anyone else encountered this and have a workaround?
    I was hoping the DataAnnotations nuget package would help, but seems that it does not.

    Any help is greatly appreciated!

  • @Jeepn Mike: thanks for your comment, I just noticed it now. I'll get back to you on this.

Add a Comment

As it will appear on the website

Not displayed

Your website