Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
<LangVersion>9.0</LangVersion>
<NoWarn>NU1701</NoWarn>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,11 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService
serviceCollection.Configure(autoValidationMvcConfiguration);
}

if (configuration.DisableBuiltInModelValidation)
{
serviceCollection.AddSingleton<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceProvider =>
new FluentValidationAutoValidationObjectModelValidator(
serviceProvider.GetRequiredService<IModelMetadataProvider>(),
serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value.ModelValidatorProviders,
configuration.DisableBuiltInModelValidation));
}
serviceCollection.AddSingleton<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceProvider =>
new FluentValidationAutoValidationObjectModelValidator(
serviceProvider.GetRequiredService<IModelMetadataProvider>(),
serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value.ModelValidatorProviders,
configuration.DisableBuiltInModelValidation));

// Add the default result factory.
serviceCollection.AddScoped<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Enums;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Results;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation;
using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Filters
Expand Down Expand Up @@ -55,45 +55,16 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC
if (actionExecutingContext.ActionArguments.TryGetValue(parameter.Name, out var subject))
{
var parameterInfo = (parameter as ControllerParameterDescriptor)?.ParameterInfo;
var parameterType = subject?.GetType();
var bindingSource = parameter.BindingInfo?.BindingSource;

var hasAutoValidateAlwaysAttribute = parameterInfo?.HasCustomAttribute<AutoValidateAlwaysAttribute>() ?? false;
var hasAutoValidateNeverAttribute = parameterInfo?.HasCustomAttribute<AutoValidateNeverAttribute>() ?? false;

if (subject != null && parameterType != null && parameterType.IsCustomType() &&
!hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) &&
serviceProvider.GetValidator(parameterType) is IValidator validator)
if (!hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)))
{
// ReSharper disable once SuspiciousTypeConversion.Global
var validatorInterceptor = validator as IValidatorInterceptor;
var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();

IValidationContext validationContext = new ValidationContext<object>(subject);

if (validatorInterceptor != null)
{
validationContext = validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext;
}

if (globalValidationInterceptor != null)
{
validationContext = globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext;
}

var validationResult = await validator.ValidateAsync(validationContext, actionExecutingContext.HttpContext.RequestAborted);

if (validatorInterceptor != null)
{
validationResult = validatorInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult;
}

if (globalValidationInterceptor != null)
{
validationResult = globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult;
}

if (!validationResult.IsValid)
var validationResult = await FluentValidationHelper.ValidateWithFluentValidationAsync(
serviceProvider, subject, actionExecutingContext);
if (validationResult != null && !validationResult.IsValid)
{
foreach (var error in validationResult.Errors)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,29 @@ public class FluentValidationAutoValidationObjectModelValidator : ObjectModelVal
{
private readonly bool disableBuiltInModelValidation;

public FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList<IModelValidatorProvider> validatorProviders, bool disableBuiltInModelValidation)
public FluentValidationAutoValidationObjectModelValidator(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@icnocop keep existing formatting

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fd34ec6

Thank you.

IModelMetadataProvider modelMetadataProvider,
IList<IModelValidatorProvider> validatorProviders,
bool disableBuiltInModelValidation)
: base(modelMetadataProvider, validatorProviders)
{
this.disableBuiltInModelValidation = disableBuiltInModelValidation;
}

public override ValidationVisitor GetValidationVisitor(ActionContext actionContext,
public override ValidationVisitor GetValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary? validationState)
{
return new FluentValidationAutoValidationValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState, disableBuiltInModelValidation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@icnocop keep existing formatting

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fd34ec6

Thank you.

return new FluentValidationAutoValidationValidationVisitor(
actionContext,
validatorProvider,
validatorCache,
metadataProvider,
validationState,
disableBuiltInModelValidation);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,82 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation
{
public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor
{
private readonly ActionContext actionContext;
private readonly bool disableBuiltInModelValidation;

public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext,
public FluentValidationAutoValidationValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary? validationState,
bool disableBuiltInModelValidation)
: base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState)
{
this.actionContext = actionContext;
this.disableBuiltInModelValidation = disableBuiltInModelValidation;
}

public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel)
{
// If built in model validation is disabled return true for later validation in the action filter.
return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel);
bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel);
return ValidateAsync(isBaseValid, key, model).GetAwaiter().GetResult();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@icnocop not sure about this one, won't this result in deadlocks?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ASP.NET Core, it should not cause a deadlock, according to ChatGPT 4.1.

image

In which scenario(s) are you thinking it could cause a deadlock?
Maybe a test can be written to verify it.

}

#if !NETCOREAPP3_1
public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel, object? container)
{
// If built in model validation is disabled return true for later validation in the action filter.
return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container);
bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container);
return ValidateAsync(isBaseValid, key, model).GetAwaiter().GetResult();
}
#endif

private async Task<bool> ValidateAsync(
bool defaultValue,
string? key,
object? model)
{
if (model == null)
{
return defaultValue;
}

var actionExecutingContext = new ActionExecutingContext(
actionContext,
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
null);

var validationResult = await FluentValidationHelper.ValidateWithFluentValidationAsync(
actionContext.HttpContext.RequestServices,
model,
actionExecutingContext);
if (validationResult == null)
{
return defaultValue;
}

foreach (var error in validationResult.Errors)
{
var keyName = string.IsNullOrEmpty(key) ? error.PropertyName : $"{key}.{error.PropertyName}";
if (!this.ModelState[keyName]?.Errors.Any(e => e.ErrorMessage == error.ErrorMessage) ?? true)
{
this.ModelState.AddModelError(keyName, error.ErrorMessage);
}
}

return defaultValue && validationResult.IsValid;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Threading.Tasks;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors;
using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation
{
public static class FluentValidationHelper
{
public static async Task<ValidationResult?> ValidateWithFluentValidationAsync(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@icnocop this method feels kinda hacky by taking a IServiceProvider as an argument, what do you think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What else do you recommend?

IServiceProvider is needed to dynamically get the validator based on the type of the model.

IServiceProvider serviceProvider,
object? model,
ActionExecutingContext actionExecutingContext)
{
if (model == null)
{
return null;
}

var modelType = model.GetType();
if (modelType == null)
{
return null;
}

if (!modelType.IsCustomType())
{
return null;
}

var validator = serviceProvider.GetValidator(modelType) as IValidator;
if (validator == null)
{
return null;
}

IValidationContext validationContext = new ValidationContext<object>(model);

var validatorInterceptor = validator as IValidatorInterceptor;
if (validatorInterceptor != null)
{
validationContext = validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext;
}

var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();
if (globalValidationInterceptor != null)
{
validationContext = globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext;
}

var validationResult = await validator.ValidateAsync(validationContext, actionExecutingContext.HttpContext.RequestAborted);

if (validatorInterceptor != null)
{
validationResult = validatorInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult;
}

if (globalValidationInterceptor != null)
{
validationResult = globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult;
}

return validationResult;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void TestAddFluentValidationAutoValidation()

AssertNotContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, TestResultFactory>(serviceCollection, ServiceLifetime.Scoped);
AssertContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>(serviceCollection, ServiceLifetime.Scoped);
AssertNotContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
AssertContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
AssertContainsServiceDescriptor<IConfigureOptions<MvcOptions>, ConfigureNamedOptions<MvcOptions>>(serviceCollection, ServiceLifetime.Singleton);
}

Expand All @@ -34,7 +34,7 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_OverriddenRe

AssertContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, TestResultFactory>(serviceCollection, ServiceLifetime.Scoped);
AssertNotContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>(serviceCollection, ServiceLifetime.Scoped);
AssertNotContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
AssertContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
AssertContainsServiceDescriptor<IConfigureOptions<MvcOptions>, ConfigureNamedOptions<MvcOptions>>(serviceCollection, ServiceLifetime.Singleton);
}

Expand All @@ -47,7 +47,7 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuilt

AssertNotContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, TestResultFactory>(serviceCollection, ServiceLifetime.Scoped);
AssertContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>(serviceCollection, ServiceLifetime.Scoped);
AssertNotContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
AssertContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
AssertContainsServiceDescriptor<IConfigureOptions<MvcOptions>, ConfigureNamedOptions<MvcOptions>>(serviceCollection, ServiceLifetime.Singleton);
}

Expand Down
Loading