From a7314a07264d3146f3d6f2cc5093ef03b3e51db6 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 18:17:51 +0000 Subject: [PATCH 01/16] Fix inconsistent field-level validation (Fixes #204) --- .../EditContextFluentValidationExtensions.cs | 183 ++++++++++++++++-- .../FluentValidationsValidator.cs | 24 ++- 2 files changed, 179 insertions(+), 28 deletions(-) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index c06c9f4..281463a 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -1,5 +1,5 @@ -using FluentValidation; -using FluentValidation.Internal; +using System.Collections; +using FluentValidation; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.DependencyInjection; using static FluentValidation.AssemblyScanner; @@ -13,17 +13,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)); 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, validator); + async (_, eventArgs) => await ValidateField(editContext, messages, eventArgs.FieldIdentifier, + serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator); } private static async Task ValidateModel(EditContext editContext, @@ -41,11 +44,13 @@ private static async Task ValidateModel(EditContext editContext, if (fluentValidationValidator.ValidateOptions is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions); + context = ValidationContext.CreateWithOptions(editContext.Model, + fluentValidationValidator.ValidateOptions); } else if (fluentValidationValidator.Options is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.Options); + context = ValidationContext.CreateWithOptions(editContext.Model, + fluentValidationValidator.Options); } else { @@ -72,25 +77,156 @@ 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)); - - validator ??= GetValidatorForModel(serviceProvider, fieldIdentifier.Model, disableAssemblyScanning); + var propertyPath = ToFluentPropertyPath(editContext, fieldIdentifier); + + if (string.IsNullOrEmpty(propertyPath)) + { + return; + } + + ValidationContext context; + + if (fluentValidationValidator.ValidateOptions is not null) + { + context = ValidationContext.CreateWithOptions(editContext.Model, + (options) => { fluentValidationValidator.ValidateOptions(options); }); + } + else if (fluentValidationValidator.Options is not null) + { + context = ValidationContext.CreateWithOptions(editContext.Model, + (options) => { fluentValidationValidator.Options(options); }); + } + else + { + context = new ValidationContext(editContext.Model); + } + + validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning); if (validator is not null) { var validationResults = await validator.ValidateAsync(context); + 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 IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning) + private class Node + { + 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(); + nodes.Push(new Node() + { + ModelObject = editContext.Model, + }); + + while (nodes.Any()) + { + var currentNode = nodes.Pop(); + object? currentModelObject = currentNode.ModelObject; + + if (currentModelObject == fieldIdentifier.Model) + { + return BuildPropertyPath(currentNode, fieldIdentifier); + } + + var nonPrimitiveProperties = currentModelObject + .GetType() + .GetProperties() + .Where(prop => !prop.PropertyType.IsPrimitive || prop.PropertyType.IsArray); + + 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); + } + + private static IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, + bool disableAssemblyScanning) { var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); try @@ -110,7 +246,8 @@ private static async Task ValidateField(EditContext editContext, return null; } - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName))) + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies() + .Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName))) { try { @@ -126,7 +263,8 @@ private static async Task ValidateField(EditContext editContext, var interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); - var modelValidatorType = AssemblyScanResults.FirstOrDefault(i => interfaceValidatorType.IsAssignableFrom(i.InterfaceType))?.ValidatorType; + var modelValidatorType = AssemblyScanResults + .FirstOrDefault(i => interfaceValidatorType.IsAssignableFrom(i.InterfaceType))?.ValidatorType; if (modelValidatorType is null) { @@ -148,7 +286,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) { @@ -175,8 +313,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 { @@ -189,7 +327,8 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in } else { - throw new InvalidOperationException($"Could not find indexer on object of type {obj.GetType().FullName}."); + throw new InvalidOperationException( + $"Could not find indexer on object of type {obj.GetType().FullName}."); } } } @@ -199,8 +338,10 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in var prop = obj.GetType().GetProperty(nextToken.ToString()); if (prop == null) { - throw new InvalidOperationException($"Could not find property named {nextToken.ToString()} on object of type {obj.GetType().FullName}."); + throw new InvalidOperationException( + $"Could not find property named {nextToken.ToString()} on object of type {obj.GetType().FullName}."); } + newObj = prop.GetValue(obj); } @@ -211,7 +352,7 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in } obj = newObj; - + nextTokenEnd = propertyPathAsSpan.IndexOfAny(Separators); if (nextTokenEnd < 0) { diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index ae8e6f9..4342348 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -1,21 +1,31 @@ -using FluentValidation; +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; using FluentValidation.Internal; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; -using System; using FluentValidation.Results; namespace Blazored.FluentValidation; public class FluentValidationValidator : ComponentBase { - [Inject] private IServiceProvider ServiceProvider { get; set; } = default!; + [Inject] + private IServiceProvider ServiceProvider { get; set; } = default!; - [CascadingParameter] private EditContext? CurrentEditContext { get; set; } + [CascadingParameter] + private EditContext? CurrentEditContext { get; set; } + + [Parameter] + public IValidator? Validator { get; set; } + + [Parameter] + public bool DisableAssemblyScanning { get; set; } + + [Parameter] + public Action>? Options { get; set; } - [Parameter] public IValidator? Validator { get; set; } - [Parameter] public bool DisableAssemblyScanning { get; set; } - [Parameter] public Action>? Options { get; set; } internal Action>? ValidateOptions { get; set; } public bool Validate(Action>? options = null) From e0a6702e8e9351e9cd0d96d929cd2adea8ac75ec Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 18:26:28 +0000 Subject: [PATCH 02/16] Undo formatting changes --- .../EditContextFluentValidationExtensions.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 281463a..fe4ffde 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -44,13 +44,11 @@ private static async Task ValidateModel(EditContext editContext, if (fluentValidationValidator.ValidateOptions is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, - fluentValidationValidator.ValidateOptions); + context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.ValidateOptions); } else if (fluentValidationValidator.Options is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, - fluentValidationValidator.Options); + context = ValidationContext.CreateWithOptions(editContext.Model, fluentValidationValidator.Options); } else { @@ -123,8 +121,8 @@ private static async Task ValidateField(EditContext editContext, private class Node { - public Node? Parent { get; set; } public object ModelObject { get; set; } + public Node? Parent { get; set; } public string? PropertyName { get; set; } public int? Index { get; set; } } @@ -246,8 +244,7 @@ private static string BuildPropertyPath(Node currentNode, FieldIdentifier fieldI return null; } - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies() - .Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName))) + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName))) { try { @@ -263,8 +260,7 @@ private static string BuildPropertyPath(Node currentNode, FieldIdentifier fieldI var interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); - var modelValidatorType = AssemblyScanResults - .FirstOrDefault(i => interfaceValidatorType.IsAssignableFrom(i.InterfaceType))?.ValidatorType; + var modelValidatorType = AssemblyScanResults.FirstOrDefault(i => interfaceValidatorType.IsAssignableFrom(i.InterfaceType))?.ValidatorType; if (modelValidatorType is null) { @@ -327,8 +323,7 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in } else { - throw new InvalidOperationException( - $"Could not find indexer on object of type {obj.GetType().FullName}."); + throw new InvalidOperationException($"Could not find indexer on object of type {obj.GetType().FullName}."); } } } @@ -338,8 +333,7 @@ private static FieldIdentifier ToFieldIdentifier(in EditContext editContext, in var prop = obj.GetType().GetProperty(nextToken.ToString()); if (prop == null) { - throw new InvalidOperationException( - $"Could not find property named {nextToken.ToString()} on object of type {obj.GetType().FullName}."); + throw new InvalidOperationException($"Could not find property named {nextToken.ToString()} on object of type {obj.GetType().FullName}."); } newObj = prop.GetValue(obj); From 90f4c91bc4107c7496b2f76ea6ce64e93b73298f Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 18:28:20 +0000 Subject: [PATCH 03/16] Undo validator formatting changes --- .../FluentValidationsValidator.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index 4342348..4662fee 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -11,20 +11,15 @@ namespace Blazored.FluentValidation; public class FluentValidationValidator : ComponentBase { - [Inject] - private IServiceProvider ServiceProvider { get; set; } = default!; + [Inject] private IServiceProvider ServiceProvider { get; set; } = default!; - [CascadingParameter] - private EditContext? CurrentEditContext { get; set; } + [CascadingParameter] private EditContext? CurrentEditContext { get; set; } - [Parameter] - public IValidator? Validator { get; set; } + [Parameter] public IValidator? Validator { get; set; } - [Parameter] - public bool DisableAssemblyScanning { get; set; } + [Parameter] public bool DisableAssemblyScanning { get; set; } - [Parameter] - public Action>? Options { get; set; } + [Parameter] public Action>? Options { get; set; } internal Action>? ValidateOptions { get; set; } From c2efc1c81f7551841111d0ff3c490a29bfac0ca2 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 18:29:33 +0000 Subject: [PATCH 04/16] Remove using statements --- src/Blazored.FluentValidation/FluentValidationsValidator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index 4662fee..a845c3f 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using System.Threading.Tasks; using FluentValidation; using FluentValidation.Internal; using Microsoft.AspNetCore.Components; From a76ede90a5f61eeeae8b77f34ea876efeb4bd4e4 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 20:22:18 +0000 Subject: [PATCH 05/16] Only run rules for property being validated --- .../EditContextFluentValidationExtensions.cs | 23 ++++++++++++++----- .../IntersectingCompositeValidatorSelector.cs | 16 +++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 src/Blazored.FluentValidation/IntersectingCompositeValidatorSelector.cs diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index fe4ffde..f1ca853 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -17,6 +17,9 @@ public static void AddFluentValidation(this EditContext editContext, IServicePro bool disableAssemblyScanning, IValidator? validator, FluentValidationValidator fluentValidationValidator) { ArgumentNullException.ThrowIfNull(editContext, nameof(editContext)); + + ValidatorOptions.Global.ValidatorSelectors.CompositeValidatorSelectorFactory = + (selectors) => new IntersectingCompositeValidatorSelector(selectors); var messages = new ValidationMessageStore(editContext); @@ -89,17 +92,26 @@ private static async Task ValidateField(EditContext editContext, if (fluentValidationValidator.ValidateOptions is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, - (options) => { fluentValidationValidator.ValidateOptions(options); }); + context = ValidationContext.CreateWithOptions(editContext.Model, (options) => + { + fluentValidationValidator.ValidateOptions(options); + options.IncludeProperties(propertyPath); + }); } else if (fluentValidationValidator.Options is not null) { - context = ValidationContext.CreateWithOptions(editContext.Model, - (options) => { fluentValidationValidator.Options(options); }); + context = ValidationContext.CreateWithOptions(editContext.Model, (options) => + { + fluentValidationValidator.Options(options); + options.IncludeProperties(propertyPath); + }); } else { - context = new ValidationContext(editContext.Model); + context = ValidationContext.CreateWithOptions(editContext.Model, (options) => + { + options.IncludeProperties(propertyPath); + }); } validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning); @@ -108,7 +120,6 @@ private static async Task ValidateField(EditContext editContext, { var validationResults = await validator.ValidateAsync(context); var errorMessages = validationResults.Errors - .Where(validationFailure => validationFailure.PropertyName == propertyPath) .Select(validationFailure => validationFailure.ErrorMessage) .Distinct(); 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 From d8d44438a1e61e81c063d5fb2323879318760d29 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 20:54:18 +0000 Subject: [PATCH 06/16] 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) { From 74e9f327cc4a84a4b4135491d52a790bafe09d9d Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 20:55:52 +0000 Subject: [PATCH 07/16] Undo formatting changes --- src/Blazored.FluentValidation/FluentValidationsValidator.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index a845c3f..3ee1bde 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -10,13 +10,9 @@ namespace Blazored.FluentValidation; public class FluentValidationValidator : ComponentBase { [Inject] private IServiceProvider ServiceProvider { get; set; } = default!; - [CascadingParameter] private EditContext? CurrentEditContext { get; set; } - [Parameter] public IValidator? Validator { get; set; } - [Parameter] public bool DisableAssemblyScanning { get; set; } - [Parameter] public Action>? Options { get; set; } internal Action>? ValidateOptions { get; set; } From e245c5002163734cdea63da0a430a36926875912 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 20:57:09 +0000 Subject: [PATCH 08/16] Undo formatting changes --- src/Blazored.FluentValidation/FluentValidationsValidator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index 3ee1bde..cbfbc8a 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -14,7 +14,6 @@ public class FluentValidationValidator : ComponentBase [Parameter] public IValidator? Validator { get; set; } [Parameter] public bool DisableAssemblyScanning { get; set; } [Parameter] public Action>? Options { get; set; } - internal Action>? ValidateOptions { get; set; } public bool Validate(Action>? options = null) From 77f9310ba42a420bc5cad867c397f0599e1281ef Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 21:35:01 +0000 Subject: [PATCH 09/16] Move property path code to static helper --- .../EditContextFluentValidationExtensions.cs | 109 +---------------- .../PropertyPathHelper.cs | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 107 deletions(-) create mode 100644 src/Blazored.FluentValidation/PropertyPathHelper.cs diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 5a7fd78..4a4ac83 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -1,5 +1,4 @@ -using System.Collections; -using FluentValidation; +using FluentValidation; using FluentValidation.Internal; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.DependencyInjection; @@ -79,7 +78,7 @@ private static async Task ValidateField(EditContext editContext, FluentValidationValidator fluentValidationValidator, IValidator? validator = null) { - var propertyPath = ToFluentPropertyPath(editContext, fieldIdentifier); + var propertyPath = PropertyPathHelper.ToFluentPropertyPath(editContext, fieldIdentifier); if (string.IsNullOrEmpty(propertyPath)) { @@ -126,110 +125,6 @@ private static async Task ValidateField(EditContext editContext, } } - private class Node - { - 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(); - nodes.Push(new Node() - { - ModelObject = editContext.Model, - }); - - while (nodes.Any()) - { - var currentNode = nodes.Pop(); - object? currentModelObject = currentNode.ModelObject; - - if (currentModelObject == fieldIdentifier.Model) - { - return BuildPropertyPath(currentNode, fieldIdentifier); - } - - var nonPrimitiveProperties = currentModelObject - .GetType() - .GetProperties() - .Where(prop => !prop.PropertyType.IsPrimitive || prop.PropertyType.IsArray); - - 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); - } - private static IValidator? GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning) { var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); diff --git a/src/Blazored.FluentValidation/PropertyPathHelper.cs b/src/Blazored.FluentValidation/PropertyPathHelper.cs new file mode 100644 index 0000000..36c2a61 --- /dev/null +++ b/src/Blazored.FluentValidation/PropertyPathHelper.cs @@ -0,0 +1,111 @@ +using System.Collections; +using Microsoft.AspNetCore.Components.Forms; + +namespace Blazored.FluentValidation; + +public 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(); + object? currentModelObject = currentNode.ModelObject; + + if (currentModelObject == fieldIdentifier.Model) + { + return BuildPropertyPath(currentNode, fieldIdentifier); + } + + var nonPrimitiveProperties = currentModelObject + .GetType() + .GetProperties() + .Where(prop => !prop.PropertyType.IsPrimitive || prop.PropertyType.IsArray); + + 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 From 43853d0fd942cee746747517b58bc1161c3a3372 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 21:35:55 +0000 Subject: [PATCH 10/16] Use var --- src/Blazored.FluentValidation/PropertyPathHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blazored.FluentValidation/PropertyPathHelper.cs b/src/Blazored.FluentValidation/PropertyPathHelper.cs index 36c2a61..05ba2f0 100644 --- a/src/Blazored.FluentValidation/PropertyPathHelper.cs +++ b/src/Blazored.FluentValidation/PropertyPathHelper.cs @@ -24,7 +24,7 @@ public static string ToFluentPropertyPath(EditContext editContext, FieldIdentifi while (nodes.Any()) { var currentNode = nodes.Pop(); - object? currentModelObject = currentNode.ModelObject; + var currentModelObject = currentNode.ModelObject; if (currentModelObject == fieldIdentifier.Model) { From eec538956f6a328f6c774f27bdc4d559bcf4e346 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 21:38:11 +0000 Subject: [PATCH 11/16] Fix warnings --- .../EditContextFluentValidationExtensions.cs | 2 +- src/Blazored.FluentValidation/PropertyPathHelper.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 4a4ac83..20849e9 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -107,7 +107,7 @@ private static async Task ValidateField(EditContext editContext, }).Selector; var compositeSelector = - new IntersectingCompositeValidatorSelector(new IValidatorSelector[] { fluentValidationValidatorSelector, changedPropertySelector }); + new IntersectingCompositeValidatorSelector(new[] { fluentValidationValidatorSelector, changedPropertySelector }); validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning); diff --git a/src/Blazored.FluentValidation/PropertyPathHelper.cs b/src/Blazored.FluentValidation/PropertyPathHelper.cs index 05ba2f0..0cae2e3 100644 --- a/src/Blazored.FluentValidation/PropertyPathHelper.cs +++ b/src/Blazored.FluentValidation/PropertyPathHelper.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Reflection; using Microsoft.AspNetCore.Components.Forms; namespace Blazored.FluentValidation; @@ -8,7 +9,7 @@ public static class PropertyPathHelper private class Node { public Node? Parent { get; set; } - public object ModelObject { get; set; } + public object? ModelObject { get; set; } public string? PropertyName { get; set; } public int? Index { get; set; } } @@ -31,10 +32,9 @@ public static string ToFluentPropertyPath(EditContext editContext, FieldIdentifi return BuildPropertyPath(currentNode, fieldIdentifier); } - var nonPrimitiveProperties = currentModelObject - .GetType() + var nonPrimitiveProperties = currentModelObject?.GetType() .GetProperties() - .Where(prop => !prop.PropertyType.IsPrimitive || prop.PropertyType.IsArray); + .Where(prop => !prop.PropertyType.IsPrimitive || prop.PropertyType.IsArray) ?? new List(); foreach (var nonPrimitiveProperty in nonPrimitiveProperties) { From 7120051a16a4514f52ac88988f03571382798816 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 21:40:37 +0000 Subject: [PATCH 12/16] Mark helper class as internal --- src/Blazored.FluentValidation/PropertyPathHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blazored.FluentValidation/PropertyPathHelper.cs b/src/Blazored.FluentValidation/PropertyPathHelper.cs index 0cae2e3..b156081 100644 --- a/src/Blazored.FluentValidation/PropertyPathHelper.cs +++ b/src/Blazored.FluentValidation/PropertyPathHelper.cs @@ -4,7 +4,7 @@ namespace Blazored.FluentValidation; -public static class PropertyPathHelper +internal static class PropertyPathHelper { private class Node { From 011afd62611eefc3e7032f11c839c843120373fa Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Wed, 6 Dec 2023 21:41:59 +0000 Subject: [PATCH 13/16] Remove factory change --- .../EditContextFluentValidationExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 20849e9..531c9a2 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -17,9 +17,6 @@ public static void AddFluentValidation(this EditContext editContext, IServicePro { ArgumentNullException.ThrowIfNull(editContext, nameof(editContext)); - ValidatorOptions.Global.ValidatorSelectors.CompositeValidatorSelectorFactory = - (selectors) => new IntersectingCompositeValidatorSelector(selectors); - var messages = new ValidationMessageStore(editContext); editContext.OnValidationRequested += From b0d9a974729ad7a0473bbf5e6f46a48e83a50800 Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Thu, 7 Dec 2023 09:43:59 +0000 Subject: [PATCH 14/16] Undo formatting change --- src/Blazored.FluentValidation/FluentValidationsValidator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Blazored.FluentValidation/FluentValidationsValidator.cs b/src/Blazored.FluentValidation/FluentValidationsValidator.cs index cbfbc8a..2494cea 100644 --- a/src/Blazored.FluentValidation/FluentValidationsValidator.cs +++ b/src/Blazored.FluentValidation/FluentValidationsValidator.cs @@ -10,7 +10,9 @@ namespace Blazored.FluentValidation; public class FluentValidationValidator : ComponentBase { [Inject] private IServiceProvider ServiceProvider { get; set; } = default!; + [CascadingParameter] private EditContext? CurrentEditContext { get; set; } + [Parameter] public IValidator? Validator { get; set; } [Parameter] public bool DisableAssemblyScanning { get; set; } [Parameter] public Action>? Options { get; set; } From a3304ae30a887c1fed96e966844f670aa9987fac Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Thu, 7 Dec 2023 09:46:20 +0000 Subject: [PATCH 15/16] Extract method --- .../EditContextFluentValidationExtensions.cs | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 531c9a2..2ee1363 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -37,20 +37,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; @@ -82,20 +69,7 @@ private static async Task ValidateField(EditContext editContext, return; } - 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 fluentValidationValidatorSelector = context.Selector; var changedPropertySelector = ValidationContext.CreateWithOptions(editContext.Model, strategy => @@ -122,6 +96,27 @@ private static async Task ValidateField(EditContext editContext, } } + 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()); From 2faff2f96902b4d7755f01a0453630a27a44e5af Mon Sep 17 00:00:00 2001 From: Matt Hetherington Date: Thu, 7 Dec 2023 10:08:31 +0000 Subject: [PATCH 16/16] Filter property names for SetValidator rules --- .../EditContextFluentValidationExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs index 2ee1363..88b8668 100644 --- a/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs +++ b/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs @@ -86,6 +86,7 @@ private static async Task ValidateField(EditContext editContext, { 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();