Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac Enumeration not working with multiple registrations of types or modules

The autofac documentation states:

when Autofac is injecting a constructor parameter of type IEnumerable<ITask> it will not look for a component that supplies IEnumerable<ITask>. Instead, the container will find all implementations of ITask and inject all of them.

But actually, it adds each registered type as many times as it has been registered. So, if you register a class twice as following:

builder.RegisterType<A>(); 
builder.RegisterType<A>(); 

Then you get two items in the enumeration!! Within a single module, it is not a problem, as you obviously take care to register only once your types. But if you have a shared module registered by multiple modules (typical diamond module dependency diagram), then you get as many items in the enumeration as the shared module has been registered by others...

Is it a bug? Is there any way to force the enumeration to provide a single item for each implementation, as stated in the documentation, no more?

like image 842
jeromerg Avatar asked Oct 31 '22 20:10

jeromerg


1 Answers

You have to see Autofac as a kind of Dictionary<IComponentRegistration>. A registration can be tied to a type or a delegate or everything else with configuration behavior.

By registering twice the Type you will have two distinct IComponentRegistration, in your case the registration will be similar. If you look at the following difference you can see that you will have two different registration that use the same Type but with different configuration.

builder.RegisterType<A>().WithConstrusctorArgument("param1", "x"); 
builder.RegisterType<A>().WithConstrusctorArgument("param1", "y"); 

In this case resolving IEnumerable<A> will give you two different instance of A and it does make sense.

Is there any way to force the enumeration to provide a single item for each implementation

I won't recommend doing that. It may be better to refactor your application not to allow this scenario, if you need that you will remove a lot of great things in Autofac. If you have a Type that may be register in more than one module it may make sense to register this Type in the main module.


By the way, if you really want to do that, you will have to find a way to distinguish all registration of your container. Then you can override the default implementation for IEnumerable<> by implementing your own IRegistrationSource, the following example will give you only one registration per type.

class CustomEnumerableRegistrationSource : IRegistrationSource
{
    public Boolean IsAdapterForIndividualComponents
    {
        get
        {
            return false;
        }
    }

    public IEnumerable<IComponentRegistration> RegistrationsFor(Service service, Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
    {
        IServiceWithType typedService = service as IServiceWithType;

        if (typedService == null)
        {
            return Enumerable.Empty<IComponentRegistration>();
        }
        if (!(typedService.ServiceType.IsGenericType && typedService.ServiceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
        {
            return Enumerable.Empty<IComponentRegistration>();
        }

        Type elementType = typedService.ServiceType.GetGenericArguments()[0];

        Service elementService = typedService.ChangeType(elementType);

        Type collectionType = typeof(List<>).MakeGenericType(elementType);

        IComponentRegistration registration = RegistrationBuilder.ForDelegate(collectionType, (c, p) =>
        {
            IEnumerable<IComponentRegistration> registrations = c.ComponentRegistry.RegistrationsFor(elementService);

            IEnumerable<Object> elements = registrations.Select(cr => c.ResolveComponent(cr, p));

            // get distinct elements by type
            Array array = elements.GroupBy(o => o.GetType()).Select(o => o.First()).ToArray();

            Array array2 = Array.CreateInstance(elementType, array.Length);
            array.CopyTo(array2, 0);

            Object collection = Activator.CreateInstance(collectionType, new Object[] { array2 });

            return collection;
        }).As(service)
          .CreateRegistration();

        return new IComponentRegistration[] { registration };
    }
}

And you can use it like this :

ContainerBuilder builder = new ContainerBuilder();

builder.RegisterType<A1>().As<IA>();
builder.RegisterType<A1>().As<IA>();
builder.RegisterType<A2>().As<IA>();

builder.RegisterSource(new CustomEnumerableRegistrationSource());

IContainer container = builder.Build();

IEnumerable<IA> services = container.Resolve<IEnumerable<IA>>();
Console.WriteLine(services.Count());

This registration is a simplified version of the original CollectionRegistrationSource. Please have a look at the source code of the CollectionRegistrationSource.cs for a more complete version of the registration.

like image 143
Cyril Durand Avatar answered Nov 11 '22 17:11

Cyril Durand