How to reuse data in FluentValidation
Asked Answered
A

6

11

For example I have validator with two validation rules:

// Rule 1
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) != 0)
    .WithMessage("User with provided Email was not found in database!");

// Rule 2
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
    .WithMessage("There are multiple users with provided Email in database!");

As you can see there are two calls to database with same method. How do I call it once and reuse the data for other rules?

Another issue when displaying error messages:

RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
    .WithMessage("There are multiple users with following Email '{0}' in database!",
    (model, email) => { return email; });

Is there a better way to display error messages not all the time writing those lambda expressions to retrieve property? Like saving model somewhere and then use it later.

Simple and easy to implement solutions would be nice!

Archimage answered 27/5, 2016 at 13:27 Comment(1)
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) <= 1) condition <= mismatch the meaning of validation messageMasurium
J
5

For #1, There isn't a way to do this I'm afraid. Validators are designed to be stateless so they can be reused across threads (in fact, it's highly recommended you create validator instances as singletons as they're very expensive to instantiate. The MVC integration does this by default). Don't mess with static fields as you'll run into threading issues.

(Edit: in this particular simple case you can just combine the rules into a single call to Must, but in general you can't share state between rules)

For #2, This depends on the property validator you're using. Most property validators actually allow you to use the {PropertyValue} placeholder, and the value will automatically be inserted. However, in this case you're using the "Must" validator (PredicateValidator) which doesn't support placeholders.

I have a list of which validators support custom placeholders here: https://github.com/JeremySkinner/FluentValidation/wiki/c.-Built-In-Validators

Jaye answered 27/5, 2016 at 16:38 Comment(1)
Yes, I agree @Jeremy Skinner, it could cause some issues with threads. Although I added one improvement. Base class that derives from AbstractValidator and also a getter inside it (non-static) that will get data from databse if it's private field is null. And inside GetDataDataFromDB just use that property. This will at least get DB data only once if GetDataDataFromDB called multiple times in this validator context.Archimage
H
2

Just came across this question while looking for a better way ;)

Another way is to override the ValidateAsync and Validate methods and store the result in a local field which can be accessed by the rules as follows:

public class MyValidator : AbstractValidator<MyCommand>
{
    User _user = User.Empty;

    public MyValidator()
    {
        RuleFor(o => o.Email)
            .Must((_) => !_user.IsEmpty)
            .WithMessage("User with provided Email was not found in database!");

        // Rule 2
        //other rules which can check _user
    }

    public override async Task<ValidationResult> ValidateAsync(ValidationContext<MyCommand> context, CancellationToken cancellation = default)
    {
        var cmd = context.InstanceToValidate;
        // you could wrap in a try block if this throws, here I'm assuming empty user
        _user = await _repository.GetUser(cmd.Email);
        return await base.ValidateAsync(context, cancellation);
    }

    public override ValidationResult Validate(ValidationContext<SubmitDecisionCommand> context) => ValidateAsync(context).Result;
}
Hellenistic answered 19/11, 2021 at 5:55 Comment(0)
M
1

Part 1

You want to reduce database calls from 2 to 1, so you need to use field to save database call result, because validator rules code actually work in "runtime".

Validator class:

public class MyValidator : Validator<UserAccount>
{
    private int? _countOfExistingMails;
    private string _currentEmail;
    private object locker = new object();

    public MyValidator()
    {
        CallEmailValidations();
        // other rules...
    }
}

Here is separate method for mail validation calls. As far as Must take expression as parameter, you can pass method name with it's arguments:

public void CallEmailValidations()
{
    RuleFor(o => o.Email).Must(x => EmailValidation(x, 0))
        .WithMessage("User with provided Email was not found in database!");

    RuleFor(o => o.Email).Must(x => EmailValidation(x, 1))
        .WithMessage("There are multiple users with provided Email in database!");
}

And validation method's body itself:

public bool EmailValidation(string email, int requiredCount)
{
    var isValid = false;

    lock(locker)
    {
        if (email != _currentEmail || _currentEmail == null)
        {
            _currentEmail = email;
            _countOfExistingMails = (int)GetDataDataFromDB(email);
        }

        if (requiredCount == 0)
        {
            isValid = _countOfExistingMails != 0; // Rule 1
        }
        else if (requiredCount == 1)
        {
            isValid = _countOfExistingMails <= 1; // Rule 2
        }
    }
    // Rule N...

    return isValid;
}

UPDATE: This code works, but better approach is to implement caching in data access layer method.

Part 2

Here is rewritten rule:

RuleFor(o => o.Email).Must((email) => GetDataDataFromDB(email) >= 1)
    .WithMessage("There are multiple users with following Email '{0}' in database!", m => m.Email)

From "C# in depth":

When the lambda expression only needs a single parameter, and that parameter can be implicitly typed, C# 3 allows you to omit the parentheses, so it now has this form

GOTCHAS:

  1. Do not pass explicitly this to lambda-expressions. It could cause preformance issues as I know. There is no reason to create extra-closure.

  2. I suppose you use DataContext in some form inside GetDataDataFromDB method. So you have to control lifetime of your context, because validator object instantiated as singletone.

Masurium answered 27/5, 2016 at 15:35 Comment(0)
H
0

What you can do is to use WhenAsync. I have created an extension method to make things easier.

public static class ValidatorExtensions
{
    public static void ResolveDataAsync<TEntity, TData>(
        this AbstractValidator<TEntity> validator,
        Func<TEntity, CancellationToken, Task<TData>> resolver,
        Action<ValueAccessor<TData>> continuation)
    {
        TData data = default;
        var isInitialized = false;
        var valueAccessor = new ValueAccessor<TData>(() =>
        {
            if (!isInitialized)
            {
                throw new InvalidOperationException("Value is not initialized at this point.");
            }

            return data;
        });

        validator.WhenAsync(async (entity, token) =>
            {
                data = await resolver(entity, token);
                return isInitialized = true;
            },
            () => continuation(valueAccessor));
    }
}

public class ValueAccessor<T>
{
    private readonly Func<T> _accessor;

    public ValueAccessor([NotNull] Func<T> accessor)
    {
        _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
    }

    public T Value => _accessor();
}

Usage:

public class ItemCreateCommandValidator : AbstractValidator<ItemCreateCommand>
{
    private readonly ICategoryRepository _categoryRepository;

    public ItemCreateCommandValidator(ICategoryRepository categoryRepository)
    {
        _categoryRepository = categoryRepository;

        this.ResolveDataAsync(CategoryResolver, data =>
        {
            RuleFor(x => x.CategoryIds)
                .NotEmpty()
                .ForEach(subcategoryRule => subcategoryRule
                    .Must(x => data.Value.ContainsKey(x))
                    .WithMessage((_, id) => $"Category with id {id} not found."));
        });
    }

    private Func<ItemCreateCommand, CancellationToken, Task<Dictionary<int, Category>>> CategoryResolver =>
        async (command, token) =>
        {
            var categories = await _categoryRepository.GetByIdsAsync(command.SubcategoryIds, token);
            return categories.ToDictionary(x => x.Id);
        };
}

Works fine to me, but there are a few GOTCHAS:

  1. The validator usually have to be defined as Scoped or Transient (Scoped is better for performance) in order to be compatible with lifecycle of it's dependencies (e.g. repository passed in constructor).

  2. You can't access the data.Value right inside ResolveDataAsync callback. This is because the value is not initialized by that time. By this time validator is in creation phase and ValidateAsync method was not called => nothing to validate => value can't be accessed.

It can be used only in AbstractValidator methods:

this.ResolveDataAsync(CategoryResolver, data =>
{
    var value = data.Value; // Throws InvalidOperationException
    RuleFor(x => x.CategoryIds)
        .NotEmpty()
        .ForEach(subcategoryRule => subcategoryRule
            .Must(data.Value.ContainsKey)  // Also throws
            .WithMessage((_, id) => $"Category with id {id} not found."));
});

These gotchas also occur with other approaches, such as overriding the ValidateAsync method, and there is not much you can do about them.

You can also call ResolveDataAsync with different resolvers depending on condition when using WhenAsync, UnlessAsync. This will help you not to load data that is not needed in all cases every time:

WhenAsync(myCondition1, () => this.ResolveDataAsync(myResolver1, data => { ... }))
UnlessAsync(myCondition2, () => this.ResolveDataAsync(myResolver2, data => { ... }))
Halsted answered 18/4, 2022 at 14:18 Comment(0)
P
0

Late to the party with this question but for those coming here in the future... this is a perfect use-case for the RootContextData property that FluentValidation makes available.

You move the fetching of the data further upstream to the invoker of the validation.

For example, we'll use the following model that can have a parent/child relationship and a flag setting whether an instance is allowed to have a child associated with it or not.

public class MyModel
{
    public Guid ID { get; set; }
    public string Name { get; set; }
    public Guid? ParentID { get; set; } = null;
    public bool AllowedChildren { get; set; } = true;
}

Next, we'll create a Validator<MyModel> that will have the rules required to validate this model. We will have two rules - one for validating that the parent model exists and a 2nd that validates whether or not the parent is allowed to have a foreign key relationship.

Note that you'll want to do null checks on the casts and you'll also want to handle, via failure or exception throwing, what happens if the Context doesn't contain the data you need.

public class MyModelValidator : AbstractValidator<MyModel>
{
    public MyModelValidator()
    {
        RuleFor(instance => instance.ID).NotEmpty();
        RuleFor(instance => instance.Name).NotEmpty();

        RuleFor(instance => instance.ParentID)
            .Custom(this.ValidateParentID)
            .Custom(this.ValidateChildrenAllowed)
            .When((instance, token) => instance.ParentID != null);
                
    }

    private void ValidateChildrenAllowed(Guid? parentID, ValidationContext<MyModel> context)
    {
        MyModel parentInstance = context.RootContextData["Parent"] as MyModel;
        if (parentInstance.AllowedChildren)
        {
            return;
        }

        context.AddFailure("The parent associated with this instance does not allow children.");
    }

    private void ValidateParentID(Guid? parentID, ValidationContext<MyModel> context)
    {
        MyModel parentInstance = context.RootContextData["Parent"] as MyModel;

        if (parentID != parentInstance.ID)
        {
            context.AddFailure("The instance being validated has a different ParentID than what was supplied.");
            return;
        }
    }        
}

Lastly, we fetch our parent from the repository so it can be used in both validation rules. We pass the parent into the ValidationContext and then provide our MyModelValidator with the Context instead of our model.

// Query and get the parent model
Guid parentID = Guid.Parse("da6cf9a5-8dec-45aa-bb68-b4da0fe770cb");
var repo = new MyModelRepository();
MyModel parent = await repo.GetModelById(parentID);

// Construct new model
var model = new MyModel
{
    Name = "foo",
    ParentID = parent.ID,
};


// Instance our Validator
var validator = new MyModelValidator();

// Create ValidationContext and pass it the model we are validating.
var context = new ValidationContext<MyModel>(model);

// Assign the parent model to the ContextData so it can be accessed during validation.
context.RootContextData["Parent"] = parent;

// Validate using the Context instead of the model explicitly.
var results = await validator.ValidateAsync(context);

Now we have stateful data queried out of the database and we're using it across multiple validation rules. You can re-use this same data across multiple validators, across multiple models, if needed in your scenario.

This keeps the stateful data out of the validators - allowing them to remain as singletons in the DI system. This also lets you make use of any caching mechanism you might have within your repository or data layer without your validation logic needing to deal with it.

Propraetor answered 27/12, 2023 at 0:42 Comment(0)
A
0

Here is my solution, I would like to share with you.

RuleSet("async", () =>
{
    RuleFor(x => x.CaseTypeId)
        .MustAsync(async (command, id, ctx, ct) =>
        {
            var caseType = await dbContext.Set<CaseType>()
                .SingleOrDefaultAsync(caseType => caseType.Id == id, ct);

            if (caseType is null)
            {
                ctx.AddFailure(nameof(id), $"Case with id {id} does not exist");
                return false;
            }

            var user = await currentUserAccessor.GetCurrentUserAsync(ct);
            if (!user.Wallet.HasBalance(caseType.Price))
            {
                ctx.AddFailure("User does not have enough balance");
                return false;
            }

            return true;
        });
});

as you can see, I am reusing the values inside of the validation rule, this validation has exactly as many Db calls as it needs.

Ausgleich answered 25/6, 2024 at 16:31 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.