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. IsValid() accepts an object parameter, so you’ll want to use the is operator to verify it’s the right type and then validate the value. In this example, it checks if the object is a DateTimeOffset and then verifies that it’s a future date.

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)

When validation fails, it returns the validation errors in the problem details JSON format, 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)

Tip: Use string interpolation instead of string.Format() with ErrorMessage to avoid runtime FormatExceptions.

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.

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.

Now test this by sending a request with Postman containing an invalid value (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.

Comments are closed.