Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
@@ -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
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation;
using Xunit;

Expand All @@ -19,9 +26,67 @@ public void TestGetValidationVisitor()
var modelValidatorProvider = Substitute.For<IModelValidatorProvider>();
var validatorCache = Substitute.For<ValidatorCache>();

var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator(modelMetadataProvider, modelMetadataProviders, true);
var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator(
modelMetadataProvider, modelMetadataProviders, true);

Assert.IsType<FluentValidationAutoValidationValidationVisitor>(
fluentValidationAutoValidationObjectModelValidator.GetValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null));
}

[Fact]
public void TryValidateModel_WithInvalidModel_ShouldUpdateModelState()
{
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddControllersWithViews();
serviceCollection.AddFluentValidationAutoValidation();
serviceCollection.AddTransient<IValidator<Test1Controller.Action1ViewModel>, Test1Controller.Action1ViewModel.Action1ViewModelValidator>();
var serviceProvider = serviceCollection.BuildServiceProvider();

var viewModel = new Test1Controller.Action1ViewModel
{
ValueMustEqual1 = 0 // Invalid value to trigger validation error
};

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
var routeData = Substitute.For<RouteData>();
var actionDescriptor = Substitute.For<ControllerActionDescriptor>();
var actionContext = new ActionContext(httpContext, routeData, actionDescriptor);
var controller = new Test1Controller
{
ControllerContext = new ControllerContext(actionContext)
};

// Act
bool result = controller.TryValidateModel(viewModel);

// Assert
Assert.False(result);
Assert.False(controller.ModelState.IsValid);
Assert.True(controller.ModelState.ContainsKey(nameof(Test1Controller.Action1ViewModel.ValueMustEqual1)));
var modelError = controller.ModelState[nameof(Test1Controller.Action1ViewModel.ValueMustEqual1)]!.Errors.Single();
Assert.NotNull(modelError);
Assert.Equal("'ValueMustEqual1' must be equal to '1'.", modelError.ErrorMessage);
}

public class Test1Controller : Controller
{
public class Action1ViewModel
{
public int ValueMustEqual1 { get; set; }

internal class Action1ViewModelValidator : AbstractValidator<Action1ViewModel>
{
public Action1ViewModelValidator()
{
this.RuleFor(x => x.ValueMustEqual1)
.Equal(1)
.WithName(nameof(ValueMustEqual1));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using NSubstitute;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation;
using Xunit;
Expand All @@ -14,11 +17,23 @@ public void TestGetValidationVisitor()
{
var modelMetadataProvider = Substitute.For<IModelMetadataProvider>();
var actionContext = Substitute.For<ActionContext>();
var httpContext = Substitute.For<HttpContext>();
var routeData = Substitute.For<RouteData>();
var actionDescriptor = Substitute.For<ActionDescriptor>();
actionContext.HttpContext = httpContext;
actionContext.RouteData = routeData;
actionContext.ActionDescriptor = actionDescriptor;
var modelValidatorProvider = Substitute.For<IModelValidatorProvider>();
var validatorCache = Substitute.For<ValidatorCache>();

var fluentValidationAutoValidationObjectModelValidator =
new FluentValidationAutoValidationValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null, true);
new FluentValidationAutoValidationValidationVisitor(
actionContext,
modelValidatorProvider,
validatorCache,
modelMetadataProvider,
null,
true);

#if NETCOREAPP3_1
Assert.True(fluentValidationAutoValidationObjectModelValidator.Validate(null, null, new TestModel(), true));
Expand Down