ASP.NET Core – Create a custom model validation attribute

There are many built-in model validation attributes available – such as [Required] and [Range] – which you can use to handle most validation scenarios. When these aren’t sufficient, you can create a custom validation attribute with your own validation logic. I’ll show an example of how to do that.

1 – Subclass ValidationAttribute and implement validation logic

To create a custom model validation attribute, subclass ValidationAttribute, override the IsValid() method, and implement your validation logic. Here’s an example:

using System.ComponentModel.DataAnnotations; public class FutureDateTimeAttribute : ValidationAttribute { public override bool IsValid(object value) { if (value is DateTimeOffset dateTimeOffset && dateTimeOffset > DateTimeOffset.Now) return true; return false; } }
Code language: C# (cs)

When a request comes in (and you’re using the attribute on a property), the framework automatically calls IsValid() with the property object during the model validation step. If it returns false, validation fails and it returns a validation error response.

Note: Since this logic gets executed on potentially every request (when the attribute is being used), keep your validation logic as light as possible.

2 – Change the error message (optional)

The default error response looks like this:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "0HMHK72C15DT8:0000000B", "errors": { "Showtime": [ "The field Showtime is invalid." ] } }
Code language: JSON / JSON with Comments (json)

The default error message – “The field <property name> is invalid” – is quite vague. If you want to provide a more specific error message, one option is to override the FormatErrorMessage() method and hardcode the error message (with the property name in it):

using System.ComponentModel.DataAnnotations; public class FutureDateTimeAttribute : ValidationAttribute { public override bool IsValid(object value) { if (value is DateTimeOffset dateTimeOffset && dateTimeOffset > DateTimeOffset.Now) return true; return false; } public override string FormatErrorMessage(string name) { return $"{name} must be a future date"; } }
Code language: C# (cs)

Hardcoding the error message like this is fine if you’re creating a very specific validation attribute.

If you’re creating a general-purpose validation attribute, you can use the ErrorMessage property to allow the error message to be specified from the outside. Overriding FormatErrorMessage() is optional.

Note: Use caution if you plan on using string.Format() with ErrorMessage. This is a recipe for runtime FormatException’s.

3 – Use the attribute

To use the attribute, stick it in on a model property:

public class MovieTicketOrder { public string Movie { get; set; } public int Seats { get; set; } [FutureDateTime] public DateTimeOffset? Showtime { get; set; } }
Code language: C# (cs)

Note: The validation logic is checking ‘object is DateTimeOffset’, which is false if the object is null or not a DateTimeOffset. Hence why it’s using a nullable property here.

Send a request with a value that should fail validation (at the time I’m sending this, this is a past datetime):

POST /movies/buytickets { "movie": "Doctor Strange in the Multiverse of Madness", "seats": 2, "showtime": "2022-05-12T13:00-04:00" }
Code language: plaintext (plaintext)

This correctly results in a 400 – Bad Request with a validation error response:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "0HMHK8RVAMMQN:00000001", "errors": { "Showtime": [ "Showtime must be a future date" ] } }
Code language: JSON / JSON with Comments (json)

Now send a request with a value that should pass validation (at the time I’m sending this, this is a future datetime):

POST /movies/buytickets { "movie": "Doctor Strange in the Multiverse of Madness", "seats": 2, "showtime": "2022-05-12T19:00-04:00" }
Code language: plaintext (plaintext)

This results in a 200 – OK response since it correctly passed validation.

Leave a Comment