Using FluentValidation In ASP.NET Core

Background

In the last article, I introduced the basic usages of FluentValidation in .NET Core with a Console App.
 
And in this article, I will introduce some usages based on ASP.NET Core.
 
Suppose we have a class named Student.
  1. public class Student  
  2. {  
  3.     public int Id { getset; }  
  4.   
  5.     public string Name { getset; }  
  6.   
  7.     public List<string> Hobbies { getset; }  
  8. }  
Now, we want to create an API to query students' hobbies.
 
So, we create a QueryStudentHobbiesDto class to define the request parameters.
  1. public class QueryStudentHobbiesDto  
  2. {  
  3.     public int? Id { getset; }  
  4.     public string Name { getset; }  
  5. }  
And let's create the validator at first.
  1. public class QueryStudentHobbiesDtoValidator: AbstractValidator<QueryStudentHobbiesDto>  
  2. {  
  3.     public QueryStudentHobbiesDtoValidator()  
  4.     {  
  5.         RuleSet("all", () =>   
  6.         {  
  7.             RuleFor(x => x.Id).Must(CheckId).WithMessage("id must greater than 0");  
  8.             RuleFor(x => x.Name).NotNull().When(x=>!x.Id.HasValue).WithMessage("name could not be null");  
  9.         });  
  10.   
  11.         RuleSet("id", () =>   
  12.         {  
  13.             RuleFor(x => x.Id).NotNull().WithMessage("id could not be null")
  14.                      .GreaterThan(0).WithMessage("id must greater than 0");  
  15.         });  
  16.   
  17.         RuleSet("name", () =>  
  18.         {  
  19.             RuleFor(x => x.Name).NotNull().WithMessage("name could not be null");  
  20.         });  
  21.     }  
  22.   
  23.     private bool CheckId(int? id)  
  24.     {  
  25.         return !id.HasValue || id.Value > 0;  
  26.     }  
  27. }   
Let's begin with a familiar way.
 
Manual validation
 
You can regard this usage as a copy of Console App sample that I showed you in the last article.
  1. // GET api/values/hobbies1  
  2. [HttpGet("hobbies1")]  
  3. public ActionResult GetHobbies1([FromQuery]QueryStudentHobbiesDto dto)  
  4. {  
  5.     var validator = new QueryStudentHobbiesDtoValidator();  
  6.     var results = validator.Validate(dto, ruleSet: "all");  
  7.   
  8.     return !results.IsValid  
  9.                ? Ok(new { code = -1, data = new List<string>(), msg = results.Errors.FirstOrDefault().ErrorMessage })  
  10.                : Ok(new { code = 0, data = new List<string> { "v1""v2" }, msg = "" });  
What we need to do are three steps.
  1. Create a new instance of the validator.
  2. Call the Validate method
  3. Return something based on the result of the Validate method.
After running up this project, we may get the following result.

Most of the time, we create a new instance directly. It is not a very good choice. We may use Dependency Injection in the next section.
 
Dependency Injection(DI)
 
There are two ways to use DI here. One is using the IValidator directly, the other one is using a middle layer to handle this, such as BLL layer.
 
Use IValidator directly
 
What we need to do is inject IValidator<QueryStudentHobbiesDto>, and call the Validate method to handle.
  1. private readonly IValidator<QueryStudentHobbiesDto> _validator;  
  2. public ValuesController(IValidator<QueryStudentHobbiesDto> validator)  
  3. {  
  4.     this._validator = validator;  
  5. }  
  6.   
  7. // GET api/values/hobbies5  
  8. [HttpGet("hobbies5")]  
  9. public ActionResult GetHobbies5([FromQuery]QueryStudentHobbiesDto dto)  
  10. {  
  11.     var res = _validator.Validate(dto, ruleSet: "all");  
  12.   
  13.     return !res.IsValid  
  14.                ? Ok(new { code = -1, data = new List<string>(), msg = res.Errors.FirstOrDefault().ErrorMessage })  
  15.                : Ok(new { code = 0, data = new List<string> { "v1""v2" }, msg = "" });  
  16. }  
 And don't forget to add the following code in Startup class.
  1. public void ConfigureServices(IServiceCollection services)  
  2. {  
  3.     //inject validator  
  4.     services.AddSingleton<IValidator<QueryStudentHobbiesDto>, QueryStudentHobbiesDtoValidator>();  
  5. }  
When we run it up, we will get the same result in a manual way.
 
Use a middle layer
 
We also can create a service class to handle the business logic.
  1. public interface IStudentService  
  2. {  
  3.     (bool flag, string msg) QueryHobbies(QueryStudentHobbiesDto dto);  
  4. }  
  5.   
  6. public class StudentService : IStudentService  
  7. {  
  8.     private readonly AbstractValidator<QueryStudentHobbiesDto> _validator;  
  9.     //private readonly IValidator<QueryStudentHobbiesDto> _validator;  
  10.   
  11.     public StudentService(AbstractValidator<QueryStudentHobbiesDto> validator)  
  12.     //public StudentService(IValidator<QueryStudentHobbiesDto> validator)  
  13.     {  
  14.         this._validator = validator;  
  15.     }  
  16.   
  17.     public (bool flag, string msg) QueryHobbies(QueryStudentHobbiesDto dto)  
  18.     {  
  19.         var res = _validator.Validate(dto, ruleSet: "all");  
  20.   
  21.         if(!res.IsValid)  
  22.         {  
  23.             return (false, res.Errors.FirstOrDefault().ErrorMessage);  
  24.         }  
  25.         else  
  26.         {  
  27.             //query ....  
  28.               
  29.             return (truestring.Empty);  
  30.         }  
  31.     }  
  32. }  
Go back to the controller.
  1. private readonly IStudentService _service;  
  2. public ValuesController(IStudentService service)  
  3. {  
  4.     this._service = service;  
  5. }  
  6.   
  7. // GET api/values/hobbies4  
  8. [HttpGet("hobbies4")]  
  9. public ActionResult GetHobbies4([FromQuery]QueryStudentHobbiesDto dto)  
  10. {  
  11.     var (flag, msg) = _service.QueryHobbies(dto);  
  12.   
  13.     return !flag  
  14.         ? Ok(new { code = -1, data = new List<string>(), msg })  
  15.         : Ok(new { code = 0, data = new List<string> { "v1""v2" }, msg = "" });  
  16. }   
This also has an easy way which is similar to Model Binding.
 
Validator customization
 
Using the CustomizeValidatorAttribute to configure how the validator will be run.
  1. // GET api/values/hobbies2  
  2. [HttpGet("hobbies2")]  
  3. public ActionResult GetHobbies2([FromQuery][CustomizeValidator(RuleSet = "all")]QueryStudentHobbiesDto dto)  
  4. {         
  5.     return Ok(new { code = 0, data = new List<string> { "v1""v2" }, msg = "" });  
  6. }  
 Let's run it up.
 
 
It didn't seem to work!
 
We should add FluentValidation in Startup class so that we can enalbe this feature!
  1. public void ConfigureServices(IServiceCollection services)  
  2. {  
  3.     //others...  
  4.       
  5.     services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)  
  6.             //when using CustomizeValidator, should add the following code.  
  7.             .AddFluentValidation(fv =>   
  8.             {  
  9.                 fv.RegisterValidatorsFromAssemblyContaining<Startup>();  
  10.                 //fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;  
  11.                 //fv.ImplicitlyValidateChildProperties = true;  
  12.             });  
  13. }  
At this time, when we visit api/values/hobbies2, we can find out that we can not get the query result but the validate message.
 
However, we don't want to return this message to the users. We should follow the previous sample here!
 
How can we format the result ?
 
We could use Filter to deal with the validate result here.
  1. public class ValidateFilterAttribute : ResultFilterAttribute  
  2. {      
  3.     public override void OnResultExecuting(ResultExecutingContext context)  
  4.     {  
  5.         base.OnResultExecuting(context);  
  6.   
  7.         //model valid not pass  
  8.         if(!context.ModelState.IsValid)  
  9.         {  
  10.             var entry = context.ModelState.Values.FirstOrDefault();  
  11.   
  12.             var message = entry.Errors.FirstOrDefault().ErrorMessage;  
  13.   
  14.             //modify the result  
  15.             context.Result = new OkObjectResult(new   
  16.             {   
  17.                 code = -1,  
  18.                 data = new JObject(),  
  19.                 msg= message,  
  20.             });  
  21.         }  
  22.     }  
  23. }  
 And mark the attribute at the action method.
  1. // GET api/values/hobbies3  
  2. [HttpGet("hobbies3")]  
  3. [ValidateFilter]  
  4. public ActionResult GetHobbies3([FromQuery][CustomizeValidator(RuleSet = "all")]QueryStudentHobbiesDto dto)  
  5. {  
  6.     //isn't valid will not visit the okobjectresult, but visit the filter  
  7.     return Ok(new { code = 0, data = new List<string> { "v1""v2" }, msg = "" });  
  8. }  
 And we will get what we want.
 
Here is the source code you can find in my GitHub page,
 

Summary

This article introduced three ways to use FluentValidation in ASP.NET Core.

I hope this will help you!