Search code examples
c#genericsautofaccontravariance

Autofac contravariance and resolving open generic types


When resolving all implementations of a generic type (with a contravariant T) from autofac I'd like to get all possible contravariant matches. This only works when registering the ContravariantRegistrationSource. But then I get too much instances for open generic implementations because it walks the inheritance tree gives me an instance per subclass.

This might sound a bit abstract, so here's 2 unit tests that demonstrate the problem. They both fail, but I'd like to get at least one of them working:

using Autofac;
using FluentAssertions;
using System.Collections.Generic;
using Xunit;
using Autofac.Features.Variance;

namespace Aiv.Vbr.QueryService.WebApi.Test.AdresMatchTests
{
    public class TestAutofacGenerics
    {
        public interface IGenericInterface<in T> { }
        public class GenericImplementation<T> : IGenericInterface<T> { }
        public class SpecificImplementation : IGenericInterface<TClass> { }
        public class TInterfaceImplementation : IGenericInterface<TInterface> { }
        public interface TInterface { }
        public class TClass : TInterface { }

        [Fact]
        public void AutofacShouldAlsoResolveContravariantImplementations()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<SpecificImplementation>().As<IGenericInterface<TClass>>();
            builder.RegisterType<TInterfaceImplementation>().As<IGenericInterface<TInterface>>();
            builder.RegisterGeneric(typeof(GenericImplementation<>)).As(typeof(IGenericInterface<>));

            var instances = builder.Build().Resolve<IEnumerable<IGenericInterface<TClass>>>();

            //This fails: only 2 types get resolved: GenericImplementation<TClass> and SpecificImplementation
            //but also expected TInterfaceImplementation
            instances.Should().HaveCount(3);
        }

        [Fact]
        public void AutofacShouldOnlyResolveOpenGenericsForSpecifiedClass()
        {
            var builder = new ContainerBuilder();
            builder.RegisterSource(new ContravariantRegistrationSource());
            builder.RegisterType<SpecificImplementation>().As<IGenericInterface<TClass>>();
            builder.RegisterType<TInterfaceImplementation>().As<IGenericInterface<TInterface>>();
            builder.RegisterGeneric(typeof(GenericImplementation<>)).As(typeof(IGenericInterface<>));

            var instances = builder.Build().Resolve<IEnumerable<IGenericInterface<TClass>>>();

            //This fails: 5 types get resolved: GenericImplementation<TClass>, GenericImplementation<Object>, 
            // GenericImplementation<TInterface>, SpecificImplementation and TInteraceImplementation
            //but did not want GenericImplementation<Object> and GenericImplementation<TInterface>
            instances.Should().HaveCount(3);
        }
    }    
}

The issue is described here and a possible solution suggested is to use a custom ContravariantRegistrationSource which is scoped, but I fail to see how this can resolve my issue. What can I do?


Solution

  • The issue is related to how ContravariantRegistrationSource and RegisterGeneric works

    When you resolve GenericImplementation<TClass> ContravariantRegistrationSource will try to resolve

    GenericImplementation<TClass>
    GenericImplementation<Object>
    GenericImplementation<TInterface>
    

    because you have

    builder.RegisterGeneric(typeof(GenericImplementation<>))
           .As(typeof(IGenericInterface<>));
    

    Autofac will returns registration for each of them.

    It is the expected behavior and unfortunately there is no easy way to fix it.

    I had the same issue with MediatR and INotificationHandler, I ended by doing my own IRegistrationSource.

    /// <summary>
    /// Returns contravariant registration source without duplicator target. 
    /// 
    /// <see cref="ContravariantRegistrationSource" /> returns all contravariant implementation of a type. 
    /// For example when we resolve IEnumerable&lt;INotificationHandler&lt;SpecificCommand&gt;&gt; it will returns a collection with GenericHandler&lt;SpecificCommand&gt;, GenericHandler&lt;BaseCommand&gt;, SpecificCommandHandler 
    /// this registration source will first look up for the native registrations and then group registration based on activator limit type. 
    /// </summary>
    /// <remarks>See https://stackoverflow.com/questions/46464944/autofac-contravariance-and-resolving-open-generic-types </remarks>
    public class ExplicitContravariantRegistrationSource : IRegistrationSource
    {
        private readonly IRegistrationSource _source = new ContravariantRegistrationSource();
        private readonly Type _type;
    
        public ExplicitContravariantRegistrationSource(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }
            if (!type.IsGenericTypeDefinition)
            {
                throw new ArgumentException("Type should be a generic type definition", nameof(type));
            }
            this._type = type;
        }
    
        public IEnumerable<IComponentRegistration> RegistrationsFor(
            Service service,
            Func<Service, IEnumerable<ServiceRegistration>> registrationAccessor)
        {
            if (service is IServiceWithType st
                    && st.ServiceType.IsGenericType
                    && this._type == st.ServiceType.GetGenericTypeDefinition())
            {
    
                // get all non contravariant registration source 
                var originalRegistrations = registrationAccessor(service).ToArray();
    
                var components = _source
                                    // retrieve all contravariant registration source
                                    .RegistrationsFor(service, registrationAccessor)
                                    // Group will ensure having only a single registration of a activator limit type
                                    .GroupBy(this.GetTargetTypeDefinitionOrSelf)
                                    // exclude groups if autofac already resolved the same activator limit type
                                    .Where(o => !originalRegistrations.Select(oo => this.GetTargetTypeDefinitionOrSelf(oo.Registration)).Contains(o.Key))
                                    // taking the last is the default behavior for autofac, it can be improved
                                    .Select(o => o.Last());
                return components;
            }
            else
            {
                return Enumerable.Empty<IComponentRegistration>();
            }
        }
    
        private Type GetTargetTypeDefinitionOrSelf(IComponentRegistration componentRegistration)
        {
            return componentRegistration.Target.Activator.LimitType.IsGenericType ?
                        componentRegistration.Target.Activator.LimitType.GetGenericTypeDefinition()
                        : componentRegistration.Target.Activator.LimitType;
        }
    
        public bool IsAdapterForIndividualComponents => _source.IsAdapterForIndividualComponents;
    }
    

    and I use it like this :

    builder.RegisterSource(new ExplicitContravariantRegistrationSource(typeof(INotificationHandler<>)));