FluentValidation preload data

Viewed 221

I want validate a request model with some ids. I try to preload all required data with a bulk request. The problem is the RuleForEach inside my WhereAsync is called before the LoadUserGroupsAsync is done or started. I start the validation with TestValidateAsync(request).

Is there a better solution for this I have unfortunately not found any solutions for it. Also I have no access to the model from outside a RuleFor, RuleForEach, Where, ...

private readonly List<UserGroup> _userGroups;
WhenAsync(async (request, cancellationToken) => await this.LoadUserGroupsAsync(request.Items, cancellationToken), () =>
{
    RuleForEach(o => o.Items).SetValidator(new UserUpdateValidator(this._userGroups));
});
private async Task<bool> LoadUserGroupsAsync(UserUpdateDto[] userUpdates, CancellationToken cancellationToken)
{
    var ids = userUpdates.Select(o => o.userGroupId);
    this._userGroups = await this._userGroupService.GetByIdsAsync(ids, cancellationToken);

    return true;
}
public class UserUpdateValidator : AbstractValidator<UserUpdateDto>
{
    public UserUpdateValidator(
       UserGroup[] groups)
    {
        RuleFor(item => item.UserGroupId).Must(userGroupId =>
        {
            var group = groups.SingleOrDefault(o => o.Id == userGroupId);
            if (group == null)
            {
                return false;
            }

            return true;
        }).WithMessage("Group is invalid");

        RuleFor(item => item.UserGroupId).Must(userGroupId =>
        {
            var group = groups.SingleOrDefault(o => o.Id == userGroupId);
            return group.Active;
        }).WithMessage("Group is inactive");

        RuleFor(item => item.Password).Must((context, password) =>
        {
            var group = groups.SingleOrDefault(o => o.Id == context.UserGroupId);
            if (group.Permissions.Contains("AllowPasswordChange"))
            {
                return true;
            }

            return false;
        }).WithMessage("It is now allowed to change the password for your user");
    }
}

Update 2021-04-28 - Add more Informations to example

1 Answers

You can use lazy loading and a wrapper object for the users. This would require calling sync methods instead of async methods to load the users, however.

You could use a Lazy<IEnumerable<User>> object, but that would probably require refactoring your child validator. I like creating a wrapper class for Lazy<IEnumerable<User>> just to make the code backwards-compatible for any other code accepting IEnumerable<T> objects:

/// <summary>
/// Represents a lazy loaded enumerable of type T
/// </summary>
public class LazyEnumerable<T> : IEnumerable<T>
{
    private readonly Lazy<IEnumerable<T>> items;

    /// <summary>
    /// Initializes a new lazy loaded enumerable.
    /// </summary>
    /// <param name="itemFactory">A lambda expression that returns the items to be iterated over.</param>
    public LazyEnumerable(Func<IEnumerable<T> itemFactory)
    {
        items = new Lazy<T>>(itemFactory);
    }

    /// <summary>
    /// Initializes a new lazy loaded enumerable.
    /// </summary>
    /// <param name="itemFactory">The Lazy<T> object used to lazily retrieve the items to be iterated over.</param>
    public LazyEnumerable(Lazy<IEnumerable<T>> items)
    {
        this.items = items;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return items.Value.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return items.Value.GetEnumerator();
    }
}

This is actually a general purpose class that can be used for any type. I wish .NET had this class in its base library, to be honest. I tend to copy and paste this in to every project, because it is so easy to use. In any event...

Then modify your validator class. Since you did not post enough code for your validator, I took a few guesses about the names of things and structure of your code:

public class YourValidator : AbstractValidator<X>
{
    private UserService _userService;
    private IEnumerable<UserGroup> _userGroups;

    public YourValidator(UserService userService)
    {
        _userService = userService;

        When((request, cancellationToken) => PreloadUserGroups(request.Items, cancellationToken), () =>
        {
            RuleForEach(o => o.Items).SetValidator(new UserUpdateValidator(_userGroups));
        });
    }

    private bool PreloadUserGroups(UserUpdateDto[] userUpdates, CancellationToken cancellationToken)
    {
        var ids = userUpdates.Select(o => o.userGroupId);

        _userGroups = new LazyEnumerable<UserGroup>(() => _userService.GetByIds(ids, cancellationToken));

        return true;
    }
}

This will lazy load the users, and since you pass the same object to all child validators it will load the users only once, regardless of how many times the collection is iterated.

Lastly, modify your child validator class to accept an IEnumerable<UserGroup> object instead of an array:

public class UserUpdateValidator : AbstractValidator<UserUpdateDto>
{
    public UserUpdateValidator(IEnumerable<UserGroup> groups)
Related