FluentValidation pour ASP.NET API

Une des prérequis d’une API est de pouvoir valider les données reçues en entrée selon des règles business définies en amont. En tant que développeur, il nous appartient de porter un soin particulier à cette validation.

Pour cela, nous pouvons valider le payload d’entrée au niveau de l’API avec des règles métier personnalisées mais le plus pratique reste d’utiliser FluentValidation .

Pour commencer , créez une API avec Visual Studio 2019 ou 2022. L’API WeatherForecast fera l’affaire.

Ensuite ,récupérez le package Nuget FluentValidation ASP.Net Core :

N’oubliez pas d’injecter FluentValidation dans votre Startup ou Program (en .NET 6.0) comme suit : (ici en .NET 6.0)

builder.Services.AddFluentValidation(s =>
{
    s.RegisterValidatorsFromAssemblyContaining<Program>();
    s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
});

Personnellement j’ai pris l’API de base que j’ai modifié pour avoir un nouveau Controlleur.

J’ai donc un OrderController avec un seul endpoint :

[HttpPost]
        public async Task<IActionResult> CreateOrder([FromBody]CreateOrderModel model)
        {
            if(await _orderService.CreateOrderAsync(model))
            {
                return Ok();
            }
            else
            {
                return  BadRequest();
            }

        }

Mon endpoint prend en entrée un CreateOrderModel défini comme suit :

 public class CreateOrderModel
    {
       public int OrderId { get; set; }
        public List<Product> Products { get; set; } = new List<Product>();

    }

    public class Product
    {
        public int ProductId { get; set; }
        public string ProductLabel { get; set; }
        public int ProductPrice { get; set; }
    }

J’ai fait au plus simple pour vous montrer comment utiliser les validateurs.

Nous allons maintenant créer un validateur pour nos entités en entrée de notre endpoint :

public class ValidatorService : IValidatorService
{
private readonly IDictionary _validators;
private readonly IServiceProvider _serviceProvider;

    public ValidatorService(IServiceProvider serviceProvider)
    {
        this._serviceProvider = serviceProvider;
        this._validators = new Dictionary<Type, Type>
        {
            { typeof(CreateOrderModel), typeof(CreateOrderValidator) },
             { typeof(Product), typeof(ProductValidator) },
        };

    }



    private AbstractValidator<T> GetValidator<T>()
    {
        var modelType = typeof(T);
        var hasValidator = this._validators.ContainsKey(modelType);
        if (hasValidator == false)
        {
            throw new Exception("Missing validator");
        }

        var validatorType = this._validators[modelType];
        var validator = _serviceProvider.GetService(validatorType) as AbstractValidator<T>;
        return validator;
    }

    public void EnsureValid<T>(T model)
    {
        var validator = this.GetValidator<T>();
        var result = validator.Validate(model);
        if (result.IsValid == false)
        {
            throw new Exception(result.ToString());
        }
    }

Voici la classe expliquée :

Nous allons maintenant créer les validateurs définis dans notre ValidatorService :

 public class CreateOrderValidator : AbstractValidator<CreateOrderModel>
        {
            public CreateOrderValidator()
            {
                RuleFor(r => r.OrderId).GreaterThan(0);
                RuleFor(r => r.Products.Count).GreaterThan(0);
                RuleForEach(r => r.Products).SetValidator(new ProductValidator());
            }
        }

        public class ProductValidator : AbstractValidator<Product>
        {
            public ProductValidator()
            {
                RuleFor(r => r.ProductId).GreaterThan(0);
                RuleFor(r => r.ProductPrice).GreaterThan(0);
                RuleFor(r => r.ProductLabel).NotNull();
                RuleFor(r => r.ProductLabel).NotEmpty();
                RuleFor(r => r.ProductLabel).Length(2,30);

            }
        }

Une fois les validateurs crées, nous pouvons les utiliser pour valider nos entrées d’API.

De mon coté , j’ai fait le choix de les valider dans un service qui correspond à l’action que je souhaite exécuter dans mon endpoint.

Voici mon service :

 public class OrderService : IOrderService
    {
        private readonly IValidatorService _validatorService;
        public OrderService(IValidatorService validatorService)
        {
            _validatorService = validatorService;
        }

        public async Task<bool> CreateOrderAsync(CreateOrderModel model)
        {
            try
            {
                _validatorService.EnsureValid(model);
                return await Task.FromResult(true);
            }
            catch (Exception)
            {
                return await Task.FromResult(false);
            }
        }
    }

J’injecte donc ma dépendence à mes validateurs pour pouvoir tester et il utilise ma méthode EnsureValid(model) pour voir si tout est correct.

Voyons voir si nous testons cela dans le swagger :

Je lui envoie l’exemple :

Et voila le retour :

Tout fonctionne comme prévu !

Vous avez maintenant tout ce qu’il vous faut pour effectuer des validations bien plus complexes et liées au métier que vous modélisez 😎

Si besoin d’un exemple deja implémentez, voici le lien vers mon projet dans GitHub ici.

Have fun coding !