Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Registering partially-closed generic type with ASP.NET Core Microsoft.Extensions.DependencyInjection

In an ASP.NET Core 1.1.2 web project, which depends on Microsoft.Extensions.DependencyInjection 1.1.1, I'm trying to register a generic FluentValidation validator both with its implementation and with its interface. At runtime, during host building, I got following exception:

An unhandled exception of type 'System.ArgumentException' occurred in Microsoft.Extensions.DependencyInjection.dll

Cannot instantiate implementation type 'MyProj.Shared.Api.WithUserCommandValidator`1[T]' for service type 'FluentValidation.IValidator`1[MyProj.Shared.Model.WithUser`1[T]]'.

   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceTable..ctor(IEnumerable`1 descriptors)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, Boolean validateScopes)
   at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services)
   at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
   at MyProj.Program.Main(String[] args) in X:\MySolution\src\MyProj\Program.cs:line 31

This is the (generic, non-abstract) validator class:

public class WithUserCommandValidator<TCommand> : AbstractValidator<WithUser<TCommand>>
    where TCommand : ICommand
{
    public WithUserCommandValidator(IValidator<TCommand> commandValidator)
    {
        RuleFor(cmd => cmd).NotNull();

        RuleFor(cmd => cmd.UserId).NotEqual(Guid.Empty);

        RuleFor(cmd => cmd.Content).NotNull()
            .SetValidator(commandValidator);
    }
}

(not sure is important to show WithUser<T> class, it just holds a Guid UserId and a T Content).

Registration happens like so:

someInterface = typeof(FluentValidation.IValidator<WithUser<>>);
someImplementation = typeof(Shared.Api.WithUserCommandValidator<>);

// this is for top-level command validators, injected by interface
services.AddScoped(someInterface, someImplementation);

// this is for nested validators, injected by implementation
services.AddScoped(someImplementation);

and at runtime those variables hold:

+ someInterface  {FluentValidation.IValidator`1[MyProj.Shared.Model.WithUser`1[TCommand]]}  System.Type {System.RuntimeType}
+ someImplementation  {MyProj.Shared.Api.WithUserCommandValidator`1[TCommand]}  System.Type {System.RuntimeType}

which seems correct.

I've looked around for similar problems and questions on SO as well, but they're not really related, as they were about generic implementation, while this is a concrete one.

I've stepped through DependencyInjection module code, and exception is thrown here. For some reason, serviceTypeInfo.IsGenericTypeDefinition returns false (shouldn't that be true, as the interface type is generic?) and so the else-branch is taken. There, instead, the implementation type return true to implementationTypeInfo.IsGenericTypeDefinition, so there you have the exception.

Runtime values are:

+ serviceTypeInfo {FluentValidation.IValidator`1[MyProj.Shared.Model.WithUser`1[TCommand]]} System.Reflection.TypeInfo {System.RuntimeType}
+ implementationTypeInfo {MyProj.Shared.Api.WithUserCommandValidator`1[TCommand]}   System.Reflection.TypeInfo {System.RuntimeType}

While fiddling, I tried registering only the implementation, without also registering interface+implementation, and that exception goes away. But I'd like to inject the interface, if possible, not the implementation.

What am I doing wrong? TA

like image 993
superjos Avatar asked Jul 08 '17 14:07

superjos


1 Answers

What you are trying to do is to map a partially closed type (namely IValidator<WithUser<T>>), which is something that is not supported by .NET Core's DI container. When it comes to generic types, the .NET Core container is a very minimalistic, simplistic implementation. It is unable to handle things like:

  • generic type constraints
  • partially closed types
  • implementations with less generic type arguments than the abstraction
  • implementations with generic type arguments in a different order than the abstraction
  • having generic type arguments being nested as part of a type argument of the partially closed abstraction (which is your exact case)
  • Curiously recurring template pattern

If you require this behavior and want to keep continuing to use the built-in container for this, you will have to register each closed implementation by hand:

AddScoped(typeof(IValidator<WithUser<Cmd1>>),typeof(WithUserCommandValidator<Cmd1>));
AddScoped(typeof(IValidator<WithUser<Cmd2>>),typeof(WithUserCommandValidator<Cmd2>));
AddScoped(typeof(IValidator<WithUser<Cmd3>>),typeof(WithUserCommandValidator<Cmd3>));
// etc

If this is leads to an unmaintainable Composition Root, you should pick a different container. The only DI Container that I know actually fully supports these kinds of generic designs is Simple Injector.

With Simple Injector, you can simply do the following registration:

container.Register(
    typeof(IValidator<>),
    typeof(WithUserCommandValidator<>),
    Lifestyle.Scoped);
like image 122
Steven Avatar answered Sep 19 '22 23:09

Steven