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?
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<INotificationHandler<SpecificCommand>> it will returns a collection with GenericHandler<SpecificCommand>, GenericHandler<BaseCommand>, 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<>)));