Sebastian Zaklada Sebastian Zaklada - 2 months ago 40
C# Question

Autofac issues when resolving generic types implementing the same interface, need to Resolve() twice or more

I have an Autofac related issue with resolving generic types which implement the same interface, but have different type constraints. It could also be that my approach to implementing this particular use case (described further down) is plain wrong, in such case I would appreciate someone to correct my thinking.

See https://gist.github.com/sebekz/4c658a5c7551ba5a2b3fd81488ea3ee7 for a console app sample and read below for additional details.

What I have is two interfaces

public interface IMultitenant
public interface IMultitenantOptional


These interfaces are implemented by a bunch of classes, which are
IQueryable<T>
generic types of responses. So I could have requests which have response types of e.g.
IQueryable<Game>
or
IQueryable<Trophy>
, where both
Game
and
Trophy
implement one of two above mentioned multitenancy interfaces.

Now, I have two very similar class definitions

public class MultiTenantHandler<TRequest, TResponse> : IResponseHandler<TRequest, TResponse>
where TResponse : IQueryable<IMultitenant>

public class MultiTenantOptionalHandler<TRequest, TResponse> : IResponseHandler<TRequest, TResponse>
where TResponse : IQueryable<IMultitenantOptional>

public interface IResponseHandler<in TRequest, TResponse>


Instances of these classes are constructor-injected by Autofac in a separate class:

public class MediatorPipeline<TRequest, TResponse> : RequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public MediatorPipeline(
IResponseHandler<TRequest, TResponse>[] responseHandlers
)
}


Autofac configuration goes like this:

builder.RegisterGeneric(typeof(MultiTenantHandler<,>))
.AsImplementedInterfaces()
.SingleInstance();
builder.RegisterGeneric(typeof(MultiTenantOptionalHandler<,>))
.AsImplementedInterfaces()
.SingleInstance();


What I expected is that when the
MediatorPipeline
class intercepted a response with a return type of
IQueryable<IMultitenant>
, an instance of
MultiTenantHandler
class will be injected into the
responseHandlers
variable. Similarily, when the
MediatorPipeline
class intercepted a response with a return type of
IQueryable<IMultitenantOptional>
, an instance of
MultiTenantOptionalHandler
class will be injected into the
responseHandlers
variable.

These injected classes post-process my responses if they are of these two specific
IQueryable
sub-types.

It all builds and works. Kind of. It's all a part of a WebAPI project and the problem is, when I run my endpoint which is supposed to return
IQueryable<IMultitenant>
for the first and second time, I get:


"message": "An exception was thrown while executing a resolve
operation. See the InnerException for details. --->
GenericArguments[1], 'System.Linq.IQueryable'1[Game]', on
'MultiTenantOptionalHandler'2[TRequest,TResponse]' violates the
constraint of type 'TResponse'. (See inner exception for details.)",
"type": "Autofac.Core.DependencyResolutionException",


First execution has this deeper in the stack trace:


"stacktrace": " at
System.RuntimeType.ValidateGenericArguments(MemberInfo definition,
RuntimeType[] genericArguments, Exception e)\r\n at
System.RuntimeType.MakeGenericType(Type[] instantiation)\r\n at
Autofac.Features.OpenGenerics.OpenGenericServiceBinder.TryBindServiceType(Service
service, IEnumerable
1 configuredOpenGenericServices, Type
openGenericImplementationType, Type& constructedImplementationType,
IEnumerable
1& constructedServices)\r\n at
Autofac.Features.OpenGenerics.OpenGenericRegistrationSource.d__0.MoveNext()\r\n
at
Autofac.Core.Registration.ComponentRegistry.GetInitializedServiceInfo(Service
service)\r\n at
Autofac.Core.Registration.ComponentRegistry.RegistrationsFor(Service
service)\r\n at
Autofac.Features.Collections.CollectionRegistrationSource.<>c__DisplayClass4.b__0(IComponentContext
c, IEnumerable
1 p)\r\n at
Autofac.Core.Activators.Delegate.DelegateActivator.ActivateInstance(IComponentContext
context, IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable
1
parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Execute()\r\n at
Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope
currentOperationScope, IComponentRegistration registration,
IEnumerable
1 parameters)\r\n at
Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate()\r\n
at
Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext
context, IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable
1
parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Execute()\r\n at
Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope
currentOperationScope, IComponentRegistration registration,
IEnumerable
1 parameters)\r\n at
Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate()\r\n
at
Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext
context, IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable
1
parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Execute()\r\n at
Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope
currentOperationScope, IComponentRegistration registration,
IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration
registration, IEnumerable`1 parameters)",


while the second execution has this in the same place:


"stacktrace": " at
System.RuntimeType.ValidateGenericArguments(MemberInfo definition,
RuntimeType[] genericArguments, Exception e)\r\n at
System.RuntimeType.MakeGenericType(Type[] instantiation)\r\n at
Autofac.Features.OpenGenerics.OpenGenericServiceBinder.TryBindServiceType(Service
service, IEnumerable
1 configuredOpenGenericServices, Type
openGenericImplementationType, Type& constructedImplementationType,
IEnumerable
1& constructedServices)\r\n at
Autofac.Features.OpenGenerics.OpenGenericRegistrationSource.d__0.MoveNext()\r\n
at
Autofac.Core.Registration.ComponentRegistry.GetInitializedServiceInfo(Service
service)\r\n at
Autofac.Core.Registration.ComponentRegistry.RegistrationsFor(Service
service)\r\n at
System.Linq.Enumerable.d__16
2.MoveNext()\r\n at
System.Linq.Enumerable.WhereSelectEnumerableIterator
2.MoveNext()\r\n
at
Autofac.Core.Registration.ComponentRegistry.GetInitializedServiceInfo(Service
service)\r\n at
Autofac.Core.Registration.ComponentRegistry.RegistrationsFor(Service
service)\r\n at
Autofac.Features.Collections.CollectionRegistrationSource.<>c__DisplayClass4.b__0(IComponentContext
c, IEnumerable
1 p)\r\n at
Autofac.Core.Activators.Delegate.DelegateActivator.ActivateInstance(IComponentContext
context, IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable
1
parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Execute()\r\n at
Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope
currentOperationScope, IComponentRegistration registration,
IEnumerable
1 parameters)\r\n at
Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate()\r\n
at
Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext
context, IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable
1
parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Execute()\r\n at
Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope
currentOperationScope, IComponentRegistration registration,
IEnumerable
1 parameters)\r\n at
Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate()\r\n
at
Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext
context, IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable
1
parameters)\r\n at
Autofac.Core.Resolving.InstanceLookup.Execute()\r\n at
Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope
currentOperationScope, IComponentRegistration registration,
IEnumerable
1 parameters)\r\n at
Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration
registration, IEnumerable`1 parameters)",


Third and all subsequent executions pass without any issues, returning expected payloads.

I would appreciate some help from Autofac experts.

EDIT:

I created a console program which illustrates the issue:

https://gist.github.com/sebekz/4c658a5c7551ba5a2b3fd81488ea3ee7

EDIT 2:

This ended up with a pull request into the Autofac repository which so far has been merged into develop with a potential for being included in one of the next Autofac releases. Please track the integration progress here https://github.com/autofac/Autofac/pull/796.

Answer

As you can see in the stacktrace the exception is caused in the TryBindService method from the OpenGenericServiceBinder class. This method tries to find a compatible service for the given type parameters. It calls the IsCompatibleWithGenericParameterConstraints method to ensure that the type parameters are compatible with the given type constraints, so I suspected that this method is wrong or at least not working as expected.

This suspicion proved to be correct, since the method returns true for all of the following calls:

typeof(MultiTenantHandler<,>).IsCompatibleWithGenericParameterConstraints(new[] { typeof(IQueryable<Game>), typeof(IQueryable<Game>) }); // expected: true
typeof(MultiTenantHandler<,>).IsCompatibleWithGenericParameterConstraints(new[] { typeof(IQueryable<Trophy>), typeof(IQueryable<Trophy>) }); // expected: false

typeof(MultiTenantOptionalHandler<,>).IsCompatibleWithGenericParameterConstraints(new[] { typeof(IQueryable<Game>), typeof(IQueryable<Game>) }); // expected: false
typeof(MultiTenantOptionalHandler<,>).IsCompatibleWithGenericParameterConstraints(new[] { typeof(IQueryable<Trophy>), typeof(IQueryable<Trophy>) }); // expected: true

So why does it return true? It uses the method ParameterCompatibleWithTypeConstraint to check whether the parameter is compatible with the given type constraint. At first this method checks if the constraint type is assignable from the parameter type, so:

typeof(IQueryable<IMultitenant>).IsAssignableFrom(typeof(IQueryable<Game>)); // true
typeof(IQueryable<IMultitenant>).IsAssignableFrom(typeof(IQueryable<Trophy>)); // false

typeof(IQueryable<IMultitenantOptional>).IsAssignableFrom(typeof(IQueryable<Game>)); // false
typeof(IQueryable<IMultitenantOptional>).IsAssignableFrom(typeof(IQueryable<Trophy>)); // true

So far it is working as expected. However, in case this check is false the ParameterCompatibleWithTypeConstraint does not return false. Instead it checks if it can create a generic type with the base type of the constraint (IQueryable<T>) and the generic argument of the parameter (Game or Trophy).

Since IQueryable<T> obviously does not define the IMultitenant or IMultitenantOptional type constraints this is always possible and the method returns true.

I am not sure why this second check is performed. It looks like a bug to me, but there may be a valid reason for this I am not seeing right now. I guess your best bet would be to create an issue on GitHub to see what the Autofac developers are thinking about this.

The question remains why it is working the second time you try to resolve the service.

I have not confirmed it, but I suspect it may have to do with caching. Maybe Autofac remembers that using the MultiTenantOptionalHandler did not work to to resolve an IResponseHandler<IRequest<IQueryable<Game>>, IQueryable<Game>>, so it uses the only other possible registration, which is the correct one (MultiTenantHandler) by coincidence.

This assumption is backed by the fact that I have to resolve the IResponseHandler<IRequest<IQueryable<Game>>, IQueryable<Game>> three times to be successful in case I add another IResponseHandler implementation with yet another type constraints.

It could also be that my approach to implementing this particular use case (described further down) is plain wrong, in such case I would appreciate someone to correct my thinking.

I don't know if your approach is best practice, but I don't see anything wrong about it and I guess it should work fine if the ParameterCompatibleWithTypeConstraint method would work as expected.

Comments