41

Below is a simplified version of my problem.

I can not flatten the model. There is a List of "children" that I need to validate a birthday.

I can not seem to reference the date in the Parent class and was wondering how this is done in Fluent Validation?

Model

[Validator(typeof(ParentValidator))]
public class Parent
{
    public string Name { get; set; }
    public DateTime Birthdate { get; set; }

    public List<Child> Children { get; set; }
}

public class Child
{
    public string ChildProperty{ get; set; }
    public DateTime Birthdate { get; set; }
}

Validator

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
         RuleFor(model => model.Name).NotEmpty();
         RuleForEach(model => model.Children).SetValidator(new ChildValidator());
    }
}

public class ChildValidator : AbstractValidator<Child>
{
    public ChildValidator()
    {
        RuleFor(model => model.ChildProperty).NotEmpty();
        //Compare birthday to make sure date is < Parents birthday
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Dustin Harrell
  • 457
  • 1
  • 5
  • 13

5 Answers5

29

Create a custom property validator like this

public class AllChildBirtdaysMustBeLaterThanParent : PropertyValidator
{
    public AllChildBirtdaysMustBeLaterThanParent()
        : base("Property {PropertyName} contains children born before their parent!")
    {
    }

    protected override bool IsValid(PropertyValidatorContext context)
    {
        var parent = context.ParentContext.InstanceToValidate as Parent;
        var list = context.PropertyValue as IList<Child>;

        if (list != null)
        {
            return ! (list.Any(c => parent.BirthDay > c.BirthDay));
        }

        return true;
    }
}

Add rules like this

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
        RuleFor(model => model.Name).NotEmpty();
        RuleFor(model => model.Children)
               .SetValidator(new AllChildBirtdaysMustBeLaterThanParent());

        // Collection validator
        RuleFor(model => model.Children).SetCollectionValidator(new ChildValidator());
    }
}

Alternative to the Custom Property validator is to use the Custom method:

    public ParentValidator()
    {
        RuleFor(model => model.Name).NotEmpty();
        RuleFor(model => model.Children).SetCollectionValidator(new ChildValidator());

        Custom(parent =>
        {
            if (parent.Children == null)
                return null;

            return parent.Children.Any(c => parent.BirthDay > c.BirthDay)
               ? new ValidationFailure("Children", "Child cannot be older than parent.")
               : null;
        });
    }

Crude way of showing indicies that failed: (should probably be name of some other identifier)

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
        RuleFor(m => m.Children).SetCollectionValidator(new ChildValidator());

        Custom(parent =>
        {
            if (parent.Children == null)
                return null;

            var failIdx = parent.Children.Where(c => parent.BirthDay > c.BirthDay).Select(c => parent.Children.IndexOf(c));
            var failList = string.Join(",", failIdx);

            return failIdx.Count() > 0
               ? new ValidationFailure("Children", "Child cannot be older than parent. Fail on indicies " + failList)
               : null;
        });
    }

}
Tommy Grovnes
  • 4,126
  • 2
  • 25
  • 40
  • 1
    I am working with this. Currently my issue is that when I make this change my partial view starts griping about "No parameterless constructor defined for this object." And it was working before hand. – Dustin Harrell Sep 10 '13 at 18:49
  • 3
    Actually a more concise error I found is that RuleForEach(model => model.Children) .SetValidator(new ChildValidator(model)); I can not pass model in the .SetValidator. The message is "The name "model" does not exist in this current context" – Dustin Harrell Sep 10 '13 at 19:10
  • 1
    As far as I can see, this is the cleanest approach for now – Tommy Grovnes Sep 10 '13 at 21:19
  • 1
    Is there another approach because this one wont even compile because model has no value in the context? – Dustin Harrell Sep 10 '13 at 21:24
  • 1
    This looks like it is going to work! Thank you sir for your time. I'm going to accept this answer but I do have one last question. Is there a way to output which Item index in this caused the error? The alternative is the one I am using. – Dustin Harrell Sep 11 '13 at 13:10
  • 2
    Updated my answer with an example for the Custom method approach, would you +1 the answer ? – Tommy Grovnes Sep 11 '13 at 13:26
  • 1
    Just a question. Why would you need fluent validator for this if you are doing the validation from the Parent's perspective instead of the Child's. Couldn't that just be as easily done with .NET's native validation? – andyh0316 Feb 01 '17 at 23:15
  • 1
    Can you use native validation across models, i.e compare the child's birthday against the parent's? @andyh0316, please show example ? Things can have changed of course, this is an old answer :) – Tommy Grovnes Feb 07 '17 at 20:44
  • 1
    @TommyGrovnes If you were validating from the parent's perspective, you can get the validationContext which also gives you access to its children models. I was looking for a way to validate from the children's level but I couldn't using the native. That would be cleaner. But it seems like that;s not possible either in fluent validation. Correct me if i'm wrong. – andyh0316 Feb 08 '17 at 07:53
29

Edit: SetCollectionValidator has been deprecated, however the same can be done now using RuleForEach:

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
         this.RuleFor(model => model.Name).NotEmpty();
         this.RuleForEach(model => model.Children)
                .SetValidator(model => new ChildValidator(model));
    }
}

public class ChildValidator : AbstractValidator<Child>
{
    public ChildValidator(Parent parent)
    {
        this.RuleFor(model => model.ChildProperty).NotEmpty();
        this.RuleFor(model => model.Birthday).Must(birthday => parent.Birthday < birthday);
    }
}
johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • 1
    How are you comparing the birthdays ? I don't understand your answer ? – Tommy Grovnes Feb 07 '17 at 20:46
  • 1
    The important part of this Question is how to do Child validation. I'm not comparing birthdays that comment is for you to add the birthday logic rules there – johnny 5 Feb 07 '17 at 20:56
  • 1
    The question is about how to compare the child's birthday to the parent's, it is not obvious from your example how that can be accomplished, adding the comparison would make it a better answer. quotes: "I need to validate a birthday." "I can not seem to reference the date in the Parent class and was wondering how this is done in Fluent Validation?" – Tommy Grovnes Feb 07 '17 at 21:10
  • 1
    Your right something looks wierd here, was this question updated in the past give me a second I'll update this – johnny 5 Feb 07 '17 at 21:20
  • 1
    @TommyGrovnes Idk what happened there but its fixed now – johnny 5 Feb 07 '17 at 21:34
  • 1
    @johnny5 Your solution gives me errors: "Validate() method must have a return type" and "SetCollectionValidator() The best overloaded method match for 'ChildValidator(MyApp.Models.Parent)' has some invalid arguments" – Patee Gutee Sep 08 '17 at 19:27
  • 1
    @PateeGutee Sorry Ill update now. its missing the return type `ValidationResult ` – johnny 5 Sep 08 '17 at 19:38
  • 1
    @johnny5 I did that and to get rid of the 2nd error, I replaced "this" with "parent" like ChildValidator(parent). However, I am now getting the "not all code paths return a value" error. – Patee Gutee Sep 08 '17 at 20:20
  • 1
    Thanks it's hard to compile from memory on my phone – johnny 5 Sep 08 '17 at 20:49
  • 1
    How does this approach handle re-use of the validator? As it looks to me it adds rules for each iteration of the Validate method. – Vincent Sep 24 '18 at 08:12
  • I find this the better solution. if you have more than one parent of different classes and no inheritance you can pass the property values instead of the parent object without breaking the hierarchy of validation. – CME64 Aug 01 '22 at 05:27
13

Nowadays the answer by @johnny-5 can be simplified even further by using the SetCollectionValidator extension method and passing the parent object to the child validator:

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
         RuleFor(model => model.Name).NotEmpty();
         RuleFor(model => model.Children)
             .SetCollectionValidator(model => new ChildValidator(model))
    }
}

public class ChildValidator : AbstractValidator<Child>
{
    public ChildValidator(Parent parent)
    {
        RuleFor(model => model.ChildProperty).NotEmpty();
        RuleFor(model => model.Birthday).Must(birthday => parent.Birthday < birthday);
    }
}
Kristoffer Jälén
  • 4,112
  • 3
  • 30
  • 54
  • 2
    @johnny 5's answer passes the parent *validator* to the child validator; yours passes the parent *model* to the child validator. – Paul Suart Feb 01 '18 at 20:40
  • SetCollectionValidator is deprecated - see https://docs.fluentvalidation.net/en/latest/upgrading-to-8.html. "SetCollectionValidator was added to FluentValidation in its initial versions to provide a way to use a child validator against each element in a collection. RuleForEach was added later and provides a more comprehensive way of validating collections (as you can define in-line rules with RuleForEach too). It doesn’t make sense to provide 2 ways to do the same thing." – Nugsson Dec 05 '22 at 15:40
8

Building on the answer of @kristoffer-jalen it is now:

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
         RuleFor(model => model.Name).NotEmpty();
         //RuleFor(model => model.Children)
         //    .SetCollectionValidator(model => new ChildValidator(model))
         RuleForEach(model => model.Children)
                .SetValidator(model => new ChildValidator(model));
    }
}

public class ChildValidator : AbstractValidator<Child>
{
    public ChildValidator(Parent parent)
    {
        RuleFor(model => model.ChildProperty).NotEmpty();
        RuleFor(model => model.Birthday).Must(birthday => parent.Birthday < birthday);
    }
}

as SetCollectionValidator is deprecated.

Badgerspot
  • 2,301
  • 3
  • 28
  • 42
0

Pass the parent to custom logic with .Must(), then do the validation manually.

public class ParentValidator : AbstractValidator<Parent>
{
    public ParentValidator()
    {
         RuleFor(model => model.Name).NotEmpty();
         RuleFor(model => model).Must(BeValidBirtDate).WithMessage("BirthDate Invalid");
    }

    public bool BeValidBirtDate(Parent parent)
    {
        foreach(var child in parent.Children)
        {
            if (DateTime.Compare(child.BirthDate, parent.BirthDate) == -1)
                return false;
        }
        return true;
    }
}