diff --git a/benchmarks/DI.Performance/ActivatorUtilitiesBenchmark.cs b/benchmarks/DI.Performance/ActivatorUtilitiesBenchmark.cs new file mode 100644 index 0000000..f345e0d --- /dev/null +++ b/benchmarks/DI.Performance/ActivatorUtilitiesBenchmark.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.Extensions.DependencyInjection.Performance +{ + public class ActivatorUtilitiesBenchmark + { + private ServiceProvider _serviceProvider; + private ObjectFactory _factory; + private object[] _factoryArguments; + + [GlobalSetup] + public void SetUp() + { + var collection = new ServiceCollection(); + collection.AddTransient(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddTransient(); + + _serviceProvider = collection.BuildServiceProvider(); + _factory = ActivatorUtilities.CreateFactory(typeof(TypeToBeActivated), new Type[] { typeof(DependencyB), typeof(DependencyC) }); + _factoryArguments = new object[] { new DependencyB(), new DependencyC() }; + } + + [Benchmark] + public void ServiceProvider() + { + _serviceProvider.GetService(); + } + + [Benchmark] + public void Factory() + { + _ = (TypeToBeActivated)_factory(_serviceProvider, _factoryArguments); + } + + [Benchmark] + public void CreateInstance() + { + ActivatorUtilities.CreateInstance(_serviceProvider, _factoryArguments); + } + + public class TypeToBeActivated + { + public TypeToBeActivated(int i) + { + throw new NotImplementedException(); + } + + public TypeToBeActivated(string s) + { + throw new NotImplementedException(); + } + + public TypeToBeActivated(object o) + { + throw new NotImplementedException(); + } + + public TypeToBeActivated(DependencyA a, DependencyB b, DependencyC c) + { + } + } + + public class DependencyA {} + public class DependencyB {} + public class DependencyC {} + } +} diff --git a/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilities.cs b/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilities.cs index eec3d9d..e2553ce 100644 --- a/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilities.cs +++ b/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilities.cs @@ -41,26 +41,41 @@ namespace Microsoft.Extensions.Internal public static object CreateInstance(IServiceProvider provider, Type instanceType, params object[] parameters) { int bestLength = -1; + var seenPreferred = false; + ConstructorMatcher bestMatcher = null; if (!instanceType.GetTypeInfo().IsAbstract) { - foreach (var matcher in instanceType + foreach (var constructor in instanceType .GetTypeInfo() .DeclaredConstructors - .Where(c => !c.IsStatic && c.IsPublic) - .Select(constructor => new ConstructorMatcher(constructor))) + .Where(c => !c.IsStatic && c.IsPublic)) { + var matcher = new ConstructorMatcher(constructor); + var isPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false); var length = matcher.Match(parameters); - if (length == -1) + + if (isPreferred) { - continue; + if (seenPreferred) + { + ThrowMultipleCtorsMarkedWithAttributeException(); + } + + if (length == -1) + { + ThrowMarkedCtorDoesNotTakeAllProvidedArguments(); + } } - if (bestLength < length) + + if (isPreferred || bestLength < length) { bestLength = length; bestMatcher = matcher; } + + seenPreferred |= isPreferred; } } @@ -203,6 +218,21 @@ namespace Microsoft.Extensions.Internal matchingConstructor = null; parameterMap = null; + if (!TryFindPreferredConstructor(instanceType, argumentTypes, ref matchingConstructor, ref parameterMap) && + !TryFindMatchingConstructor(instanceType, argumentTypes, ref matchingConstructor, ref parameterMap)) + { + var message = $"A suitable constructor for type '{instanceType}' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor."; + throw new InvalidOperationException(message); + } + } + + // Tries to find constructor based on provided argument types + private static bool TryFindMatchingConstructor( + Type instanceType, + Type[] argumentTypes, + ref ConstructorInfo matchingConstructor, + ref int?[] parameterMap) + { foreach (var constructor in instanceType.GetTypeInfo().DeclaredConstructors) { if (constructor.IsStatic || !constructor.IsPublic) @@ -222,11 +252,43 @@ namespace Microsoft.Extensions.Internal } } - if (matchingConstructor == null) + return matchingConstructor != null; + } + + // Tries to find constructor marked with ActivatorUtilitiesConstructorAttribute + private static bool TryFindPreferredConstructor( + Type instanceType, + Type[] argumentTypes, + ref ConstructorInfo matchingConstructor, + ref int?[] parameterMap) + { + var seenPreferred = false; + foreach (var constructor in instanceType.GetTypeInfo().DeclaredConstructors) { - var message = $"A suitable constructor for type '{instanceType}' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor."; - throw new InvalidOperationException(message); + if (constructor.IsStatic || !constructor.IsPublic) + { + continue; + } + + if (constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false)) + { + if (seenPreferred) + { + ThrowMultipleCtorsMarkedWithAttributeException(); + } + + if (!TryCreateParameterMap(constructor.GetParameters(), argumentTypes, out int?[] tempParameterMap)) + { + ThrowMarkedCtorDoesNotTakeAllProvidedArguments(); + } + + matchingConstructor = constructor; + parameterMap = tempParameterMap; + seenPreferred = true; + } } + + return matchingConstructor != null; } // Creates an injective parameterMap from givenParameterTypes to assignable constructorParameters. @@ -353,5 +415,15 @@ namespace Microsoft.Extensions.Internal } } } + + private static void ThrowMultipleCtorsMarkedWithAttributeException() + { + throw new InvalidOperationException($"Multiple constructors were marked with {nameof(ActivatorUtilitiesConstructorAttribute)}."); + } + + private static void ThrowMarkedCtorDoesNotTakeAllProvidedArguments() + { + throw new InvalidOperationException($"Constructor marked with {nameof(ActivatorUtilitiesConstructorAttribute)} does not accept all given argument types."); + } } } diff --git a/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilitiesConstructorAttribute.cs b/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilitiesConstructorAttribute.cs new file mode 100644 index 0000000..67ffa13 --- /dev/null +++ b/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilitiesConstructorAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +#if ActivatorUtilities_In_DependencyInjection +namespace Microsoft.Extensions.DependencyInjection +#else +namespace Microsoft.Extensions.Internal +#endif +{ + /// + /// Marks the constructor to be used when activating type using . + /// + +#if ActivatorUtilities_In_DependencyInjection + public +#else + // Do not take a dependency on this class unless you are explicitly trying to avoid taking a + // dependency on Microsoft.AspNetCore.DependencyInjection.Abstractions. + internal +#endif + class ActivatorUtilitiesConstructorAttribute: Attribute + { + } +} diff --git a/src/DI.Specification.Tests/ActivatorUtilitiesTests.cs b/src/DI.Specification.Tests/ActivatorUtilitiesTests.cs index f9dff32..7a3ee94 100644 --- a/src/DI.Specification.Tests/ActivatorUtilitiesTests.cs +++ b/src/DI.Specification.Tests/ActivatorUtilitiesTests.cs @@ -205,6 +205,62 @@ namespace Microsoft.Extensions.DependencyInjection.Specification Assert.Equal(expectedMessage, ex.Message); } + [Theory] + [InlineData("", "string")] + [InlineData(5, "IFakeService, int")] + public void TypeActivatorCreateInstanceUsesFirstMathchedConstructor(object value, string ctor) + { + // Arrange + var serviceCollection = new TestServiceCollection(); + serviceCollection.AddSingleton(); + var serviceProvider = CreateServiceProvider(serviceCollection); + var type = typeof(ClassWithAmbiguousCtors); + + // Act + var instance = ActivatorUtilities.CreateInstance(serviceProvider, type, value); + + // Assert + Assert.Equal(ctor, ((ClassWithAmbiguousCtors)instance).CtorUsed); + } + + [Theory] + [MemberData(nameof(CreateInstanceFuncs))] + public void TypeActivatorUsesMarkedConstructor(CreateInstanceFunc createFunc) + { + // Arrange + var serviceCollection = new TestServiceCollection(); + serviceCollection.AddSingleton(); + var serviceProvider = CreateServiceProvider(serviceCollection); + + // Act + var instance = CreateInstance(createFunc, serviceProvider, "hello"); + + // Assert + Assert.Equal("IFakeService, string", instance.CtorUsed); + } + + [Theory] + [MemberData(nameof(CreateInstanceFuncs))] + public void TypeActivatorThrowsOnMultipleMarkedCtors(CreateInstanceFunc createFunc) + { + // Act + var exception = Assert.Throws(() => CreateInstance(createFunc, null, "hello")); + + // Assert + Assert.Equal("Multiple constructors were marked with ActivatorUtilitiesConstructorAttribute.", exception.Message); + } + + [Theory] + [MemberData(nameof(CreateInstanceFuncs))] + public void TypeActivatorThrowsWhenMarkedCtorDoesntAcceptArguments(CreateInstanceFunc createFunc) + { + // Act + var exception = Assert.Throws(() => CreateInstance(createFunc, null, 0, "hello")); + + // Assert + Assert.Equal("Constructor marked with ActivatorUtilitiesConstructorAttribute does not accept all given argument types.", exception.Message); + } + [Fact] public void GetServiceOrCreateInstanceRegisteredServiceTransient() { diff --git a/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtors.cs b/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtors.cs index 90015f3..9f0e978 100644 --- a/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtors.cs +++ b/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtors.cs @@ -7,14 +7,17 @@ namespace Microsoft.Extensions.DependencyInjection.Specification.Fakes { public ClassWithAmbiguousCtors(string data) { + CtorUsed = "string"; } public ClassWithAmbiguousCtors(IFakeService service, string data) { + CtorUsed = "IFakeService, string"; } public ClassWithAmbiguousCtors(IFakeService service, int data) { + CtorUsed = "IFakeService, int"; } public ClassWithAmbiguousCtors(IFakeService service, string data1, int data2) @@ -22,6 +25,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification.Fakes FakeService = service; Data1 = data1; Data2 = data2; + + CtorUsed = "IFakeService, string, string"; } public IFakeService FakeService { get; } @@ -29,5 +34,6 @@ namespace Microsoft.Extensions.DependencyInjection.Specification.Fakes public string Data1 { get; } public int Data2 { get; } + public string CtorUsed { get; set; } } } \ No newline at end of file diff --git a/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtorsAndAttribute.cs b/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtorsAndAttribute.cs new file mode 100644 index 0000000..65b2494 --- /dev/null +++ b/src/DI.Specification.Tests/Fakes/ClassWithAmbiguousCtorsAndAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.DependencyInjection.Specification.Fakes +{ + public class ClassWithAmbiguousCtorsAndAttribute + { + public ClassWithAmbiguousCtorsAndAttribute(string data) + { + CtorUsed = "string"; + } + + [ActivatorUtilitiesConstructor] + public ClassWithAmbiguousCtorsAndAttribute(IFakeService service, string data) + { + CtorUsed = "IFakeService, string"; + } + + public ClassWithAmbiguousCtorsAndAttribute(IFakeService service, IFakeOuterService service2, string data) + { + CtorUsed = "IFakeService, IFakeService, string"; + } + + public string CtorUsed { get; set; } + } +} \ No newline at end of file diff --git a/src/DI.Specification.Tests/Fakes/ClassWithMultipleMarkedCtors.cs b/src/DI.Specification.Tests/Fakes/ClassWithMultipleMarkedCtors.cs new file mode 100644 index 0000000..af364fb --- /dev/null +++ b/src/DI.Specification.Tests/Fakes/ClassWithMultipleMarkedCtors.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.DependencyInjection.Specification.Fakes +{ + public class ClassWithMultipleMarkedCtors + { + [ActivatorUtilitiesConstructor] + public ClassWithMultipleMarkedCtors(string data) + { + } + + [ActivatorUtilitiesConstructor] + public ClassWithMultipleMarkedCtors(IFakeService service, string data) + { + } + } +} \ No newline at end of file