Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public static class ServiceCollectionExtensions
/// <param name="serviceCollection">The service collection.</param>
/// <param name="autoValidationEndpointsConfiguration">The configuration delegate used to configure the FluentValidation AutoValidation Endpoints validation.</param>
/// <returns>The service collection.</returns>
public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection serviceCollection,
Action<AutoValidationEndpointsConfiguration>? autoValidationEndpointsConfiguration = null)
public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection serviceCollection, Action<AutoValidationEndpointsConfiguration>? autoValidationEndpointsConfiguration = null)
{
var configuration = new AutoValidationEndpointsConfiguration();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results;
using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions;

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters
{
public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter
public class FluentValidationAutoValidationEndpointFilter(ILogger<FluentValidationAutoValidationEndpointFilter> logger) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext endpointFilterInvocationContext, EndpointFilterDelegate next)
{
Expand All @@ -18,44 +19,60 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter
{
if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator)
{
logger.LogDebug("Starting validation for argument of type '{Type}'.", argument.GetType().Name);

var validatorInterceptor = validator as IValidatorInterceptor;
var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();

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

if (validatorInterceptor != null)
{
logger.LogDebug("Invoking validator interceptor BeforeValidation for argument '{Argument}'.", argument.GetType().Name);
validationContext = await validatorInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext;
}

if (globalValidationInterceptor != null)
{
logger.LogDebug("Invoking global validation interceptor BeforeValidation for argument '{Argument}'.", argument.GetType().Name);
validationContext = await globalValidationInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext;
}

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

if (validatorInterceptor != null)
{
logger.LogDebug("Invoking validator interceptor AfterValidation for argument '{Argument}'.", argument.GetType().Name);
validationResult = await validatorInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult;
}

if (globalValidationInterceptor != null)
{
logger.LogDebug("Invoking global validation interceptor AfterValidation for argument '{Argument}'.", argument.GetType().Name);
validationResult = await globalValidationInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult;
}

if (!validationResult.IsValid)
{
logger.LogDebug("Validation result not valid for argument '{Argument}': {ErrorCount} validation errors found.", argument.GetType().Name, validationResult.Errors.Count);

var fluentValidationAutoValidationResultFactory = serviceProvider.GetService<IFluentValidationAutoValidationResultFactory>();

logger.LogDebug("Creating result for path '{Path}'.", endpointFilterInvocationContext.HttpContext.Request.Path);

if (fluentValidationAutoValidationResultFactory != null)
{
logger.LogTrace("Creating result for path '{Path}' using a custom result factory.", endpointFilterInvocationContext.HttpContext.Request.Path);

return fluentValidationAutoValidationResultFactory.CreateResult(endpointFilterInvocationContext, validationResult);
}

logger.LogTrace("Creating result for path '{Path}' using the default result factory.", endpointFilterInvocationContext.HttpContext.Request.Path);

return new FluentValidationAutoValidationDefaultResultFactory().CreateResult(endpointFilterInvocationContext, validationResult);
}

logger.LogDebug("Validation result valid for argument '{Argument}'.", argument.GetType().Name);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes
{
[AttributeUsage(AttributeTargets.Parameter)]
public class AutoValidateAlwaysAttribute : Attribute
{
}
public class AutoValidateAlwaysAttribute : Attribute;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter)]
public class AutoValidateNeverAttribute : Attribute
{
}
public class AutoValidateNeverAttribute : Attribute;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AutoValidationAttribute : Attribute
{
}
public class AutoValidationAttribute : Attribute;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration;
Expand All @@ -21,27 +22,25 @@

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Filters
{
public class FluentValidationAutoValidationActionFilter : IAsyncActionFilter
public class FluentValidationAutoValidationActionFilter(IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory, IOptions<AutoValidationMvcConfiguration> autoValidationMvcConfiguration, ILogger<FluentValidationAutoValidationActionFilter> logger) : IAsyncActionFilter
{
private readonly IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory;
private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration;

public FluentValidationAutoValidationActionFilter(IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory, IOptions<AutoValidationMvcConfiguration> autoValidationMvcConfiguration)
{
this.fluentValidationAutoValidationResultFactory = fluentValidationAutoValidationResultFactory;
this.autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value;
}
private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value;

public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingContext, ActionExecutionDelegate next)
{
var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor;

logger.LogDebug("Starting validation for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);

if (IsValidController(actionExecutingContext.Controller))
{
var endpoint = actionExecutingContext.HttpContext.GetEndpoint();
var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor;
var serviceProvider = actionExecutingContext.HttpContext.RequestServices;

if (endpoint != null && ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && !endpoint.Metadata.OfType<AutoValidationAttribute>().Any()) || endpoint.Metadata.OfType<AutoValidateNeverAttribute>().Any()))
{
logger.LogDebug("Skipping validation for action '{Action}' on controller '{Controller}' due to validation strategy or AutoValidateNeverAttribute.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);

HandleUnvalidatedEntries(actionExecutingContext);

await next();
Expand All @@ -64,6 +63,8 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC

if (subject != null && parameterType != null && parameterType.IsCustomType() && !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) && serviceProvider.GetValidator(parameterType) is IValidator validator)
{
logger.LogDebug("Validating parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}'.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);

// ReSharper disable once SuspiciousTypeConversion.Global
var validatorInterceptor = validator as IValidatorInterceptor;
var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();
Expand All @@ -72,11 +73,13 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC

if (validatorInterceptor != null)
{
logger.LogDebug("Invoking validator interceptor BeforeValidation for parameter '{Parameter}'.", parameter.Name);
validationContext = await validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext;
}

if (globalValidationInterceptor != null)
{
logger.LogDebug("Invoking global validation interceptor BeforeValidation for parameter '{Parameter}'.", parameter.Name);
validationContext = await globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext;
}

Expand All @@ -85,21 +88,31 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC

if (validatorInterceptor != null)
{
logger.LogDebug("Invoking validator interceptor AfterValidation for parameter '{Parameter}'.", parameter.Name);
validationResult = await validatorInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult;
}

if (globalValidationInterceptor != null)
{
logger.LogDebug("Invoking global validation interceptor AfterValidation for parameter '{Parameter}'.", parameter.Name);
validationResult = await globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult;
}

if (!validationResult.IsValid)
{
logger.LogDebug("Validation result not valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}': {ErrorCount} validation errors found.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName, validationResult.Errors.Count);

foreach (var error in validationResult.Errors)
{
logger.LogTrace("Adding validation error '{ErrorMessage}' for '{ParameterName}' to ModelState.", error.ErrorMessage, parameter.Name);

actionExecutingContext.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
else
{
logger.LogDebug("Validation result valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}'.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);
}
}
}
}
Expand All @@ -108,13 +121,28 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC

if (!actionExecutingContext.ModelState.IsValid)
{
logger.LogDebug("ModelState is not valid for action '{Action}' on controller '{Controller}'. Creating validation problem details.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);

var problemDetailsFactory = serviceProvider.GetRequiredService<ProblemDetailsFactory>();
var validationProblemDetails = problemDetailsFactory.CreateValidationProblemDetails(actionExecutingContext.HttpContext, actionExecutingContext.ModelState);

logger.LogTrace("Creating action result for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);

actionExecutingContext.Result = await fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, validationResults);

if (actionExecutingContext.Result != null)
{
logger.LogTrace("Action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);
}
else
{
logger.LogTrace("No action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);
}

return;
}

logger.LogDebug("ModelState is valid for action '{Action}' on controller '{Controller}'. Proceeding with action execution.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName);
}

await next();
Expand All @@ -126,6 +154,8 @@ private bool IsValidController(object controller)

if (controllerType.HasCustomAttribute<NonControllerAttribute>())
{
logger.LogDebug("Controller '{Controller}' is marked with NonControllerAttribute. Skipping validation.", controllerType.Name);

return false;
}

Expand All @@ -147,11 +177,17 @@ private void HandleUnvalidatedEntries(ActionExecutingContext context)
{
if (autoValidationMvcConfiguration.DisableBuiltInModelValidation)
{
logger.LogDebug("Skipping validation of unvalidated entries due to DisableBuiltInModelValidation being set to true.");

foreach (var modelStateEntry in context.ModelState.Values.Where(modelStateEntry => modelStateEntry.ValidationState == ModelValidationState.Unvalidated))
{
modelStateEntry.ValidationState = ModelValidationState.Skipped;
}
}
else
{
logger.LogDebug("Skipping validation of unvalidated entries due to DisableBuiltInModelValidation being set to false.");
}
}
}
}
Loading
Loading