Fluent API Custom Validators in ASP.NET Core MVC

Fluent API Custom Validators in ASP.NET Core MVC

In this article, I will discuss How to Implement Fluent API Custom Validators in ASP.NET Core MVC Applications with Examples. Please read our previous article discussing Fluent API Async Validator in ASP.NET Core MVC Applications.

Fluent API Custom Validators in ASP.NET Core MVC:

When working with Fluent API Validation, custom validators can be useful when the built-in validators are insufficient for our business needs. We can create Custom Validators for both Synchronous and Asynchronous Validation. We can use the Custom or Must method for Synchronous Validation or the CustomAsync or MustAsync method for Asynchronous Validation.

How Do We Implement a Custom Validator with Fluent API?

To implement a Custom Validator using Fluent API in ASP.NET Core MVC, we need to follow the below steps:

  • Create the Custom Validator Class: This class will contain the validation logic. It should implement IValidator<T> where T is the type of the model you’re validating.
  • Define Validation Rules: Within your custom validator class, define the rules by using Custom or CustomAsync methods provided by Fluent Validation. For example, RuleFor(x => x.Property).Custom((value, context) => { /* custom validation logic here */ });
  • Register the Validator: In the Program.cs, add your custom validator to the service collection so it can be discovered and used by ASP.NET Core. For example, builder.Services.AddValidatorsFromAssemblyContaining<MyCustomValidator>();
  • Apply the Validator: Ensure your controller actions are set up to check the ModelState or explicitly invoke the validation process by creating an instance of the Validator and invoking the Validate or ValidateAsync method by passing the model object.

Let’s understand how to create custom validations using the Fluent API Custom, Must, CustomAsync, and MustAsync Validator methods in an ASP.NET Core MVC Application.

Example: User Registration

In ASP.NET Core MVC, using Custom Synchronous and Asynchronous Validation methods allows you to add more dynamic checks that might involve database access or other external services. Let us understand this with one real-time scenario where we might want to validate a user’s registration data against an existing database to ensure that email addresses and usernames are unique.

Assume you have a user registration form where users need to provide a username and an email. Both the username and the email must be unique in your system.

Define the Model:

First, define the registration form model. We will use the following RegistrationModel to understand Fluent API Custom Validators in an ASP.NET Core MVC Application. Create a class file named RegistrationModel.cs within the Models folder and copy and paste the following code. As you can see, this model class contains three properties.

namespace FluentAPIDemo.Models
{
    public class RegistrationModel
    {
        public string Username { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
    }
}
Create Custom Validator

You need to create a custom validator class to access the database and check for Username and Email uniqueness. In the following class, we have hard-coded the data source, but you must check it against the database table in real-time. So, create a class file named UserValidator.cs within the Models folder and copy and paste the following code.

namespace FluentAPIDemo.Models
{
    public class UserValidator
    {
        private readonly List<RegistrationModel> users = new List<RegistrationModel>()
        {
            new RegistrationModel(){Username= "User1", Email="User1@Example.com", Password = "User1Password"},
            new RegistrationModel(){Username= "User2", Email="User2@Example.com", Password = "User2Password"},
            new RegistrationModel(){Username= "User3", Email="User3@Example.com", Password = "User3Password"},
            new RegistrationModel(){Username= "User4", Email="User4@Example.com", Password = "User4Password"},
        };

        public bool BeUniqueUsername(string username)
        {
            // Check if any user in the database has the same username
            return users.Any(u => u.Username.Equals(username, System.StringComparison.OrdinalIgnoreCase));
        }

        public async Task<bool> BeUniqueEmailAsync(string email)
        {
            // Check if any user in the database has the same email
            var result = users.Any(u => u.Email.Equals(email, System.StringComparison.OrdinalIgnoreCase));
            return await Task.FromResult(result);
        }
    }
}
Explanation
  • BeUniqueUsername Method: This method uses LINQ’s Any() to determine if any user in the list has the same username as the one provided. It returns false if such a user exists, else false. The StringComparison.OrdinalIgnoreCase makes the comparison case-insensitive.
  • BeUniqueEmailAsync Method: Similarly, this method checks for the uniqueness of the email. It is marked with async; to fulfill the method’s signature as async, we return Task.FromResult(…).
Registering UserValidator:

Add the following code to the Program class to register the UserValidator with the built-in dependency injection container.

builder.Services.AddTransient<UserValidator>();

Next, using FluentValidation, we need to apply these custom methods to validate the registration form.

Creating Custom Validator Using Custom and CustomAsync Method in ASP.NET Core:

Let us see how to implement the Custom Validation Logic using the Custom and CustomAsync Validator method. So, create a class file named RegistrationModelValidator.cs and copy and paste the following code. As you can see, this class is inherited from the AbstractValidator<RegistrationModel> class. As you can see, the UserValidator is injected into the RegistrationModelValidator, and using this instance, we are calling the BeUniqueUsername and BeUniqueEmailAsync method using the Custom and CustomAsync Validator method. The following code is self-explained, so please go through the comment lines.

using FluentValidation;
namespace FluentAPIDemo.Models
{
    public class RegistrationModelValidator : AbstractValidator<RegistrationModel>
    {
        public RegistrationModelValidator(UserValidator userValidator)
        {
            RuleFor(x => x.Username)
                //Specifies that the username cannot be empty
                .NotEmpty().WithMessage("Username is required.")

                //Adds a custom validation rule where the uniqueness of the username is checked.
                //This is a synchronous custom validator:
                .Custom((userName, context) =>
                {
                    //userValidator.BeUniqueUsername(userName) is called to check if the username is unique
                    bool isExists = userValidator.BeUniqueUsername(userName);

                    //If isExists is true (meaning the userName is exists in the database)
                    if (isExists)
                    {
                        // adds a failure message to the validation context,
                        // indicating that the username is already registered
                        context.AddFailure($"{userName} is already registered");
                    }
                });

            RuleFor(p => p.Email)
                //Ensures the email field is not empty, with a corresponding error message if the check fails
                .NotEmpty().WithMessage("Email is required.")

                //Validates that the input is in a proper email format, with a custom message if the format is incorrect.
                .EmailAddress().WithMessage("Email is not a valid email address.")

                //An asynchronous custom validator for email uniqueness:
                .CustomAsync(async (email, context, cancellation) =>
                {
                    //It uses userValidator.BeUniqueEmailAsync(email) to check asynchronously if the email is unique.
                    bool isExists = await userValidator.BeUniqueEmailAsync(email);

                    //If isExists is true (meaning the email is exists in the database)
                    if (isExists)
                    {
                        // adds a failure message to the validation context,
                        // indicating that the email is already registered
                        context.AddFailure($"{email} is already registered");
                    }
                });
        }
    }
}
Registering the Validator:

Add the following code to the Program class to register Fluent API Validation, the Model, and the corresponding validator. As we are using Asynchronous Validation, we should disable Auto validation.

//Enables integration between FluentValidation and ASP.NET MVC's automatic validation pipeline.
//builder.Services.AddFluentValidationAutoValidation();

//Enables Integration Between FluentValidation and ASP.NET client-side validation.
builder.Services.AddFluentValidationClientsideAdapters();

//Registering Model and Validator to show the error message on client side
builder.Services.AddTransient<IValidator<RegistrationModel>, RegistrationModelValidator>();
Use in Controller:

Finally, use the RegistrationModelValidator in your controller to handle user registration. Modify the Home Controller as follows. Now that we have disabled Auto Validation, we need to create an instance of lValidator and call the ValidateAsync method by passing the RegistrationModel object to check whether the model validation is successful, as shown in the code below.

using FluentAPIDemo.Models;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;

namespace FluentAPIDemo.Controllers
{
    public class HomeController : Controller
    {
        private readonly IValidator<RegistrationModel> _validator;

        public HomeController(IValidator<RegistrationModel> validator)
        {
            _validator = validator;
        }

        public IActionResult Register()
        {
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> Register(RegistrationModel model)
        {
            var validationResult = await _validator.ValidateAsync(model);
            if (!validationResult.IsValid)
            {
                foreach (var error in validationResult.Errors)
                {
                    ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
                }
                return View(model);
            }

            // Continue with adding the product...
            return RedirectToAction("Success");
        }

        public string Success()
        {
            return "Registration Successful";
        }
    }
}
Displaying Errors in Views:

We must bind the model properties in the Razor View and display validation messages using ASP.NET Core’s built-in tag helpers. Add a view named Register.cshtml within the Views/Home folder and copy and paste the following code.

@model FluentAPIDemo.Models.RegistrationModel
@{
    ViewData["Title"] = "Register";
}

<h1>User Register</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Register" asp-controller="Home" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group mt-3">
                <label asp-for="Username" class="control-label"></label>
                <input asp-for="Username" class="form-control" />
                <span asp-validation-for="Username" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <label asp-for="Email" class="control-label"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <label asp-for="Password" class="control-label"></label>
                <input asp-for="Password" type="password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <input type="submit" value="Register" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

With the above changes in place, run the application, provide the existing User Name and Email, and click on the Create button to see if everything is working as expected, as shown in the image below.

Creating Custom Validator Using Custom and CustomAsync Method in ASP.NET Core

So, by using the Custom and CustomAsync methods, we can create validation rules according to our business requirements, which are not provided by the built-in Fluent API Validator.

Custom Validation using Must and MustAsync Fluent API Methods in ASP.NET Core MVC

You can also write custom validation logic in an ASP.NET Core MVC Application using Must and MustAsync Methods. These methods enable custom validation logic for specific properties synchronously or asynchronously, respectively. These methods are useful when the built-in Fluent API validators do not meet our business needs.

Let us understand the Use of Must and MustAsync with a Real-time Example. Let us create an application for Venue Reservations. We will validate the Booking model for our venue reservation system. The validation will ensure the booking date and time are valid, check for venue availability, and use dependency injection to access external services.

Define the Model:

Let’s consider the following Booking model to understand Custom Validation using Must and MustAsync Fluent API Methods. So, add a class file named Booking.cs within the Models folder and then copy and paste the following code.

namespace FluentAPIDemo.Models
{
    public class Booking
    {
        public int VenueId { get; set; }
        public DateTime StartDate { get; set; }
        public DateTime EndDate { get; set; }
        public string CustomerEmail { get; set; }
    }
}
Booking Service Validator:

You need to create a custom service class to access the database, check if a venue is available on a given date, and verify if a customer is registered and active. For simplicity, I am providing basic in-memory implementations in the following class. So, create a class file named BookingService.cs within the Models folder and copy and paste the following code.

using System;
using System.Collections.Generic;
using System.Linq;

namespace FluentAPIDemo.Models
{
    public class BookingService
    {
        private readonly List<BookedVenue> _bookedVenues = new List<BookedVenue>()
        {
            new BookedVenue(1, DateTime.Today.AddDays(1)), // Venue 1 booked tomorrow
            new BookedVenue(2, DateTime.Today.AddDays(2))  // Venue 2 booked in two days
        };

        private readonly Dictionary<string, bool> _customers = new Dictionary<string, bool>
        {
            { "User1@Example.com", true },
            { "User2@Example.com", false }  // Inactive customer
        };

        public bool IsVenueAvailable(int venueId, DateTime date)
        {
            // Check if the venue is booked on the given date
            return !_bookedVenues.Any(b => b.VenueId == venueId && b.Date.Date == date.Date);
        }

        public bool IsCustomerActive(string email)
        {
            // Check if the customer email exists and is active
            return _customers.TryGetValue(email, out bool isActive) && isActive;
        }
    }

    public class BookedVenue
    {
        public int VenueId { get; }
        public DateTime Date { get; }

        public BookedVenue(int venueId, DateTime date)
        {
            VenueId = venueId;
            Date = date;
        }
    }
}

The above BookingService class uses data encapsulated within lists and dictionaries to manage and check states related to bookings and customer activity. The IsVenueAvailable method ensures that venues cannot be double-booked for the same date, while the IsCustomerActive method allows the system to verify the active status of a customer before allowing them to make a booking.

Registering UserValidator:

Add the following code to the Program class to register the UserValidator with the built-in dependency injection container.

builder.Services.AddTransient<BookingService>();

Next, using FluentValidation, we need to apply these custom methods to validate the registration form.

Creating Custom Validator with Must and MustAsync Methods:

Let us see how to implement the Custom Validation Logic using the Must and MustAsync Validator method. So, create a class file named BookingValidator.cs and copy and paste the following code. As you can see, this class is inherited from the AbstractValidator<Booking> class. As you can see, the BookingService is injected into the BookingValidator, and using this instance, we are calling the IsVenueAvailable and IsCustomerActive method using the Custom and CustomAsync Validator method. The following code is self-explained, so please go through the comment lines.

using FluentValidation;
namespace FluentAPIDemo.Models
{
    public class BookingValidator : AbstractValidator<Booking>
    {
        public BookingValidator(BookingService bookingService)
        {
            // Validate StartDate and EndDate
            RuleFor(booking => booking.StartDate)
                .Must(date => date > DateTime.Now)
                .WithMessage("The start date must be in the future.");

            RuleFor(booking => booking.EndDate)
                .GreaterThanOrEqualTo(booking => booking.StartDate)
                .WithMessage("The end date must be after the start date.");

            //Synchronous Checking Venue Availability
            RuleFor(booking => booking)
                .Must(booking => bookingService.IsVenueAvailable(booking.VenueId, booking.StartDate))
                .WithMessage("The selected venue is not available on the start date.");

            //Asynchronous Checking to Validate Customer Status
            RuleFor(p => p.CustomerEmail)
                //Ensures the email field is not empty, with a corresponding error message if the check fails
                .NotEmpty().WithMessage("Email is required.")
                //Validates that the input is in a proper email format, with a custom message if the format is incorrect.
                .EmailAddress().WithMessage("Email is not a valid email address.")
                //Asynchronous Checking Customer Status
                .MustAsync(async (email, cancellationToken) =>
                    await bookingService.IsCustomerActive(email))
                .WithMessage("The customer must be active.");
        }
    }
}
Explanation
  • Must Method: The Must method is used for simple, CPU-bound checks like date comparisons, which don’t require IO operations.
  • MustAsync Method: The MustAsync method is ideally used for IO-bound operations such as database calls or API requests. 
Registering the Validator:

Add the following code to the Program class to register the Model and corresponding validator.

builder.Services.AddTransient<IValidator<Booking>, BookingValidator>();

Using Booking Validator in ASP.NET Core MVC Actions:

Let’s create an ASP.NET Core MVC controller that uses the Booking model and the BookingValidator. Modify the Home Controller as follows. Now that we have disabled Auto Validation, we need to create an instance of lValidator and call the ValidateAsync method by passing the BookingModel object to check whether the model validation is successful, as shown in the code below.

using FluentAPIDemo.Models;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;

namespace FluentAPIDemo.Controllers
{
    public class HomeController : Controller
    {
        private readonly IValidator<Booking> _validator;

        public HomeController(IValidator<Booking> validator)
        {
            _validator = validator;
        }

        public IActionResult Register()
        {
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> Register(Booking model)
        {
            var validationResult = await _validator.ValidateAsync(model);
            if (!validationResult.IsValid)
            {
                foreach (var error in validationResult.Errors)
                {
                    ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
                }
                return View(model);
            }

            // Continue with adding the product...
            return RedirectToAction("Success");
        }

        public string Success()
        {
            return "Registration Successful";
        }
    }
}
Displaying Errors in Views:

You can use the tag helpers to bind and display errors. Modify the Register.cshtml within the Views/Home folder as follows. The following view uses tag helpers to bind model properties to form fields, automatically handles validation messages, and uses a partial view to include client-side validation scripts.

@model FluentAPIDemo.Models.Booking
@{
    ViewData["Title"] = "Booking";
}

<h1>User Register</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Register" asp-controller="Home" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group mt-3">
                <label asp-for="VenueId" class="control-label"></label>
                <input asp-for="VenueId" class="form-control" />
                <span asp-validation-for="VenueId" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <label asp-for="EndDate" class="control-label"></label>
                <input asp-for="EndDate" class="form-control" />
                <span asp-validation-for="EndDate" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <label asp-for="CustomerEmail" class="control-label"></label>
                <input asp-for="CustomerEmail" class="form-control" />
                <span asp-validation-for="CustomerEmail" class="text-danger"></span>
            </div>

            <div class="form-group mt-3">
                <input type="submit" value="Booking" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

With the above changes in place, run the application, provide an inactive User Email, provide the wrong Event Date, and click the Booking button to see if everything is working as expected, as shown in the image below.

Custom Validation using Must and MustAsync Fluent API Methods in ASP.NET Core MVC

When Should We Use Fluent API Custom Validators in ASP.NET Core MVC?

In ASP.NET Core MVC, using Fluent API custom validators is useful when you need to implement complex validation rules that are not easily handled by the built-in Data Annotations or built-in Fluent API Validator methods. Here are some of the scenarios where you might choose to use Fluent API Custom Validators ASP.NET Core Application including MVC, Web API:

  • Complex Business Rules: If your validation logic involves multiple properties of the model or complex business rules that depend on external data, Fluent API Custom Validators allow you to encapsulate this logic neatly. For example, you might want to validate that a booking date is not only in the future but also on a day when a particular venue is available.
  • Reusable Validation Logic: When you have validation rules that are used across different models or parts of your application, Fluent API can help you centralize and reuse this logic without duplicating code. For instance, if several models require a tax ID number to be validated in a specific format, you can create a custom validator for this purpose.
  • Integration with ASP.NET Core’s Validation Pipeline: Custom Validators created with Fluent API can easily integrate with ASP.NET Core’s model validation system. This means they work well with the existing framework features like model binding, producing validation results that automatically translate into HTTP responses.
  • Custom Validation Responses: If you need to return detailed validation responses or customize the format of the validation error messages, Fluent API allows more flexibility compared to Data Annotations. This can be particularly useful for APIs where you might want to provide more detailed error information to the client.
When Should We Use Fluent API Custom, CustomAsync, Must, and MustAsync Methods in ASP.NET Core MVC?

When deciding between using Custom, CustomAsync, Must, and MustAsync methods in Fluent Validation for ASP.NET Core MVC Application, the choice largely depends on the complexity of the validation requirement, whether the validation involves asynchronous operations, and the level of control needed over the validation process.

Must:

This is a synchronous method that takes a predicate function. The function takes the property value as a parameter and returns a boolean indicating whether the property meets a specific condition.

  • Simple Condition: The validation rule is straightforward and can be expressed as a simple condition or predicate.
  • Synchronous Operation: No asynchronous operation is needed (e.g., no database or external service checks).
  • Example Scenarios: Validating that a number falls within a specific range; checking if a string meets certain criteria (e.g., format or contains specific characters).
MustAsync:

This is the asynchronous version of Must. It is useful when the validation rule needs to perform asynchronous operations, such as database lookups or API calls, to determine validity.

  • Asynchronous Condition: The validation involves asynchronous operations such as database checks or calls to external APIs.
  • Non-blocking Requirement: It’s important to keep the operation non-blocking, especially in web applications, to maintain responsiveness.
  • Example Scenarios: Checking if a username is available by querying a database asynchronously. Verifying if a record exists or meets certain criteria in an external API.
Custom:

This method is synchronous and allows for custom validation logic. You can define a function that takes the property value and a validation context and then returns a ValidationResult. This method is useful when the validation depends on multiple properties of the model.

  • Complex Validation Logic: The validation depends on multiple properties or requires complex logic that doesn’t neatly fit into a single predicate.
  • Detailed Error Reporting: You need to add multiple failure messages or detailed custom responses based on various conditions within the same rule.
  • Example Scenarios: Ensuring a date range is valid where the start date must be earlier than the end date, and both dates might depend on other conditions in the model. Validating a model where the validation outcome depends on a combination of fields (e.g., if Field A is true, then Field B must have a specific value).
CustomAsync:

This is the asynchronous version of Custom. It is similar in usage but allows for including async operations in the validation process.

  • Complex Asynchronous Validation: Similar to Custom, but where the validation logic includes asynchronous operations.
  • Handling of Asynchronous Dependencies: The validation involves dependencies that are inherently asynchronous, such as real-time data fetching or validation against a service that requires waiting for a response.
  • Example scenarios: Verify a file path by asynchronously checking whether the file exists on a remote server. Conducting a multi-step validation where steps involve fetching data from a database or a web service that must not block the main execution thread.
Choosing the Right Method
  • Simplicity vs. Complexity: Use Must/MustAsync for straightforward conditions and Custom/CustomAsync for more complex scenarios involving multiple conditions or complex logic.
  • Synchronous vs. Asynchronous Needs: Choose between synchronous and asynchronous methods based on whether the validation logic must perform IO operations or other asynchronous tasks.
  • Single vs. Multiple Validation Points: If your validation rule needs to generate different messages based on different conditions, Custom or CustomAsync provides the necessary control.
  • Validation Context Usage: The Custom and CustomAsync methods provide access to the entire validation context, allowing for more detailed inspection and manipulation of the validation outcomes, which is particularly useful when the validation decision involves multiple fields or complex logic.

In the next article, I will discuss Real-Time Examples of Fluent API Validations in ASP.NET Core MVC Applications. In this article, I try to explain the Fluent API Custom Validators in ASP.NET Core MVC with Examples. I hope you enjoy the Fluent API Custom Validators in the ASP.NET Core MVC article.

Leave a Reply

Your email address will not be published. Required fields are marked *