Как я могу получить доступ к элементу коллекции, проверяемому при использовании RuleForEach?

Я использую FluentValidation для проверки объекта, и потому что у этого объекта есть член коллекции, я пытаюсь использовать RuleForEach. Например, предположим, что у нас есть Customer и Orders, и мы хотим, чтобы заказ клиента не имел общего значения, превышающего максимально допустимое для этого клиента:

this.RuleForEach(customer => customer.Orders)
    .Must((customer, orders) => orders.Max(order => order.TotalValue) <= customer.MaxOrderValue)

До сих пор так хорошо. Однако мне также необходимо записать дополнительную информацию о контексте ошибки (например, где в файле данных была обнаружена ошибка). Я обнаружил, что это довольно сложно достичь с помощью FluentValidation, и лучшим решением на данный момент является использование метода WithState. Например, если я обнаружил, что данные адреса клиента неверны, я могу сделать что-то вроде этого:

this.RuleFor(customer => customer.Address)
    .Must(...)
    .WithState(customer => GetErrorContext(customer.Address))

(где GetErrorContext - мой метод для извлечения соответствующих деталей.

Теперь проблема заключается в том, что при использовании RuleForEach сигнатура метода предполагает, что я предоставил выражение, которое ссылается на Customer, а не на конкретный Order, который вызвал отказ проверки. И у меня, похоже, нет способа сказать, какой порядок имел проблему. Следовательно, я не могу сохранить соответствующую контекстную информацию.

Другими словами, я могу это сделать только:

this.RuleForEach(customer => customer.Orders)
    .Must((customer, orders) => orders.Max(order => order.TotalValue) <= customer.MaxOrderValue)
    .WithState(customer => ...)

... когда я действительно хотел это сделать:

this.RuleForEach(customer => customer.Orders)
    .Must((customer, orders) => orders.Max(order => order.TotalValue) <= customer.MaxOrderValue)
    .WithState(order => ...)

Нет ли способа получить доступ к деталям (или даже индексу) элементов (ов) коллекции, которые не удалось?

Я предполагаю, что другой способ взглянуть на это - я хочу, чтобы WithState имел эквивалент WithStateForEach...

Ответы

Ответ 1

В настоящее время в FluentValidation нет функциональности, что позволяет установить состояние проверки так, как вы хотите. RuleForEach был разработан для предотвращения создания тривиальных валидаторов для простых элементов коллекции, и реализация не охватывала все возможные варианты использования.

Вы можете создать отдельный класс проверки для Order и применить его с помощью метода SetCollectionValidator. Чтобы получить доступ к Customer.MaxOrderValue в Must - добавить свойство в Order, ссылающееся на Customer:

public class CustomerValidator
{
    public CustomerValidator()
    {
        RuleFor(customer => customer.Orders).SetCollectionValidator(new OrderValidator());
    }
}

public class OrderValidator
{
    public OrderValidator()
    {
         RuleFor(order => order.TotalValue)
             .Must((order, total) => total <= order.Customer.MaxOrderValue)
             .WithState(order => GetErrorInfo(order)); // pass order info into state
    }
}

Если вы все еще хотите использовать метод RuleForEach, вы можете использовать сообщение об ошибке вместо пользовательского состояния, поскольку оно имеет доступ к объектам объектов родителя и дочернего элемента в одном из перегрузок:

public class CustomerValidator
{
    public CustomerValidator()
    {
        RuleForEach(customer => customer.Orders)
            .Must((customer, order) => order.TotalValue) <= customer.MaxOrderValue)
            .WithMessage("order with Id = {0} have error. It total value exceeds {1}, that is maximum for {2}",
                (customer, order) => order.Id,
                (customer, order) => customer.MaxOrderValue,
                (customer, order) => customer.Name);
    }
}

Если вам нужно собрать все индексы (или идентификаторы) неудачных заказов - вы можете сделать это с помощью правила Custom, как здесь:

public CustomerValidator()
{
    Custom((customer, validationContext) =>
    {
        var isValid = true;
        var failedOrders = new List<int>();

        for (var i = 0; i < customer.Orders.Count; i++)
        {
            if (customer.Orders[i].TotalValue > customer.MaxOrderValue)
            {
                isValid = false;
                failedOrders.Add(i);
            }
        }

        if (!isValid){
            var errorMessage = string.Format("Error: {0} orders TotalValue exceed maximum TotalValue allowed", string.Join(",", failedOrders));
            return new ValidationFailure("", errorMessage) // return indexes of orders through error message
            {
                CustomState = GetOrdersErrorInfo(failedOrders) // set state object for parent model here
            };
        }

        return null;
    });
}

P.S.

Не забывайте, что ваша цель - выполнить проверку, а не использовать FluentValidation везде. Иногда мы реализуем логику проверки как отдельный метод, который работает с ViewModel и заполняет ModelState в ASP.NET MVC.

Если вы не можете найти решение, соответствующее вашим требованиям, тогда ручная реализация была бы лучше, чем корявая реализация с библиотекой.