Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 421
friggle Avatar asked Sep 06 '13 19:09

friggle


2 Answers

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");
     });
   }
}
like image 71
friggle Avatar answered Nov 15 '22 16:11

friggle


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.

like image 36
Rodrigo Lira Avatar answered Nov 15 '22 14:11

Rodrigo Lira