Fluent validation

De Banane Atomic
Version datée du 10 septembre 2024 à 15:34 par Nicolas (discussion | contributions) (→‎Validate elements of a collection)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigationAller à la recherche

Liens

Exemple

Cs.svg
var mc = new MyClass();
var validator = new MyClassValidator();

ValidationResult results = validator.Validate(mc);

if (!results.IsValid)
{
    foreach (var failure in results.Errors)
    {
        Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " +
                          failure.ErrorMessage);
    }

    string allMessages = results.ToString();
    Console.WriteLine(allMessages);
}

public sealed class MyClassValidator : AbstractValidator<MyClass>
{
    public MyClassValidator()
    {
        this.RuleFor(x => x.Name)
            .NotNull()
            .Must(name => name != null)
            .Must(IsValid)
            .WithMessage("{PropertyName} is invalid.");  // change error message. Default is: 'Name' must not be empty.
    }

    private static bool IsValid(string value)
    {
        return value != null;
    }
}

public sealed class MyClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Output

Json.svg
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|84df05e2-41e0d4841bb61293.",
    "errors": {
        "Name": [
            "Name is invalid."
        ]
    }
}

Built-in Validators

Enum Validator

Checks whether a numeric value is valid to be in that enum.

Cs.svg
RuleFor(x => x.ErrorLevel).IsInEnum();

Set validator for complex properties

Cs.svg
public sealed class MyClassValidator : AbstractValidator<MyClass> {
    public MyClassValidator() {
        RuleFor(mc => mc.MySubClass).SetValidator(new MySubClassValidator());
    }
}

public sealed class MySubClassValidator : AbstractValidator<MySubClass> {
    public MySubClassValidator() {
        RuleFor(msc => msc.Name).NotNull();
    }
}

public sealed class MyClass
{
    public int Id { get; set; }
    public MySubClass MySubClass { get; set; }
}

public sealed class MySubClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Validation on multiple properties

Cs.svg
this.RuleFor(x => new { x.Property1, x.Property2 })
    .Must(x => x.Property1.Count > 0 || x.Property2 .Count > 0)
    .OverridePropertyName(x => x.Property1);

Validate elements of a collection

If your root object is a collection, be aware that FV doesn't handle IReadOnlyCollection
Cs.svg
public sealed class MyClassValidator : AbstractValidator<MyClass> {
    public MyClassValidator() {
        // use RuleForEach to apply rules on each element
        RuleForEach(x => x.Items)
            .NotEmpty()
            // use ChildRules for complex object
            .ChildRules(items =>
            {
                items.RuleFor(x => x.Name).NotNull();
            });

        RuleForEach(mc => mc.Items)
            .SetValidator(new ItemValidator());

        // use ForEach inside a RuleFor to apply rules on each element
        RuleFor(x => x.Ids)
            .NotEmpty()
            .ForEach(idRule => idRule.Must(x => x > 0));

        RuleFor(x => x.Items)
            .NotEmpty()
            .ForEach(itemRule => itemRule.ChildRules(items =>
            {
                items.RuleFor(x => x.Name).NotNull();
            }));
    }
}

public sealed class MyClass {
    public List<int> Ids { get; } = new List<int>();
    public List<Item> Items { get; } = new List<Item>();
}

public sealed class Item {
    public string Name { get; set; }
}

Conditions

Cs.svg
When(x => x.Property > 0, () => {
    RuleFor(x => x.Name).NotNull();
}).Otherwise(() => {
    RuleFor(x => x.Name).MinimumLength(3);
});

RuleFor(x => x.Name)
    .NotNull()
    .When(x => x.Property > 0);

// code of length 4 or null
RuleFor(x => x.Code)
    .Length(4)
    .Unless(x => string.IsNullOrEmpty(x.Code));

Overriding the error message

Cs.svg
this.RuleFor(x => x.Name)
    .WithMessage("Error message on {PropertyName}.");
Placeholder
{PropertyName} The name of the property being validated
{PropertyValue} The value of the property being validated,
these include the predicate validator (‘Must’ validator), the email and the regex validators.

Built-in Validators and their Placeholders

Custom message placeholders

ItemValidator.cs
public ItemValidator(IItemService itemService)
{
    this.itemService = itemService;
    RuleFor(x => x).MustAsync(HasNoSimilarItemAsync)
                   .OverridePropertyName("SimilarItem")
                   .WithMessage("A similar item already exists. Name: {SimilarItemName} - Date: {SimilarItemDate:dd/MM/yyyy}");
}

private async Task<bool> HasNoSimilarItemAsync(
    Item rootObject,
    Item item,
    PropertyValidatorContext context,
    CancellationToken token)
{
    var similarItems = await itemService.FindSimilarItemsAsync(item);
    if (similarItems.Any())
    {
        context.MessageFormatter.AppendArgument("SimilarItemName", similarItems[0].Name)
                                .AppendArgument("SimilarItemDate", similarItems[0].Date);
        return false;
     }
     
     return true;
}

Custom validator

Cs.svg
RuleFor(x => x.Elements)
    .Custom((list, context) =>
    {
        if(list.Count > 10)
            context.AddFailure("The list must contain 10 items or fewer");
    }
);

Cascade mode

Cs.svg
// test first if Name is not null, then even if Name is null test if lenght is >= 3
RuleFor(x => x.Name)
    .NotNull()
    .MinimumLength(3);

// test first if Name is not null, then only if Name is not null test if lenght is >= 3
RuleFor(x => x.Name)
    .Cascade(CascadeMode.Stop)
    .NotNull()
    .MinimumLength(3);

// change the default cascade mode
public MyClassValidator() {
    // First set the cascade mode
    RuleLevelCascadeMode = CascadeMode.Stop;

    RuleFor(x => x.Name).NotNull().MinimumLength(3);

Async Validation

Cs.svg
var validator = new MyClassValidator();
var validationResult = await validator.ValidateAsync(myClass);

public sealed class MyClassValidator : AbstractValidator<MyClass>
{
    SomeExternalWebApiClient client;

    public MyClassValidator(SomeExternalWebApiClient client)
    {
        this.client = client;

        this.RuleFor(x => x.Name)
            .MustAsync(async (name, cancellation) => {
            bool nameExists = await client.NameExists(name);
            return !nameExists;
        }).WithMessage("Name is not valid.");
    }

    private static bool IsValid(string value)
    {
        return value != null;
    }
}

Throwing Exceptions

Cs.svg
try
{
    validator.ValidateAndThrow(mc);
}
catch (ValidationException e)
{
    foreach (var failure in e.Errors)
    {
        Console.WriteLine(failure.ErrorMessage);
    }
}

ASP.NET Core integration

You should not use asynchronous rules when using ASP.NET automatic validation as ASP.NET’s validation pipeline is not asynchronous.
Bash.svg
# install the nuget package
dotnet add package FluentValidation.AspNetCore
Startup.cs
// automatic validation
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
            .AddFluentValidation(fv =>
            {
                // automatic registration of the validators
                fv.RegisterValidatorsFromAssemblyContaining<PersonValidator>();
                // disable other validator providers like DataAnnotations
                fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
            });

    // manual registration of the validators
    services.AddTransient<IValidator<Person>, PersonValidator>();
PersonController.cs
public class PersonController : Controller {
    [HttpPost]
    public IActionResult Create(Person person) {
        if(!ModelState.IsValid) {
            return BadRequest(ModelState);
        }

        // manual async validation
        var personValidator = new PersonValidator();
        var validationResult = await personValidator.ValidateAsync(person);
        validationResult.AddToModelState(ModelState, null);
        if (!ModelState.IsValid)
        {
            return BadRequest(new ValidationProblemDetails(ModelState));
        }

Use filter to check the model validity

Ainsi il n'est plus nécessaire de tester ModelState.IsValid dans chaque Controller.

App_Start\WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
    config.Filters.Add(new ValidateModelStateFilter());
ValidateModelStateFilter.cs
public class ValidateModelStateFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response =
                actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

Unit tests

MyValidatorTest.cs
public class MyValidatorTest
{
    private readonly MyValidatorTest validator = new MyValidatorTest();

    [Fact]
    public void ValidationOfProp3_AskProp1AndProp2_Error()
    {
        // Arrange
        var query = new MyQuery
        {
            Prop1 = "value1",
            Prop2 = "value2"
        };

        // Act
        var result = this.validator.TestValidate(query);

        // Assert
        result.ShouldHaveValidationErrorFor(x => x.Prop3, query)
              .WithErrorCode(nameof(ValidationMessageResources.MyErrorMessage))
              .WithErrorMessage(ValidationMessageResources.MyErrorMessage);
        }

WebApi 2 integration

Integration with ASP.NET WebApi 2 is no longer supported as of FluentValidation 9. Use ASP.NET Core. link

Installer le package NuGet FluentValidation.WebApi

Validator attribute

App_Start\WebApiConfig.cs
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // ...
        FluentValidationModelValidatorProvider.Configure(config);
ItemController.cs
[HttpPost]
[Route("")]
[ResponseType(typeof(Item))]
public IHttpActionResult Post(Item item)
{
    // if the item is null, the validator is not executed
    if (item == null)
    {
        return this.BadRequest("Item cannot be null.");
    }

    // if there are validation errors
    if (!ModelState.IsValid)
    {
        return this.BadRequest(ModelState);
    }
Item.cs
// link the class with its validator
[Validator(typeof(ItemValidator))]
public sealed class Item { }

Dependency injection with Autofac

App_Start\WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
    var builder = new ContainerBuilder();

    // register all the validator
    AssemblyScanner.FindValidatorsInAssembly(typeof(WebApiConfig).Assembly)
        .ForEach(result =>
        {
            builder.RegisterType(result.ValidatorType).As(result.InterfaceType);
        });

    // register the ValidatorFactory because its ctor needs an IComponentContext
    builder.RegisterType<AutofacValidatorFactory>();

    IContainer container = builder.Build();

    FluentValidationModelValidatorProvider.Configure(config,
        provider => { provider.ValidatorFactory = container.Resolve<AutofacValidatorFactory>(); });
AutofacValidatorFactory.cs
internal class AutofacValidatorFactory : ValidatorFactoryBase
{
    private readonly IComponentContext context;

    public AutofacValidatorFactory(IComponentContext context)
    {
        this.context = context;
    }

    public override IValidator CreateInstance(Type validatorType)
    {
        if (this.context.TryResolve(validatorType, out var instance))
        {
            var validator = instance as IValidator;
            return validator;
        }

        return null;
    }
}