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.

2 thoughts on “ASP.NET Core – Create a custom model validation attribute”

  1. I’m trying to use this to add custom data validation to my ASP.NET Core 6 MVC project. While the data validation appears to be working at the controller level (the model is being rejected as invalid), I cannot get jQuery validation to correctly display any kind of error message. The form fails to submit, but the user has no feedback as to what the problem is.

    I’ve confirmed that the on the form field is correctly displaying other validation errors, so the issue appears to be something related to the interaction between this new custom tag and jQuery. Do I need to somehow configure jQuery to interact with this custom attribute? If so, any chance you could add that to this very excellent tutorial?

    I am a fairly novice ASP.NET developer, so I apologize if this is a question with an obvious answer.

    Reply
    • First, I’m glad this article helped you with custom validation attributes.

      Now let’s work on your problem. From what I understand, you’re doing client-side validation with jQuery Validator. It’s working for the built-in attributes, such as [Required], but it’s not working for the custom validation attribute.

      To get this working, you need to add the data validation tags to the field + add JS validation logic that is linked to the validation tags. I’ll show the steps for this. I’ll refer to the FutureDateTimeAttribute I showed in this article.

      1. In the custom validation attribute class, implement the IClientModelValidator interface and its AddValidation() method.

      public class FutureDateTimeAttribute : ValidationAttribute, IClientModelValidator //1. Add this interface
      {
          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";
          }

          //1. Implement this
          public void AddValidation(ClientModelValidationContext context)
          {
              var fieldName = context.Attributes["name"];
              context.Attributes.TryAdd("data-val", "true");
              context.Attributes.TryAdd("data-val-futuredatetime", FormatErrorMessage(fieldName));
          }
      }

      This adds two tags to any field using this custom validation attribute:
      data-val=true
      data-val-futuredatetime=”(Name) must be a future date.”. The “futuredatetime” part of this tag is used to lookup the JS validation function you’ll add in the next step.

      2. Add a new JS file at wwwroot/js/customValidator.js and add two functions to jQuery Validator:

      $.validator.addMethod('futuredatetime', function (value, element, params) {
          return new Date(value) > new Date(); //it's valid if it's a future date
      });

      $.validator.unobtrusive.adapters.add('futuredatetime', function (options) {
          options.rules['futuredatetime'] = [];
          options.messages['futuredatetime'] = options.message;
      });

      3. Add the customValidator.js script to your view.

      For example, add it here: Views\Shared\_ValidationScriptsPartial.cshtml

      <script src="~/js/customValidator.js"></script>

      4. Test it
      Run the code and test the field by putting an invalid value.

      Reply

Leave a Comment