autofac/Autofac.Extensions.DependencyInjection

Support for nested open generics seems missing (but works with Microsofts service container)

AntMaster7 opened this issue · 5 comments

I ran into issues when I try to resolve services in the form of A<B<<C<,>>>. It appears that the service container from Microsoft (conforming container) does support this kind of services. I am not sure if that is an issue in the AutofacServiceProviderFactory or in Autofac itself. I could solve the issue by writing a custom service provider factory (provided below) that solves the problem. Thats why I created the issue in this repository.

However, it would be nice if Autofac would support that out of the box or maybe there are reasons why this is not supported? I would also be happy if someone could tell me if my approach for the time being is correct. Maybe I am doing it completely wrong.

If its helpful I can create a demo project. But I wanted to check first before spending time on this.

using Autofac;
using Autofac.Builder;
using Autofac.Extensions.DependencyInjection;
using Autofac.Features.OpenGenerics;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Reflection;

namespace PremiumNet;

/// <summary>
/// Wraps the AutofacServiceProviderFactory from Autofac and adds support for nested open generics
/// by letting autofac only populate services that are not nested open generics.
/// All nested open generic services will be registered separately using Autofac's API.
/// </summary>
public class ServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
{
    private AutofacServiceProviderFactory autofacServiceProviderFactory = new();

    public ContainerBuilder CreateBuilder(IServiceCollection services)
    {
        static bool IsGenericTypeDefinitionRecursive(Type type) =>
            type is not null && (type.IsGenericTypeDefinition || type.GetGenericArguments().Any(IsGenericTypeDefinitionRecursive));

        var servicesWithNestedOpenGenerics = services
            .Where(service => service.ServiceType.GenericTypeArguments.Any(IsGenericTypeDefinitionRecursive))
            .ToList();

        servicesWithNestedOpenGenerics.ForEach(service => services.Remove(service));

        // Remove nested open generics because autofac can not resolve them and will throw an exception
        var modifiedServiceCollection = new ServiceCollection();
        foreach (var service in services.Except(servicesWithNestedOpenGenerics))
        {
            modifiedServiceCollection.Add(service);
        }

        var builder = autofacServiceProviderFactory.CreateBuilder(modifiedServiceCollection);

        servicesWithNestedOpenGenerics.ForEach(RegisterServiceWithNestedOpenGenerics);

        void RegisterServiceWithNestedOpenGenerics(ServiceDescriptor descriptor)
        {
            var registrationBuilder = builder
                .RegisterGeneric((context, types) =>
                {
                    if (descriptor.ImplementationType is null)
                    {
                        throw new NotSupportedException();
                    }

                    var implementationType = descriptor.ImplementationType
                        .GetGenericTypeDefinition().MakeGenericType(types);

                    // Get most specific constructor with resolvable parameters
                    var constructor = implementationType // TODO: Cache constructor info
                        .GetConstructors(BindingFlags.Public | BindingFlags.Instance)
                        .OrderByDescending(constructor => constructor.GetParameters().Count())
                        .FirstOrDefault(constructor => constructor.GetParameters().All(parameter => context.IsRegistered(parameter.ParameterType)));

                    if (constructor is null)
                    {
                        throw new InvalidOperationException($"No resolvable constructor found in {implementationType.FullName}.");
                    }

                    var parameters = constructor.GetParameters()
                        .Select(prm => context.Resolve(prm.ParameterType)).ToArray();

                    return constructor.Invoke(parameters);
                })
                .As(descriptor.ServiceType);

            ConfigureLifetime(registrationBuilder, descriptor);
        }

        void ConfigureLifetime(IRegistrationBuilder<object, OpenGenericDelegateActivatorData, DynamicRegistrationStyle> registrationBuilder, ServiceDescriptor service)
        {
            switch (service.Lifetime)
            {
                case ServiceLifetime.Singleton:
                    registrationBuilder.SingleInstance();
                    break;
                case ServiceLifetime.Scoped:
                    registrationBuilder.InstancePerLifetimeScope();
                    break;
                case ServiceLifetime.Transient:
                    registrationBuilder.InstancePerDependency();
                    break;
            }
        }

        return builder;
    }

    public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder) =>
        autofacServiceProviderFactory.CreateServiceProvider(containerBuilder);
}

Just checking, have you tried this with plain Autofac, registering a nested open generic, then resolving it? I'd expect it to work to be honest, but only 90% sure.

You may want to check if we have a unit test for it over in the Autofac repo.

It's possible there's an issue with the mapping of MSDI to Autofac open generics here; do you get an error when registering, or does the service just fail to resolve?

I use plain Autofac with the DependencyInjection extensions. Without my special decorator it crashes when I build the web application with var app = builder.Build(). The exception reads:

System.ArgumentException: 'The type 'Sharpflare.Common.Command2[Sharpflare.Common.GetCollectionFromRepo1[TDto],Sharpflare.Common.DtoCollection1[TDto]]' is not assignable to service 'Sharpflare.Common.Command2'.'

Its happening in the RegistrationBuilder.CreateRegistration method from Autofac. I will check if there is a unit test for it later.

It appears that this is my error. Instead of registering A<B<<C<,>>> I can simply register A<> and then it works. The issue appears to have been with mixing type parameters and type arguments. Took me only like 8 hours to figure that out :(

EDIT: Well, maybe its still an issue because the whole type needs to be open and Microsofts container could apparently deal with that.

I may be misreading this or something, but in the description of the issue, the thing that "fixes this" is some modification to the way the ServiceDescriptor gets converted into Autofac registrations. That's not the service provider factory, that's the Populate method.

It might be worth backing up a bit and simplifying the issue way, way down, so we can get to the root of it.

First, it would be good to see the class hierarchy. Even if it's just public class A<T>{} sort of stuff, just list them.

Next, a simple unit test showing what you expect to pass, just in plain Autofac. No MEDI stuff, no provider factories, no service collections. builder.RegisterType<T>() level stuff. Even if it doesn't pass or even compile, let's see what is expected.

Then another test, this time with just IServiceCollection and builder.Populate(collection) stuff. The barest small amount of AEDI wrapped around it, no service provider factory.

I just want to make sure everyone is talking about the same thing and not getting confused between what is supported in core Autofac, what's in AEDI, and what the gaps may be, without any additional frameworks or components or app stacks involved.

My assumption that this feature works with Microsofts service container was wrong. It turned out that in the project where this issue happened, both the nested open generic and the "closed generic" versions of the services were registered. This led to the illusion that Microsofts service container supports this feature. The difference is that Microsofts service container did not throw an exception while building the container. Only once I used Autofac I noticed the issue because Autofac checks if the registrations are valid beforehand.

I did nevertheless create a sample that demonstrates what I initially wanted:

builder.RegisterType(typeof(A<>).MakeGenericType(typeof(B<>))).AsSelf(); // Register A<B<>>

// With that I wanted to be able to then resolve services like A<B<C>>

public class A<T> { }

public class B<T> { }

public class C { }

Because this feature is not supported in Microsofts service container I see no need for it to be implemented in Autofac. I will close the issue.