48

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

5 Answers 5

36

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);
    }
}
Sign up to request clarification or add additional context in comments.

11 Comments

How are you comparing the birthdays ? I don't understand your answer ?
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
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?"
Your right something looks wierd here, was this question updated in the past give me a second I'll update this
@TommyGrovnes Idk what happened there but its fixed now
|
30

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

}

9 Comments

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.
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"
As far as I can see, this is the cleanest approach for now
Is there another approach because this one wont even compile because model has no value in the context?
Updated my answer with an example for the Custom method approach, would you +1 the answer ?
|
14

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

2 Comments

@johnny 5's answer passes the parent validator to the child validator; yours passes the parent model to the child validator.
SetCollectionValidator is deprecated - see 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."
9

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.

1 Comment

i tried this, at runtime, the SetValidator method is trying to look for an instance of the model from the DI and i get exception that the DI has no such object instance. any idea baout it?
1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.