Aller au contenu

Fluent validation

De Banane Atomic

Liens

Exemple

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.StartsWith("A") || name.EndsWith("Z"))
            .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

{
    "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.

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

Set validator for complex properties

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

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

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

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

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

Cascade mode

// 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

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

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

ASP.NET Core integration

The Nuget package FluentValidation.AspNetCore has been deprecated. AutoValidation is no more available, use manual validation.
You should not use asynchronous rules when using ASP.NET automatic validation as ASP.NET’s validation pipeline is not asynchronous.
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtension
# dotnet add package FluentValidation.AspNetCore  # deprecated
Program.cs
// automatic validation
builder.Services
    .AddValidatorsFromAssemblyContaining<CreateUpdateJobRequestValidator>();
    //.AddFluentValidationAutoValidation();  // deprecated

// disable other validator providers like DataAnnotations
services.AddFluentValidation(fv => {
    fv.DisableDataAnnotationsValidation = true;
});

// manual registration of the validators
services.AddTransient<IValidator<CreateUpdateJobRequest>, CreateUpdateJobRequestValidator>();
JobsController.cs
public class JobsController(IValidator<CreateReplaceJobRequest> createReplaceJobRequestValidator) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Create(CreateReplaceJobRequest createUpdateJobRequest)
    {
        var validationResult = await createReplaceJobRequestValidator.ValidateAsync(createUpdateJobRequest);
        validationResult.AddToModelState(ModelState);
        if (!ModelState.IsValid)
            return BadRequest(new ValidationProblemDetails(ModelState));
ValidationResultExtension.cs
public static class ValidationResultExtension
{
    /// <summary>
    /// Stores the errors in a ValidationResult object to the specified modelstate dictionary.
    /// </summary>
    /// <param name="result">The validation result to store</param>
    /// <param name="modelState">The ModelStateDictionary to store the errors in.</param>
    public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState)
    {
        if (!result.IsValid)
        {
            foreach (var error in result.Errors)
            {
                modelState.AddModelError(error.PropertyName, error.ErrorMessage);
            }
        }
    }
}

Exclude validators from loading from assembly

Program.cs
builder.Services
    .AddValidatorsFromAssemblyContaining<PersonValidator>(ServiceLifetime.Scoped, x => x.ValidatorType != typeof(OtherValidator));  // do not load OtherValidator

builder.Services
    .AddValidatorsFromAssemblyContaining<AssetModelRunValidator>(
        ServiceLifetime.Scoped,
        x => !x.ValidatorType.IsDefined(typeof(ExcludeValidatorAttribute), inherit: true));
ExcludeValidatorAttribute.cs
[AttributeUsage(AttributeTargets.Class)]
public class ExcludeValidatorAttribute : Attribute { }
OtherValidator.cs
[ExcludeValidator]
public class OtherValidator : AbstractValidator<Other>
{ }

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