ryanelian / FluentValidation.Blazor

Fluent Validation-powered Blazor component for validating standard <EditForm> :milky_way: :white_check_mark:
https://www.nuget.org/packages/Accelist.FluentValidation.Blazor
MIT License
234 stars 27 forks source link

Library cannot be used with InjectValidator extension method from FluentValidation.DependencyInjectionExtensions #2

Closed Sebazzz closed 5 years ago

Sebazzz commented 5 years ago

It is currently not possible to use any IServiceProvider in validators.

Apparently it is because the service provider has not been set. This should be easily fixed, like so: https://github.com/JeremySkinner/FluentValidation/blob/48047d3bc558d2c95de5c13800dd5f091b4c0bbb/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs#L83

ryanelian commented 5 years ago

Interesting. What is this? What does that do? Please elaborate the side effect and use cases of doing that.

That method is also apparently is NOT the built-in method of FluentValidation core library, only the integration library: https://github.com/JeremySkinner/FluentValidation/blob/master/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs#L82


In my code, IServiceProvider is accessible just like any other services because I'm registering the validator into my Dependency Injection.

On Startup.cs

services.AddTransient<IValidator<EditAccountFormModel>, EditAccountFormModelValidator>();

(Example: https://github.com/ryanelian/FluentValidation.Blazor/blob/master/Demo/Startup.cs#L36 registering validator https://github.com/ryanelian/FluentValidation.Blazor/blob/master/Demo/Models/FormModel2.cs#L15-L32 which calls another service as part of the validation logic https://github.com/ryanelian/FluentValidation.Blazor/blob/master/Demo/Services/EmailCheckerService.cs)

Allows you to do this:

image

^ Look at the debugger values

Sebazzz commented 5 years ago

Interesting. What is this? What does that do?

This allows injecting scope-dependent services in the validator without relying on things like IHttpContextAccessor. This is especially useful if you have a factory for a child validator (especially for things like string).

ryanelian commented 5 years ago

Isn't current method of registering validator to the DI already allowed injecting services to the validator?

My example above doesn't rely on IHttpContextAccessor but INJECTS IServiceProvider and MyDbContext to the validator which then be used to get the IHttpContextAccessor (read again)

Check again:

(Example: https://github.com/ryanelian/FluentValidation.Blazor/blob/master/Demo/Startup.cs#L36 registering validator https://github.com/ryanelian/FluentValidation.Blazor/blob/master/Demo/Models/FormModel2.cs#L15-L32 which calls another service as part of the validation logic https://github.com/ryanelian/FluentValidation.Blazor/blob/master/Demo/Services/EmailCheckerService.cs)

Please give specific code example for your specific use case because I can't understand what you meant.

Sebazzz commented 5 years ago

There are two IServiceProvider: one global and one which is able to provide per-scope services. Note that a scope in Blazor different as opposed to a scope in regular MVC: Because Blazor is a websockets connection there is a long running scope and no HTTP context.

ryanelian commented 5 years ago

There are two IServiceProvider:

This is the first time I've ever heard of this. Please give reference to the documentation and code examples?

Sebazzz commented 5 years ago

Try: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.0#request-services

The services available within an ASP.NET Core request from HttpContext are exposed through the HttpContext.RequestServices collection.

Request Services represent the services configured and requested as part of the app. When the objects specify dependencies, these are satisfied by the types found in RequestServices, not ApplicationServices.

Some instances can only be retrieved through the service provider for the specific scope.

If you look at CircuitFactory.CreateCircuitHost you see that at line 44 a new serviceprovider is initiated.

ryanelian commented 5 years ago

Okay...

image

That doesn't explain what you told me about there being two IServiceProvider

I can't find the code you quoted on the page:

image


Anyway, what's stopping you from requesting a service from the validator (like my examples above)? Just give me your service code example so I can work with real example.

Sebazzz commented 5 years ago

That doesn't explain what you told me about there being two IServiceProvider

One ServiceProvider is scope based and will resolve scope-based services, while the other will not.

I can't find the code you quoted on the page

That is the Blazor source code, check out the 3.0.0 tag on the AspNetCore repository.

As for the validation, take this rule for instance:

       this.RuleFor(e => e.Passphrase).
            InjectValidator((sp, context) =>
            {
                var validatorFactory = sp.GetRequiredService<PassphraseValidatorFactory>();
                MyCommand obj = context.InstanceToValidate;
                return validatorFactory.MakePassphraseValidator(obj.RetroId, obj.JoiningAsManager, obj.Passphrase);
            });

It depends on a PassphraseValidatorFactory which takes a DbContext which is request scoped.

ryanelian commented 5 years ago

As for the validation, take this rule for instance:

Ah, so you're using the CUSTOM MVC validation rule . Not built-in in-the-box rule for FluentValidation.

(https://github.com/JeremySkinner/FluentValidation/blob/master/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs#L64 https://github.com/JeremySkinner/FluentValidation/blob/master/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs#L84-L109 note that it's in the integration project, not the core FluentValidation project)

Keep in mind, this project is designed to be integrated with the vanilla / core FluentValidation project as a Blazor Component, not as MVC integration library. They are two different beasts.

As for your rule, it's not too hard to rewrite it for my library:

public class SebazzFormValidator : AbstractValidator<SebazzFormModel> 
{
    private readonly SebazzRequiredService MyService;

    public SebazzFormValidator(SebazzRequiredService svc)
    {
        this.MyService = svc;

        RuleFor(e => e.Passphrase).Must(ValidatePassphrase).WithMessage("ErrorWhenValidatingPassPhrase")
    }

    public bool ValidatePassphrase(value)
    {
        // var something = MyService.UseMyMethod(value);
        // etc. etc.
    }
}

As you can see, whatever service / factory you require will be injected through the constructor, as per ASP.NET Core Dependency Injection Best Practice:

image


You can then register the validator in the DI alongside your required service:

services.AddTransient<IValidator<SebazzFormModel>, SebazzFormValidator>();
services.AddTransient<SebazzRequiredService>();

If you need support for scoped validation, use OwningComponentBase class provided by the Blazor framework: https://docs.microsoft.com/en-us/aspnet/core/blazor/dependency-injection?view=aspnetcore-3.0#utility-base-component-classes-to-manage-a-di-scope

Is my above example clear enough?

ryanelian commented 5 years ago

Upon observation, it also might be possible to add support for FluentValidation.DependencyInjectionExtensions although very hacky-in-nature and prone to breaking. Not sure if want to support this:

(The author explicitly wrote Making use of InjectValidator or GetServiceProvider is only supported when using the automatic MVC integration. in the class)

image

Setting Root Context Data _FV_ServiceProvider value to IServiceProvider will allow consumers of FluentValidation.Blazor to use InjectValidator methods.

EDIT

Disregard that, it exploded when attempted.

[2019-10-07T20:47:33.420Z] Error: System.NullReferenceException: Object reference not set to an instance of an object.
   at FluentValidation.DependencyInjectionExtensions.<>c__2`2.<InjectValidator>b__2_0(IServiceProvider s, ValidationContext`1 ctx) in /home/jskinner/code/FluentValidation/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs:line 85
   at FluentValidation.DependencyInjectionExtensions.<>c__DisplayClass3_0`2.<InjectValidator>b__0(IValidationContext context) in /home/jskinner/code/FluentValidation/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs:line 102
   at FluentValidation.Validators.ChildValidatorAdaptor.GetValidator(PropertyValidatorContext context) in /home/jskinner/code/FluentValidation/src/FluentValidation/Validators/ChildValidatorAdaptor.cs:line 86
   at FluentValidation.Validators.ChildValidatorAdaptor.Validate(PropertyValidatorContext context) in /home/jskinner/code/FluentValidation/src/FluentValidation/Validators/ChildValidatorAdaptor.cs:line 49
   at FluentValidation.Internal.PropertyRule.InvokePropertyValidator(ValidationContext context, IPropertyValidator validator, String propertyName) in /home/jskinner/code/FluentValidation/src/FluentValidation/Internal/PropertyRule.cs:line 423
   at FluentValidation.Internal.PropertyRule.Validate(ValidationContext context)+MoveNext() in /home/jskinner/code/FluentValidation/src/FluentValidation/Internal/PropertyRule.cs:line 282
   at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()
   at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
   at FluentValidation.AbstractValidator`1.Validate(ValidationContext`1 context) in /home/jskinner/code/FluentValidation/src/FluentValidation/AbstractValidator.cs:line 115
   at FluentValidation.AbstractValidator`1.FluentValidation.IValidator.Validate(ValidationContext context) in /home/jskinner/code/FluentValidation/src/FluentValidation/AbstractValidator.cs:line 69
   at FluentValidation.FluentValidator.ValidateModel(EditContext editContext, ValidationMessageStore messages) in D:\VS\FluentValidation.Blazor\FluentValidation.Blazor\FluentValidator.cs:line 97
   at FluentValidation.FluentValidator.<>c__DisplayClass14_0.<AddValidation>b__0(Object sender, ValidationRequestedEventArgs eventArgs) in D:\VS\FluentValidation.Blazor\FluentValidation.Blazor\FluentValidator.cs:line 79
   at Microsoft.AspNetCore.Components.Forms.EditContext.Validate()
   at Microsoft.AspNetCore.Components.Forms.EditForm.HandleSubmitAsync()
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)

EDIT 2: Closing this issue, because the goal of this project is to enable Blazor integration against FluentValidation with Dependency Injection / IServiceProvider support; NOT FluentValidation.AspNetCore or FluentValidation.DependencyInjectionExtensions integrations.

ryanelian commented 5 years ago

Added InjectValidator workaround with SetValidator plus standard Dependency Injection to README.

https://github.com/ryanelian/FluentValidation.Blazor#fluentvalidationdependencyinjectionextensions-functions