Skip to content

Commit

Permalink
Merge pull request #23 from SharpGrip/21-enable-the-user-to-customize…
Browse files Browse the repository at this point in the history
…-the-validation-context-based-on-mvc-context

add interceptor logic per validator
  • Loading branch information
mvdgun authored Nov 17, 2023
2 parents 1bc6f68 + 82dc38e commit 42bf37d
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,32 @@ public FluentValidationAutoValidationEndpointFilter(IServiceProvider serviceProv

if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator)
{
var validationInterceptor = serviceProvider.GetService<IValidationInterceptor>();
// ReSharper disable once SuspiciousTypeConversion.Global
var validatorInterceptor = validator as IValidatorInterceptor;
var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();

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

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

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

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

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

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

if (!validationResult.IsValid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors
{
public interface IValidationInterceptor
/// <summary>
/// Allows global intercepting and altering of the validation process by implementing this interface on a custom class and registering it with the service collection.
///
/// The interceptor methods of instances of this interface will get called on each validation attempt.
/// </summary>
public interface IGlobalValidationInterceptor
{
public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext);
public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors
{
/// <summary>
/// Allows intercepting and altering of the validation process by implementing this interface on a validator.
///
/// The interceptor methods of instances of this interface will only get called when the implementing validator gets validated.
/// </summary>
public interface IValidatorInterceptor : IGlobalValidationInterceptor
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,32 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC
{
if (serviceProvider.GetValidator(parameterType) is IValidator validator)
{
var validationInterceptor = serviceProvider.GetService<IValidationInterceptor>();
// ReSharper disable once SuspiciousTypeConversion.Global
var validatorInterceptor = validator as IValidatorInterceptor;
var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();

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

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

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

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

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

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

if (!validationResult.IsValid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors
{
public interface IValidationInterceptor
/// <summary>
/// Allows global intercepting and altering of the validation process by implementing this interface on a custom class and registering it with the service collection.
///
/// The interceptor methods of instances of this interface will get called on each validation attempt.
/// </summary>
public interface IGlobalValidationInterceptor
{
public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext);
public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors
{
/// <summary>
/// Allows intercepting and altering of the validation process by implementing this interface on a validator.
///
/// The interceptor methods of instances of this interface will only get called when the implementing validator gets validated.
/// </summary>
public interface IValidatorInterceptor : IGlobalValidationInterceptor
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<ItemGroup>
<PackageReference Include="FluentValidation" Version="[10.0,]" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
90 changes: 78 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@

## Introduction

SharpGrip FluentValidation AutoValidation is an extension of the [FluentValidation](https://github.com/FluentValidation/FluentValidation) (v10+) library enabling automatic asynchronous validation in MVC controllers and minimal APIs (endpoints).
The library [FluentValidation.AspNetCore](https://github.com/FluentValidation/FluentValidation.AspNetCore) is no longer being maintained and is unsupported. As a result, support for automatic validation provided by this library is no longer available.
This library re-introduces this functionality for MVC controllers and introduces automatic validation for minimal APIs (endpoints). It enables developers to easily implement automatic validation in their projects.
SharpGrip FluentValidation AutoValidation is an extension of the [FluentValidation](https://github.com/FluentValidation/FluentValidation) (v10+) library enabling automatic asynchronous validation in
MVC controllers and minimal APIs (endpoints).
The library [FluentValidation.AspNetCore](https://github.com/FluentValidation/FluentValidation.AspNetCore) is no longer being maintained and is unsupported. As a result, support for automatic
validation provided by this library is no longer available.
This library re-introduces this functionality for MVC controllers and introduces automatic validation for minimal APIs (endpoints). It enables developers to easily implement automatic validation in
their projects.

## Installation

Expand Down Expand Up @@ -123,24 +126,62 @@ public class CustomResultFactory : IFluentValidationAutoValidationResultFactory
```

## Validation interceptors

Note: Using validation interceptors is considered to be an advanced feature and is not needed for most use cases.

Validation interceptors allow you to intercept and alter the validation process by implementing the `IValidationInterceptor` interface in a custom class.
The interface defines a `BeforeValidation` and a `AfterValidation` method.
Validation interceptors allow you to intercept and alter the validation process by either implementing the `IGlobalValidationInterceptor` interface in a custom class or by implementing
the `IValidatorInterceptor` on a single validator.
During the validation process both instances get resolved and called (if they are present) creating a mini pipeline of validation interceptors:

```
==> IValidatorInterceptor.BeforeValidation()
==> IGlobalValidationInterceptor.BeforeValidation()
Validation
==> IValidatorInterceptor.AfterValidation()
==> IGlobalValidationInterceptor.AfterValidation()
```

The `BeforeValidation` method gets called before validation and allows you to return a custom `IValidationContext` which gets passed to the validator.
Both interfaces define a `BeforeValidation` and a `AfterValidation` method.

The `BeforeValidation` method gets called before validation and allows you to return a custom `IValidationContext` which gets passed to the validator.
In case you return `null` the default `IValidationContext` will be passed to the validator.

The `AfterValidation` method gets called after validation and allows you to return a custom `IValidationResult` which gets passed to the `IFluentValidationAutoValidationResultFactory`.
In case you return `null` the default `IValidationResult` will be passed to the `IFluentValidationAutoValidationResultFactory`.

### MVC controllers

```
// Register your custom validation interceptor with the service collection.
builder.Services.AddTransient<IValidationInterceptor, CustomValidationInterceptor>();
// Example of a global validation interceptor.
builder.Services.AddTransient<IGlobalValidationInterceptor, CustomGlobalValidationInterceptor>();
public class CustomGlobalValidationInterceptor : IGlobalValidationInterceptor
{
public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext)
{
// Return a custom `IValidationContext` or null.
return null;
}
public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext)
{
// Return a custom `ValidationResult` or null.
return null;
}
}
public class CustomValidationInterceptor : IValidationInterceptor
// Example of a single validator interceptor.
private class TestValidator : AbstractValidator<TestModel>, IValidatorInterceptor
{
public TestValidator()
{
RuleFor(x => x.Parameter1).Empty();
RuleFor(x => x.Parameter2).Empty();
RuleFor(x => x.Parameter3).Empty();
}
public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext)
{
// Return a custom `IValidationContext` or null.
Expand All @@ -156,12 +197,37 @@ public class CustomValidationInterceptor : IValidationInterceptor
```

### Minimal APIs (endpoints)

```
// Register your custom validation interceptor with the service collection.
builder.Services.AddTransient<IValidationInterceptor, CustomValidationInterceptor>();
// Example of a global validation interceptor.
builder.Services.AddTransient<IGlobalValidationInterceptor, CustomGlobalValidationInterceptor>();
public class CustomGlobalValidationInterceptor : IValidationInterceptor
{
public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext)
{
// Return a custom `IValidationContext` or null.
return null;
}
public class CustomValidationInterceptor : IValidationInterceptor
public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext)
{
// Return a custom `ValidationResult` or null.
return null;
}
}
// Example of a single validator interceptor.
private class TestValidator : AbstractValidator<TestModel>, IValidatorInterceptor
{
public TestValidator()
{
RuleFor(x => x.Parameter1).Empty();
RuleFor(x => x.Parameter2).Empty();
RuleFor(x => x.Parameter3).Empty();
}
public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext)
{
// Return a custom `IValidationContext` or null.
Expand Down
10 changes: 5 additions & 5 deletions Tests/FluentValidation.AutoValidation.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.16">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.console" Version="2.5.1">
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.console" Version="2.6.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.runner.msbuild" Version="2.5.1">
<PackageReference Include="xunit.runner.msbuild" Version="2.6.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down

0 comments on commit 42bf37d

Please sign in to comment.