SharpGrip / FluentValidation.AutoValidation

SharpGrip FluentValidation AutoValidation is an extension of the FluentValidation library enabling automatic asynchronous validation in MVC controllers and minimal APIs (endpoints).
https://sharpgrip.net
MIT License
103 stars 7 forks source link

Auto Validation does not fire when using SimpleInjector as DI container #37

Open nhaberl opened 1 week ago

nhaberl commented 1 week ago

Hello,

I am workong on asp.net 6 with Simple Injector as DI container which works perfectly but when trying to use AutoValidation nothing is fired.

services.AddFluentValidationAutoValidation(configuration => { configuration.OverrideDefaultResultFactoryWith<CustomResultFactory>(); }); container.Register(typeof(IValidator<>), assemblies, Lifestyle.Singleton);

When trying to validate manually in Controller it works, so I guess the Validator is correct

public class InsertRequestValidator : AbstractValidator<InsertRequestDto> { public InsertRequestValidator() { RuleFor(request => request.Count).InclusiveBetween(1, 1000) .WithMessage("Oida des is afoch zu zvü"); } }

[ApiController] [Route("test")] public class TestController : BaseController

[HttpPost] [Route("command")] public async Task<IActionResult> Command(InsertRequestDto request) { // var validationResult = validator.Validate(request); // if (!validationResult.IsValid) // { // return BadRequest(validationResult.Errors); // } var x = await commandEngine.ExecuteAsync<InsertCommand, int>(new InsertCommand() {Count = request.Count}); return HandleResult(x); }

I have really no idea what I am doing wrong, any help appreciated

mvdgun commented 1 week ago

Hi there,

I’m not familiar with SimpleInjector, as I haven’t used it. Does it integrate well with the default .NET DI provider? One possibility that comes to mind is that the RequestServices property of HttpContext might not be correctly overridden by your SimpleInjector container.

If you try adding an IGlobalValidationInterceptor, can you check whether it's being invoked?

Another potential issue could be that SimpleInjector might not handle resolving open generics the same way the default DI does. This could affect the filter's ability to locate the validator.

public static object? GetValidator(this IServiceProvider serviceProvider, Type type)
{
    return serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(type));
}
dotnetjunkie commented 1 week ago

Although Simple Injector does integrate with ASP.NET Core, it does so by running Simple Injector side-by-side with the built-in DI Container. This is contrary to how Containers like Autofac function, which tend to replace the buil-in Container. With Simple Injector it's common practice to leave framework and third-party components registered in the built-in Container, while having application components registered inside of Simple Injector.

But this model has consequences and migth be the reason why @nhaberl is running into trouble, because when validators are registered inside Simple Injector, any orchestration by AutoValidation that is registered inside of MS.DI, won't know about the existence of these Simpe Injector-registered validators, unless some special wiring exists.

I'm not familiar with AutoValidation so I can't specifically describe the fix, but @mvdgun, hopefully you can point me at the class or code that ensures that validators are resolved and invoked. That would give me some starting point to look for.

But in more general terms, I think validators can be considered application components as they will contain application-specific business rules and will have dependencies on other application components. It’s best practice to register application components in Simple Injector. But that does require the MS.DI-registered orchestration to either be replaced with something specific that resolves the validators from Simple Injector -or- requires the registration for the orchestration to be made in Simple Injector as well and ensure that the registration for that piece of orchestration inside of MS.DI to simply resolve that component from Simple Injector (cross wiring). E.g. services.AddTransient<IValidationDispatcher>(c => siContainer.GetInstance<IValidationDispatcher>());

One last note for @nhaberl, please make sure that any code examples that you post are well formatted, readable, and preferably syntax highlighted to make it as easy for us as possible to help you.

mvdgun commented 1 week ago

Hi Steven,

Thank you for the detailed explanation!

As for your questions:

Validator registration This is the responsibility of the user of the library, in my docs I point them to the official FluentValidation docs at: https://docs.fluentvalidation.net/en/latest/di.html.

Validator resolving Inside the MVC filter I am grabbing the service provider from the actionExecutingContext.HttpContext.RequestServices property.

https://github.com/SharpGrip/FluentValidation.AutoValidation/blob/f4dc05a09dc6ab263d83985aaea805c3d8033886/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs#L39

From the service provider I resolve a validator via the extension method below:

https://github.com/SharpGrip/FluentValidation.AutoValidation/blob/f4dc05a09dc6ab263d83985aaea805c3d8033886/FluentValidation.AutoValidation.Shared/src/Extensions/ServiceProviderExtensions.cs#L10

Validator invoking Invoking happens at:

https://github.com/SharpGrip/FluentValidation.AutoValidation/blob/f4dc05a09dc6ab263d83985aaea805c3d8033886/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs#L84

If I need to resolve validators from any source other than a IServiceProvider I'm afraid there is no quick fix, I do like to be surprised though :).

I hope I have answered your question, if not please let me know!

dotnetjunkie commented 1 week ago

Generally speaking, by taking a hard dependency on IServiceProvider you are tightly coupling to the .NET DI infrastructure, making it harder, if not impossible, for non-conformig containers, like Simple Injector, to plug in to your reusable library. It's, therefore, advised to define interception points that decouple the library from the container. MVC, for instance, contains abstractions such as IContollerFactory and IControllerActivator for this purpose.

But before adding such interception point, let's try something different first, because the main abstraction you are pulling from the DI infrastructure is the IValidator<T>. So perhaps we can make a generic dispatcher implementation that forwards the call to a Simple Injector-registered component.

@nhaberl, please add the following code and report back whether that helps:

services.AddTransient(
    typeof(IValidator<T>),
    typeof(SimpleInjectorValidatorDispatcher<>));

class SimpleInjectorValidatorDispatcher<T>(Container container) : AbstractValidator<T>
{
    public override Task<ValidationResult> ValidateAsync(
        ValidationContext<T> context, CancellationToken cancellation = default)
    {
        var validator = container.GetService<IValidator<T>>();

        if (validator is null)
        {
            return Task.FromResult(new ValidationResult());
        }
        else
        {
            return validator.ValidateAsync(context, cancellation);
        }
    }
}
nhaberl commented 1 week ago

@dotnetjunkie thanks a lot, this works! ...although I do not exactly know why here :)

{
  "title": "Validation errors",
  "validationErrors": {
    "Count": [
      "Please provide an appropriate range bewteen 1 and 1000"
    ]
  }
}
mvdgun commented 1 week ago

Hi @nhaberl, so it all works? I assume the validation message is as expected?

nhaberl commented 1 week ago

Thanks, yes the code provided works and it showed the message like expected. Did not try async validations explicitely but will give Feedback tomorrow..

dotnetjunkie commented 6 days ago

...although I do not exactly know why here

Let me explain. I previously referred to MVC's IControllerActivator, and I think this is a good analogy to understand what I did in my example.

When a request comes in, MVC creates a controller for that. For the creation of controllers, MVC uses a built-in class that does this: DefaultControllerActivator. But instead of taking a hard dependency on DefaultControllerActivator it hides this implementation behind the IControllerActivator abstraction. This DefaultControllerActivator is registered by MVC in the IServiceCollection and at runtime, MVC requests the registered IControllerActivator from the IServiceProvider. This allows anyone to intercept or replace the creation of controller instances by swapping out the default IControllerActivator from the ISericeCollection with their own one.

Simple Injector users, for instance, use such a custom IControllerActivator implementation (typically without knowing, because it's provided by the Simple Injector integration packages). This custom implementation requests controllers from the Simple Injector container. This custom implementation is registered inside the IServiceCollection. When MVC requests the IControllerActivator from its IServiceProvider it then gets the custom Simple Injector implementation instead of the default one. When MVC calls the resolved IControllerActivator, this custom implementation resolves controller instances from the Simple Injector container, instead of from MS.DI. This allows Simple Injector to come the complete object graph for a controller, which likely depends on other application-specific components, which are also registered inside of Simple Injector.

This is exactly what is happening in the implementation I provided in my previous comment, although this might be a bit harder to see. But because of the lack of an IValidatorFactory, my implementation uses (or, so you will, abuses) the IValidator<T> abstraction for this. This works because the only abstraction that AutoValidation resolves is IValidator<T>.

The only IValidator<T> implementation registered inside the IServiceCollection will be the SimpleInjectorValidatorDispatcher<T>. This means that for any IValidator<T> that AutoValidation resolves from the IServiceProvider, it doesn't get a 'real' validator, it gets a specific/closed SimpleInjectorValidatorDispatcher<T> implementation (even in the absence of a real validator, a SimpleInjectorValidatorDispatcher<T> is returned).

However, when AutoValidation calls that SimpleInjectorValidatorDispatcher<T>, the dispatcher will go to the Simple Injector container to request the real validator, as the validators are registered with Simple Injector. This means that the SimpleInjectorValidatorDispatcher<T> functions as a factory, like the IControllerActivator does. But in contrast to the IControllerActivator implementation, the dispatcher is a generic class. Generic factories aren't unique; SignalR, for instance, has a generic factory abstraction (it's IHubActivator<T>) that can be used to intercept the creation of hub classes.

I hope this makes sense.