-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unify ActivatorUtilities
methods CreateInstance
and CreateFactory
#50688
Unify ActivatorUtilities
methods CreateInstance
and CreateFactory
#50688
Conversation
Using [ActivatorUtilitiesconstructor] attribute on a constructor should force `ActivatorUtilities.CreateInstance` to use that constructor regardless of the constructor definition order. This test shows that it is not the case in current implementaion.
Calling `ActivatorUtilities.CreateInstance` without additional arguments should prefer parameterless constructor as there are no guarantees that parameters, for which arguments were not supplied, can be resolved from the ServiceProvider.
Current test for `ActivatorUtilities.CreateInstance` checked that the first matched constructor is being used. Ctor definition order should not affect which ctor is being picked. Instead, the ctor should be picked based on how similar the provided argumetns are to the ctor's parameters. This would reuls in more predictable behaviour, where the best constructor is being chosen regardless of the class member ordering.
Previously `ActivatorUtilities.CreateInstance` and `ActivatorUtilities.CreateFactory` behaved differently: former depended on ctor definition order to disambiguate ctors, while latter failed altogether. The behavior is now unified and addresses concerns raised in dotnet#45119 dotnet#42339 and dotnet#46132. Constructors are chosen based on the following factors in a given order: 1) Preferred ctor (having [ActivatorUtilitiesConstructor] attribute) 2) Ctor with the most surjective parameters based on the given arguments. In other words constructor that has the least unresolved parameters is chosen. 3) Ctor with more resolved parameters is prefered. If two ctors have all of the supplied arguments, but one of them has additional parameters with default values, then the one with more parameters is chosen.
Tagging subscribers to this area: @eerhardt, @maryamariyan |
} | ||
|
||
return bestMatcher.CreateInstance(provider); | ||
Type[] argumentTypes = parameters |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid LINQ usage, you could replace Select
and ToArray
with Array.ConvertAll
. For least allocations, you could use a manual for
loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed this to use a simple for
loop. 👍
@@ -209,8 +179,9 @@ private static MethodInfo GetMethodInfo<T>(Expression<T> expr) | |||
ConstructorInfo? constructorInfo = null; | |||
int?[]? parameterMap = null; | |||
|
|||
if (!TryFindPreferredConstructor(instanceType, argumentTypes, ref constructorInfo, ref parameterMap) && | |||
!TryFindMatchingConstructor(instanceType, argumentTypes, ref constructorInfo, ref parameterMap)) | |||
if (instanceType.IsAbstract || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you elaborate on the IsAbstract
check? AFAIU, it was not preset in the CreateFactory
flow. Was it implicitly covered by expression build failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in case of CreateFactory
and an abstract type it used to fail with the following exception:
System.InvalidOperationException: Can't compile a NewExpression with a constructor declared on an abstract class
at System.Linq.Expressions.Compiler.LambdaCompiler.EmitNewExpression(Expression expr) in C:\Git\runtime\src\libraries\System.Linq.Expressions\src\System\Linq\Expressions\Compiler\LambdaCompiler.Expressions.cs:line 632
at System.Linq.Expressions.Compiler.LambdaCompiler.EmitExpression(Expression node, CompilationFlags flags) in C:\Git\runtime\src\libraries\System.Linq.Expressions\src\System\Linq\Expressions\Compiler\LambdaCompiler.Generated.cs:line 120
at System.Linq.Expressions.Compiler.LambdaCompiler.EmitLambdaBody(CompilerScope parent, Boolean inlined, CompilationFlags flags) in C:\Git\runtime\src\libraries\System.Linq.Expressions\src\System\Linq\Expressions\Compiler\LambdaCompiler.Lambda.cs:line 232
at System.Linq.Expressions.Compiler.LambdaCompiler.EmitLambdaBody() in C:\Git\runtime\src\libraries\System.Linq.Expressions\src\System\Linq\Expressions\Compiler\LambdaCompiler.Lambda.cs:line 195
at System.Linq.Expressions.Compiler.LambdaCompiler.Compile(LambdaExpression lambda) in C:\Git\runtime\src\libraries\System.Linq.Expressions\src\System\Linq\Expressions\Compiler\LambdaCompiler.cs:line 187
at System.Linq.Expressions.Expression`1.Compile() in C:\Git\runtime\src\libraries\System.Linq.Expressions\src\System\Linq\Expressions\LambdaExpression.cs:line 225
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactory(Type instanceType, Type[] argumentTypes) in C:\Git\runtime\src\libraries\Microsoft.Extensions.DependencyInjection.Abstractions\src\ActivatorUtilities.cs:line 104
at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.CreateInstanceFromFactory(IServiceProvider provider, Type type, Object[] args) in C:\Git\runtime\src\libraries\Microsoft.Extensions.DependencyInjection.Specification.Tests\src\ActivatorUtilitiesTests.cs:line 26
at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.<>c__DisplayClass29_0.<AAACreateInstance_WithAbstractTypeAndPublicConstructor_ThrowsCorrectException>b__0() in C:\Git\runtime\src\libraries\Microsoft.Extensions.DependencyInjection.Specification.Tests\src\ActivatorUtilitiesTests.cs:line 384
at Xunit.Assert.RecordException(Func`1 testCode) in C:\Dev\xunit\xunit\src\xunit.assert\Asserts\Record.cs:line 50
Since abstract type is not desired in neither CreateInstance
nor CreateFactory
I assume it is fine to throw the same exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess so, an explicit check beats an implicit check in my book.
It's up to maintainers to decide though, 'cause some software somewhere can handle the original exception with regards to the message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Let's see what others say.
@@ -198,27 +198,29 @@ public void TypeActivatorRethrowsOriginalExceptionFromConstructor(CreateInstance | |||
} | |||
|
|||
[Theory] | |||
[InlineData(typeof(string))] | |||
[InlineData(typeof(int))] | |||
public void TypeActivatorCreateFactoryDoesNotAllowForAmbiguousConstructorMatches(Type paramType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is no longer true, then this PR does not fix #46132. I'm not implying it should, but this PR seems to bring breaking changes to both CreateInstance
and CreateFactory
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following @tarekgh comments:
[..] we must consider the purpose of the API. it is creating an instance of object. If the user code giving two ways (i.e. two legit constructors) and then we fail that will look weird too.
The current behavior can be easily defined as, CreateInstance tries to create instance of the object by finding any legit constructor satisfy the creation using the input parameters.
This PR tries to solve the #46132 issue by choosing the most similar ctor in terms of the ctor parameters and the supplied arguments, instead of relying on ctor definition order as it was done previously. I thought this should address #46132. I specifically tried to cover it by adding tests for this case in 7479663.
Given:
public class Foo
{
public Foo(A a, B b, C c, S s) { ... }
public Foo(A a, C c, S s) { ... }
}
ActivatorUtilities.CreateInstance(provider, typeof(Foo), new C(), new A());
It would always choose the Foo(A a, C c, S s)
ctor regardless of definition order, as it is closer to the supplied arguments than the other ctor.
Does this makes sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I described the expected behavior I want to see.
Using (A, B) arguments where (B, A) parameters are expected looks magical and throws type (signature) safety out of the window, IMHO.
It all comes to what we want. And I don't want the snippet to work. I want it to throw to keep everything simple.
Originally we have magical poorly described CreateInstance
and strict, but well defined CreateFactory
. I want the former to behave as the later: that way I'll have fewer surprises and hard to debug errors, I'm fine with failing fast during development/deployment though. But I'm only one user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point. It could possibly be easier to just fail when there are multiple choices for given arguments.
Although, if we had a way to check if IServiceProvider
has a parameter type registered (or decided to go with .GetService
to check that), then I think failing would be too strict and we should rather choose the constructor with the longest list of all resolved parameters. Looking from user point of view, giving the provider
parameter implies you want to resolve additional services.
Using (A, B) arguments where (B, A) parameters are expected looks magical and throws type (signature) safety out of the >window, IMHO.
Regarding this, don't you feel this is a bit too strict for this method? It makes sense for Activator.CreateInstance
to behave like that, because it has all arguments explicitly provided. In this case we expect to resolve 0..*
additional arguments from a service provider and there's little guarantee in the parameter ordering, ex.:
// We start with this working example
public Foo(A a, B b) { ... }
ActivatorUtilities.CreateInstance(provider, typeof(Foo), new A(), new B());
// After some time someone adds an additional dependency in between the parameters
public Foo(A a, X x, B b) { ... }
// Should the following fail, even if `X` is available in the service provider?
ActivatorUtilities.CreateInstance(provider, typeof(Foo), new A(), new B());
For me it feels like given the dynamic nature of this method the parameters should be treated more like a unordered set rather than a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although, if we had a way to check if
IServiceProvider
has a parameter type registered
Unfortunately, the lack of this feature is a major pain for DI adaptors/implementations/wrappers/anything in itself.
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs
Outdated
Show resolved
Hide resolved
var instance = ActivatorUtilities.CreateInstance(serviceProvider, type); | ||
|
||
// Assert | ||
Assert.Equal("Parameterless", ((ClassWithParameterlessCtor)instance).CtorUsed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If IFakeService is in the service collection, it should be using the IFakeService constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thing is we only have an IServiceProvider
at this point and the only way to figure out if a type is registered in DI container is to actually create an instance of that type. Do you feel we should still try check that, despite the possible side effects?
Co-authored-by: xtqqczze <[email protected]>
8de4da5
to
ef32f76
Compare
{ | ||
foreach (ConstructorInfo? constructor in instanceType.GetConstructors()) | ||
argumentTypes = new Type[parameters.Length]; | ||
for (int index = 0; index < argumentTypes.Length; index++) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (int index = 0; index < argumentTypes.Length; index++) | |
for (int index = 0; index < parameters.Length; index++) |
Fix CQ issue:
@lawrence-laz are you still actively working on this? It seems like there is a lot of discussion going on, should we close the PR and move that discussion to an issue instead? |
Lets instead convert this to draft while that discussion is happening. |
Friendly ping @lawrence-laz. |
Sorry for not responding for a while, I'm preoccupied with some work right now. I originally suspended work on this PR due to still ongoing discussions in #46132. This PR has unified I think it makes sense to convert this to draft until then, so I did that. |
Draft Pull Request was automatically closed for inactivity. Please let us know if you'd like to reopen it. |
This pull request addresses issues #45119, #42339 and #46132.
For more details please look into commit messages and the discussion in #46132.