web-dev-qa-db-fra.com

Comment puis-je accéder à l'élément de collection en cours de validation lors de l'utilisation de RuleForEach?

J'utilise FluentValidation pour valider un objet, et parce que cet objet a un membre de collection, j'essaie d'utiliser RuleForEach. Par exemple, supposons que nous ayons Customer et Orders, et nous voulons nous assurer qu'aucune commande client n'a une valeur totale qui dépasse le maximum autorisé pour ce client:

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

Jusqu'ici tout va bien. Cependant, je dois également enregistrer des informations supplémentaires sur le contexte de l'erreur (par exemple, où dans un fichier de données l'erreur a été trouvée). J'ai trouvé cela assez difficile à réaliser avec FluentValidation, et ma meilleure solution jusqu'à présent est d'utiliser la méthode WithState. Par exemple, si je trouve que les détails de l'adresse du client sont incorrects, je peux faire quelque chose comme ceci:

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

(où GetErrorContext est une de mes méthodes pour extraire les détails pertinents.

Maintenant, le problème que j'ai, c'est que lorsque vous utilisez RuleForEach, la signature de la méthode suppose que je fournirai une expression qui fait référence à Customer, pas au Order qui a provoqué l'échec de la validation. Et je semble n'avoir aucun moyen de dire quelle commande a eu un problème. Par conséquent, je ne peux pas stocker les informations de contexte appropriées.

En d'autres termes, je ne peux que faire ceci:

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

... quand je voulais vraiment faire ça:

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

N'y a-t-il vraiment aucun moyen d'accéder aux détails (ou même à l'index) des éléments de collection qui ont échoué?

Je suppose qu'une autre façon de voir les choses est que je veux que WithState ait un équivalent WithStateForEach...

23
Gary McGill

Actuellement, il n'y a aucune fonctionnalité dans FluentValidation, qui permet de définir l'état de validation comme vous le souhaitez. RuleForEach a été conçu pour empêcher la création de validateurs triviaux pour les éléments de collection simples, et son implémentation ne couvrait pas tous les cas d'utilisation possibles.

Vous pouvez créer une classe de validateur distincte pour Order et l'appliquer à l'aide de la méthode SetCollectionValidator. Accéder Customer.MaxOrderValue dans Must méthode - ajoutez une propriété à Order, qui fait référence en arrière à 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
    }
}

Si vous souhaitez toujours utiliser la méthode RuleForEach, vous pouvez utiliser un message d'erreur au lieu d'un état personnalisé, car il a accès aux objets d'entité d'élément parent et enfant dans l'une des surcharges:

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

Si vous devez collecter tous les index (ou identificateurs) des commandes ayant échoué - vous pouvez le faire avec la règle Custom, comme ici:

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.

N'oubliez pas que votre objectif est d'implémenter la validation, de ne pas utiliser FluentValidation partout. Parfois, nous implémentons une logique de validation en tant que méthode distincte, qui fonctionne avec ViewModel et remplit ModelState dans ASP.NET MVC.

Si vous ne trouvez pas de solution qui corresponde à vos besoins, une implémentation manuelle serait meilleure que une implémentation béquille avec bibliothèque.

28
Evgeny Levin