-
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
ActivatorUtilities.CreateInstance depends on the order of constructors' definitions #46132
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
@GrabYourPitchforks sorry, isn't it |
@quixoticaxis You're right - apologies! |
Tagging subscribers to this area: @eerhardt, @maryamariyan Issue DetailsDescriptionCurrent behaviorThe effect of calling Expected behaviorThe call throws because it failed to disambiguate the appropriate constructor. This behavior will be in-line with the behavior of Reproductionusing Microsoft.Extensions.DependencyInjection;
using static System.Console;
namespace AUCI
{
public class A { }
public class B { }
public class C { }
public class S { }
public class Creatable
{
public Creatable(
A a,
B b,
C c,
S s)
{
WriteLine("Creatable.Long");
}
public Creatable(
A a,
C c,
S s)
{
WriteLine("Creatable.Short");
}
}
public class Program
{
public static void Main(string[] args)
{
var services = new ServiceCollection();
services.AddScoped<S>();
using var provider = services.BuildServiceProvider();
var instance = ActivatorUtilities.CreateInstance(
provider,
typeof(Creatable),
// Please, note that the order of parameters does not match the order of constructors' arguments
new C(), new A());
}
}
} The code above throws when invoking public class Creatable
{
public Creatable(
A a,
C c,
S s)
{
WriteLine("Creatable.Short");
}
public Creatable(
A a,
B b,
C c,
S s)
{
WriteLine("Creatable.Long");
}
} It's counter-intuitive, IMHO. Additional findingsI've looked through the implementation and debugged it:
ConfigurationThe issue is not specific to configuration. Regression?Does not seem to be a regression. Other informationIt's probably a corner case, but it still would be great to remove the undefined (as I understand it) behavior.
|
This may be what causes #45119. It isn't clear to me how this should be fixed, though. If
then, I think it would be best if ActivatorUtilities selected the A(D, E) constructor and requested only instances of D and E from the IServiceProvider. However, IServiceProvider does not provide a way for ActivatorUtilities to ask whether it can provide B and C, without actually creating an instance of B. Thus, the IServiceProvider might have to create an instance of B, and perhaps instances of services required by B, even though ActivatorUtilities would be unable to use them in the end. Perhaps that would not matter in practice. |
@KalleOlaviNiemitalo I personally would prefer the call to fail in any case of ambiguity and make using an attribute a must for the case when the class has more than one constructor and Also a separate method or an overload that takes |
There is no ambiguity here. what is happening is you are providing the class with the two constructors. public Creatable(A a, B b, C c,S s)
public Creatable(A a, C c,S s) It tries to find which constructor can be used to create the object when passing the parameters When having The right fix here is when searching for suitable constructor to use to create the object, it should check if all parameters of such constructor can be created and choose it if it does. Otherwise, it shouldn't select this constructor. The only thing with this is, would be there any perf concern doing that? that is the part that need farther investigation. @davidfowl do you think the perf will matter much in such cases? to be more specific: In the line Line 63 in 2c8f2dc
before setting the best matcher, it should perform the following checks on all missing constructor parameters: provider.GetService(_parameters[index].ParameterType) != null ||
ParameterDefaultValue.TryGetDefaultValue(_parameters[index], out object? defaultValue) // We may store the created object if needed so we don't have to recreate it again in `CreateInstance(IServiceProvider provider)`. or try not creating the parameter object at all. |
@tarekgh Sorry, I probably don't understand something.
You say that there is no ambiguity, then say that the code picks the first of the constructors. Is there any notion of constructor/member ordering in C#? |
Yes, we use reflection and it give us the constructors (and methods) in specific order. Usually, we encourage to not taking dependency on such order. but for ActivatorUtlities just go through the list we get from the reflection.
Yes, you shouldn't assume any order in general. but at the end if you are requesting the list of constructors, would be the reflection to decide which order will give to you. If you look at https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructors?view=net-5.0 it is clearly state in the Remarks section You may try manually run foreach (ConstructorInfo? constructor in instanceType.GetConstructors()) Line 41 in 2c8f2dc
And look at the order in your case and when you switch the order in the source code too. |
This is problematic, there's another issue for this somewhere. |
@davidfowl might you mean #46272 for GetProperties? |
@tarekgh this is what I was speaking about. As far as the documented contract goes, it does not even guarantee the ordering being stable between two calls to the API. Maybe we treat the term "ambiguous" differently, but I suppose that the choice between two equally suitable variants based on implementation details can be called "ambiguous" (or "undefined", but I cannot see any meaningful way to define the current behavior.) |
@quixoticaxis 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. |
Sorry, misclicked "Close". @maryamariyan could you, please, move the issue back to the 6.0.0 milestone? |
@tarekgh I see, thank you for the explanation. |
CreateFactory and CreateInstance should behave similarly. Today they don't and that's broken |
This issue is mentioned in the work planned for .NET 7 story, but it doesn't look like it's been touched in a while. Is it still planned for .NET 7? If someone where to have a crack at it now, would it make it into .NET 7? |
We've moved it from the work planned for .NET 7 story. It is no longer planned for .NET 7 (unfortunately). |
Seeing that this issue is still open, I have an additional concern that I'd like you to consider when tackling this. Orleans is using ActivatorUtilities to create objects, but would prefer to have internalized attributes so that they can follow a consistent naming scheme (for ease of discovery). That is, we'd like to be able to define an OrleansConstructorAttribute and then somehow tell ActivatorUtilities that this corresponds to the ActivatorUtilitiesConstructorAttribute. ActivatorUtilities would effectively have to match attribute names against a list of possible names, rather than looking for a single name match. Given the broad utility of ActivatorUtilities, I can see how quite a few projects could benefit from being able to reuse this logic without sacrificing discoverability of their attributes. |
@mnmr - that is a new scenario, not what this issue is tracking. Please open a new API proposal issue for your scenario. |
I've started working on this issue and looked at the above conversations, linked issues, and use cases and approaches that were considered. Given all the information presented in this issue, here's a run down of the conclusions and plan of action and the basic idea behind what the 1 - It first looks a constructor using Find longest constructor which: Rank all the matched constructors with the number of arguments that fitted the above criteria. If we find only one constructor with the biggest rank, we pick that to instantiate the item with. Note about ordering:
In a later comment I'll build up all test cases and provide cases that would end up having behaviour change due to this. |
@maryamariyan thanks for the write-up. This all seems reasonable to me. I'd also love to see specific examples of scenarios that: 1. didn't work before, but now work, and 2. worked before but now work differently (e.g. different overload chosen, or throws). Also my understanding is that @eerhardt @davidfowl and any other folks watching: I think it's super important for us to get feedback on whether this is overall a good change, including any breaking aspects (such as throwing on cases that would now be considered ambiguous). |
There are 2 instances of Maui hitting this issue that caused them to stop using dotnet/maui#4318 (comment) That first one is the canonical case for what needs to be fixed here: Fails: public LoginView(RandomItem item)
{
//this is constructor is invoked by code and RandomItem is not registered in DI
InitializeComponent();
}
public LoginView()
{
InitializeComponent();
} Works: public LoginView()
{
InitializeComponent();
}
public LoginView(RandomItem item)
{
//this is constructor is invoked by code and RandomItem is not registered in DI
InitializeComponent();
} The only difference is the order in which the ctors are defined. |
Took use cases in comment #46132 (comment) and updated the expected result with latest considerations. Key use cases to consider:
|
I think no matter what we do, we need to align the behaviors of CreateFactory and CreateInsatance. If we're going to have to break people no matter what to align behaviors, I think going with the stricter unambiguous CreateFactory behavior makes sense. |
Agreed - where it is possible, we should have aligned behaviors between the two. One thing to remember is that |
I think it should be fine that CreateFactory(Type, Type[])
// Simply picks an unambiguous constructor based on the provided types
// vs.
CreateInstance(IServiceProvider, Type, Object[])
CreateInstance<T>(IServiceProvider, Object[])
// Uses `Object[]` instances to look for required types in a constructor (the behaviour today is buggy and inaccurate)
// Proposal:
// Use `IServiceProvider` to look for registered services
// Pick the longest constructor with all the required instances
// Pick the longest constructor with all the most registered services and including default parameters Contrast that with As a result, I am proposing for |
Here's the latest consensus:
For example, given: public class A
{
A(B b) { }
}
General idea:
|
If none are found, try to fallback to CreateFactory, more than one throw |
This change will be in .NET 8.0 Preview 1 [Breaking change]: ActivatorUtilities.CreateInstance behaves consistently independent of ctor definition order |
Description
Current behavior
The effect of calling
ActivatorUtilities.CreateInstance
methods (any of the overloads) depends on the order in which the constructors of the type being created are defined. The instance of the class is created if constructors are defined in one order, but the creation throws if constructors are defined in reversed order (see the code below).Expected behavior
The call throws because it failed to disambiguate the appropriate constructor. This behavior will be in-line with the behavior of
Activator.CreateInstance
.Reproduction
The code above throws when invoking
ActivatorUtilities.CreateInstance
, but works fine if we define the constructors in reversed order:It's counter-intuitive, IMHO.
Additional findings
I've looked through the implementation and debugged it:
ConstructorMatcher.Match
returns0
(meaning that the constructor is the worst possible match, the constructor does not match our parameters at all);ConstructorMatcher.Match
returns0
, so it's not better than the first one and we dismiss it;Configuration
The issue is not specific to configuration.
Regression?
Does not seem to be a regression.
Other information
It's probably a corner case, but it still would be great to remove the undefined (as I understand it) behavior.
The text was updated successfully, but these errors were encountered: