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

Type activator should be order agnostic #67493

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6d88e34
Type activator depends on constructors definition order if preferred …
mapogolions Apr 2, 2022
4381820
constructor marked as preferred should be considered the first option
mapogolions Apr 2, 2022
a859ea8
prevent parameter matching in case it is known that it will fail befo…
mapogolions Apr 2, 2022
6f74f65
break loop if a perfect constructor is found
mapogolions Apr 2, 2022
ed5655f
add empty line
mapogolions Apr 2, 2022
2491f50
replace the wasteful array construction operation
mapogolions Apr 3, 2022
5dab06b
rename local variable
mapogolions Apr 3, 2022
9cdbf31
fix indent, rename unit test
mapogolions Apr 3, 2022
d9c5458
remove nullable operator
mapogolions Apr 3, 2022
d646ee1
reproduce issue #46132
mapogolions Apr 3, 2022
92f188b
fix issue 46132
mapogolions Apr 3, 2022
1012cf0
remove prioritized constructor matcher
mapogolions Apr 3, 2022
fb7264a
specify issue
mapogolions Apr 3, 2022
9beb319
refactoring
mapogolions Apr 3, 2022
d93c558
fix sorting
mapogolions Apr 4, 2022
a935ddc
add additional checks
mapogolions Apr 4, 2022
6d8fdc2
chooses default constructor no matter order
mapogolions Apr 29, 2022
fe1c38e
type activator uses longest available constructor when multiple const…
mapogolions Apr 29, 2022
7e4a363
refactoring
mapogolions Apr 29, 2022
7aabe2c
update method signature
mapogolions Apr 29, 2022
9927510
should use the longest available constructor among constructors with …
mapogolions Apr 29, 2022
68ff999
Update src/libraries/Microsoft.Extensions.DependencyInjection/tests/D…
mapogolions Apr 29, 2022
175c637
Update src/libraries/Microsoft.Extensions.DependencyInjection/tests/D…
mapogolions Apr 29, 2022
e03f785
Update src/libraries/Microsoft.Extensions.DependencyInjection.Abstrac…
mapogolions Apr 29, 2022
35f0d21
remove dead code
mapogolions Apr 29, 2022
d9052f1
fix typo
mapogolions Apr 29, 2022
9ca1980
Merge branch 'main' into type-activator-should-be-order-agnostic
mapogolions Apr 29, 2022
219e21b
rename test cases, remove useless code
mapogolions Apr 30, 2022
686d660
remove assert
mapogolions Apr 30, 2022
3facdfe
add extra test for the longest available constructor rule
mapogolions Apr 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,49 +31,66 @@ public static object CreateInstance(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType,
params object[] parameters)
{
int bestLength = -1;
bool seenPreferred = false;
string? message = $"A suitable constructor for type '{instanceType}' could not be located. Ensure the type is concrete and all parameters of a public constructor are either registered as services or passed as arguments. Also ensure no extraneous arguments are provided.";
if (instanceType.IsAbstract)
{
throw new InvalidOperationException(message);
}

ConstructorMatcher bestMatcher = default;
ConstructorInfo[] constructors = instanceType.GetConstructors();
if (constructors.Length == 0)
{
throw new InvalidOperationException(message);
}

if (!instanceType.IsAbstract)
ConstructorInfo? preferredConstructor = null;
foreach (ConstructorInfo constructor in constructors)
{
foreach (ConstructorInfo? constructor in instanceType.GetConstructors())
if (!constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false))
{
var matcher = new ConstructorMatcher(constructor);
bool isPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false);
int length = matcher.Match(parameters);

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

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

if (isPreferred || bestLength < length)
{
bestLength = length;
bestMatcher = matcher;
}

seenPreferred |= isPreferred;
continue;
}
if (preferredConstructor is not null)
{
ThrowMultipleCtorsMarkedWithAttributeException();
}
preferredConstructor = constructor;
}

if (bestLength == -1)
if (preferredConstructor is not null)
{
string? message = $"A suitable constructor for type '{instanceType}' could not be located. Ensure the type is concrete and all parameters of a public constructor are either registered as services or passed as arguments. Also ensure no extraneous arguments are provided.";
throw new InvalidOperationException(message);
var matcher = new ConstructorMatcher(preferredConstructor);
matcher.Match(parameters);
if (matcher.MatchedLength == -1)
{
ThrowMarkedCtorDoesNotTakeAllProvidedArguments();
}
return matcher.CreateInstance(provider, throwIfFailed: true)!;
}
var matchers = new ConstructorMatcher[constructors.Length];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking about 2 optimizations here:

  1. When there is only a single constructor, just use it.
  2. When there are more than one, is there a way to not allocate an array here? Possibly we could stackalloc up to a reasonable count? Maybe 5 or 10? If the Type has more than that, then allocating an array seems OK since it won't be that common.

Maybe if we do (2), then special-casing (1) becomes unnecessary.

for (int i = 0; i < constructors.Length; i++)
{
ConstructorInfo constructor = constructors[i];
var matcher = new ConstructorMatcher(constructor);
matcher.Match(parameters);
matchers[i] = matcher;
}

return bestMatcher.CreateInstance(provider);
Array.Sort(matchers, (a, b) =>
{
return a.MatchedLength == b.MatchedLength
? b.ParametersCount - a.ParametersCount : b.MatchedLength - a.MatchedLength;
});

object? instance = null;
for (int i = 0; i < matchers.Length; i++)
{
ConstructorMatcher matcher = matchers[i];
if (matcher.MatchedLength == -1) break;
bool lastChance = i == matchers.Length - 1;
instance = matcher.CreateInstance(provider, throwIfFailed: lastChance);
if (instance is not null) return instance;
}
throw new InvalidOperationException(message);
}

/// <summary>
Expand Down Expand Up @@ -334,12 +351,21 @@ public ConstructorMatcher(ConstructorInfo constructor)
_constructor = constructor;
_parameters = _constructor.GetParameters();
_parameterValues = new object?[_parameters.Length];
ParametersCount = _parameters.Length;
}

public int Match(object[] givenParameters)
public int MatchedLength { get; private set; } = -1;
public int ParametersCount { get; }

public void Match(object[] givenParameters)
{
if (givenParameters.Length > _parameters.Length)
{
MatchedLength = -1;
return;
}
int applyIndexStart = 0;
int applyExactLength = 0;
MatchedLength = 0;
for (int givenIndex = 0; givenIndex != givenParameters.Length; givenIndex++)
{
Type? givenType = givenParameters[givenIndex]?.GetType();
Expand All @@ -357,21 +383,21 @@ public int Match(object[] givenParameters)
applyIndexStart++;
if (applyIndex == givenIndex)
{
applyExactLength = applyIndex;
MatchedLength = applyIndex;
}
}
}
}

if (givenMatched == false)
{
return -1;
MatchedLength = -1;
return;
}
}
return applyExactLength;
}

public object CreateInstance(IServiceProvider provider)
public object? CreateInstance(IServiceProvider provider, bool throwIfFailed = false)
{
for (int index = 0; index != _parameters.Length; index++)
{
Expand All @@ -382,7 +408,11 @@ public object CreateInstance(IServiceProvider provider)
{
if (!ParameterDefaultValue.TryGetDefaultValue(_parameters[index], out object? defaultValue))
{
throw new InvalidOperationException($"Unable to resolve service for type '{_parameters[index].ParameterType}' while attempting to activate '{_constructor.DeclaringType}'.");
if (throwIfFailed)
{
throw new InvalidOperationException($"Unable to resolve service for type '{_parameters[index].ParameterType}' while attempting to activate '{_constructor.DeclaringType}'.");
}
return null;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ public void TypeActivatorCreateFactoryDoesNotAllowForAmbiguousConstructorMatches
}

[Theory]
[InlineData("", "string")]
[InlineData("", "IFakeService, string")]
[InlineData(5, "IFakeService, int")]
public void TypeActivatorCreateInstanceUsesFirstMathchedConstructor(object value, string ctor)
public void TypeActivatorCreateInstanceUsesLongestAvailableConstructor(object value, string ctor)
{
// Arrange
var serviceCollection = new TestServiceCollection();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Xunit;

namespace Microsoft.Extensions.DependencyInjection.Tests
{
public class ActivatorUtilitiesTests
{
[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this test. Can you also add one that ensures the default constructor is picked in this scenario?

dotnet/maui#4318 (comment)

Basically:

        [Theory]
        [InlineData(typeof(DefaultConstructorFirst)]
        [InlineData(typeof(DefaultConstructorLast)]
        public void ChoosesDefaultConstructorNoMatterOrder(Type instanceType)
        {
            var services = new ServiceCollection();
            using var provider = services.BuildServiceProvider();

            var instance = ActivatorUtilities.CreateInstance(provider, instanceType);

            Assert.NotNull(instance);
        }

public class DefaultConstructorFirst
{
    public A A { get; }
    public B B { get; }

	public DefaultConstructorFirst() {}

    public DefaultConstructorFirst(ClassA a)
    {
        A = a;
    }

    public DefaultConstructorFirst(ClassA a, ClassB b)
    {
        A = a;
        B = b;
    }
}

public class DefaultConstructorLast
{
    public A A { get; }
    public B B { get; }

    public DefaultConstructorLast(ClassA a, ClassB b)
    {
        A = a;
        B = b;
    }

    public DefaultConstructorLast(ClassA a)
    {
        A = a;
    }

	public DefaultConstructorLast() {}
}

public void ShouldUseTheLongestAvailableConstructor()
{
var services = new ServiceCollection();
services.AddScoped<B>();
services.AddScoped<S>();
using var provider = services.BuildServiceProvider();
var a = new A();
var c = new C();

var instance = ActivatorUtilities.CreateInstance<Creatable>(provider, c, a);

Assert.NotNull(instance.B);
}

[Fact]
public void ShouldUseTheLongestAvailableConstructorOnlyIfConstructorsHaveTheSamePriority()
{
var services = new ServiceCollection();
services.AddScoped<B>();
services.AddScoped<S>();
using var provider = services.BuildServiceProvider();
var a = new A();
var c = new C();

var instance = ActivatorUtilities.CreateInstance<Creatable>(provider, a, c);

Assert.Null(instance.B);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should B be null here? There is a scoped service for B, so shouldn't the ctor that takes a B be picked?

Copy link
Contributor Author

@mapogolions mapogolions Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand from dotnet/aspnetcore#2915, the longest available constructor should only be used if competing constructors
have the same priority/score (the value of the MatchedLength property reflects it)

Creatable class has two constructors

  • Ctor(A a, B b, C c, S s)
  • Ctor(A a, C c, S s) : this(a, null, c, s)

Let's look at the following 3 examples

  1. ActivatorUtilities.CreateInstace(provider, new A(), new C());

According to the algorithm that was invented and used now and which I took as a basis, the first ctor is given score 1, the second one is given score 2 (2 given arguments match sequentially). As result the second constructor will be picked up (b is null)

  1. ActivatorUtilities.CreateInstance(provider, new A())

The first ctor is given score 1, the second ctor is given score 1. We fall into a situation where we have competing constructors. In this case, the rule about the longest available constructor comes into play.

  1. ActivatorUtilities.CreateInstance(provider, new C(), new A()) or ActivatorUtilities.CreateInstance(provider)
    Same as above, except for the fact that competing constructors are given score 0.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the discussion in #46132, it is asking for an ambiguous exception to be thrown to be thrown in this case. Which seems like the right thing IMO. If there are multiple ctors that we can't really pick between, it is better to throw and say "use the ActivatorUtilitiesConstructorAttribute to disambiguate". It is really hard to define perfect behavior here when the "given arguments" and the "services available" can intermix.

See also all the discussion on #46132 for all the scenarios, and the intended behaviors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mapogolions - any thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I don't see a way to satisfy all the mentioned requirements (especially ambiguity detection). Feel free to close this as a dead end.

Copy link
Member

@eerhardt eerhardt Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using the new (in 6.0) IServiceProviderIsService interface to test if a Type is available as a service in the IServiceProvider?

/// <summary>
/// Optional service used to determine if the specified type is available from the <see cref="IServiceProvider"/>.
/// </summary>
public interface IServiceProviderIsService
{
/// <summary>
/// Determines if the specified service type is available from the <see cref="IServiceProvider"/>.
/// </summary>
/// <param name="serviceType">An object that specifies the type of service object to test.</param>
/// <returns>true if the specified service is a available, false if it is not.</returns>
bool IsService(Type serviceType);
}

If the IServiceProvider doesn't support this new interface, then using the algorithm proposed here?

}

[Theory]
[InlineData(typeof(DefaultConstructorFirst))]
[InlineData(typeof(DefaultConstructorLast))]
public void ChoosesDefaultConstructorNoMatterOrder(Type instanceType)
{
var services = new ServiceCollection();
using var provider = services.BuildServiceProvider();

var instance = ActivatorUtilities.CreateInstance(provider, instanceType);

Assert.NotNull(instance);
}

[Fact]
public void ShouldTryToUseAllAvailableConstructorsBeforeThrowingActivationException()
{ // https://github.com/dotnet/runtime/issues/46132
var services = new ServiceCollection();
services.AddScoped<S>();
using var provider = services.BuildServiceProvider();
var a = new A();
var c = new C();

var instance = ActivatorUtilities.CreateInstance<Creatable>(
provider, c, a);

Assert.Same(a, instance.A);
Assert.Same(c, instance.C);
Assert.NotNull(instance.S);
Assert.Null(instance.B);
}

[Theory]
[InlineData(typeof(IClassWithAttribute.FirstConstructorWithAttribute))]
[InlineData(typeof(IClassWithAttribute.LastConstructorWithAttribute))]
public void ConstructorWithAttributeShouldHaveTheHighestPriorityNoMatterDefinitionOrder(Type instanceType)
{ // https://github.com/dotnet/runtime/issues/42339
var services = new ServiceCollection();
var a = new A();
services.AddSingleton(a);
using var provider = services.BuildServiceProvider();

var instance = (IClassWithAttribute)ActivatorUtilities
.CreateInstance(provider, instanceType, new B(), new C());

Assert.Same(a, instance.A);
}
}

internal class A { }
internal class B { }
internal class C { }
internal class S { }

internal class Creatable
{
public A A { get; }
public B B { get; }
public C C { get; }
public S S { get; }
public Creatable(A a, B b, C c, S s)
{
A = a;
B = b;
C = c;
S = s;
}

public Creatable(A a, C c, S s) : this (a, null, c, s) { }
}

internal interface IClassWithAttribute
{
public A A { get; }
public B B { get; }
public C C { get; }

public class FirstConstructorWithAttribute : IClassWithAttribute
{
public A A { get; }
public B B { get; }
public C C { get; }

[ActivatorUtilitiesConstructor]
public FirstConstructorWithAttribute(A a, B b, C c)
{
A = a;
B = b;
C = c;
}

public FirstConstructorWithAttribute(B b, C c) : this(null, b, c) { }
}

public class LastConstructorWithAttribute : IClassWithAttribute
{
public A A { get; }
public B B { get; }
public C C { get; }

public LastConstructorWithAttribute(B b, C c) : this(null, b, c) { }


[ActivatorUtilitiesConstructor]
public LastConstructorWithAttribute(A a, B b, C c)
{
A = a;
B = b;
C = c;
}
}
}

internal class DefaultConstructorFirst
{
public A A { get; }
public B B { get; }

public DefaultConstructorFirst() { }

public DefaultConstructorFirst(A a)
{
A = a;
}

public DefaultConstructorFirst(A a, B b)
{
A = a;
B = b;
}
}

internal class DefaultConstructorLast
{
public A A { get; }
public B B { get; }

public DefaultConstructorLast(A a, B b)
{
A = a;
B = b;
}

public DefaultConstructorLast(A a)
{
A = a;
}

public DefaultConstructorLast() { }
}
}