Question

I'm using FluentValidation to validate a collection inside of an object, comparing an element of the collection items to an element of the parent object.

The goal output is to receive ValidationFailures for each failed item in the collection, not just to fail the collection.

I have a software order, containing a list of software items. If the order is for a legacy system, the selected software can only be legacy software, and vice-versa, a non-legacy system can only have non-legacy software.

My model:

public class SoftwareOrder
{
   public bool IsLegacySystem;
   public List<SoftwareItem> Software;
   (...other fields...)
}
public class SoftwareItem
{
   public bool Selected;
   public bool IsLegacySoftware;
   public int SoftwareId;
}

Validators:

public class SoftwareOrderValidator : AbstractValidator<SoftwareOrder>
{
   public SoftwareOrderValidator()
   {
     (..other rules..)

     When(order => order.IsLegacySystem == true, () =>
     {
        RuleForEach(order => order.SoftwareItem)
           .SetValidator(new SoftwareItemValidator(true));
     });
     When(order => order.IsLegacySystem == false, () =>
     {
        RuleForEach(order => order.SoftwareItem)
           .SetValidator(new SoftwareItemValidator(false));
     });
   }
}
public class SoftwareItemValidator : AbstractValidator<SoftwareItem>
{
   public SoftwareItemValidator(bool IsLegacySystem)
   {
     When(item => item.Selected, () =>
     {
        RuleFor(item => item.IsLegacySoftware)
            .Equal(IsLegacySystem).WithMessage("Software is incompatible with system");
     });
   }
}

As you can see, I'm accomplishing this by having a When for each condition. It works, but it violates DRY and is not practical to use in a situation with more than just two conditions.

I'd ideally like to have a single RuleForEach that could do this, no Whens needed, something like:

RuleForEach(order => order.SoftwareItem)
   .SetValidator(new SoftwareItemValidator(order => order.IsLegacySystem));

But I can't see any way to pass IsLegacySystem into that constructor.

Was it helpful?

Solution

I decided to give this another shot, 2 years later, after seeing how many views this unanswered question had gotten. I've come up with two answers.

The first answer is the best solution for the situation described in the question.

public class SoftwareOrderValidator : AbstractValidator<SoftwareOrder>
{
   public SoftwareOrderValidator()
   {
      RuleForEach(order => order.SoftwareItem)
         .Must(BeCompatibleWithSystem)
         .WithMessage("Software is incompatible with system");

   }

   private bool BeCompatibleWithSystem(SoftwareOrder order, SoftwareItem item)
   {
      if (item.Selected)
         return (order.IsLegacySystem == item.IsLegacySoftware);
      else
         return true;
   }
}

Predicate Validators (a.k.a Must) can take both object & property as arguments. This allows you to directly compare against IsLegacySystem, or any other property of the parent object.

You probably shouldn't use this second answer. If you believe you need to pass arguments into an AbstractValidator's constructor, I would encourage you to re-assess and find a different approach. With that warning said, here is one way to accomplish it.

Basically, use a dummy Must() to allow you to set a variable outside of a lambda, outside of the constructor. Then you can use that to get that value into the constructor of the second validator.

public class SoftwareOrderValidator : AbstractValidator<SoftwareOrder>
{
   private bool _isLegacySystem;

   public SoftwareOrderValidator()
   {
      RuleFor(order => order.IsLegacySystem)
         .Must(SetUpSoftwareItemValidatorConstructorArg);

      RuleForEach(order => order.SoftwareItem)
         .SetValidator(new SoftwareItemValidator(_isLegacySystem));

   }

   private bool SetUpSoftwareItemValidatorConstructorArg(bool isLegacySystem)
   {
      _isLegacySystem = isLegacySystem;
      return true;
   }
}
public class SoftwareItemValidator : AbstractValidator<SoftwareItem>
{
   public SoftwareItemValidator(bool IsLegacySystem)
   {
     When(item => item.Selected, () =>
     {
        RuleFor(item => item.IsLegacySoftware)
            .Equal(IsLegacySystem).WithMessage("Software is incompatible with system");
     });
   }
}

OTHER TIPS

I know this is an old question and an answer has been given already, but I stumbled upon this question today and found out that the current version of FluentValidation (I'm using 6.2.1.0) has a new overload for SetValidator that allows you to pass a Func as a parameter.

So, you can do:

RuleForEach(x => x.CollectionProperty)
    // x below will referente the Parent class
    .SetValidator(x => new CollectionItemValidator(x.ParentProperty);

Hopefully this can help someone out there.

How about this approach which uses the Custom method:

public class SoftwareOrder
{
    public bool IsLegacySystem;
    public List<SoftwareItem> Software;
}
public class SoftwareItem
{
    public bool Selected;
    public bool IsLegacySoftware;
    public int SoftwareId;
}

public class SoftwareOrderValidator : AbstractValidator<SoftwareOrder>
{
    public SoftwareOrderValidator()
    {
        Custom(order =>
        {
            if (order.Software == null)
                return null;

            return order.Software.Any(item => order.IsLegacySystem != item.IsLegacySoftware)
               ? new ValidationFailure("Software", "Software is incompatible with system")
               : null;
        });

        // Validations other than legacy check
        RuleFor(order => order.Software).SetCollectionValidator(new SoftwareItemValidator());
    }
}

public class SoftwareItemValidator : AbstractValidator<SoftwareItem>
{
    // Validation rules other than legacy check
    public SoftwareItemValidator()
    {
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top