Validations are a requirement to make sure the data we are receiving is valid and it complies to certain and specific rules we assign , so as a result , we get to obtain the data in a form that we define and control based on the needs of our applications.
Fluent Validation is somewhat of an alternative to Data Annotations , also , as the name suggests , it writes very fluently , you will see that shortly.
So , without further talking , let's jump straight to the essence of our topic.
Why would you choose FluentValidation over Data Annotations ?
This question is valid , let's answer it.
Data Annotations is very productive and less typing than FluentValidation , and for small models , it's even more viable than FluentValidations , but when we scale our models up a bit , Data Annotations starts to bloat the code file with attributes residing on top of every property , which makes the code less readable and a bit disturbing .
We will be applying Data Annotations on a RegisterUserDto model , the model has the following properties
public class RegisterUserDto
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public string? PasswordConfirmation { get; set; }
}
This picture depicts Data Annotations on our model and the pitfall of using it in a real world scenario
I reckon this representation is sufficient enough for us to want to go with an alternative , cause this is only one DTO , if every model we create has to carry all these attributes in it , it'll be challenging for the long term to maintain and check these files , so , we will use FluentValidations instead , now , we will have to download a couple of packages , so , open your terminal inside your project directory , or use the NuGet Package Manager if you are using Visual Studio and download these 2 packages.
1: dotnet add package FluentValidation.AspNetCore
2: dotnet add package FluentValidation.DependencyInjectionExtensions
Now , let's create our validation rules for our RegisterUserDto Model.
Firstly , create a folder named Validators in your project , and add a class file named RegisterUserDtoValidator.cs
Secondly , add this line to the top of your file
using FluentValidation;
This using directive will give us access to the generic class AbstractValidator that we will use to type out our validation rules
Thirdly , make the class inherit from the AbstractValidator and pass the RegisterUserDto model to it like the following
public class RegisterUserDtoValidator : AbstractValidator<RegisterUserDto>
{
}
Lastly , create an empty constructor for our class
public class RegisterUserDtoValidator : AbstractValidator<RegisterUserDto>
{
public RegisterUserDtoValidator()
{
//Rules go here.
}
}
Alrightyyy
We're ready to start typing our own custom validation rules for the model we have.
Be aware that by using FluentValidation , there'll be way more typing and a lot of syntax , with that out of the way , let's build our first rule for the FirstName property
Ok , that's way more than typing [Required] [MaxLength] and [MinLength] isn't it ? It definitely is , but let's break it down.
We used the RuleFor() method to write validation rule for a specific property , that property was defined inside the parentheses , within a Lambda Expression
p => p.FirstName
This line basically says , write a rule for a property , and this property is FirstName.
Ok , after that , we said
NotNull().WithMessage("Some Message")
I suppose this line is self-explanatory , it's the same as doing the [Required(ErrorMessage = "Some Message")] ,
essentially , it's just saying , this property shouldn't be null and in case we breached that rule , send a message back which we can define inside the WithMessage();
extension method.
MaximumLength();
, MinimumLength();
, these 2 also define restrictions on the required length of a property , which are also pretty obvious , the way we do it is mostly like this
Rule().WithMessage(Message);
This is analogous to all the rules we'll apply , so for practice sake , take a quick pause , and try to make a similar rule for the LastName property with a custom message , a custom min and max length of your choice.
OK , let's move to the Email property , it's almost identical to the ones we've previously written , but for emails , there's a unique extension method , EmailAddress();
, this one adds restrictions on the shape of the data we receive , like does it have an @ sign in it or not , if it doesn't , then throw the error message , which you can add by , again using the WithMessage();
extension method.
This here shows the rules I specified on the Email property
And , that's it for 3 properties , additionally , the password property is similar to first and last name , make it required , add some error messages and define min and max length for it , straight simple.
Finally , PasswordConfirmation Property
This one is a bit more interesting than the preceding ones , cause we need to make sure that both the Password and PasswordConfirmation properties match , otherwise , that would be a validation breach , SO , LET'S CODE IT.
RuleFor(p => p.PasswordConfirmation)
.NotNull().WithMessage("The password confirmation field is required")
.Equal(p => p.Password)
.WithMessage("Password and Password Confirmation don't match");
Ok , what's new here is the Equal();
extension method , this method compares the property we are applying the rule on , to the property we specify inside the parentheses using a Lambda Expression , p => p.Password
.
If they don't match , then send back the validation message.
Final Result
This picture displays the rules we've made , the message could be different based off your needs and your use cases , but for our case , this should do it.
Final steps
We still have to register our validator in the DI container so we can inject it where we want , let's do that quickly , go to program.cs and add this line.
builder.Services.AddValidatorsFromAssemblyContaining<RegisterUserDtoValidator>();
This line adds all the Validators that reside within the folder where the RegisterUserDtoValidator lives , so if you have other validators there , they will also be registered , there are also other methods to register validators differently , but we won't go over them now.
Now after doing that , we can use Dependency Injection to make use of these validation rules inside our controllers.
I have created an authentication controller , so we can test and see these validations in action.
Open your controller and declare a private field of type IValidator in this case the type is RegisterUserDto , pay attention , not the RegisterUserDtoValidator , rather it's the type we want to apply the validations on , which is the RegisterUserDto in our case , then create a constructor and use it to inject an instance of the IValidator<RegisterUserDto>
and assign it to our private field.
private readonly IValidator<RegisterUserDto> _registerValidator;
public AuthController(IValidator<RegisterUserDto> registerValidator)
{
_registerValidator = registerValidator;
}
Now , I have a RegisterUser endpoint which receives a RegisterUserDto as a parameter , and if the model we are receiving is valid , we then pass this model to the auth service which isn't covered here , don't worry about how to process work , we are merely concentrating on FluentValidation here
Focus on ValidationResult here , this what will validate our model and check to see if it complies to rules we defined earlier
ValidationResult validationResult = await _registerValidator.ValidateAsync(model);
This line takes our model , and uses the _registerValidator to call ValidateAsync(model);
and check if our model is valid , if the result of the validation is a success or true , then we process the request and register the user , else , we call the extension method that we will create shortly on the validationResult variable
validationResult.AddToModelState(ModelState);
This line will take all the validation breaches our model has done and add them to the model state so we can send them back to the user
Just add this code inside in the controller file but outside the controller class
public static class Extensions
{
public static void AddToModelState(this ValidationResult result , ModelStateDictionary modelState)
{
foreach (var error in result.Errors)
{
modelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
}
This extension method extends on the ValidationResult type and takes a ModelStateDictionary as a parameter so we can read the errors inside the ValidationResult we are calling this method on and add them as Key:Value pairs to the model state dictionary , you can see the adding part inside the foreach loop block.
Ok , now after we called AddToModelState(ModelState);
on our validationResult variable , all the validation messages will be added to the ModelState , so we can return them to our user like this
return BadRequest(ModelState);
This will return a 400 status code and will take the validation error messages along its way to display them for the user.
Let's do a quick test to see it in action , I'll send a request with invalid values so we can see them
As you can see , the data I've entered isn't valid and it doesn't comply to the rules we declared , so when I send the request , I'll get a 400 status code with the messages like the following
CONGRATS
If you really made it all the way down here without getting bored or intimated , then give yourself a pat on the back , cause you've successfully learned a new way to add custom validations to your data models and you made it work.
I know this was a little prolonged and took some time to actually apply , but believe me once you're accustomed to doing it , it'll start becoming very handy and useful.
Thank you very much for stopping by and bearing with me through this long article , I hope you learned something that's worth your time.
For Now , GoodBye ππ»
Top comments (2)
Good article, would suggest adding a sample project that resides in a GitHub repository which allows those interested to see what you wrote about without the need to type everything in.
Thanks for noting! I'll make sure to start doing that more often, and if it's possible, I'll create a Github repo for this article and include it as you requested. Cheers!