Skip to content
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 - matching preferred constructor depends on the constructors order #98959

Open
ktyl opened this issue Feb 26, 2024 · 10 comments

Comments

@ktyl
Copy link

ktyl commented Feb 26, 2024

Description

Selecting the preferred constructor by ActivatorUtilitiesConstructor attribute works differently depending on the order of constructors.

Reproduction Steps

Let's consider the following class:

public class Foo 
{
    public Foo(Bar bar)
    {
        this.Bar = bar;
    }

    public Foo(Baz bar)
    {
        this.Baz = bar;
    } 
 
    public Bar Bar { get; }
    public Baz Baz { get; }
}

public class Bar {}
public class Baz {}

Let's assume that I want to instantiate this class using the Dependency Injection container:

var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<Foo>();
serviceCollection.AddTransient<Bar>();
serviceCollection.AddTransient<Baz>();

var serviceProvider = serviceCollection.BuildServiceProvider();

var foo = serviceProvider.GetService<Foo>();

This obviously fails because ActivatorUtilities class finds two matching constructors.
Fortunately there is a [ActivatorUtilitiesConstructorAttribute](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilitiesConstructorAttribute.cs) so let's adjust the class Foo:

public class Foo 
{
    [ActivatorUtilitiesConstructor]
    public Foo(Bar bar)
    {
        this.Bar = bar;
    }

    public Foo(Baz bar)
    {
        this.Baz = bar;
    } 
 
    public Bar Bar { get; }
    public Baz Baz { get; }
}

// the same wiring as before
var foo = serviceProvider.GetService<Foo>();

This still throws the same error.

Expected behavior

If [ActivatorUtilitiesConstructor] is used the constructor should be prioritized above all other constructors if all its dependencies are satisfied, regardless of the order of the constructors in the source code.

Actual behavior

I have digged in the source code and I think the reason is a mistake in ActivatorUtilities class.
The following snippet of code (https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs#L95)

if (isPreferred)
{
    if (seenPreferred)
    {
        ThrowMultipleCtorsMarkedWithAttributeException();
    }

    if (length == -1)
    {
        ThrowMarkedCtorDoesNotTakeAllProvidedArguments();
    }
}

if (isPreferred || bestLength < length)
{
    bestLength = length;
    bestMatcher = matcher;
    multipleBestLengthFound = false;
}
else if (bestLength == length)
{
    multipleBestLengthFound = true;
}

isPreferred is properly set to true based on the existance of the attribute on the first constructor. But when the second constructor (public Foo(Baz baz)) is analyzed the condition in line 108 (if (isPreferred || bestLength < length)) is false, but the else statement in line 114 (else if (bestLength == length)) is evaluated to true and therefore the constructor is also considered to be the best candidate.

If the constructors appeared in an opposite order the condition in line 108 would be true and the code would have worked properly.

Regression?

No response

Known Workarounds

Add the constructor decorated with ActivatorUtilitiesConstructorAttribute after all other constructors in your class.

Configuration

No response

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Feb 26, 2024
@ghost
Copy link

ghost commented Feb 26, 2024

Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Selecting the preferred constructor by ActivatorUtilitiesConstructor attribute works differently depending on the order of constructors.

Reproduction Steps

Let's consider the following class:

public class Foo 
{
    public Foo(Bar bar)
    {
        this.Bar = bar;
    }

    public Foo(Baz bar)
    {
        this.Baz = bar;
    } 
 
    public Bar Bar { get; }
    public Baz Baz { get; }
}

public class Bar {}
public class Baz {}

Let's assume that I want to instantiate this class using the Dependency Injection container:

var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<Foo>();
serviceCollection.AddTransient<Bar>();
serviceCollection.AddTransient<Baz>();

var serviceProvider = serviceCollection.BuildServiceProvider();

var foo = serviceProvider.GetService<Foo>();

This obviously fails because ActivatorUtilities class finds two matching constructor.
Fortunately there is a [ActivatorUtilitiesConstructorAttribute](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilitiesConstructorAttribute.cs) so let's adjust the class Foo:

public class Foo 
{
    [ActivatorUtilitiesConstructor]
    public Foo(Bar bar)
    {
        this.Bar = bar;
    }

    public Foo(Baz bar)
    {
        this.Baz = bar;
    } 
 
    public Bar Bar { get; }
    public Baz Baz { get; }
}

// the same wiring as before
var foo = serviceProvider.GetService<Foo>();

This still throws the same error.

Expected behavior

If [ActivatorUtilitiesConstructor] is used the constructor should be prioritized above all other constructors if all its dependencies are satisfied, regardless of the order of the constructors in the source code.

Actual behavior

I have digged in the source code and I think the reason is a mistake in ActivatorUtilities class.
The following snippet of code (https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs#L95)

if (isPreferred)
{
    if (seenPreferred)
    {
        ThrowMultipleCtorsMarkedWithAttributeException();
    }

    if (length == -1)
    {
        ThrowMarkedCtorDoesNotTakeAllProvidedArguments();
    }
}

if (isPreferred || bestLength < length)
{
    bestLength = length;
    bestMatcher = matcher;
    multipleBestLengthFound = false;
}
else if (bestLength == length)
{
    multipleBestLengthFound = true;
}

isPreferred is properly set to true based on the existance of the attribute on the first constructor. But when the second constructor (public Foo(Baz baz)) is analyzed the condition in line 108 (if (isPreferred || bestLength < length)) is false, but the else statement in line 114 (else if (bestLength == length)) is evaluated to true and therefore the constructor is also considered to be the best candidate.

If the constructors appeared in an opposite order the condition in line 108 would be true and the code would have worked properly.

Regression?

No response

Known Workarounds

Add the constructor decorated with ActivatorUtilitiesConstructorAttribute after all other constructors in your class.

Configuration

No response

Other information

No response

Author: ktyl
Assignees: -
Labels:

untriaged, area-Extensions-DependencyInjection

Milestone: -

@steveharter steveharter self-assigned this Feb 28, 2024
@steveharter
Copy link
Member

Verified repro on v7 - v9P1.

@steveharter steveharter added bug and removed untriaged New issue has not been triaged by the area owner labels Feb 29, 2024
@steveharter steveharter added this to the 9.0.0 milestone Feb 29, 2024
@ktyl
Copy link
Author

ktyl commented Feb 29, 2024

Thanks @steveharter for verifying. Is there a chance that it can get fixed in the future releases of v8 as well? On the other side I can imagine that as this would change the behavior of the runtime it could be considered to be a breaking change in some cases...

@steveharter
Copy link
Member

I looked at this in greater detail: Calling GetService() does not check for [ActivatorUtilitiesConstructor]. It goes through code on CreateConstructorCallSite().

Only ActivatorUtilities.CreateInstance() checks for [ActivatorUtilitiesConstructor].

However, changing your repro to use

var foo = ActivatorUtilities.CreateInstance<Foo>(serviceProvider);

instead of

var foo = serviceProvider.GetService<Foo>();

still results in the same issue - the ordering of the constructors affects the check for [ActivatorUtilitiesConstructor] which it should not.

@steveharter
Copy link
Member

I assume the semantics of ActivatorUtilities.CreateInstance() should be:

  • We always pick the constructor with the most matched parameters first.
  • If there is more than one such constructor (with the most matched parameters), we select the one with [ActivatorUtilitiesConstructor] (if there is one), otherwise we throw.

@tarekgh does this sound correct? I may push a PR soon that does this.

@tarekgh
Copy link
Member

tarekgh commented Mar 1, 2024

@steveharter

Mostly correct. The following comment describing it:

https://source.dot.net/#Microsoft.Extensions.DependencyInjection.Abstractions/ActivatorUtilities.cs,75

The only difference is, when seeing a constructor with [ActivatorUtilitiesConstructor], this will be selected even if we encountered a constructor with longer parameters. But after encountering [ActivatorUtilitiesConstructor] and then getting a constructor with longer parameters, the one with the longer parameters will be selected. I am not sure if the reflection always enumerate constructors according to the parameters count or this is not guaranteed? If reflection guarantee that, then what you wrote is accurate. if not, then it is not.

CC @eerhardt

@steveharter
Copy link
Member

I am not sure if the reflection always enumerate constructors according to the parameters count or this is not guaranteed? I

Reflection will return the constructors in the order they are defined.

However it looks like ctor lookup from GetService() does order the constructors.

But after encountering [ActivatorUtilitiesConstructor] and then getting a constructor with longer parameters, the one with the longer parameters will be selected.

So reflection ordering does matter then. That doesn't seem like a good thing IMO. In either case, I will still put up a PR to address the case where ordering with the same number of parameters doesn't always respect [ActivatorUtilitiesConstructor].

Aside, ideally, it seems like provider.GetService() should use ActivatorUtilities.CreateInstance(provider) when it is time to find and call the constructor for a service. That would also support [ActivatorUtilitiesConstructor] for both cases; today they have different semantics.

@steveharter
Copy link
Member

Moving to V10; this area is still broke and\or confusing. The DI GetService() does not use the attribute - see #98959 (comment).

@steveharter steveharter modified the milestones: 9.0.0, 10.0.0 Jul 24, 2024
@julealgon
Copy link

julealgon commented Jul 24, 2024

Does anybody know why the semantics between services.GetService<T> and ActivatorUtilities.CreateInstance<T> are different like this?

We have a few places in our application that rely on ActivatorUtilities for adding DI support to some custom frameworks and we got bit by the breaking changes in behavior back when .NET 8 version launched, yet the same breaking change didn't impact normal service resolution.

This always felt like a poor design to me. Feels like there shouldn't be any difference in activating a registered type, vs constructing an unregistered one as long as you didn't register a construction lambda in the registration. Both operations are semantically identical.

@tarekgh
Copy link
Member

tarekgh commented Jul 24, 2024

Does anybody know why the semantics between services.GetService and ActivatorUtilities.CreateInstance are different like this?

That is what @steveharter tracking to fix. We are keeping this issue open to address that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants