diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 755a44b..b217c43 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -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, @@ -40,20 +40,7 @@ private static async Task ValidateModel(EditContext editContext, if (validator is not null) { - ValidationContext context; - - if (fluentValidationValidator.ValidateOptions is not null) - { - context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions); - } - else if (fluentValidationValidator.Options is not null) - { - context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.Options); - } - else - { - context = new ValidationContext(editContext.Model); - } + var context = ConstructValidationContext(editContext, fluentValidationValidator); var asyncValidationTask = validator.ValidateAsync(context); editContext.Properties[PendingAsyncValidation] = asyncValidationTask; @@ -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(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.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(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 ConstructValidationContext(EditContext editContext, + FluentValidationValidator fluentValidationValidator) + { + ValidationContext context; + + if (fluentValidationValidator.ValidateOptions is not null) + { + context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions); + } + else if (fluentValidationValidator.Options is not null) + { + context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.Options); + } + else + { + context = new ValidationContext(editContext.Model); + } + + return context; + } + private static IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning) { var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index c6a6a9e..4cd5273 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using System; +using FluentValidation; using FluentValidation.Internal; using FluentValidation.Results; diff --git a/src/Blazored.FluentValidation/IntersectingCompositeValidatorSelector.cs b/src/Blazored.FluentValidation/IntersectingCompositeValidatorSelector.cs new file mode 100644 index 0000000..88c24e2 --- /dev/null +++ b/src/Blazored.FluentValidation/IntersectingCompositeValidatorSelector.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using FluentValidation.Internal; + +namespace Blazored.FluentValidation; + +internal class IntersectingCompositeValidatorSelector : IValidatorSelector { + private readonly IEnumerable _selectors; + + public IntersectingCompositeValidatorSelector(IEnumerable selectors) { + _selectors = selectors; + } + + public bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context) { + return _selectors.All(s => s.CanExecute(rule, propertyPath, context)); + } +} \ No newline at end of file diff --git a/src/Blazored.FluentValidation/PropertyPathHelper.cs b/src/Blazored.FluentValidation/PropertyPathHelper.cs new file mode 100644 index 0000000..b156081 --- /dev/null +++ b/src/Blazored.FluentValidation/PropertyPathHelper.cs @@ -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(); + 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(); + + 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(); + 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); + } +} \ No newline at end of file