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 Validations. We can use the Custom method for synchronous validation or the CustomAsync method for asynchronous validation. Let us understand how to use Fluent API Custom and CustomAsync Validator methods in ASP.NET Core MVC Application.

Define the Model:

We will use the following Product model to understand Fluent API Custom Validators in ASP.NET Core MVC Application. So, create a class file named Product.cs and copy and paste the following code. As you can see this model class contains two properties.

namespace FluentAPIDemo.Models
{
    public class Product
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}
Creating Custom Fluent API Validators
Synchronous Custom Validator:

Suppose we have a unique requirement that the product’s name should not contain the string “XYZ”. Let us see how we can implement this Custom Validation Logic using the Custom Validator method. So, create a class file named ProductValidator.cs and copy and paste the following code. As you can see, this class is inherited from the AbstractValidator<Product> class. Here, you can see, using the Custom method we have written the custom logic which is going to validate whether the product name contains the string “XYZ” or not.

using FluentValidation;
namespace FluentAPIDemo.Models
{
    public class ProductValidator : AbstractValidator<Product>
    {
        public ProductValidator()
        {
            RuleFor(p => p.Name)
                .Custom((name, context) =>
                {
                    if (name.Contains("XYZ"))
                    {
                        context.AddFailure($"Product name should not contain 'XYZ'. You entered: {name}");
                    }
                });

            RuleFor(p => p.Price)
                .GreaterThan(0);
        }
    }
}
Registering the Validator:

Add the following code with the Program class to register Fluent API Validation and the Model and corresponding validator.

//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 the client side
builder.Services.AddTransient<IValidator<Product>, ProductValidator>();
Using the Validator in ASP.NET Core MVC Actions:

Modify the Home Controller as follows. As we have enabled the Auto Validation, you can directly use the ModelState to check whether the model validation is successful or not which is shown in the below code.

using FluentAPIDemo.Models;
using Microsoft.AspNetCore.Mvc;
namespace FluentAPIDemo.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult AddProduct()
        {
            return View();
        }

        [HttpPost]
        public IActionResult AddProduct(Product product)
        {
            if (!ModelState.IsValid)
            {
                return View(product);  // Return with validation errors.
            }

            // 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 AddProduct.cshtml view and copy and paste the following code.

@model FluentAPIDemo.Models.Product

@{
    ViewData["Title"] = "Register";
}

<h1>Add Product</h1>

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

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

            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

With the above changes in place, run the application and see if everything is working as expected, as shown in the below image.

Fluent API Custom Validators in ASP.NET Core MVC

Asynchronous Custom Validator:

If you need to perform asynchronous operations as part of your custom validation, like checking a database or invoking an API, you can use the CustomAsync Validator method. Suppose we want to ensure the product’s name is unique.

Let us first create the service to check whether the product name is unique. In this case, we are going to use the following ProductService.  So, create a class file named ProductService.cs and copy and paste the following code. Here, you can see, the IsProductNameUniqueAsync method is used to check if the product name is unique or not.

namespace FluentAPIDemo.Models
{
    public interface IProductService
    {
        Task<bool> IsProductNameUniqueAsync(string name);
    }

    public class ProductService : IProductService
    {
        public Task<bool> IsProductNameUniqueAsync(string name)
        {
            List<string> products = new List<string>()
            {
                "MyProduct1", "MyProduct2", "MyProduct3", "MyProduct4"
            };

            if(!products.Contains(name))
            {
                return Task.FromResult(true);
            }
            else
            {
                return Task.FromResult(false);
            }
        }
    }
}

Next, create the Custom Async Validator. So, modify the ProductValidator class as follows. Here, you can see within the CustomAsync Validator method, we are invoking the IsProductNameUniqueAsync method which is going to check whether the product name is unique or not.

using FluentValidation;
namespace FluentAPIDemo.Models
{
    public class ProductValidator : AbstractValidator<Product>
    {
        private readonly IProductService _productService;

        public ProductValidator(IProductService productService)
        {
            _productService = productService;

            //Sync Validator to check whether the Product Name contains XYZ
            RuleFor(p => p.Name)
                .Custom((name, context) =>
                {
                    if (name.Contains("XYZ"))
                    {
                        context.AddFailure($"Product name should not contain 'XYZ'. You entered: {name}");
                    }
                });

            //Sync Validator to check whether the Product Price > 0
            RuleFor(p => p.Price)
                .GreaterThan(0);

            //Async Validator to check whether the Product Name is unique or not
            RuleFor(p => p.Name)
            .CustomAsync(async (name, context, cancellation) =>
            {
                bool isUnique = await _productService.IsProductNameUniqueAsync(name);
                if (!isUnique)
                {
                    context.AddFailure($"Product name '{name}' already exists.");
                }
            });
        }
    }
}
Next, registering the Validator and Services.
//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 the client side
builder.Services.AddTransient<IValidator<Product>, ProductValidator>();

//Registering The Product Service
builder.Services.AddTransient<IProductService, ProductService>();
Next, modify the Home Controller.

As we are using asynchronous validation i.e. setting validation rules using the CustomAsync method, we need to use the asynchronous action method. With the async validator in place, Auto Validation will not work, so we need to create an instance of ProductValidator and invoke the ValidateAsync method by passing the Product object as a parameter that needs to be validated.

using FluentAPIDemo.Models;
using Microsoft.AspNetCore.Mvc;
namespace FluentAPIDemo.Controllers
{
    public class HomeController : Controller
    {
        private readonly IProductService _productService;
        public HomeController(IProductService productService)
        {
            _productService = productService;
        }

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

        [HttpPost]
        public async Task<IActionResult> AddProduct(Product product)
        {
            var validationResult = await new ProductValidator(_productService).ValidateAsync(product);

            if (!validationResult.IsValid)
            {
                foreach (var error in validationResult.Errors)
                {
                    ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
                }
                return View(product); // Return with validation errors.
            }

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

        public string Success()
        {
            return "Registration Successful";
        }
    }
}

With the above changes in place, run the application, and you should see the validation works as expected. So, by using the Custom and CustomAsync methods, we can create validation rules as per 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

In an ASP.NET Core MVC Application, you can also write the custom validation logic using FluentValidation’s Must and MustAsync methods, which 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.

Define the Model:

Let’s consider the following Product model to understand Custom validation using Must and MustAsync Fluent API Methods.

namespace FluentAPIDemo.Models
{
    public class Product
    {
        public string Name { get; set; }
        public DateTime ExpiryDate { get; set; }
    }
}
Set Up the Validator with Must and MustAsync methods:
Using Must:

Suppose we want to ensure that the product’s ExpiryDate is always in the future:

using FluentValidation;
namespace FluentAPIDemo.Models
{
    public class ProductValidator : AbstractValidator<Product>
    {
        public ProductValidator()
        {
            RuleFor(p => p.ExpiryDate)
                .Must(date => date > DateTime.Now)
                .WithMessage("Expiry date must be in the future.");
        }
    }
}
Registering the Validator:

Add the following code with the Program class to register Fluent API Validation and the Model and corresponding validator.

//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 the client side
builder.Services.AddTransient<IValidator<Product>, ProductValidator>();
Using the Validator in ASP.NET Core MVC Actions:

Modify the Home Controller as follows:

using FluentAPIDemo.Models;
using Microsoft.AspNetCore.Mvc;
namespace FluentAPIDemo.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult AddProduct()
        {
            return View();
        }

        [HttpPost]
        public IActionResult AddProduct(Product product)
        {
            if (!ModelState.IsValid)
            {
                return View(product);  // Return with validation errors.
            }

            // 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. Add AddProduct.cshtml view and copy and paste the following code.

@model FluentAPIDemo.Models.Product

@{
    ViewData["Title"] = "Register";
}

<h1>Add Product</h1>

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

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

            <div class="form-group">
                <label asp-for="ExpiryDate" class="control-label"></label>
                <input asp-for="ExpiryDate" type="date" class="form-control" />
                <span asp-validation-for="ExpiryDate" class="text-danger"></span>
            </div>
            
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
Output:

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

Using MustAsync:

For asynchronous validation, imagine a requirement where the product name must be unique, and we need to check an external service or database. The following service will check whether the product name is unique.

namespace FluentAPIDemo.Models
{
    public interface IProductService
    {
        Task<bool> IsProductNameUniqueAsync(string name);
    }

    public class ProductService : IProductService
    {
        public Task<bool> IsProductNameUniqueAsync(string name)
        {
            List<string> products = new List<string>()
            {
                "MyProduct1", "MyProduct2", "MyProduct3", "MyProduct4"
            };

            if(!products.Contains(name))
            {
                return Task.FromResult(true);
            }
            else
            {
                return Task.FromResult(false);
            }
        }
    }
}

Next, modify the Validator as follows:

using FluentValidation;
namespace FluentAPIDemo.Models
{
    public class ProductValidator : AbstractValidator<Product>
    {
        private readonly IProductService _productService;

        public ProductValidator(IProductService productService)
        {
            _productService = productService;

            //Synchronous Checking Product Expire Date
            RuleFor(p => p.ExpiryDate)
                .Must(date => date > DateTime.Now)
                .WithMessage("Expiry date must be in the future.");

            //Asynchronous Checking Product Unique Name
            RuleFor(p => p.Name)
                .MustAsync(async (name, cancellationToken) =>
                    await _productService.IsProductNameUniqueAsync(name))
                .WithMessage("Product name must be unique.");
        }
    }
}

Next, registering the Validator and Services.

//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 the client side
builder.Services.AddTransient<IValidator<Product>, ProductValidator>();

//Registering The Product Service
builder.Services.AddTransient<IProductService, ProductService>();

Next, modify the Home Controller.

using FluentAPIDemo.Models;
using Microsoft.AspNetCore.Mvc;
namespace FluentAPIDemo.Controllers
{
    public class HomeController : Controller
    {
        private readonly IProductService _productService;
        public HomeController(IProductService productService)
        {
            _productService = productService;
        }

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

        [HttpPost]
        public async Task<IActionResult> AddProduct(Product product)
        {
            var validationResult = await new ProductValidator(_productService).ValidateAsync(product);

            if (!validationResult.IsValid)
            {
                foreach (var error in validationResult.Errors)
                {
                    ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
                }
                return View(product); // Return with validation errors.
            }

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

        public string Success()
        {
            return "Registration Successful";
        }
    }
}

With the above changes in place, run the application, and you should see the validation works as expected.

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

So, using the Must and MustAsync methods, we can create custom validation logic as per our business needs, ensuring a robust and flexible validation process in our ASP.NET Core MVC application.

Differences Between Fluent API CustomAsync and MustAsync Methods in ASP.NET Core MVC

Both Custom and Must Methods (or CustomAsync and MustAsync Methods) in Fluent API are designed to provide custom validation logic to our model properties, but they are used slightly differently. Let us understand the differences between them.

Custom:
  • To use the “Custom” method, you need a delegate with two parameters.
  • The value of the property is being validated.
  • The CustomContext object provides more context and allows you to add validation errors.
  • This method creates more complex custom validations where you may want to add multiple failures or have more control over the validation process.
  • Custom Use Cases: More control over the validation process. The ability to add multiple validation failures. Access the CustomContext object to get more information about the validation or the instance being validated.
Must:
  • The Must method takes a delegate (a predicate) that returns a boolean. The delegate should return true if validation succeeds and false otherwise.
  • This method is ideal for straightforward validations where you want to test a condition.
  • Must Use Cases: Simpler and more concise for basic checks. Typically used for simple conditional checks on the property.

When checking a simple condition, use Must. For more control over validation and adding multiple failures for a single property, use Custom.

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 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 *