Skip to content

Commit

Permalink
Ensure ColletionIndex placeholder can be accessed in child validators.
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremySkinner committed Feb 29, 2020
1 parent 865208c commit ac054b1
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 6 deletions.
1 change: 1 addition & 0 deletions Changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ FluentValidationModelValidatorProvider and FluentValidationModelValidator are no
Add additional overload of SetValidator that takes a Func that receives the current property value.
Work around a bug in ASP.NET Core's integration testing components that can cause ConfigureServices to run multiple times.
SourceLink integration.
{CollectionIndex} placeholder can now be accessed in child validators.

8.6.2 - 29 February 2020
Fix CollectionIndex placeholder not working with async workflow.
Expand Down
64 changes: 64 additions & 0 deletions src/FluentValidation.Tests/ForEachRuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,70 @@ public AppropriatenessAnswerViewModelRequiredValidator()
result.IsValid.ShouldBeFalse();
}

[Fact]
public void Can_access_parent_index() {
var personValidator = new InlineValidator<Person>();
var orderValidator = new InlineValidator<Order>();

orderValidator.RuleFor(order => order.ProductName)
.NotEmpty()
.WithMessage("{CollectionIndex} must not be empty");

// Two rules - one for each collection syntax.

personValidator.RuleFor(x => x.Orders)
.NotEmpty()
.ForEach(order => {
order.SetValidator(orderValidator);
});

personValidator.RuleForEach(x => x.Orders).SetValidator(orderValidator);

var result = personValidator.Validate(new Person() {
Orders = new List<Order> {
new Order() { ProductName = "foo"},
new Order(),
new Order() { ProductName = "bar" }
}
});

result.IsValid.ShouldBeFalse();
result.Errors[0].ErrorMessage.ShouldEqual("1 must not be empty");
result.Errors[0].ErrorMessage.ShouldEqual("1 must not be empty");
}

[Fact]
public async Task Can_access_parent_index_async() {
var personValidator = new InlineValidator<Person>();
var orderValidator = new InlineValidator<Order>();

orderValidator.RuleFor(order => order.ProductName)
.NotEmpty()
.WithMessage("{CollectionIndex} must not be empty");

// Two rules - one for each collection syntax.

personValidator.RuleFor(x => x.Orders)
.NotEmpty()
.ForEach(order => {
order.SetValidator(orderValidator);
});

personValidator.RuleForEach(x => x.Orders).SetValidator(orderValidator);

var result = await personValidator.ValidateAsync(new Person() {
Orders = new List<Order> {
new Order() { ProductName = "foo"},
new Order(),
new Order() { ProductName = "bar" }
}
});

result.IsValid.ShouldBeFalse();
result.Errors[0].ErrorMessage.ShouldEqual("1 must not be empty");
result.Errors[0].ErrorMessage.ShouldEqual("1 must not be empty");
}

public class OrderValidator : AbstractValidator<Order> {
public OrderValidator() {
RuleFor(x => x.ProductName).NotEmpty();
Expand Down
1 change: 1 addition & 0 deletions src/FluentValidation/FluentValidation.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Changes in 9.0.0:
* ASP.NET Core: FluentValidationModelValidatorProvider and FluentValidationModelValidator are now public.
* Work around a bug in ASP.NET Core's integration testing components that can cause ConfigureServices to run multiple times.
* SourceLink integration.
* {CollectionIndex} placeholder can now be accessed in child validators.

Full release notes can be found at https://github.com/FluentValidation/FluentValidation/blob/master/Changelog.txt
FluentValidation 8 is a major release. Please read the upgrade notes at https://fluentvalidation.net/upgrading-to-8
Expand Down
1 change: 1 addition & 0 deletions src/FluentValidation/Internal/CollectionPropertyRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,6 @@ public class CollectionPropertyRule<TProperty> : PropertyRule {

return results;
}

}
}
52 changes: 46 additions & 6 deletions src/FluentValidation/Validators/ChildValidatorAdaptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdapt
public override IEnumerable<ValidationFailure> Validate(PropertyValidatorContext context) {
if (Options.Condition != null && !Options.Condition(context)) {
return Enumerable.Empty<ValidationFailure>();
}
}

if (context.PropertyValue == null) {
return Enumerable.Empty<ValidationFailure>();
Expand All @@ -53,18 +53,29 @@ public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdapt
}

var newContext = CreateNewValidationContextForChildValidator(context.PropertyValue, context);
return validator.Validate(newContext).Errors;

// If we're inside a collection with RuleForEach, then preserve the CollectionIndex placeholder
// and pass it down to child validator by caching it in the RootContextData which flows through to
// the child validator. PropertyValidator.PrepareMessageFormatterForValidationError handles extracting this.
HandleCollectionIndex(context, out object originalIndex, out object currentIndex);

var results = validator.Validate(newContext).Errors;

// Reset the collection index
ResetCollectionIndex(context, originalIndex, currentIndex);

return results;
}

public override async Task<IEnumerable<ValidationFailure>> ValidateAsync(PropertyValidatorContext context, CancellationToken cancellation) {
if (Options.Condition != null && !Options.Condition(context)) {
return Enumerable.Empty<ValidationFailure>();
}
}

if (Options.AsyncCondition != null && !await Options.AsyncCondition(context, cancellation)) {
return Enumerable.Empty<ValidationFailure>();
}

if (context.PropertyValue == null) {
return Enumerable.Empty<ValidationFailure>();
}
Expand All @@ -76,7 +87,16 @@ public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdapt
}

var newContext = CreateNewValidationContextForChildValidator(context.PropertyValue, context);

// If we're inside a collection with RuleForEach, then preserve the CollectionIndex placeholder
// and pass it down to child validator by caching it in the RootContextData which flows through to
// the child validator. PropertyValidator.PrepareMessageFormatterForValidationError handles extracting this.
HandleCollectionIndex(context, out object originalIndex, out object currentIndex);

var result = await validator.ValidateAsync(newContext, cancellation);

ResetCollectionIndex(context, originalIndex, currentIndex);

return result.Errors;
}

Expand All @@ -99,5 +119,25 @@ public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdapt
public override bool ShouldValidateAsynchronously(ValidationContext context) {
return context.IsAsync() || Options.AsyncCondition != null;
}

private void HandleCollectionIndex(PropertyValidatorContext context, out object originalIndex, out object index) {
originalIndex = null;
if (context.MessageFormatter.PlaceholderValues.TryGetValue("CollectionIndex", out index)) {
context.ParentContext.RootContextData.TryGetValue("__FV_CollectionIndex", out originalIndex);
context.ParentContext.RootContextData["__FV_CollectionIndex"] = index;
}
}

private void ResetCollectionIndex(PropertyValidatorContext context, object originalIndex, object index) {
// Reset the collection index
if (index != null) {
if (originalIndex != null) {
context.ParentContext.RootContextData["__FV_CollectionIndex"] = originalIndex;
}
else {
context.ParentContext.RootContextData.Remove("__FV_CollectionIndex");
}
}
}
}
}
}
12 changes: 12 additions & 0 deletions src/FluentValidation/Validators/PropertyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ public abstract class PropertyValidator : IPropertyValidator {
protected virtual void PrepareMessageFormatterForValidationError(PropertyValidatorContext context) {
context.MessageFormatter.AppendPropertyName(context.DisplayName);
context.MessageFormatter.AppendPropertyValue(context.PropertyValue);

// If there's a collection index cached in the root context data then add it
// to the message formatter. This happens when a child validator is executed
// as part of a call to RuleForEach. Usually parameters are not flowed through to
// child validators, but we make an exception for collection indices.
if (context.ParentContext.RootContextData.TryGetValue("__FV_CollectionIndex", out var index)) {
// If our property validator has explicitly added a placeholder for the collection index
// don't overwrite it with the cached version.
if (!context.MessageFormatter.PlaceholderValues.ContainsKey("CollectionIndex")) {
context.MessageFormatter.AppendArgument("CollectionIndex", index);
}
}
}

/// <summary>
Expand Down

0 comments on commit ac054b1

Please sign in to comment.