Resolves to Least Complex Signature, not Most Complex
RandyBuchholz opened this issue · comments
Default Constructor will always be called
When DI instantiates an object it should look for the constructor with the highest number of resolvable parameters. The name ServiceContainerPicksConstructorWithLongestMatches
in test https://github.com/aspnet/DependencyInjection/blob/rel/1.1.1/src/Microsoft.Extensions.DependencyInjection.Specification.Tests/DependencyInjectionSpecificationTests.cs#L640 seems to imply this, and it makes the most sense.
This is not working correctly. The resolution seems to be working in the reverse order. If a class has a parameterless/default constructor, it is always be used.
** As a result, DI appears to only support a single constructor
Repo
https://github.com/Randy-Buchholz/DIOverload/tree/master/DIOverload
Reproduce
Three interfaces and implementations
public interface IInterfaceOne { }
public class ImplementationOne : IInterfaceOne {
public ImplementationOne() { }
public ImplementationOne(IInterfaceTwo i2) { }
public ImplementationOne(IInterfaceTwo i2, IInterfaceThree i3) { }
}
public interface IInterfaceTwo { }
public class ImplementationTwo : IInterfaceTwo { }
public interface IInterfaceThree { }
public class ImplementationThree : IInterfaceThree { }
Only register two
services.AddTransient<IInterfaceOne, ImplementationOne>();
services.AddTransient<IInterfaceTwo, ImplementationTwo>();
GlobalServices.Provider= services.BuildServiceProvider(); // To get ServiceProvider in classes
Instantiate class using DI
var services = GlobalServices.Provider;
var instantiated = ActivatorUtilities.CreateInstance(services, typeof(ImplementationOne));}
DI should call the constructor with the highest number of ("LongestMatches") registered parameters. This would be the second constructor. It should be looking in this order with the results -
public ImplementationOne(IInterfaceTwo i2, IInterfaceThree i3)
} - Not called param 2 not registered
public ImplementationOne(IInterfaceTwo i2)
- Called
public ImplementationOne()
- Not reached
The actual behavior is that public ImplementationOne()
is being called. If this constructor is removed DI will call ImplementationOne(IInterfaceTwo i2)
. DI is finding the constructors, but in the wrong order.
It seems that the order of the constructors in the target class matters.
This calls ImplementationOne()
public class ImplementationOne : IInterfaceOne
{
public ImplementationOne() { }
public ImplementationOne(IInterfaceTwo i2) { }
public ImplementationOne(IInterfaceTwo i2, IInterfaceThree i3) { }
}
This calls ImplementationOne(IInterfaceTwo i2)
public class ImplementationOne : IInterfaceOne
{
public ImplementationOne(IInterfaceTwo i2) { }
public ImplementationOne() { }
public ImplementationOne(IInterfaceTwo i2, IInterfaceThree i3) { }
}
@RandyBuchholz Assuming the type is registered, resolving it from DI i.e. serviceProvider.GetService<ImplementationOne>()
should get you the longest matching constructor. ActivatorUtilties
uses the service locator pattern where the IOC container is opaque to it. Consequently it cannot create the best matching constructor until it actually realizes these services which we want to avoid.
Note: ActivatorUtilites
behaves somewhat like Activator.CreateInstance
to find the best matching constructor when you pass in parameters to it:
public class ImplementationOne
{
public ImplementationOne() { }
public ImplementationOne(string a) { }
}
Invoking ActivatorUtilities.CreteInstance(services, typeof(ImplementationOne), "a-value")
should invoke the second ctor.
@pranavkm GetService<T>()
does work correctly.
Passing parameters on CreateInstance
works, but doesn't really provide a clean solution. An interface can't be passed, only objects. Passing a value/object bypasses resolution - the value passed is used and the interface is not resolved. It needs manual resolution outside the "Creation Scope".
// Calls 2nd
ActivatorUtilities.CreateInstance(services, typeof(ImplementationOne), services.GetService<IInterfaceTwo>());
As you said, it behaves much like the regular Activator.CreateInstance()
.
Perhaps ActivatorUtilities.CreateInstance()
should have overloads that take Type[]
ActivatorUtilities.CreateInstance(IServiceProvider, Type objectType, Type[] diParameters)
ActivatorUtilities.CreateInstance(IServiceProvider, Type objectType, Type[] diParameters, params object[] dataParameters )
This would allow implementing smarter matching strategies and keep the instantiations together inside CreateInstance()
.
Has there been any discussion about supporting a "CanResolve" or "ResolvesTo" type of functionality on ServiceProvider
? It may not be something to go on IServiceProvider
, though that would prob be nice. Something like
Type ResolvesTo(Type unresolved)
It would return the type the input type resolves to and maybe even the constructor signatures.
Certainly an interesting idea, but we're closing because there are no plans to implement this additional functionality. The ActivatorUtilties
helper class works the way it does for the reasons @pranavkm mentions.