Skip to content

Commit

Permalink
v1.0.4 cached reflections + xml summaries
Browse files Browse the repository at this point in the history
  • Loading branch information
ogulcanturan committed Oct 21, 2024
1 parent 595df77 commit c9fae42
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 77 deletions.
2 changes: 1 addition & 1 deletion samples/Sample.Api/Controllers/SamplesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class SamplesController : ControllerBase
{
[HttpPost]
[Validate(typeof(GetSampleRequestValidatorModel))]
public IActionResult GetSample(GetSampleRequestValidatorModel model)
public IActionResult GetSample(GetSampleRequestValidatorModel validatorModel)
{
return Ok();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static class RequestValidatorsServiceCollectionExtensions
{
public static IServiceCollection AddRequestValidators(this IServiceCollection services)
{
// Singleton or Scoped based on your Validator
// Register singleton or scoped based on your Validator
services.AddSingleton<IValidator<GetSampleRequestValidatorModel>, GetSampleRequestValidator>();

return services;
Expand Down
20 changes: 0 additions & 20 deletions src/Ogu.FluentValidation.AspNetCore.Attribute/Extensions.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,35 @@

namespace Ogu.FluentValidation.AspNetCore.Attribute
{
/// <summary>
/// Defines a contract for handling invalid validation results when the [<see cref="ValidateAttribute"/>] or [<see cref="ValidateAsyncAttribute"/>] attributes are applied.
/// During action execution, if validation failures are detected, this interface's methods will be invoked.
/// If the [<see cref="ValidateAsyncAttribute"/>] attribute is applied, the asynchronous method <see cref="GetResultAsync"/> will be called;
/// otherwise, the synchronous method <see cref="GetResult"/> is invoked.
/// If the interface hasn't implemented or not registered, a default <see cref="BadRequestObjectResult"/> will be returned on validation failure.
/// </summary>
public interface IInvalidValidationResponse
{
/// <summary>
/// Called when validation failures occur during the execution of a non-async action with the [<see cref="ValidateAttribute"/>].
/// If validation errors are present, this method is invoked to generate the response.
/// If not implemented, a default <see cref="BadRequestObjectResult"/> will be returned.
/// </summary>
/// <param name="model">The model that failed validation.</param>
/// <param name="validationFailures">A list of <see cref="ValidationFailure"/> objects representing the validation errors.</param>
/// <returns>An <see cref="IActionResult"/> that represents the response returned to the client.</returns>
IActionResult GetResult(object model, List<ValidationFailure> validationFailures);

/// <summary>
/// Called when validation failures occur for an async request.
/// This method is triggered when the [<see cref="ValidateAsyncAttribute"/>] is applied to the request.
/// Implement this method to customize the async response returned in case of validation failures.
/// If not implemented, a default <see cref="BadRequestObjectResult"/> will be returned.
/// </summary>
/// <param name="model">The model that failed validation.</param>
/// <param name="validationFailures">A list of <see cref="ValidationFailure"/> objects representing the validation errors.</param>
/// <param name="cancellationToken">Optional token to cancel the operation.</param>
/// <returns>A <see cref="Task{IActionResult}"/> representing the async result to be returned as the response.</returns>
Task<IActionResult> GetResultAsync(object model, List<ValidationFailure> validationFailures, CancellationToken cancellationToken = default);
}
}
14 changes: 10 additions & 4 deletions src/Ogu.FluentValidation.AspNetCore.Attribute/InternalConstants.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
using System;
using FluentValidation;
using System;
using System.Collections.Concurrent;
using System.Threading;

namespace Ogu.FluentValidation.AspNetCore.Attribute
{
public static class InternalConstants
internal static class InternalConstants
{
public const string ValidateAsyncMethodName = "ValidateAsync";
public const string ValidateMethodName = "Validate";
internal const string ValidateAsyncMethodName = "ValidateAsync";

internal const string ValidateMethodName = "Validate";

internal static readonly Type CancellationTokenType = typeof(CancellationToken);

internal static readonly Type IValidatorTType = typeof(IValidator<>);

internal static readonly Type IInvalidValidationResponseType = typeof(IInvalidValidationResponse);

internal static readonly Lazy<ConcurrentDictionary<string, bool>> ActionUuidToHasSkipValidateAttribute = new Lazy<ConcurrentDictionary<string, bool>>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Ogu.FluentValidation.AspNetCore.Attribute
internal class InvalidValidationResponse : IInvalidValidationResponse
{
private readonly Func<List<ValidationFailure>, IActionResult> _invalidResponse;

public InvalidValidationResponse(Func<List<ValidationFailure>, IActionResult> invalidResponse)
{
_invalidResponse = invalidResponse ?? throw new ArgumentNullException(nameof(invalidResponse));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>Oğulcan TURAN</Authors>
<Copyright>Copyright (c) Oğulcan TURAN 2023</Copyright>
<PackageProjectUrl>https://github.com/ogulcanturan/FluentValidation.AspNetCore.Attribute</PackageProjectUrl>
<PackageProjectUrl>https://github.com/ogulcanturan/Ogu.FluentValidation.AspNetCore.Attribute</PackageProjectUrl>
<RepositoryUrl>https://github.com/ogulcanturan/Ogu.FluentValidation.AspNetCore.Attribute</RepositoryUrl>
<PackageTags>fluentvalidation;fluentvalidationattribute;fluentattribute;fluentvalidationaspnetcoreattribute</PackageTags>
<RepositoryType>git</RepositoryType>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using Ogu.FluentValidation.AspNetCore.Attribute;
using System;
using System.Collections.Generic;

namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers an implementation of <see cref="IInvalidValidationResponse"/> in the service collection.
/// This method allows you to provide a custom response for validation failures.
/// </summary>
/// <param name="services">The service collection to which the implementation will be added.</param>
/// <param name="invalidResponse">
/// A function that takes a list of <see cref="ValidationFailure"/> objects
/// and returns an <see cref="IActionResult"/>.
/// This function is invoked when validation failures occur.
/// </param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered implementation.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="invalidResponse"/> is <c>null</c>.
/// </exception>
public static IServiceCollection AddInvalidValidationResponse(this IServiceCollection services, Func<List<ValidationFailure>, IActionResult> invalidResponse)
{
invalidResponse = invalidResponse ?? throw new ArgumentNullException(nameof(invalidResponse));

services.AddSingleton<IInvalidValidationResponse>(new InvalidValidationResponse(invalidResponse));

return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

namespace Ogu.FluentValidation.AspNetCore.Attribute
{
/// <summary>
/// An attribute that, when applied to a method, skips model validation for that method.
/// </summary>
/// <remarks>
/// Use this attribute on action methods to bypass any validation logic that would normally be applied.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public class SkipValidateAttribute : System.Attribute { }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using FluentValidation;
using FluentValidation.Results;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
Expand All @@ -13,24 +12,55 @@

namespace Ogu.FluentValidation.AspNetCore.Attribute
{
/// <summary>
/// An attribute that validates the specified model asynchronously before an action method is invoked.
/// </summary>
/// <remarks>
/// This attribute can be applied to both methods and classes.
/// It is used to ensure that the models passed to the action method meet validation criteria before further processing.
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class ValidateAsyncAttribute : ActionFilterAttribute
{
private static readonly Lazy<ConcurrentDictionary<Type, (Type, MethodInfo)>>
LazyModelTypeToGenericTypeAndMethodInfoTuple =
private static readonly Lazy<ConcurrentDictionary<Type, (Type genericValidatorTType, MethodInfo validateMethod)>>
LazyModelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple =
new Lazy<ConcurrentDictionary<Type, (Type, MethodInfo)>>(LazyThreadSafetyMode.ExecutionAndPublication);

/// <summary>
/// Initializes a new instance of the <see cref="ValidateAttribute"/> class for a single model type.
/// </summary>
/// <param name="modelType">The type of the model to validate.</param>
/// <param name="order">The order in which the action filter attribute is applied. Default is 0.</param>
/// <param name="isCancellationTokenActive"></param>
public ValidateAsyncAttribute(Type modelType, int order = 0, bool isCancellationTokenActive = true) : this(new[] { modelType }, order, isCancellationTokenActive) { }

/// <summary>
/// Initializes a new instance of the <see cref="ValidateAsyncAttribute"/> class for multiple model types.
/// </summary>
/// <param name="modelTypes">An array of model types to validate.</param>
/// <param name="order">The order in which the action filter attribute is applied. Default is 0.</param>
/// <param name="isCancellationTokenActive"></param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="modelTypes"/> is null.</exception>
public ValidateAsyncAttribute(Type[] modelTypes, int order = 0, bool isCancellationTokenActive = true)
{
ModelTypes = modelTypes ?? throw new ArgumentNullException(nameof(modelTypes));
IsCancellationTokenActive = isCancellationTokenActive;
Order = order;
}

/// <summary>
/// An array of model types to validate.
/// </summary>
public Type[] ModelTypes { get; }

/// <summary>
/// Indicates whether the active <see cref="ActionContext.HttpContext"/> cancellation token is being used.
/// If <c>true</c>, the cancellation token from the current <see cref="ActionContext.HttpContext"/> will be utilized;
/// otherwise, no cancellation token will be applied.
/// </summary>
/// <remarks>
/// The default value is <c>true</c>
/// </remarks>
public bool IsCancellationTokenActive { get; }

public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
Expand All @@ -41,8 +71,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
return;
}

if (!InternalConstants.ActionUuidToHasSkipValidateAttribute.Value.TryGetValue(
controllerActionDescriptor.Id, out var hasSkipValidateAttribute))
if (!InternalConstants.ActionUuidToHasSkipValidateAttribute.Value.TryGetValue(controllerActionDescriptor.Id, out var hasSkipValidateAttribute))
{
hasSkipValidateAttribute = controllerActionDescriptor.MethodInfo.GetCustomAttribute<SkipValidateAttribute>() != null;

Expand All @@ -56,7 +85,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
return;
}

var modelTypeToGenericTypeAndMethodInfoTuple = LazyModelTypeToGenericTypeAndMethodInfoTuple.Value;
var modelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple = LazyModelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple.Value;

var cancellationToken = IsCancellationTokenActive
? context.HttpContext.RequestAborted
Expand All @@ -71,31 +100,36 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
continue;
}

if (modelTypeToGenericTypeAndMethodInfoTuple.TryGetValue(modelType, out var genericTypeAndMethodInfo))
{
var service = context.HttpContext.RequestServices.GetService(genericTypeAndMethodInfo.Item1);
await (modelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple.TryGetValue(modelType, out var genericValidatorTTypeAndValidateMethodInfo)
? ProcessCachedAsync(context, genericValidatorTTypeAndValidateMethodInfo, model, cancellationToken)
: ProcessAsync(context, modelType, modelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple, model, cancellationToken));
}

var validationResult = await (Task<ValidationResult>)genericTypeAndMethodInfo.Item2.Invoke(service, new[] { model, cancellationToken });
await base.OnActionExecutionAsync(context, next);
}

await HandleValidationResultAsync(context, validationResult, model, cancellationToken);
}
else
{
var validatorType = typeof(IValidator<>).MakeGenericType(modelType);
private static async Task ProcessCachedAsync(ActionExecutingContext context, (Type genericValidatorTType, MethodInfo validateMethodInfo) tuple, object model, CancellationToken cancellationToken)
{
var service = context.HttpContext.RequestServices.GetService(tuple.genericValidatorTType);

var resolvedValidatorFromDependencyInjection = context.HttpContext.RequestServices.GetRequiredService(validatorType);
var validationResult = await (Task<ValidationResult>)tuple.validateMethodInfo.Invoke(service, new[] { model, cancellationToken });

var validateMethod = validatorType.GetMethod(InternalConstants.ValidateAsyncMethodName, new[] { modelType, InternalConstants.CancellationTokenType }) ?? throw new Exception($"The package 'FluentValidation' version '{typeof(IValidator).Assembly.GetName().Version}' is not supported.");
await HandleValidationResultAsync(context, validationResult, model, cancellationToken);
}

modelTypeToGenericTypeAndMethodInfoTuple.TryAdd(modelType, (validatorType, validateMethod));
private static async Task ProcessAsync(ActionExecutingContext context, Type modelType, ConcurrentDictionary<Type, (Type, MethodInfo)> modelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple, object model, CancellationToken cancellationToken)
{
var validatorType = InternalConstants.IValidatorTType.MakeGenericType(modelType);

var validationResult = await (Task<ValidationResult>)validateMethod.Invoke(resolvedValidatorFromDependencyInjection, new[] { model, cancellationToken });
var resolvedValidatorFromDependencyInjection = context.HttpContext.RequestServices.GetRequiredService(validatorType);

await HandleValidationResultAsync(context, validationResult, model, cancellationToken);
}
}
var validateMethod = validatorType.GetMethod(InternalConstants.ValidateAsyncMethodName, new[] { modelType, InternalConstants.CancellationTokenType }) ?? throw new Exception($"The package 'FluentValidation' version '{InternalConstants.IValidatorTType.Assembly.GetName().Version}' is not supported.");

await base.OnActionExecutionAsync(context, next);
modelTypeToGenericValidatorTTypeAndValidateMethodInfoTuple.TryAdd(modelType, (validatorType, validateMethod));

var validationResult = await (Task<ValidationResult>)validateMethod.Invoke(resolvedValidatorFromDependencyInjection, new[] { model, cancellationToken });

await HandleValidationResultAsync(context, validationResult, model, cancellationToken);
}

private static async Task HandleValidationResultAsync(ActionExecutingContext context, ValidationResult validationResult, object model, CancellationToken cancellationToken)
Expand All @@ -105,7 +139,7 @@ private static async Task HandleValidationResultAsync(ActionExecutingContext con
return;
}

context.Result = context.HttpContext.RequestServices.GetService(typeof(IInvalidValidationResponse)) is IInvalidValidationResponse invalidResponse
context.Result = context.HttpContext.RequestServices.GetService(InternalConstants.IInvalidValidationResponseType) is IInvalidValidationResponse invalidResponse
? await invalidResponse.GetResultAsync(model, validationResult.Errors, cancellationToken)
: new BadRequestObjectResult(validationResult.Errors);
}
Expand Down
Loading

0 comments on commit c9fae42

Please # to comment.