Using FluentValidation In ASP.NET Core

Background

In the last article, I introduced the basic usage 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.

public class Student  
{  
    public int Id { get; set; }   
    public string Name { get; set; }   
    public List<string> Hobbies { get; set; }  
}  

Now, we want to create an API to query students' hobbies.

So, we create a QueryStudentHobbiesDto class to define the request parameters.

public class QueryStudentHobbiesDto  
{  
    public int? Id { get; set; }  
    public string Name { get; set; }  
}  

And let's create the validator first.

public class QueryStudentHobbiesDtoValidator: AbstractValidator<QueryStudentHobbiesDto>  
{  
    public QueryStudentHobbiesDtoValidator()  
    {  
        RuleSet("all", () =>   
        {  
            RuleFor(x => x.Id).Must(CheckId).WithMessage("id must greater than 0");  
            RuleFor(x => x.Name).NotNull().When(x=>!x.Id.HasValue).WithMessage("name could not be null");  
        });  
  
        RuleSet("id", () =>   
        {  
            RuleFor(x => x.Id).NotNull().WithMessage("id could not be null")
                     .GreaterThan(0).WithMessage("id must greater than 0");  
        });  
  
        RuleSet("name", () =>  
        {  
            RuleFor(x => x.Name).NotNull().WithMessage("name could not be null");  
        });  
    }  
  
    private bool CheckId(int? id)  
    {  
        return !id.HasValue || id.Value > 0;  
    }  
}   

Let's begin with a familiar way.

Manual validation

You can regard this usage as a copy of the Console App sample that I showed you in the last article.

// GET api/values/hobbies1  
[HttpGet("hobbies1")]  
public ActionResult GetHobbies1([FromQuery]QueryStudentHobbiesDto dto)  
{  
    var validator = new QueryStudentHobbiesDtoValidator();  
    var results = validator.Validate(dto, ruleSet: "all");  
  
    return !results.IsValid  
               ? Ok(new { code = -1, data = new List<string>(), msg = results.Errors.FirstOrDefault().ErrorMessage })  
               : 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.

Code

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 the BLL layer.

Use IValidator directly

What we need to do is inject IValidator<QueryStudentHobbiesDto> and call the Validate method to handle.

private readonly IValidator<QueryStudentHobbiesDto> _validator;  
public ValuesController(IValidator<QueryStudentHobbiesDto> validator)  
{  
    this._validator = validator;  
}  
  
// GET api/values/hobbies5  
[HttpGet("hobbies5")]  
public ActionResult GetHobbies5([FromQuery]QueryStudentHobbiesDto dto)  
{  
    var res = _validator.Validate(dto, ruleSet: "all");  
  
    return !res.IsValid  
               ? Ok(new { code = -1, data = new List<string>(), msg = res.Errors.FirstOrDefault().ErrorMessage })  
               : Ok(new { code = 0, data = new List<string> { "v1", "v2" }, msg = "" });  
}  

 And don't forget to add the following code in the Startup class.

public void ConfigureServices(IServiceCollection services)  
{  
    //inject validator  
    services.AddSingleton<IValidator<QueryStudentHobbiesDto>, QueryStudentHobbiesDtoValidator>();  
}  

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.

public interface IStudentService  
{  
    (bool flag, string msg) QueryHobbies(QueryStudentHobbiesDto dto);  
}  
  
public class StudentService : IStudentService  
{  
    private readonly AbstractValidator<QueryStudentHobbiesDto> _validator;  
    //private readonly IValidator<QueryStudentHobbiesDto> _validator;  
  
    public StudentService(AbstractValidator<QueryStudentHobbiesDto> validator)  
    //public StudentService(IValidator<QueryStudentHobbiesDto> validator)  
    {  
        this._validator = validator;  
    }  
  
    public (bool flag, string msg) QueryHobbies(QueryStudentHobbiesDto dto)  
    {  
        var res = _validator.Validate(dto, ruleSet: "all");  
  
        if(!res.IsValid)  
        {  
            return (false, res.Errors.FirstOrDefault().ErrorMessage);  
        }  
        else  
        {  
            //query ....  
              
            return (true, string.Empty);  
        }  
    }  
}  

Go back to the controller.

private readonly IStudentService _service;  
public ValuesController(IStudentService service)  
{  
    this._service = service;  
}  
  
// GET api/values/hobbies4  
[HttpGet("hobbies4")]  
public ActionResult GetHobbies4([FromQuery]QueryStudentHobbiesDto dto)  
{  
    var (flag, msg) = _service.QueryHobbies(dto);  
  
    return !flag  
        ? Ok(new { code = -1, data = new List<string>(), msg })  
        : Ok(new { code = 0, data = new List<string> { "v1", "v2" }, msg = "" });  
}   

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.

// GET api/values/hobbies2  
[HttpGet("hobbies2")]  
public ActionResult GetHobbies2([FromQuery][CustomizeValidator(RuleSet = "all")]QueryStudentHobbiesDto dto)  
{         
    return Ok(new { code = 0, data = new List<string> { "v1", "v2" }, msg = "" });  
}  

 Let's run it up.

 Code

It didn't seem to work!

We should add FluentValidation in the Startup class so that we can enable this feature!

public void ConfigureServices(IServiceCollection services)  
{  
    //others...  
      
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)  
            //when using CustomizeValidator, should add the following code.  
            .AddFluentValidation(fv =>   
            {  
                fv.RegisterValidatorsFromAssemblyContaining<Startup>();  
                //fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;  
                //fv.ImplicitlyValidateChildProperties = true;  
            });  
}  

At this time, when we visit api/values/hobbies2, we can find out that we can not get the query result but the validated message.

Code

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 validated result here.

public class ValidateFilterAttribute : ResultFilterAttribute  
{      
    public override void OnResultExecuting(ResultExecutingContext context)  
    {  
        base.OnResultExecuting(context);  
  
        //model valid not pass  
        if(!context.ModelState.IsValid)  
        {  
            var entry = context.ModelState.Values.FirstOrDefault();  
  
            var message = entry.Errors.FirstOrDefault().ErrorMessage;  
  
            //modify the result  
            context.Result = new OkObjectResult(new   
            {   
                code = -1,  
                data = new JObject(),  
                msg= message,  
            });  
        }  
    }  
}  

 And mark the attribute at the action method.

// GET api/values/hobbies3  
[HttpGet("hobbies3")]  
[ValidateFilter]  
public ActionResult GetHobbies3([FromQuery][CustomizeValidator(RuleSet = "all")]QueryStudentHobbiesDto dto)  
{  
    //isn't valid will not visit the okobjectresult, but visit the filter  
    return Ok(new { code = 0, data = new List<string> { "v1", "v2" }, msg = "" });  
}  

 And we will get what we want.

Code

Here is the source code you can find on my GitHub page,

FluentValidationDemo/AspNetCoreDemo

Summary

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

I hope this will help you!