From d8d44438a1e61e81c063d5fb2323879318760d29 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 20:54:18 +0000 Subject: [PATCH] Only execute rules for property when validating at field level --- .../EditContextFluentValidationExtensions.cs | 72 +++++++++---------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index f1ca853..5a7fd78 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -1,5 +1,6 @@ using System.Collections; using FluentValidation; +using FluentValidation.Internal; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.DependencyInjection; using static FluentValidation.AssemblyScanner; @@ -13,23 +14,20 @@ public static class EditContextFluentValidationExtensions private static readonly List AssemblyScanResults = new(); public const string PendingAsyncValidation = "AsyncValidationTask"; - public static void AddFluentValidation(this EditContext editContext, IServiceProvider serviceProvider, - bool disableAssemblyScanning, IValidator? validator, FluentValidationValidator fluentValidationValidator) + public static void AddFluentValidation(this EditContext editContext, IServiceProvider serviceProvider, bool disableAssemblyScanning, IValidator? validator, FluentValidationValidator fluentValidationValidator) { ArgumentNullException.ThrowIfNull(editContext, nameof(editContext)); - + ValidatorOptions.Global.ValidatorSelectors.CompositeValidatorSelectorFactory = (selectors) => new IntersectingCompositeValidatorSelector(selectors); var messages = new ValidationMessageStore(editContext); editContext.OnValidationRequested += - async (sender, _) => await ValidateModel((EditContext)sender!, messages, serviceProvider, - disableAssemblyScanning, fluentValidationValidator, validator); - + async (sender, _) => await ValidateModel((EditContext)sender!, messages, serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator); + editContext.OnFieldChanged += - async (_, eventArgs) => await ValidateField(editContext, messages, eventArgs.FieldIdentifier, - serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator); + async (_, eventArgs) => await ValidateField(editContext, messages, eventArgs.FieldIdentifier, serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator); } private static async Task ValidateModel(EditContext editContext, @@ -87,38 +85,36 @@ private static async Task ValidateField(EditContext editContext, { return; } - + ValidationContext context; if (fluentValidationValidator.ValidateOptions is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, (options) => - { - fluentValidationValidator.ValidateOptions(options); - options.IncludeProperties(propertyPath); - }); + context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions); } else if (fluentValidationValidator.Options is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, (options) => - { - fluentValidationValidator.Options(options); - options.IncludeProperties(propertyPath); - }); + context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.Options); } else { - context = ValidationContext.CreateWithOptions(editContext.Model, (options) => - { - options.IncludeProperties(propertyPath); - }); + context = new ValidationContext(editContext.Model); } - validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning); + var fluentValidationValidatorSelector = context.Selector; + var changedPropertySelector = ValidationContext.CreateWithOptions(editContext.Model, strategy => + { + strategy.IncludeProperties(propertyPath); + }).Selector; + + var compositeSelector = + new IntersectingCompositeValidatorSelector(new IValidatorSelector[] { 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 .Select(validationFailure => validationFailure.ErrorMessage) .Distinct(); @@ -132,12 +128,12 @@ private static async Task ValidateField(EditContext editContext, private class Node { - public object ModelObject { get; set; } public Node? Parent { get; set; } + public object ModelObject { get; set; } public string? PropertyName { get; set; } public int? Index { get; set; } } - + private static string ToFluentPropertyPath(EditContext editContext, FieldIdentifier fieldIdentifier) { var nodes = new Stack(); @@ -155,7 +151,7 @@ private static string ToFluentPropertyPath(EditContext editContext, FieldIdentif { return BuildPropertyPath(currentNode, fieldIdentifier); } - + var nonPrimitiveProperties = currentModelObject .GetType() .GetProperties() @@ -173,11 +169,11 @@ private static string ToFluentPropertyPath(EditContext editContext, FieldIdentif PropertyName = nonPrimitiveProperty.Name, ModelObject = instance }; - + return BuildPropertyPath(node, fieldIdentifier); } - - if (instance is IEnumerable enumerable) + + if(instance is IEnumerable enumerable) { var itemIndex = 0; foreach (var item in enumerable) @@ -191,7 +187,7 @@ private static string ToFluentPropertyPath(EditContext editContext, FieldIdentif }); } } - else if (instance is not null) + else if(instance is not null) { nodes.Push(new Node() { @@ -234,8 +230,7 @@ private static string BuildPropertyPath(Node currentNode, FieldIdentifier fieldI return string.Join('.', pathParts); } - private static IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, - bool disableAssemblyScanning) + private static IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning) { var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); try @@ -293,7 +288,7 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in var obj = editContext.Model; var nextTokenEnd = propertyPath.IndexOfAny(Separators); - + // Optimize for a scenario when parsing isn't needed. if (nextTokenEnd < 0) { @@ -320,8 +315,8 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in // we've got an Item property var indexerType = prop.GetIndexParameters()[0].ParameterType; var indexerValue = Convert.ChangeType(nextToken.ToString(), indexerType); - - newObj = prop.GetValue(obj, new[] { indexerValue }); + + newObj = prop.GetValue(obj, new [] { indexerValue }); } else { @@ -346,7 +341,6 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in { throw new InvalidOperationException($"Could not find property named {nextToken.ToString()} on object of type {obj.GetType().FullName}."); } - newObj = prop.GetValue(obj); } @@ -357,7 +351,7 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in } obj = newObj; - + nextTokenEnd = propertyPathAsSpan.IndexOfAny(Separators); if (nextTokenEnd < 0) {