Custom model validation attributes in ASP.NET Core

While working on ASP.NET Core Web API, we have some customized business requirements for the user input. To implement those requirements, .NET provides a set of attributes to restrict users to input only validated data or return an error if the data inserted is invalid. In this blog, I will discuss step by step how to define a custom validation attribute for specific business requirements. I will use that attribute in a single line to validate user input.

What is the validation attribute in ASP.NET Core?

Validation attributes allow developers to specify validation rules for model properties. It provides an optional Error message property that displays for invalid input. They are applied to model properties in ASP.NET Core APIs and apply the defined rule on those properties.

Some of the most common built-in attributes are. 

  • [Required(ErrorMessage = "Name is Required")] makes the property a must-insert.
  • [EmailAddress(ErrorMessage = "Invalid email address")] restricts user input as a valid email format.
  • [StringLength(100, MinimumLength = 6)] defines the minimum and maximum character length in the string property. 

However, our applications sometimes require the implementation of business logic validations which cannot be met by any built-in options. Such requirements are often more complex or deep to be fulfilled by the provided set of attributes. .NET solves this problem with the ValidationAttribute class that we can inherit to define custom validation for input model property.

Suppose we require customers to be at least 18 years old, younger people cannot register in our application. To implement the case in the application involves the following steps.

Step 1: Create a separate directory for attributes.

Let's create a directory for data attributes:

Step 2: Create an attribute class in a new file

Creating a file for validation:

Step 3: importing DataAnnotations namespace

Import the System.ComponentModel.DataAnnotation namespace that provides the ValidationAttribute class which we will inherit in our custom attribute:

using System.ComponentModel.DataAnnotations;

Step 4:  Inheriting ValidationAttribute class from the DataAnnotations namespace

public class CustomerDateOfBirthValidation : ValidationAttribute
{
    public const string MINIMUM_DATE_OF_BIRTH =
        "The Customer age must be greater or equal to 18 years";
    private int MINIMUM_AGE = 18;
}

Here We have defined a default error message and minimum age which we will use for validation.

Step 5: Override method to define personalized check

In this step, we override the IsValid method from the inherited class. This is the main code where we define our business logic for an incoming property value:

protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
    var valueString = value != null ? value.ToString() : null;

    // If value is empty, show Success
    if(string.IsNullOrWhiteSpace(valueString))
    {
        return ValidationResult.Success;
    }

    // If dob cannot be parsed in Date then it is invalid
    if(!DateTime.TryParse(valueString, out DateTime dob))
    {
        return new ValidationResult("Please provide the date of birth in a valid format");
    }

    // Check if the age is under the minimum age. finding the calendar date of 18 years ago
    var minDateOfBirth = DateTime.Now.Date.AddYears(MINIMUM_AGE * -1);

    if(dob > minDateOfBirth)
    {
        return new ValidationResult(MINIMUM_DATE_OF_BIRTH);
    }
    
    return ValidationResult.Success;
}

IsValid takes two parameters, object? value is the value passed in the property on which we perform checks. ValidationContext validationContext holds other information about the value parameter and metadata about the validation process.

In the earlier line, we convert the value parameter into the string to parse and read conveniently. Then we check if it is in acceptable date format and can be parsed as a date. Otherwise, return a failing validation result.

Would your users appreciate fewer errors?

➡️ Reduce errors by 90% with elmah.io error logging and uptime monitoring ⬅️

In the later part, we calculate minDateOfBirth which is the date 18 years back from the current date. By which we check if the entered date is earlier or not. If dob (entered value) is later than the minDateOfBirth, we return ValidationResult with the message. 

Lastly, we return ValidationResult.Success in passing the validation.

Step 6: Using the attribute

Now it is time to use the custom attribute in the model:

using dapperCRUD.DataAnnotations;

namespace dapperCRUD.Models
{
    public class Customer
    {
        // ...

        [CustomerDateOfBirthValidation]
        public DateTime DateOfBirth { get; set; }

    }
}

It is time to check by running the project. I ran the project and navigated to the swagger documentation of the API in the browser.

The API endpoint gives a validation error on the dateOfBirth field when tested through the Swagger UI request:

Let's give a valid date:

The endpoint creates a user successfully as the date of birth is more than 18 years ago than the current date (2024-07-10).

Summary

In this blog post, we discussed how to create custom data validation attributes in ASP.NET Core to implement business logic. We performed a step-by-step solution to define our attribute to check customers' date of birth and implement under age restriction for new customers to register. System.ComponentModel.DataAnnotations provides a ValidationAttribute class which allows its child class to override the IsValid method to add custom validation checks and logic and return appropriate messages for invalid inputs.