Fluent validation

De Banane Atomic
Révision datée du 12 novembre 2019 à 09:17 par Nicolas (discussion | contributions) (→‎Unit tests)
(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("Name is invalid.");  // change error message. Default is: 'Name' must not be empty.
    }

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

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

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; }
}

Validate elements of a collection

Cs.svg
public class sealed 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 class sealed MyClass {
    public List<int> Ids { get; } = new List<int>();
    public List<Item> Items { get; } = new List<Item>();
}

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

Conditions

Cs.svg
When(myClass => myClass.MyCondition, () => {
    RuleFor(myClass => customer.Name).NotNull();
}).Otherwise(() => {
    RuleFor(customer => customer.Name).MinimumLength(3);
});

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

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.StopOnFirstFailure).NotNull().MinimumLength(3);

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

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

Async Validation

Cs.svg
var validator = new MyClassValidator();
var result = 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);
    }
}

WebApi 2 integration

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;
    }
}

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()
    {
        var query = new MyQuery
        {
            Prop1 = "value1",
            Prop2 = "value2"
        };
        this.validator.ShouldHaveValidationErrorFor(x => x.Prop3, query)
            .WithSeverity(Severity.Error)
            .WithErrorMessage(ValidationMessageResources.MyErrorMessage);
        }