Child Model Validation using Parent Model Values. Fluent Validation. MVC4

25,608

Solution 1

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

}

Solution 2

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

Solution 3

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

Solution 4

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.

Share:
25,608
Dustin Harrell
Author by

Dustin Harrell

Software Developer in the Little Rock area

Updated on January 27, 2021

Comments

  • Dustin Harrell
    Dustin Harrell over 3 years

    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
        }
    }
    
  • Dustin Harrell
    Dustin Harrell over 10 years
    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
    Dustin Harrell over 10 years
    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"
  • Tommy Grovnes
    Tommy Grovnes over 10 years
    As far as I can see, this is the cleanest approach for now
  • Dustin Harrell
    Dustin Harrell over 10 years
    Is there another approach because this one wont even compile because model has no value in the context?
  • Dustin Harrell
    Dustin Harrell over 10 years
    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.
  • Tommy Grovnes
    Tommy Grovnes over 10 years
    Updated my answer with an example for the Custom method approach, would you +1 the answer ?
  • andyh0316
    andyh0316 over 7 years
    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?
  • Tommy Grovnes
    Tommy Grovnes over 7 years
    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
    Tommy Grovnes over 7 years
    How are you comparing the birthdays ? I don't understand your answer ?
  • johnny 5
    johnny 5 over 7 years
    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
  • Tommy Grovnes
    Tommy Grovnes over 7 years
    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?"
  • johnny 5
    johnny 5 over 7 years
    Your right something looks wierd here, was this question updated in the past give me a second I'll update this
  • johnny 5
    johnny 5 over 7 years
    @TommyGrovnes Idk what happened there but its fixed now
  • andyh0316
    andyh0316 over 7 years
    @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.
  • Patee Gutee
    Patee Gutee over 6 years
    @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"
  • johnny 5
    johnny 5 over 6 years
    @PateeGutee Sorry Ill update now. its missing the return type ValidationResult
  • Patee Gutee
    Patee Gutee over 6 years
    @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.
  • johnny 5
    johnny 5 over 6 years
    Thanks it's hard to compile from memory on my phone
  • Paul Suart
    Paul Suart over 6 years
    @johnny 5's answer passes the parent validator to the child validator; yours passes the parent model to the child validator.
  • Vincent
    Vincent over 5 years
    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.