Search code examples
c#lambdafluentvalidation

Pass an element of the object to a FluentValidation SetValidator's constructor


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.


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