Skip to content

ActivatorUtilities - matching preferred constructor depends on the constructors order #98959

Open
@ktyl

Description

@ktyl

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions