Skip to content

Commit

Permalink
Merge pull request #10 from Liero/feature/v1.1
Browse files Browse the repository at this point in the history
Feature/v1.1
  • Loading branch information
Liero authored Jul 18, 2022
2 parents eff3aa1 + c57e45f commit 52fac03
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 94 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# FluentValidation
A library for using FluentValidation with Blazor that supports **async validation, severity levels** and more.
A battle tested library for using FluentValidation with Blazor that supports **async validation, severity levels** and more.

For introduction, see this [blog post](https://blog.vyvojari.dev/advanced-validation-in-blazor-using-fluentvalidation/)

Expand Down Expand Up @@ -88,7 +88,7 @@ or per FluentValidationValidator component
}
```

See [DefaultValidatorFactory.cs](vNext.BlazorComponents.FluentValidation\DefaultValidatorFactory.cs) for more info.
See [DefaultValidatorFactory.cs](vNext.BlazorComponents.FluentValidation/DefaultValidatorFactory.cs) for more info.

## Validating Complex Models

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#nullable enable

using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;

namespace vNext.BlazorComponents.FluentValidation
{
public class AssemblyScannerValidatorFactory : IValidatorFactory
{
static readonly List<string> ScannedAssembly = new();
static readonly List<AssemblyScanner.AssemblyScanResult> AssemblyScanResults = new();

public IValidator? CreateValidator(ValidatorFactoryContext context)
{

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName)))
{
try
{
AssemblyScanResults.AddRange(AssemblyScanner.FindValidatorsInAssembly(assembly, false));
}
catch (Exception)
{
}

ScannedAssembly.Add(assembly.FullName!);
}


Type modelType = context.Model.GetType();

static int CommonPrefixLength(string? str1, string? str2) =>
(str1 ?? string.Empty).TakeWhile((c, i) => str2?.Length < i && c == str2[c]).Count();


Type? modelValidatorType = AssemblyScanResults.Where(i => context.ValidatorType.IsAssignableFrom(i.InterfaceType))
.OrderByDescending(e => e.ValidatorType.Assembly == modelType.Assembly) //prefer current assebly
.ThenByDescending(e => CommonPrefixLength(e.ValidatorType.FullName, modelType.FullName)) //prefer similar namespace
.ThenBy(e => e.ValidatorType.Namespace?.Length)
.FirstOrDefault()?.ValidatorType;

if (modelValidatorType != null)
{
return (IValidator)ActivatorUtilities.CreateInstance(context.ServiceProvider, modelValidatorType);
}
return null;
}
}
}
51 changes: 11 additions & 40 deletions vNext.BlazorComponents.FluentValidation/DefaultValidatorFactory.cs
Original file line number Diff line number Diff line change
@@ -1,64 +1,35 @@
#nullable enable

using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;

namespace vNext.BlazorComponents.FluentValidation
{

/// <summary>
/// Returns validator from ServiceProvider or by scanning assemlies
/// </summary>
public class DefaultValidatorFactory : IValidatorFactory
{
static readonly List<string> ScannedAssembly = new();
static readonly List<AssemblyScanner.AssemblyScanResult> AssemblyScanResults = new();
AssemblyScannerValidatorFactory? _assemblyScannerValidatorFactory;
ServiceProviderValidatorFactory? _serviceProviderValidatorFactory;

public bool DisableAssemblyScanning { get; set; }
public bool DisableServiceProvider { get; set; }

public IValidator? CreateValidator(ValidatorFactoryContext context)
{
if (context.ServiceProvider.GetService(context.ValidatorType) is IValidator validator)
IValidator? result = null;
if (!DisableServiceProvider)
{
return validator;
_serviceProviderValidatorFactory ??= new();
result = _serviceProviderValidatorFactory.CreateValidator(context);
}
if (!DisableAssemblyScanning)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName)))
{
try
{
AssemblyScanResults.AddRange(AssemblyScanner.FindValidatorsInAssembly(assembly, false));
}
catch (Exception)
{
}

ScannedAssembly.Add(assembly.FullName!);
}


Type modelType = context.Model.GetType();

static int CommonPrefixLength(string? str1, string? str2) =>
(str1 ?? string.Empty).TakeWhile((c, i) => str2?.Length < i && c == str2[c]).Count();


Type? modelValidatorType = AssemblyScanResults.Where(i => context.ValidatorType.IsAssignableFrom(i.InterfaceType))
.OrderByDescending(e => e.ValidatorType.Assembly == modelType.Assembly) //prefer current assebly
.ThenByDescending(e => CommonPrefixLength(e.ValidatorType.FullName, modelType.FullName)) //prefer similar namespace
.ThenBy(e => e.ValidatorType.Namespace?.Length)
.FirstOrDefault()?.ValidatorType;

if (modelValidatorType != null)
{
return (IValidator)ActivatorUtilities.CreateInstance(context.ServiceProvider, modelValidatorType);
}
_assemblyScannerValidatorFactory ??= new();
result ??= _assemblyScannerValidatorFactory.CreateValidator(context);
}

return null;
return result;
}
}
}
146 changes: 97 additions & 49 deletions vNext.BlazorComponents.FluentValidation/FluentValidationValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using static FluentValidation.AssemblyScanner;

namespace vNext.BlazorComponents.FluentValidation
{
Expand All @@ -28,27 +29,61 @@ public class FluentValidationValidator : ComponentBase

[Parameter] public IValidator? Validator { get; set; }

/// <summary>
/// Minimum severity to be treated as an error.
/// For example, if Severity == Error, then any validation messages with Severity warning will be ignored
/// </summary>
[Parameter] public Severity Severity { get; set; } = Severity.Info;

/// <summary>
/// Determines how validator are resolved for <see cref="EditContext.Model"/>, or <see cref="FieldIdentifier.Model"/> in case of complex models
/// </summary>
/// <seealso cref="DefaultValidatorFactory"/>
[Parameter] public IValidatorFactory ValidatorFactory { get; set; } = default!;
[Parameter] public Action<ValidationStrategy<object>>? ValidationStrategyOptions { get; set; }

public EditContext EditContext => CurrentEditContext ?? throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " +
$"inside an {nameof(EditForm)}.");

public ValidationMessageStore ValidationMessageStore => _validationMessageStore ?? throw new InvalidOperationException("FluentValidationValidator not initialized.");

public virtual async Task<bool> Validate()
{
return await EditContext.ValidateAsync();
}

public virtual Task<ValidationResult> ValidateModelAsync(bool updateValidationState = true)
=> ValidateModel(ValidationMessageStore, updateValidationState);

public virtual Task<ValidationResult> ValidateFieldAsync(Expression<Func<object>> accessor, bool updateValidationState = true)
=> ValidateFieldAsync(FieldIdentifier.Create(accessor), updateValidationState);

public virtual async Task<ValidationResult> ValidateFieldAsync(FieldIdentifier fieldIdentifier, bool updateValidationState = true)
=> await ValidateField(ValidationMessageStore, fieldIdentifier, updateValidationState);


public virtual void ClearMessages()
{
_validationMessageStore!.Clear();
_validationMessageStore?.Clear();
validationResults?.Errors?.Clear();
EditContext.NotifyValidationStateChanged();
}

/// <summary>
/// get validator for <see cref="FieldIdentifier.Model"/> of <paramref name="fieldIdentifier"/>.
/// If <paramref name="fieldIdentifier"/> is default, return <see cref="EditContext.Model"/>
/// </summary>
/// <seealso cref="ValidatorFactory"/>
public virtual IValidator? ResolveValidator(FieldIdentifier fieldIdentifier = default)
{
if (EditContext == null) throw new InvalidOperationException("EditContext is null");
object model = fieldIdentifier.Model ?? EditContext.Model;
Type interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
var ctx = new ValidatorFactoryContext(interfaceValidatorType, ServiceProvider, EditContext, model, fieldIdentifier);
return ValidatorFactory.CreateValidator(ctx);
}

protected override void OnInitialized()
{
ValidatorFactory ??= ServiceProvider.GetService<IValidatorFactory>() ?? new DefaultValidatorFactory();
Expand All @@ -57,10 +92,10 @@ protected override void OnInitialized()
EditContext.Properties["ValidationMessageStore"] = _validationMessageStore;

EditContext.OnValidationRequested +=
async (sender, eventArgs) => await ValidateModel(_validationMessageStore);
async (sender, eventArgs) => await ValidateModel(ValidationMessageStore, true);

EditContext.OnFieldChanged +=
async (sender, eventArgs) => await ValidateField(_validationMessageStore, eventArgs.FieldIdentifier);
async (sender, eventArgs) => await ValidateField(ValidationMessageStore, eventArgs.FieldIdentifier, true);
}

protected virtual string MapValidationFailureToMessage(ValidationFailure failure, ValidationResult result, ValidationContext<object> validationContext)
Expand All @@ -72,62 +107,79 @@ protected virtual string MapValidationFailureToMessage(ValidationFailure failure
return $"[{failure.Severity}] {failure.ErrorMessage}";
}

protected virtual IValidator? GetValidator(FieldIdentifier fieldIdentifier = default)
protected virtual async Task<ValidationResult> ValidateModel(ValidationMessageStore messages, bool updateValidationState)
{
if (EditContext == null) throw new InvalidOperationException("EditContext is null");
object model = fieldIdentifier.Model ?? EditContext.Model;
Type interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
var ctx = new ValidatorFactoryContext(interfaceValidatorType, ServiceProvider, EditContext, model, fieldIdentifier);
return ValidatorFactory.CreateValidator(ctx);
}


protected virtual async Task ValidateModel(ValidationMessageStore messages)
{
if (EditContext == null) throw new InvalidOperationException("EditContext is null");

IValidator? validator = GetValidator();
IValidator? validator = ResolveValidator();

if (validator is not null)
{
ValidationContext<object> context = CreateValidationContext(validator);

Task<ValidationResult> validateAsyncTask = validator.ValidateAsync(context);
EditContext.Properties[EditContextExtensions.PROPERTY_VALIDATEASYNCTASK] = validateAsyncTask;
validationResults = await validateAsyncTask;

messages.Clear();
foreach (var failure in validationResults.Errors.Where(f => f.Severity <= Severity))
Task<ValidationResult> validateAsyncTask = validator.ValidateAsync(context);
if (updateValidationState)
{
var fieldIdentifier = ToFieldIdentifier(EditContext, failure.PropertyName);
string errorMessage = MapValidationFailureToMessage(failure, validationResults, context);
messages.Add(fieldIdentifier, errorMessage);
EditContext.Properties[EditContextExtensions.PROPERTY_VALIDATEASYNCTASK] = validateAsyncTask;
}
var validationResults = await validateAsyncTask;
if (updateValidationState)
{
this.validationResults = validationResults;
messages.Clear();
foreach (var failure in validationResults.Errors.Where(f => f.Severity <= Severity))
{
try
{
var fieldIdentifier = ToFieldIdentifier(EditContext, failure.PropertyName);
string errorMessage = MapValidationFailureToMessage(failure, validationResults, context);
messages.Add(fieldIdentifier, errorMessage);
}
catch (InvalidOperationException ex)
{
ServiceProvider.GetService<ILogger<FluentValidationValidator>>()?.LogError(ex, $"An error occured while parsing ValidationFailure(PropertyName={failure.PropertyName})");
}
}

EditContext.NotifyValidationStateChanged();
EditContext.NotifyValidationStateChanged();
}
return validationResults;
}
else
{
var emptyValidationResult = new ValidationResult();
if (updateValidationState)
{
EditContext.Properties[EditContextExtensions.PROPERTY_VALIDATEASYNCTASK] = Task.FromResult(emptyValidationResult);
}
return emptyValidationResult;
}
}

protected virtual async Task ValidateField(ValidationMessageStore messages, FieldIdentifier fieldIdentifier)
protected virtual async Task<ValidationResult> ValidateField(ValidationMessageStore messages, FieldIdentifier fieldIdentifier, bool updateValidationState)
{
var properties = new[] { fieldIdentifier.FieldName };

IValidator? validator = GetValidator(fieldIdentifier);
IValidator? validator = ResolveValidator(fieldIdentifier);

if (validator is not null)
{
var context = CreateValidationContext(validator, fieldIdentifier);
var validationResults = await validator.ValidateAsync(context);

messages.Clear(fieldIdentifier);
var fieldMessages = validationResults.Errors
.Where(failure => failure.Severity <= Severity)
.Select(failure => MapValidationFailureToMessage(failure, validationResults, context));
if (updateValidationState)
{
messages.Clear(fieldIdentifier);
var fieldMessages = validationResults.Errors
.Where(failure => failure.Severity <= Severity)
.Select(failure => MapValidationFailureToMessage(failure, validationResults, context));

messages.Add(fieldIdentifier, fieldMessages);
messages.Add(fieldIdentifier, fieldMessages);
EditContext.NotifyValidationStateChanged();
}

EditContext.NotifyValidationStateChanged();
return validationResults;
}
return new ValidationResult();
}

protected virtual ValidationContext<object> CreateValidationContext(IValidator validator, FieldIdentifier fieldIdentifier = default)
Expand All @@ -145,12 +197,12 @@ protected virtual void ConfigureValidationStrategy(ValidationStrategy<object> op
{
ValidationStrategyOptions(options);
}

if (fieldIdentifier.FieldName is not null)
{
options.IncludeProperties(fieldIdentifier.FieldName);
}

}

protected static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
Expand Down Expand Up @@ -182,7 +234,9 @@ protected static FieldIdentifier ToFieldIdentifier(EditContext editContext, stri
// It's an indexer
// This code assumes C# conventions (one indexer named Item with one param)
nextToken = nextToken.Substring(0, nextToken.Length - 1);
var prop = obj.GetType().GetProperty("Item");

var prop = obj.GetType().GetProperties().Where(e => e.Name == "Item" && e.GetIndexParameters().Length == 1).FirstOrDefault()
?? obj.GetType().GetInterfaces().FirstOrDefault(e => e.IsGenericType && e.GetGenericTypeDefinition() == typeof(IReadOnlyList<>) || e.GetGenericTypeDefinition() == typeof(IList<>))?.GetProperty("Item"); //e.g. arrays

if (prop is not null)
{
Expand All @@ -191,19 +245,13 @@ protected static FieldIdentifier ToFieldIdentifier(EditContext editContext, stri
var indexerValue = Convert.ChangeType(nextToken, indexerType);
newObj = prop.GetValue(obj, new object[] { indexerValue });
}
else if (obj is IEnumerable<object> objEnumerable && int.TryParse(nextToken, out int indexerValue)) //e.g. hashset
{
newObj = objEnumerable.ElementAt(indexerValue);
}
else
{
// If there is no Item property
// Try to cast the object to array
if (obj is object[] array)
{
var indexerValue = Convert.ToInt32(nextToken);
newObj = array[indexerValue];
}
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}.");
}
}
else
Expand Down
Loading

0 comments on commit 52fac03

Please sign in to comment.