Skip to content

Commit

Permalink
Updated support for fluent validation to follow latest updates (#1172)
Browse files Browse the repository at this point in the history
* Updated support for fluent validation to follow latest updates

* some fluent validator blazor fixes
  • Loading branch information
david-driscoll authored Jun 29, 2022
1 parent 66773e7 commit b7b7ed7
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 197 deletions.
3 changes: 2 additions & 1 deletion Directory.Packages.support.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<Project>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'netcoreapp3.1'">
<PackageVersion Update="Microsoft.AspNetCore.Components.Web" Version="3.1.26" />
Expand All @@ -8,5 +8,6 @@
<PackageVersion Update="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.26" />
<PackageVersion Update="System.Net.Http.Json" Version="3.2.1" />
<PackageVersion Update="Microsoft.Extensions.Hosting" Version="3.1.26" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="4.7.0" />
</ItemGroup>
</Project>
4 changes: 4 additions & 0 deletions sample/Sample.Restful.Client/Sample.Restful.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
/>
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NSwag.ApiDescription.Client" />
<PackageReference
Include="System.ComponentModel.Annotations"
Condition="'$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'netcoreapp3.1'"
/>
</ItemGroup>
<ItemGroup>
<OpenApiProjectReference
Expand Down
92 changes: 44 additions & 48 deletions src/AspNetCore.Blazor/Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Blazor;
/// </summary>
public class FluentValidator : ComponentBase
{
private static readonly char[] separators = { '.', '[' };
private static readonly char[] _separators = { '.', '[' };

private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
{
Expand All @@ -27,7 +27,7 @@ private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string

while (true)
{
var nextTokenEnd = propertyPath.IndexOfAny(separators);
var nextTokenEnd = propertyPath.IndexOfAny(_separators);
if (nextTokenEnd < 0)
{
return new FieldIdentifier(obj, propertyPath);
Expand Down Expand Up @@ -69,87 +69,83 @@ private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string
}
}

/// <summary>
/// The validator to validate against
/// </summary>
[Parameter]
public IValidator Validator { get; set; } = null!;

[Inject] private IValidatorFactory ValidatorFactory { get; set; } = null!;

[CascadingParameter] private EditContext CurrentEditContext { get; set; } = null!;

/// <inheritdoc />
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException(
$"{nameof(FluentValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidator)} " +
$"inside an {nameof(EditForm)}."
);
}

AddFluentValidation(Validator);
}

private void AddFluentValidation(IValidator validator)
private static void AddFluentValidation(IValidator? validator, EditContext editContext, IServiceProvider services)
{
var messages = new ValidationMessageStore(CurrentEditContext);
var messages = new ValidationMessageStore(editContext);

CurrentEditContext.OnValidationRequested +=
(_, _) => ValidateModel(messages, validator);
editContext.OnValidationRequested +=
(_, _) => ValidateModel(messages, editContext, validator ?? services.GetValidator(editContext.Model.GetType()));

CurrentEditContext.OnFieldChanged +=
(_, eventArgs) => ValidateField(messages, eventArgs.FieldIdentifier, validator);
editContext.OnFieldChanged +=
(_, eventArgs) => ValidateField(messages, editContext, eventArgs.FieldIdentifier, validator ?? services.GetValidator(editContext.Model.GetType()));
}

private async void ValidateModel(ValidationMessageStore messages, IValidator? validator = null)
private static async void ValidateModel(
ValidationMessageStore messages,
EditContext editContext,
IValidator? validator = null
)
{
validator ??= GetValidatorForModel(CurrentEditContext.Model);

if (validator != null)
{
var context = new ValidationContext<object>(CurrentEditContext.Model);
var context = new ValidationContext<object>(editContext.Model);

var validationResults = await validator.ValidateAsync(context);

messages.Clear();
foreach (var validationResult in validationResults.Errors)
{
var fieldIdentifier = ToFieldIdentifier(CurrentEditContext, validationResult.PropertyName);
var fieldIdentifier = ToFieldIdentifier(editContext, validationResult.PropertyName);
messages.Add(fieldIdentifier, validationResult.ErrorMessage);
}

CurrentEditContext.NotifyValidationStateChanged();
editContext.NotifyValidationStateChanged();
}
}

private async void ValidateField(
private static async void ValidateField(
ValidationMessageStore messages,
EditContext editContext,
FieldIdentifier fieldIdentifier,
IValidator? validator = null
)
{
var properties = new[] { fieldIdentifier.FieldName };
var context = new ValidationContext<object>(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));

validator ??= GetValidatorForModel(fieldIdentifier.Model);

if (validator != null)
{
var properties = new[] { fieldIdentifier.FieldName };
var context = new ValidationContext<object>(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));
var validationResults = await validator.ValidateAsync(context);

messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));

CurrentEditContext.NotifyValidationStateChanged();
editContext.NotifyValidationStateChanged();
}
}

private IValidator? GetValidatorForModel(object? model)
/// <summary>
/// The validator to validate against
/// </summary>
[Parameter]
[DisallowNull]
public IValidator? Validator { get; set; } = null!;

[Inject] private IServiceProvider Services { get; set; } = null!;

[CascadingParameter] private EditContext CurrentEditContext { get; set; } = null!;

/// <inheritdoc />
protected override void OnInitialized()
{
return model == null ? null : ValidatorFactory.GetValidator(model.GetType());
if (CurrentEditContext == null)
{
throw new InvalidOperationException(
$"{nameof(FluentValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidator)} " +
$"inside an {nameof(EditForm)}."
);
}

AddFluentValidation(Validator, CurrentEditContext, Services);
}
}
17 changes: 0 additions & 17 deletions src/AspNetCore/Conventions/AspNetCoreConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,6 @@ private static IEnumerable<Assembly> GetAssemblyClosure(Assembly assembly)
}
}

private readonly ValidatorConfiguration? _validatorConfiguration;
private readonly FluentValidationMvcConfiguration? _validationMvcConfiguration;

/// <summary>
/// Configure aspnet with some logical defaults
/// </summary>
/// <param name="validatorConfiguration"></param>
/// <param name="validationMvcConfiguration"></param>
public AspNetCoreConvention(
ValidatorConfiguration? validatorConfiguration = null,
FluentValidationMvcConfiguration? validationMvcConfiguration = null
)
{
_validatorConfiguration = validatorConfiguration;
_validationMvcConfiguration = validationMvcConfiguration;
}

/// <summary>
/// Registers the specified context.
/// </summary>
Expand Down
6 changes: 1 addition & 5 deletions src/AspNetCore/Conventions/FluentValidationConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,7 @@ static bool getNullableValue(Nullability nullability, Type propertyType)
public void Register(IConventionContext context, IConfiguration configuration, IServiceCollection services)
{
services.WithMvcCore()
.AddFluentValidation(
config => config.ValidatorFactoryType ??= typeof(ValidatorFactory)
);
services.RemoveAll<IValidatorFactory>();
services.AddSingleton<IValidatorFactory, ValidatorFactory>();
.AddFluentValidation();
services.AddSingleton<IValidatorInterceptor, ValidatorInterceptor>();
services
.Configure<MvcOptions>(mvcOptions => mvcOptions.Filters.Insert(0, new ValidationExceptionFilter()))
Expand Down
6 changes: 0 additions & 6 deletions src/Foundation/Conventions/FluentValidationConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,6 @@ public void Register(IConventionContext context, IConfiguration configuration, I
services.TryAddEnumerable(ServiceDescriptor.Describe(item.InterfaceType, item.ValidatorType, ServiceLifetime.Singleton));
}

if (services.FirstOrDefault(z => z.ServiceType == typeof(IValidatorFactory)) is { } s && s.Lifetime != ServiceLifetime.Singleton)
{
services.Remove(s);
}

services.TryAdd(ServiceDescriptor.Describe(typeof(IValidatorFactory), typeof(ValidatorFactory), ServiceLifetime.Singleton));
services.TryAddEnumerable(ServiceDescriptor.Describe(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>), _options.MediatorLifetime));
services.TryAddEnumerable(
ServiceDescriptor.Describe(typeof(IStreamPipelineBehavior<,>), typeof(ValidationStreamPipelineBehavior<,>), _options.MediatorLifetime)
Expand Down
23 changes: 23 additions & 0 deletions src/Foundation/FluentValidationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,27 @@ this IRuleBuilder<T, TProperty> builder

return builder.SetAsyncValidator(new PolymorphicPropertyValidator<T, TProperty>());
}

/// <summary>
/// Get a fluent validation validator if defined.
/// </summary>
/// <param name="serviceProvider"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IValidator<T>? GetValidator<T>(this IServiceProvider serviceProvider)
{
return GetValidator(serviceProvider, typeof(T)) as IValidator<T> ?? null;
}

/// <summary>
/// Get a fluent validation validator if defined.
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="type"></param>
/// <returns></returns>
public static IValidator? GetValidator(this IServiceProvider serviceProvider, Type? type)
{
if (type is null) return null;
return serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(type)) as IValidator ?? null;
}
}
2 changes: 1 addition & 1 deletion src/Foundation/Validation/PolymorphicPropertyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public override async Task<bool> IsValidAsync(ValidationContext<T> context, TPro
// bail out if the property is null
if (value is not { }) return true;

var factory = context.GetServiceProvider().GetRequiredService<IValidatorFactory>();
var factory = context.GetServiceProvider().GetService<IValidatorFactory>();
var validator = factory.GetValidator(value.GetType());
return validator is null || ( await validator.ValidateAsync(context, cancellation).ConfigureAwait(false) ).IsValid;
}
Expand Down
28 changes: 10 additions & 18 deletions src/Foundation/Validation/ValidationPipelineBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,20 @@ namespace Rocket.Surgery.LaunchPad.Foundation.Validation;

internal class ValidationPipelineBehavior<T, R> : IPipelineBehavior<T, R> where T : IRequest<R>
{
private readonly IValidatorFactory _validatorFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IValidator<T>? _validator;

public ValidationPipelineBehavior(IValidatorFactory validatorFactory, IServiceProvider serviceProvider)
public ValidationPipelineBehavior(IValidator<T>? validator = null)
{
_validatorFactory = validatorFactory;
_serviceProvider = serviceProvider;
_validator = validator;
}

public async Task<R> Handle(T request, CancellationToken cancellationToken, RequestHandlerDelegate<R> next)
{
var validator = _validatorFactory.GetValidator<T>();
if (validator != null)
if (_validator is not null)
{
var context = new ValidationContext<T>(request);
context.SetServiceProvider(_serviceProvider);

var response = await validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
var response = await _validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
{
throw new ValidationException(response.Errors);
Expand All @@ -36,24 +32,20 @@ public async Task<R> Handle(T request, CancellationToken cancellationToken, Requ

internal class ValidationStreamPipelineBehavior<T, R> : IStreamPipelineBehavior<T, R> where T : IStreamRequest<R>
{
private readonly IValidatorFactory _validatorFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IValidator<T>? _validator;

public ValidationStreamPipelineBehavior(IValidatorFactory validatorFactory, IServiceProvider serviceProvider)
public ValidationStreamPipelineBehavior(IValidator<T>? validator = null)
{
_validatorFactory = validatorFactory;
_serviceProvider = serviceProvider;
_validator = validator;
}

public async IAsyncEnumerable<R> Handle(T request, [EnumeratorCancellation] CancellationToken cancellationToken, StreamHandlerDelegate<R> next)
{
var validator = _validatorFactory.GetValidator<T>();
if (validator != null)
if (_validator is not null)
{
var context = new ValidationContext<T>(request);
context.SetServiceProvider(_serviceProvider);

var response = await validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
var response = await _validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
{
throw new ValidationException(response.Errors);
Expand Down
53 changes: 0 additions & 53 deletions src/Foundation/Validation/ValidatorFactory.cs

This file was deleted.

Loading

0 comments on commit b7b7ed7

Please sign in to comment.