'How to reuse data in FluentValidation

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!



Solution 1:[1]

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

Solution 2:[2]

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.

Solution 3:[3]

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

Solution 4:[4]

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 => { ... }))

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Community
Solution 3
Solution 4 Artem Balianytsia