Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inconsistent field-level validation (Fixes #204) #205

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public static void AddFluentValidation(this EditContext editContext, IServicePro

editContext.OnValidationRequested +=
async (sender, _) => await ValidateModel((EditContext)sender!, messages, serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator);

editContext.OnFieldChanged +=
async (_, eventArgs) => await ValidateField(editContext, messages, eventArgs.FieldIdentifier, serviceProvider, disableAssemblyScanning, validator);
async (_, eventArgs) => await ValidateField(editContext, messages, eventArgs.FieldIdentifier, serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator);
}

private static async Task ValidateModel(EditContext editContext,
Expand All @@ -40,20 +40,7 @@ private static async Task ValidateModel(EditContext editContext,

if (validator is not null)
{
ValidationContext<object> context;

if (fluentValidationValidator.ValidateOptions is not null)
{
context = ValidationContext<object>.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions);
}
else if (fluentValidationValidator.Options is not null)
{
context = ValidationContext<object>.CreateWithOptions(editContext.Model, fluentValidationValidator.Options);
}
else
{
context = new ValidationContext<object>(editContext.Model);
}
var context = ConstructValidationContext(editContext, fluentValidationValidator);

var asyncValidationTask = validator.ValidateAsync(context);
editContext.Properties[PendingAsyncValidation] = asyncValidationTask;
Expand Down Expand Up @@ -86,24 +73,65 @@ private static async Task ValidateField(EditContext editContext,
FieldIdentifier fieldIdentifier,
IServiceProvider serviceProvider,
bool disableAssemblyScanning,
FluentValidationValidator fluentValidationValidator,
IValidator? validator = null)
{
var properties = new[] { fieldIdentifier.FieldName };
var context = new ValidationContext<object>(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));
var propertyPath = PropertyPathHelper.ToFluentPropertyPath(editContext, fieldIdentifier);

if (string.IsNullOrEmpty(propertyPath))
{
return;
}

var context = ConstructValidationContext(editContext, fluentValidationValidator);

validator ??= GetValidatorForModel(serviceProvider, fieldIdentifier.Model, disableAssemblyScanning);
var fluentValidationValidatorSelector = context.Selector;
var changedPropertySelector = ValidationContext<object>.CreateWithOptions(editContext.Model, strategy =>
{
strategy.IncludeProperties(propertyPath);
}).Selector;

var compositeSelector =
new IntersectingCompositeValidatorSelector(new[] { fluentValidationValidatorSelector, changedPropertySelector });

validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning);

if (validator is not null)
{
var validationResults = await validator.ValidateAsync(context);
var validationResults = await validator.ValidateAsync(new ValidationContext<object>(editContext.Model, new PropertyChain(), compositeSelector));
var errorMessages = validationResults.Errors
.Where(validationFailure => validationFailure.PropertyName == propertyPath)
.Select(validationFailure => validationFailure.ErrorMessage)
.Distinct();

messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));
messages.Add(fieldIdentifier, errorMessages);

editContext.NotifyValidationStateChanged();
}
}

private static ValidationContext<object> ConstructValidationContext(EditContext editContext,
FluentValidationValidator fluentValidationValidator)
{
ValidationContext<object> context;

if (fluentValidationValidator.ValidateOptions is not null)
{
context = ValidationContext<object>.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions);
}
else if (fluentValidationValidator.Options is not null)
{
context = ValidationContext<object>.CreateWithOptions(editContext.Model, fluentValidationValidator.Options);
}
else
{
context = new ValidationContext<object>(editContext.Model);
}

return context;
}

private static IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning)
{
var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
Expand Down
3 changes: 2 additions & 1 deletion src/Blazored.FluentValidation/FluentValidationsValidator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentValidation;
using System;
using FluentValidation;
using FluentValidation.Internal;
using FluentValidation.Results;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation;
using FluentValidation.Internal;

namespace Blazored.FluentValidation;

internal class IntersectingCompositeValidatorSelector : IValidatorSelector {
private readonly IEnumerable<IValidatorSelector> _selectors;

public IntersectingCompositeValidatorSelector(IEnumerable<IValidatorSelector> selectors) {
_selectors = selectors;
}

public bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context) {
return _selectors.All(s => s.CanExecute(rule, propertyPath, context));
}
}
111 changes: 111 additions & 0 deletions src/Blazored.FluentValidation/PropertyPathHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Collections;
using System.Reflection;
using Microsoft.AspNetCore.Components.Forms;

namespace Blazored.FluentValidation;

internal static class PropertyPathHelper
{
private class Node
{
public Node? Parent { get; set; }
public object? ModelObject { get; set; }
public string? PropertyName { get; set; }
public int? Index { get; set; }
}

public static string ToFluentPropertyPath(EditContext editContext, FieldIdentifier fieldIdentifier)
{
var nodes = new Stack<Node>();
nodes.Push(new Node()
{
ModelObject = editContext.Model,
});

while (nodes.Any())
{
var currentNode = nodes.Pop();
var currentModelObject = currentNode.ModelObject;

if (currentModelObject == fieldIdentifier.Model)
{
return BuildPropertyPath(currentNode, fieldIdentifier);
}

var nonPrimitiveProperties = currentModelObject?.GetType()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an issue with this bit of code, in a model it kept adding DateTime types on the NodeStack in an endless loop, eventually ending in out of memory. I think additional type guards are needed here for types such as DateTime. This change to check for additional simple types fixed the issue with DateTime not being a primitive.
jafin@2de01ab

.GetProperties()
.Where(prop => !prop.PropertyType.IsPrimitive || prop.PropertyType.IsArray) ?? new List<PropertyInfo>();

foreach (var nonPrimitiveProperty in nonPrimitiveProperties)
{
var instance = nonPrimitiveProperty.GetValue(currentModelObject);

if (instance == fieldIdentifier.Model)
{
var node = new Node()
{
Parent = currentNode,
PropertyName = nonPrimitiveProperty.Name,
ModelObject = instance
};

return BuildPropertyPath(node, fieldIdentifier);
}

if(instance is IEnumerable enumerable)
{
var itemIndex = 0;
foreach (var item in enumerable)
{
nodes.Push(new Node()
{
ModelObject = item,
Parent = currentNode,
PropertyName = nonPrimitiveProperty.Name,
Index = itemIndex++
});
}
}
else if(instance is not null)
{
nodes.Push(new Node()
{
ModelObject = instance,
Parent = currentNode,
PropertyName = nonPrimitiveProperty.Name
});
}
}
}

return string.Empty;
}

private static string BuildPropertyPath(Node currentNode, FieldIdentifier fieldIdentifier)
{
var pathParts = new List<string>();
pathParts.Add(fieldIdentifier.FieldName);
var next = currentNode;

while (next is not null)
{
if (!string.IsNullOrEmpty(next.PropertyName))
{
if (next.Index is not null)
{
pathParts.Add($"{next.PropertyName}[{next.Index}]");
}
else
{
pathParts.Add(next.PropertyName);
}
}

next = next.Parent;
}

pathParts.Reverse();

return string.Join('.', pathParts);
}
}
Loading