Back to: ASP.NET Core Tutorials For Beginners and Professionals
Fluent API Validation in ASP.NET Core MVC
In this article, I will briefly introduce Fluent API Validations in ASP.NET Core MVC Applications. Please read our previous article discussing Data Annotations in ASP.NET Core MVC Applications. When it comes to creating validation rules for models in ASP.NET Core MVC (or any ASP.NET Core Web Application), we can adopt two different approaches, i.e., Data Annotation and Fluent API Validations. We have already discussed Model Validation using Data Annotation. In this article and in our few upcoming articles, we are going to discuss Model Validations using Fluent API.
What is Fluent API Validation in ASP.NET Core MVC?
Fluent API validation in ASP.NET Core MVC is an approach to model validation that uses a fluent interface provided by the FluentValidation library. This approach constructs validation rules in a clear, flexible, understandable, and readable manner, which is an alternative to the traditional Data Annotation approach.
So, instead of directly applying data annotation attributes to model properties, we can define validation rules in a separate class with Fluent Validation. This “fluent” approach is based on the Fluent Interface design pattern, which uses method chaining to create code that reads like natural language.
Fluent API Validation Methods in ASP.NET Core
In ASP.NET Core, when we refer to Fluent API validation, we often talk about the library FluentValidation. The FluentValidation provides a rich set of methods to define validation rules fluently. Here are some of the common validation methods provided by FluentValidation:
Basic Validators:
- NotNull() / NotEmpty(): Check the property is not null or empty.
- Equal(): Checks if the property equals another value or property.
- NotEqual(): Checks the property is not equal to another value or property.
- Length(): Checks the length of a string property that falls within the given min and max range.
- Matches(): Checks the property matches a given regex pattern.
Number Validators:
- InclusiveBetween(): Checks the property value falls between or equals to the specified limits. This can also be used for DateTime ranges.
- ExclusiveBetween(): Checks the property value falls between but not equal to the specified limits. This can also be used for DateTime values.
- LessThan() / LessThanOrEqualTo(): For comparison with another value. This can also be used for DateTime values.
- GreaterThan() / GreaterThanOrEqualTo(): Same as above, but for greater values. This can also be used for DateTime values.
String Validators:
- EmailAddress(): Checks the property is a valid email address.
- CreditCard(): Checks the property is a valid credit card number.
Conditionally Applying Rules:
- When(): Apply rules conditionally based on properties or external factors.
- Unless(): The opposite of When(), the rule is applied unless the condition is true.
Collections:
- Must(): This allows for more complex custom logic. For instance, you could validate that a list has unique elements. It can also be used to write custom logic for validation.
- ForEach(): Apply a rule to each element in a collection.
Custom Validators:
- Custom(): Allows you to specify a custom validation function.
Other Validators:
- CascadeMode: Determines how the library should continue validation once a rule has failed.
- WithMessage(): Allows custom error messages to be set for validation rules.
- WithName(): Overrides the display name of a property.
- DependentRules(): Specifies that several rules should be grouped together as dependent rules.
Async Validators:
- MustAsync(): Asynchronously checks a condition.
- CustomAsync(): Asynchronously validates using custom logic.
Comparison Validators:
- Equal() / NotEqual(): This can also be used to compare properties of the same model.
How Do We Use Fluent API Validation in ASP.NET Core MVC?
To Fluent Validation in ASP.NET Core MVC, we need to add the FluentValidation package, which is a popular choice for using Fluent API validation. Let us proceed and see the step-by-step process of integrating and using FluentValidation in an ASP.NET Core MVC Application. So, first, create a new ASP.NET Core MVC Project with Model View Controller Project Template and name the project FluentAPIDemo.
Install FluentValidation Package:
First, we need to add the FluentValidation.AspNetCore package to our project. We can add using NuGet Package Manager for the Solution, as shown below.
Using Package Manager Console: Install-Package FluentValidation.AspNetCore
Creating the Model Class:
We will use Fluent API Validations to validate the model class property values. Let us first create a model class for registering a user. Create a class file named RegistrationModel.cs within the Models folder and copy and paste the following code. This simple model class has three properties: Username, Email, and Password, and we want to validate these properties.
namespace FluentAPIDemo.Models { public class RegistrationModel { public string? Username { get; set; } public string? Email { get; set; } public string? Password { get; set; } } }
Creating a Fluent API Validator:
A validator class in Fluent Validation is where you define the rules for each property of the model you want to validate. To implement Fluent API Validation to Validate the Model Properties, we need to create a Fluent API Validator class, and for each model class, we need to create a separate Validator class. For example, for our Registration Model, we need to create a Validator Class, and as part of the Validator class, we need to write the logic to validate the properties, as per our business requirement.
So, create a class file named RegistrationValidator.cs within the Models folder and copy and paste the following code. Here, we use the built-in Fluent API validation methods to validate the RegistrationModel properties.
using FluentValidation; namespace FluentAPIDemo.Models { public class RegistrationValidator : AbstractValidator<RegistrationModel> { public RegistrationValidator() { RuleFor(x => x.Username) .NotEmpty().WithMessage("Username is Required.") .Length(5, 30).WithMessage("Username must be between 5 and 30 characters."); RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is Required.") .EmailAddress().WithMessage("Valid Email Address is Required."); RuleFor(x => x.Password) .NotEmpty().WithMessage("Password is Required.") .Length(6, 100).WithMessage("Password must be between 6 and 100 characters."); } } }
Understanding the above Code:
Namespace and Usings: The code uses the FluentValidation library. The using FluentValidation; statement allows access to the FluentValidation classes and methods.
Class Definition: The RegistrationValidator class is defined, which inherits from AbstractValidator<T> generic class. This generic base class is provided by Fluent Validation, where T represents the type of object to validate, in this case, RegistrationModel. So, it is the base class that we need to extend to define our validation rules for a specific model.
Validator Constructor: The constructor public RegistrationValidator() sets up rules for the properties of RegistrationModel. The Validation Rules are as follows:
Username Validation:
- RuleFor(x => x.Username): Defines a validation rule for the Username property of RegistrationModel.
- .NotEmpty(): Specifies that the Username must not be empty. If it is empty, the message “Username is Required.” will be shown.
- .Length(5, 30): Ensures that the length of the Username must be between 5 and 30 characters. If not, the message “Username must be between 5 and 30 characters.” is displayed.
Email Validation:
- RuleFor(x => x.Email): Sets a rule for the Email property.
- .NotEmpty(): The Email cannot be empty, and if it is, “Email is Required.” will be the error message.
- .EmailAddress(): Validates that the input is a valid email address format. If it fails, “Valid Email Address is Required.” is shown as the error message.
Password Validation:
- RuleFor(x => x.Password): Defines a rule for the Password property.
- .NotEmpty(): Checks that the Password is not empty, displaying “Password is Required.” if it is.
- .Length(6, 100): Ensures the Password length is between 6 and 100 characters. If the length criteria are not met, “Password must be between 6 and 100 characters.” will be shown.
Points to Remember:
- RuleFor(x => x.PropertyName): This is the starting method to define a validation rule for a specific property on the model. It sets the context for further chained methods determining how the property will be validated.
- WithMessage(“…”): This method allows us to provide a custom error message for the preceding validation rule. If the validation fails, this message will be added to the ModelState errors in an ASP.NET Core MVC application.
Registering Fluent API and the Validator:
Once we have the Model class and Validator class, we need to enable Fluent API validation for our Project. To enable Fluent API Validation, we need to use the AddFluentValidationAutoValidation service, as shown below, within the Program class of our application. This will enable integration between FluentValidation and ASP.NET MVC’s automatic validation pipeline. That means the ASP.NET Core framework will check the validation automatically.
builder.Services.AddFluentValidationAutoValidation();
If you want to enable Client-Side validation, then you need to register the following AddFluentValidationClientsideAdapters method. This is not required if you work with an ASP.NET Core Web API Project. This enables integration between FluentValidation and ASP.NET client-side validation.
builder.Services.AddFluentValidationClientsideAdapters();
Then, we must register each Model class and the corresponding Validator class to the built-in dependency injection container. Here, we are using the AddTransient method to register the same as follows.
builder.Services.AddTransient<IValidator<RegistrationModel>, RegistrationValidator>();
With the above changes in place, our Program class should look as shown below:
using FluentAPIDemo.Models; using FluentValidation; using FluentValidation.AspNetCore; using System.Drawing; namespace FluentAPIDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); //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>, RegistrationValidator>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); } } }
The FluentValidation will automatically validate controller action method parameters, populating the ModelState with validation errors. You can then check ModelState.IsValid in your action methods to determine if the received input is valid.
Validating in Controllers:
Now, we need to Validate the RegistrationModel within the Controller action method. You need to remember that FluentValidation will automatically validate the model and populate the ModelState. So, modify the Home Controller class as follows:
using FluentAPIDemo.Models; using Microsoft.AspNetCore.Mvc; namespace FluentAPIDemo.Controllers { public class HomeController : Controller { public IActionResult Register() { return View(); } [HttpPost] public IActionResult Register(RegistrationModel model) { if (!ModelState.IsValid) { // Validation failed, return to the form with errors return View(model); } // Handle successful validation logic here return RedirectToAction("Success"); } public string Success() { return "Registration Successful"; } } }
Display Errors in Views:
We can display errors using the validation tag helpers in our Razor views. So, add the Register.cshtml view and copy and paste the following code.
@model FluentAPIDemo.Models.RegistrationModel @{ ViewData["Title"] = "Register"; } <h1>Register User</h1> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Register"> <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" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group mt-3""> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form> </div> </div>
Now, run the application, navigate to Home/Register, and check whether everything works as expected.
Here, you can observe that the Client-Side validation is not working. To ensure the client-side validation is working, we must include jQuery, jQuery Validation, and jQuery Unobtrusive Validation in our project. These can be added via CDNs or local files. For example, include them via CDNs in your _Layout.cshtml:
<!-- jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> <!-- jQuery Validation Plugin --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.2/jquery.validate.min.js"></script> <!-- jQuery Unobtrusive Validation (to bridge ASP.NET Core MVC validation with jQuery Validate) --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"></script>
To enable client-side validation using local files, you can add the following files to your _Layout.cshtml. The order is also important; please include them in the following order.
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
However, as we already discussed, the above files are already added within the _ValidationScriptsPartial partial view. In the _Layout file, one optional section is provided named Scripts. So, we can include a section called Scripts and call the _ValidationScriptsPartial partial view inside that section. Please modify the Register.cshtml view as follows. We can add the Script section and call the _ValidationScriptsPartial Partial View wherever the client-side validation is required.
@model FluentAPIDemo.Models.RegistrationModel @{ ViewData["Title"] = "Register"; } <h1>Register User</h1> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Register"> <div asp-validation-summary="ModelOnly" 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" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group mt-3"> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
With the above changes, you can observe that the client-side validation is also working as expected.
Validating Manually:
Instead of using the ModelState.IsValid to check the Validation automatically, we can also check the validation manually by creating an instance of the Validator class and then invoking the Validate method. For a better understanding, please look at the following code.
using FluentAPIDemo.Models; using Microsoft.AspNetCore.Mvc; namespace FluentAPIDemo.Controllers { public class HomeController : Controller { public IActionResult Register() { return View(); } [HttpPost] public IActionResult Register(RegistrationModel model) { RegistrationValidator validator = new RegistrationValidator(); var validationResult = validator.Validate(model); if (!validationResult.IsValid) { return View(model); } //if (!ModelState.IsValid) //{ // // Validation failed, return to the form with errors // return View(model); //} // Handle successful validation logic here return RedirectToAction("Success"); } public string Success() { return "Registration Successful"; } } }
With the above changes, run the application and see the expected behavior. Note that not all rules defined in FluentValidation will work with ASP.NET’s client-side validation. For example, rules defined using a condition (with When/Unless), Custom Validators, or Calls to Must will not run on the client side. The client-side validation supports the following Validator Methods in ASP.NET Core:
- NotNull/NotEmpty
- Matches (regex)
- InclusiveBetween (range)
- CreditCard
- EqualTo (cross-property equality comparison)
- MaxLength
- MinLength
- Length
What happens when using Fluent Validation and Data Annotations in ASP.NET Core MVC with the Same Model Properties?
When Fluent Validation and Data Annotations are used in an ASP.NET Core MVC application to validate the same model properties, both sets of validation rules are applied.
- Data Annotations: ASP.NET Core Processes Data Annotations validation automatically when model binding occurs. This means that when the framework attempts to bind incoming data to a model, it checks the Data Annotations rules.
- Fluent Validation: If you have configured Fluent Validation to integrate with ASP.NET Core’s automatic validation pipeline, it generally runs after Data Annotations during the model binding process. If you invoke Fluent Validation manually (e.g., inside a controller action), you control when it runs relative to Data Annotations.
Example:
Let’s understand this with an example. To include the Data Annotation Validations, let’s modify the RegistrationModel.cs class file as follows. You can see we have decorated the Model Properties with Data Annotation Attributes.
using System.ComponentModel.DataAnnotations; namespace FluentAPIDemo.Models { public class RegistrationModel { [Required(ErrorMessage = "Please Enter Username")] public string? Username { get; set; } [Required(ErrorMessage = "Please Enter Email")] [EmailAddress(ErrorMessage = "Please Enter Valid Email Address")] public string? Email { get; set; } [Required(ErrorMessage = "Please Enter Password")] public string? Password { get; set; } } }
Next, modify the Home Controller as follows. The ASP.NET Core framework will automatically handle Data annotation and Fluent validations at the time of Model Binding as we have enabled the Auto Validations for Fluent API.
using FluentAPIDemo.Models; using Microsoft.AspNetCore.Mvc; namespace FluentAPIDemo.Controllers { public class HomeController : Controller { public IActionResult Register() { return View(); } [HttpPost] public IActionResult Register(RegistrationModel model) { if (!ModelState.IsValid) { // Validation failed, return to the form with errors return View(model); } // Handle successful validation logic here return RedirectToAction("Success"); } public string Success() { return "Registration Successful"; } } }
To see the effect, please modify the Register.cshtml file as follows. Here, we are disabling the client-side validation by removing the script section that invokes the Partial View.
@model FluentAPIDemo.Models.RegistrationModel @{ ViewData["Title"] = "Register"; } <h1>Register User</h1> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Register"> <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" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group mt-3"> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form> </div> </div> @* @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } } *@
Now, run the application and click on the Create button without providing any data, and you will get the following error. In each individual span element where the model-specific error is displayed, you will not see both error messages as they always overwrite the previous error message with the latest one. However, if you observe the validation summary, it shows all the error messages, as shown in the image below, which proves that both Validations are working.
Issues When Use Both:
- Redundancy: Duplication of validation rules can lead to confusion and increased maintenance overhead.
- Performance: Running both validations may slightly impact performance, although this is generally minimal unless the rules are complex or the model is large.
Best Practices:
- Avoid Duplication: Ensure that validations are not redundantly defined in both systems.
- Clear Separation: Use Data Annotations for simple, declarative validations and Fluent Validation for complex logic.
- Documentation: Clearly document which validations are applied where, especially if both systems are used extensively across the application.
Why Do We Need to Use Fluent API Validation in ASP.NET Core?
Fluent API validation offers several advantages over traditional validation methods in ASP.NET Core, such as Data Annotations. Here are some reasons why developers choose Fluent API validation over Data Annotation:
- Readability and Clarity: Fluent API uses method chaining to express configuration and setup in an easy-to-read and understandable way. This can make the code more clear and self-documenting. For example, a validation rule might look like this: .RuleFor(model => model.Property).NotEmpty().WithMessage(“Property cannot be empty”);
- Centralized Validation Rules: This allows you to define all validation rules for a model in one place. This centralization helps avoid duplication of validation logic across different parts of an application, such as controllers, views, services, etc., making the code cleaner and reducing maintenance overhead. This also makes the rules easier to manage and modify.
- Customization and Flexibility: Fluent API validators are highly customizable, allowing developers to define complex validation rules that are not easily implemented using data annotations. You can also create and use custom validation methods in the validation chain.
- Customizable Error Messages: Fluent API allows users to customize error messages easily and dynamically based on validation contexts. This flexibility makes error responses more meaningful for the end user.
- Reusability: Since validation rules are defined in separate classes (usually one per model), you can reuse these validators across different application parts, ensuring consistent validation logic throughout.
- Integration with ASP.NET Core Features: These validators work seamlessly with ASP.NET Core MVC features like model binding, automatic HTTP 400 responses on validation failures, and integration with ModelState. This means you can easily tie your custom validation logic into the standard ASP.NET Core pipeline and handle errors effectively.
- Support for Asynchronous Validation: Fluent API supports asynchronous validation rules, which are essential for non-blocking I/O operations, such as database or network calls, improving the performance of your web application.
- Interdependent Validations: Fluent API can handle complex scenarios where the validity of one field may depend on the state or value of another field within the model. This interdependency is more awkward to implement with traditional attribute-based validation.
Performance Differences Between Data Annotation and Fluent API Validation in ASP.NET Core MVC:
Data Annotation: Data Annotations may lead to slightly faster performance because they are less complex and integrated directly into the model properties. However, the difference is often negligible in most applications.
Fluent API Validation: In runtime performance, there might be a negligible performance overhead with Fluent Validation because it involves compiling and executing more complex validation rules. However, this difference is generally small and can be offset by the benefits of more powerful validation capabilities.
When to Use?
The performance difference between the two approaches is typically small and should not be the primary factor in deciding which one to use.
- For Simple Scenarios: Data Annotation Validation may be more efficient due to its simplicity and direct integration.
- For Complex Scenarios: Fluent API Validation might be more effective despite its slight performance overhead due to its flexibility, maintainability benefits, and ease of handling complex validation.
In the next article, I will discuss Model Validations in ASP.NET Core MVC Applications using Fluent API Validation. In this article, I try to explain Fluent API Validations in ASP.NET Core MVC with Examples. I hope you enjoy the Fluent API Validations in ASP.NET Core MVC article.