using BlazorEditForms.Model; using FluentValidation; using FluentValidation.Internal; using FluentValidation.Results; using FluentValidation.Validators; using Microsoft.AspNetCore.Components.Forms; namespace BlazorEditForms; public static class ValidationFailureExtensions { private const string FieldIdentifierKey = $"{nameof(FieldIdentifier)}_Key"; public static FieldIdentifier? GetFieldIdentifier(this ValidationFailure failure) { failure.FormattedMessagePlaceholderValues.TryGetValue(FieldIdentifierKey, out var fieldIdentifier); return (FieldIdentifier?)fieldIdentifier; } public static void SetFieldIdentifier(this ValidationFailure failure, FieldIdentifier fieldIdentifier) { failure.FormattedMessagePlaceholderValues[FieldIdentifierKey] = fieldIdentifier; } } public class CustomFluentValidationManager : IDisposable { static CustomFluentValidationManager() { ValidatorOptions.Global.OnFailureCreated = (failure, context, value, rule, component) => { if (!string.IsNullOrEmpty(rule.PropertyName)) { failure.SetFieldIdentifier(new FieldIdentifier(context.InstanceToValidate, rule.PropertyName)); } return failure; }; } private readonly EventHandler _validationRequestedHandler; private readonly EventHandler _fieldChangedHandler; private readonly EditContext _editContext; private readonly IServiceProvider _serviceProvider; private readonly IValidator _validator; private readonly ValidationMessageStore _validationMessageStore; public CustomFluentValidationManager(EditContext editContext, IServiceProvider serviceProvider) { _editContext = editContext; _serviceProvider = serviceProvider; _validator = new AggregateValidator(); _validationRequestedHandler = async (sender, eventArgs) => await ValidateModel(); _editContext.OnValidationRequested += _validationRequestedHandler; _fieldChangedHandler = async (sender, eventArgs) => await ValidateField(eventArgs.FieldIdentifier); _editContext.OnFieldChanged += _fieldChangedHandler; // This validation message store is _internally_ connected to the edit context and essentially serves as a // separate API to manage the validation messages that can (with public methods) only be _extracted from_ the // edit context. _validationMessageStore = new ValidationMessageStore(_editContext); } private async Task ValidateModel() { var validationContext = ValidationContext.CreateWithOptions(_editContext.Model, strategy => { strategy.IncludeRulesNotInRuleSet(); }); var validationResult = await _validator.ValidateAsync(validationContext); _validationMessageStore.Clear(); foreach (var failure in validationResult.Errors) { var fieldIdentifier = failure.GetFieldIdentifier(); if (fieldIdentifier.HasValue) { _validationMessageStore.Add(fieldIdentifier.Value, failure.ErrorMessage); } } _editContext.NotifyValidationStateChanged(); } private async Task ValidateField(FieldIdentifier fieldIdentifier) { var validationContext = ValidationContext.CreateWithOptions(_editContext.Model, strategy => { strategy.UseCustomSelector(new FieldIdentifierSelector(fieldIdentifier)); }); var validationResult = await _validator.ValidateAsync(validationContext); _validationMessageStore.Clear(fieldIdentifier); foreach (var failure in validationResult.Errors) { var failureIdentifier = failure.GetFieldIdentifier(); if (fieldIdentifier.Equals(failureIdentifier)) { _validationMessageStore.Add(fieldIdentifier, failure.ErrorMessage); } } _editContext.NotifyValidationStateChanged(); } private class FieldIdentifierSelector : IValidatorSelector { private readonly FieldIdentifier _fieldIdentifier; public FieldIdentifierSelector(FieldIdentifier fieldIdentifier) { _fieldIdentifier = fieldIdentifier; } public bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context) { return rule.Components.Any(c => c.Validator is IChildValidatorAdaptor) || (context.InstanceToValidate == _fieldIdentifier.Model && rule.PropertyName == _fieldIdentifier.FieldName); } } public void Dispose() { _editContext.OnValidationRequested -= _validationRequestedHandler; _editContext.OnFieldChanged -= _fieldChangedHandler; } }