Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add customer result factory logic #6

Merged
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 @@ -10,7 +10,7 @@
<PackageId>SharpGrip.FluentValidation.AutoValidation.Endpoints</PackageId>
<Title>SharpGrip FluentValidation AutoValidation Endpoints</Title>
<Description>SharpGrip FluentValidation AutoValidation Endpoints is an extension of the FluentValidation library enabling automatic asynchronous validation in minimal APIs (endpoints).</Description>
<PackageTags>sharpgrip;validation;fluent-validation;minimal-api</PackageTags>
<PackageTags>sharpgrip;validation;fluent-validation;endpoints;minimal-api</PackageTags>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Microsoft.AspNetCore.Http.HttpResults;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results;

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Configuration
{
public class AutoValidationEndpointsConfiguration
{
/// <summary>
/// Holds the overridden result factory. This property is meant for infrastructure and should not be used by application code.
/// </summary>
public Type? OverriddenResultFactory { get; private set; }

/// <summary>
/// Overrides the default result factory with a custom result factory. Custom result factories are required to implement <see cref="IFluentValidationAutoValidationResultFactory"/>.
/// The default result factory returns the validation errors wrapped in a <see cref="ValidationProblem"/> object.
/// </summary>
/// <see cref="FluentValidationAutoValidationDefaultResultFactory"/>
/// <typeparam name="TResultFactory">The custom result factory implementing <see cref="IFluentValidationAutoValidationResultFactory"/>.</typeparam>
public void OverrideDefaultResultFactoryWith<TResultFactory>() where TResultFactory : IFluentValidationAutoValidationResultFactory
{
OverriddenResultFactory = typeof(TResultFactory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions
public static class EndpointRouteExtensions
{
/// <summary>
/// Adds asynchronous minimal API automatic validation to the specified <see cref="T:Microsoft.AspNetCore.Builder.RouteHandlerBuilder" />.
/// Adds asynchronous minimal API automatic validation to the specified <see cref="RouteHandlerBuilder" />.
/// </summary>
/// <param name="routeHandlerBuilder">The route handler builder.</param>
/// <returns>The route handler builder.</returns>
Expand All @@ -20,7 +20,7 @@ public static RouteHandlerBuilder AddFluentValidationAutoValidation(this RouteHa
}

/// <summary>
/// Adds asynchronous minimal API Fluent Validation automatic validation to the specified <see cref="T:Microsoft.AspNetCore.Routing.RouteGroupBuilder" />.
/// Adds asynchronous minimal API Fluent Validation automatic validation to the specified <see cref="RouteGroupBuilder" />.
/// </summary>
/// <param name="routeGroupBuilder">The route group builder.</param>
/// <returns>The route group builder.</returns>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Configuration;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results;

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds asynchronous Endpoints Fluent Validation automatic validation to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <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)
{
var configuration = new AutoValidationEndpointsConfiguration();

if (autoValidationEndpointsConfiguration != null)
{
autoValidationEndpointsConfiguration.Invoke(configuration);
serviceCollection.Configure(autoValidationEndpointsConfiguration);
}

// Add the default result factory.
serviceCollection.AddScoped<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>();

// If the custom result factory is not null, replace the default result factory with the overridden result factory.
if (configuration.OverriddenResultFactory != null)
{
serviceCollection.Replace(new ServiceDescriptor(typeof(IFluentValidationAutoValidationResultFactory), configuration.OverriddenResultFactory, ServiceLifetime.Scoped));
}

return serviceCollection;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results;
using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions;

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters
Expand All @@ -29,14 +29,14 @@ public FluentValidationAutoValidationEndpointFilter(IServiceProvider serviceProv

if (!validationResult.IsValid)
{
var errors = new Dictionary<string, string[]>();
var fluentValidationAutoValidationResultFactory = serviceProvider.GetService<IFluentValidationAutoValidationResultFactory>();

foreach (var errorGrouping in validationResult.Errors.GroupBy(error => error.PropertyName))
if (fluentValidationAutoValidationResultFactory != null)
{
errors.Add(errorGrouping.Key, errorGrouping.Select(error => error.ErrorMessage).ToArray());
return fluentValidationAutoValidationResultFactory.CreateResult(context, validationResult);
}

return Results.ValidationProblem(errors);
return new FluentValidationAutoValidationDefaultResultFactory().CreateResult(context, validationResult);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using FluentValidation.Results;
using Microsoft.AspNetCore.Http;
using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions;

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Results
{
public class FluentValidationAutoValidationDefaultResultFactory : IFluentValidationAutoValidationResultFactory
{
public IResult CreateResult(EndpointFilterInvocationContext context, ValidationResult validationResult)
{
return TypedResults.ValidationProblem(validationResult.ToValidationProblemErrors());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation.Results;
using Microsoft.AspNetCore.Http;

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Results
{
public interface IFluentValidationAutoValidationResultFactory
{
/// <summary>
/// Creates an <see cref="IResult"/> object to be executed by the endpoint.
/// </summary>
/// <param name="context">The <see cref="EndpointFilterInvocationContext"/> associated with the current request/response.</param>
/// <param name="validationResult">The <see cref="ValidationResult"/> object containing the validation failures.</param>
/// <returns>The <see cref="IResult"/> object to be executed by the endpoint.</returns>
public IResult CreateResult(EndpointFilterInvocationContext context, ValidationResult validationResult);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
using SharpGrip.FluentValidation.AutoValidation.Mvc.Enums;
using System;
using Microsoft.AspNetCore.Mvc;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Enums;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Results;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration
{
public class AutoValidationMvcConfiguration
{
/// <summary>
/// Disables the built-in model validation.
/// Disables the built-in .NET model (data annotations) validation.
/// </summary>
public bool DisableBuiltInModelValidation { get; set; }

/// <summary>
/// Configures the validation strategy. Validation strategy <see cref="T:SharpGrip.FluentValidation.AutoValidation.Mvc.Enums.ValidationStrategy"/> enables asynchronous automatic validation on all controllers inheriting from <see cref="T:Microsoft.AspNetCore.Mvc.ControllerBase"/>.
/// Validation strategy <see cref="SharpGrip.FluentValidation.AutoValidation.Mvc.Enums.ValidationStrategy.Annotations"/> enables asynchronous automatic validation on controllers inheriting from <see cref="T:Microsoft.AspNetCore.Mvc.ControllerBase"/> decorated (class or method) with a <see cref="T:SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes.FluentValidationAutoValidationAttribute"/> attribute.
/// Configures the validation strategy. Validation strategy <see cref="Enums.ValidationStrategy.All"/> enables asynchronous automatic validation on all controllers inheriting from <see cref="ControllerBase"/>.
/// Validation strategy <see cref="Enums.ValidationStrategy.Annotations"/> enables asynchronous automatic validation on controllers inheriting from <see cref="ControllerBase"/> decorated (class or method) with a <see cref="FluentValidationAutoValidationAttribute"/> attribute.
/// </summary>
public ValidationStrategy ValidationStrategy { get; set; } = ValidationStrategy.All;

/// <summary>
/// Holds the overridden result factory. This property is meant for infrastructure and should not be used by application code.
/// </summary>
public Type? OverriddenResultFactory { get; private set; }

/// <summary>
/// Overrides the default result factory with a custom result factory. Custom result factories are required to implement <see cref="IFluentValidationAutoValidationResultFactory"/>.
/// The default result factory returns the default <see cref="ValidationProblemDetails"/> object wrapped in a <see cref="BadRequestObjectResult"/>>.
/// </summary>
/// <see cref="FluentValidationAutoValidationDefaultResultFactory"/>
/// <typeparam name="TResultFactory">The custom result factory implement <see cref="IFluentValidationAutoValidationResultFactory"/>.</typeparam>
public void OverrideDefaultResultFactoryWith<TResultFactory>() where TResultFactory : IFluentValidationAutoValidationResultFactory
{
OverriddenResultFactory = typeof(TResultFactory);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Enums
using Microsoft.AspNetCore.Mvc;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Enums
{
public enum ValidationStrategy
{
/// <summary>
/// Enables asynchronous automatic validation on all controllers inheriting from <see cref="T:Microsoft.AspNetCore.Mvc.ControllerBase"/>.
/// Enables asynchronous automatic validation on all controllers inheriting from <see cref="ControllerBase"/>.
/// </summary>
All = 1,

/// <summary>
/// Enables asynchronous automatic validation on controllers inheriting from <see cref="T:Microsoft.AspNetCore.Mvc.ControllerBase"/> decorated with a <see cref="T:SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes.FluentValidationAutoValidationAttribute"/> attribute.
/// Enables asynchronous automatic validation on controllers inheriting from <see cref="ControllerBase"/> decorated with a <see cref="FluentValidationAutoValidationAttribute"/> attribute.
/// </summary>
Annotations = 2
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Filters;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Results;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds asynchronous MVC Fluent Validation automatic validation to the specified <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" />.
/// Adds asynchronous MVC Fluent Validation automatic validation to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="autoValidationMvcConfiguration">The configuration delegate used to configure the FluentValidation AutoValidation MVC validation.</param>
Expand All @@ -39,6 +41,15 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService
configuration.DisableBuiltInModelValidation));
}

// Add the default result factory.
serviceCollection.AddScoped<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>();

// If the custom result factory is not null, replace the default result factory with the overridden result factory.
if (configuration.OverriddenResultFactory != null)
{
serviceCollection.Replace(new ServiceDescriptor(typeof(IFluentValidationAutoValidationResultFactory), configuration.OverriddenResultFactory, ServiceLifetime.Scoped));
}

// Create a default instance of the `ModelStateInvalidFilter` to access the non static property `Order` in a static context.
var modelStateInvalidFilter = new ModelStateInvalidFilter(new ApiBehaviorOptions {InvalidModelStateResponseFactory = context => new OkResult()}, NullLogger.Instance);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,39 @@
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Enums;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Results;
using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Filters
{
public class FluentValidationAutoValidationActionFilter : IAsyncActionFilter
{
private readonly IServiceProvider serviceProvider;
private readonly IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory;
private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration;

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

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.Controller is ControllerBase controllerBase)
{
var actionDescriptor = context.ActionDescriptor;
var endpoint = context.HttpContext.GetEndpoint();
var controllerActionDescriptor = (ControllerActionDescriptor) context.ActionDescriptor;

if (autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && !(endpoint?.Metadata.OfType<FluentValidationAutoValidationAttribute>().Any() ?? false))
{
Expand All @@ -39,16 +45,13 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
return;
}

foreach (var parameter in actionDescriptor.Parameters)
foreach (var parameter in controllerActionDescriptor.Parameters)
{
if (context.ActionArguments.TryGetValue(parameter.Name, out var subject))
{
var parameterType = parameter.ParameterType;

// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
var bindingSource = parameter.BindingInfo?.BindingSource;

// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (subject != null && (bindingSource == BindingSource.Body || (bindingSource == BindingSource.Query && parameterType.IsClass)))
{
if (serviceProvider.GetValidator(parameterType) is IValidator validator)
Expand All @@ -69,9 +72,9 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE

if (!context.ModelState.IsValid)
{
var validationProblem = controllerBase.ProblemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);
var validationProblemDetails = controllerBase.ProblemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);

context.Result = new BadRequestObjectResult(validationProblem);
context.Result = fluentValidationAutoValidationResultFactory.CreateActionResult(context, validationProblemDetails);

return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Results
{
public class FluentValidationAutoValidationDefaultResultFactory : IFluentValidationAutoValidationResultFactory
{
public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails)
{
return new BadRequestObjectResult(validationProblemDetails);
}
}
}
Loading