Child Model Validation using Parent Model Values. Fluent Validation. MVC4
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.
Comments
-
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 over 10 yearsI 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 over 10 yearsActually 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 over 10 yearsAs far as I can see, this is the cleanest approach for now
-
Dustin Harrell over 10 yearsIs there another approach because this one wont even compile because model has no value in the context?
-
Dustin Harrell over 10 yearsThis 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 over 10 yearsUpdated my answer with an example for the Custom method approach, would you +1 the answer ?
-
andyh0316 over 7 yearsJust 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 over 7 yearsCan 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 over 7 yearsHow are you comparing the birthdays ? I don't understand your answer ?
-
johnny 5 over 7 yearsThe 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 over 7 yearsThe 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 over 7 yearsYour right something looks wierd here, was this question updated in the past give me a second I'll update this
-
johnny 5 over 7 years@TommyGrovnes Idk what happened there but its fixed now
-
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 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 over 6 years@PateeGutee Sorry Ill update now. its missing the return type
ValidationResult
-
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 over 6 yearsThanks it's hard to compile from memory on my phone
-
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 over 5 yearsHow does this approach handle re-use of the validator? As it looks to me it adds rules for each iteration of the Validate method.