Skip to content

Commit

Permalink
Merge pull request #205 from matthetherington/bugfix/204-inconsistent…
Browse files Browse the repository at this point in the history
…-field-level-validation

Fix inconsistent field-level validation (Fixes #204)
  • Loading branch information
pwelter34 authored Oct 7, 2024
2 parents f02c9be + 80254ae commit 8afbf82
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 22 deletions.
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()
.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);
}
}

0 comments on commit 8afbf82

Please sign in to comment.