aspnet / DependencyInjection

[Archived] Contains common DI abstractions that ASP.NET Core and Entity Framework Core use. Project moved to https://github.com/aspnet/Extensions

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.