diff --git a/src/libraries/Common/src/Extensions/ParameterDefaultValue/ParameterDefaultValue.netstandard.cs b/src/libraries/Common/src/Extensions/ParameterDefaultValue/ParameterDefaultValue.netstandard.cs index 089c64afe03b00..78486aa1e37ef8 100644 --- a/src/libraries/Common/src/Extensions/ParameterDefaultValue/ParameterDefaultValue.netstandard.cs +++ b/src/libraries/Common/src/Extensions/ParameterDefaultValue/ParameterDefaultValue.netstandard.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.Serialization; namespace Microsoft.Extensions.Internal { diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs index 43e8f5627a2d28..559095b73c9f4c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs @@ -17,6 +17,11 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class ActivatorUtilities { +#if NET8_0_OR_GREATER + // Maximum number of fixed arguments for ConstructorInvoker.Invoke(arg1, etc). + private const int FixedArgumentThreshold = 4; +#endif + private static readonly MethodInfo GetServiceInfo = GetMethodInfo>((sp, t, r, c) => GetService(sp, t, r, c)); @@ -140,7 +145,6 @@ public static ObjectFactory CreateFactory( return CreateFactoryReflection(instanceType, argumentTypes); } #endif - CreateFactoryInternal(instanceType, argumentTypes, out ParameterExpression provider, out ParameterExpression argumentArray, out Expression factoryExpressionBody); var factoryLambda = Expression.Lambda>( @@ -174,7 +178,6 @@ public static ObjectFactory return (serviceProvider, arguments) => (T)factory(serviceProvider, arguments); } #endif - CreateFactoryInternal(typeof(T), argumentTypes, out ParameterExpression provider, out ParameterExpression argumentArray, out Expression factoryExpressionBody); var factoryLambda = Expression.Lambda>( @@ -235,16 +238,22 @@ private static MethodInfo GetMethodInfo(Expression expr) return mc.Method; } - private static object? GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired) + private static object? GetService(IServiceProvider sp, Type type, Type requiredBy, bool hasDefaultValue) { object? service = sp.GetService(type); - if (service == null && !isDefaultParameterRequired) + if (service is null && !hasDefaultValue) { - throw new InvalidOperationException(SR.Format(SR.UnableToResolveService, type, requiredBy)); + ThrowHelperUnableToResolveService(type, requiredBy); } return service; } + [DoesNotReturn] + private static void ThrowHelperUnableToResolveService(Type type, Type requiredBy) + { + throw new InvalidOperationException(SR.Format(SR.UnableToResolveService, type, requiredBy)); + } + private static BlockExpression BuildFactoryExpression( ConstructorInfo constructor, int?[] parameterMap, @@ -289,53 +298,114 @@ private static BlockExpression BuildFactoryExpression( } #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP + [DoesNotReturn] + private static void ThrowHelperArgumentNullExceptionServiceProvider() + { + throw new ArgumentNullException("serviceProvider"); + } + private static ObjectFactory CreateFactoryReflection( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType, Type?[] argumentTypes) { FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructor, out int?[] parameterMap); + Type declaringType = constructor.DeclaringType!; + +#if NET8_0_OR_GREATER + ConstructorInvoker invoker = ConstructorInvoker.Create(constructor); ParameterInfo[] constructorParameters = constructor.GetParameters(); if (constructorParameters.Length == 0) { return (IServiceProvider serviceProvider, object?[]? arguments) => - constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: null, culture: null); + invoker.Invoke(); } - FactoryParameterContext[] parameters = new FactoryParameterContext[constructorParameters.Length]; + // Gather some metrics to determine what fast path to take, if any. + bool useFixedValues = constructorParameters.Length <= FixedArgumentThreshold; + bool hasAnyDefaultValues = false; + int matchedArgCount = 0; + int matchedArgCountWithMap = 0; for (int i = 0; i < constructorParameters.Length; i++) { - ParameterInfo constructorParameter = constructorParameters[i]; - bool hasDefaultValue = ParameterDefaultValue.TryGetDefaultValue(constructorParameter, out object? defaultValue); + hasAnyDefaultValues |= constructorParameters[i].HasDefaultValue; - parameters[i] = new FactoryParameterContext(constructorParameter.ParameterType, hasDefaultValue, defaultValue, parameterMap[i] ?? -1); + if (parameterMap[i] is not null) + { + matchedArgCount++; + if (parameterMap[i] == i) + { + matchedArgCountWithMap++; + } + } + } + + // No fast path; contains default values or arg mapping. + if (hasAnyDefaultValues || matchedArgCount != matchedArgCountWithMap) + { + return InvokeCanonical(); + } + + if (matchedArgCount == 0) + { + // All injected; use a fast path. + Type[] types = GetParameterTypes(); + return useFixedValues ? + (serviceProvider, arguments) => ReflectionFactoryServiceOnlyFixed(invoker, types, declaringType, serviceProvider) : + (serviceProvider, arguments) => ReflectionFactoryServiceOnlySpan(invoker, types, declaringType, serviceProvider); } - Type declaringType = constructor.DeclaringType!; - return (IServiceProvider serviceProvider, object?[]? arguments) => + if (matchedArgCount == constructorParameters.Length) { - if (serviceProvider is null) + // All direct with no mappings; use a fast path. + return (serviceProvider, arguments) => ReflectionFactoryDirect(invoker, serviceProvider, arguments); + } + + return InvokeCanonical(); + + ObjectFactory InvokeCanonical() + { + FactoryParameterContext[] parameters = GetFactoryParameterContext(); + return useFixedValues ? + (serviceProvider, arguments) => ReflectionFactoryCanonicalFixed(invoker, parameters, declaringType, serviceProvider, arguments) : + (serviceProvider, arguments) => ReflectionFactoryCanonicalSpan(invoker, parameters, declaringType, serviceProvider, arguments); + } + + Type[] GetParameterTypes() + { + Type[] types = new Type[constructorParameters.Length]; + for (int i = 0; i < constructorParameters.Length; i++) { - throw new ArgumentNullException(nameof(serviceProvider)); + types[i] = constructorParameters[i].ParameterType; } + return types; + } +#else + ParameterInfo[] constructorParameters = constructor.GetParameters(); + if (constructorParameters.Length == 0) + { + return (IServiceProvider serviceProvider, object?[]? arguments) => + constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: null, culture: null); + } + + FactoryParameterContext[] parameters = GetFactoryParameterContext(); + return (serviceProvider, arguments) => ReflectionFactoryCanonical(constructor, parameters, declaringType, serviceProvider, arguments); +#endif // NET8_0_OR_GREATER - object?[] constructorArguments = new object?[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) + FactoryParameterContext[] GetFactoryParameterContext() + { + FactoryParameterContext[] parameters = new FactoryParameterContext[constructorParameters.Length]; + for (int i = 0; i < constructorParameters.Length; i++) { - ref FactoryParameterContext parameter = ref parameters[i]; - constructorArguments[i] = ((parameter.ArgumentIndex != -1) - // Throws an NullReferenceException if arguments is null. Consistent with expression-based factory. - ? arguments![parameter.ArgumentIndex] - : GetService( - serviceProvider, - parameter.ParameterType, - declaringType, - parameter.HasDefaultValue)) ?? parameter.DefaultValue; + ParameterInfo constructorParameter = constructorParameters[i]; + bool hasDefaultValue = ParameterDefaultValue.TryGetDefaultValue(constructorParameter, out object? defaultValue); + parameters[i] = new FactoryParameterContext(constructorParameter.ParameterType, hasDefaultValue, defaultValue, parameterMap[i] ?? -1); } - return constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, constructorArguments, culture: null); - }; + return parameters; + } } +#endif // NETSTANDARD2_1_OR_GREATER || NETCOREAPP private readonly struct FactoryParameterContext { @@ -352,7 +422,6 @@ public FactoryParameterContext(Type parameterType, bool hasDefaultValue, object? public object? DefaultValue { get; } public int ArgumentIndex { get; } } -#endif private static void FindApplicableConstructor( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType, @@ -360,11 +429,11 @@ private static void FindApplicableConstructor( out ConstructorInfo matchingConstructor, out int?[] matchingParameterMap) { - ConstructorInfo? constructorInfo = null; - int?[]? parameterMap = null; + ConstructorInfo? constructorInfo; + int?[]? parameterMap; - if (!TryFindPreferredConstructor(instanceType, argumentTypes, ref constructorInfo, ref parameterMap) && - !TryFindMatchingConstructor(instanceType, argumentTypes, ref constructorInfo, ref parameterMap)) + if (!TryFindPreferredConstructor(instanceType, argumentTypes, out constructorInfo, out parameterMap) && + !TryFindMatchingConstructor(instanceType, argumentTypes, out constructorInfo, out parameterMap)) { throw new InvalidOperationException(SR.Format(SR.CtorNotLocated, instanceType)); } @@ -377,9 +446,12 @@ private static void FindApplicableConstructor( private static bool TryFindMatchingConstructor( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType, Type?[] argumentTypes, - [NotNullWhen(true)] ref ConstructorInfo? matchingConstructor, - [NotNullWhen(true)] ref int?[]? parameterMap) + [NotNullWhen(true)] out ConstructorInfo? matchingConstructor, + [NotNullWhen(true)] out int?[]? parameterMap) { + matchingConstructor = null; + parameterMap = null; + foreach (ConstructorInfo? constructor in instanceType.GetConstructors()) { if (TryCreateParameterMap(constructor.GetParameters(), argumentTypes, out int?[] tempParameterMap)) @@ -407,10 +479,13 @@ private static bool TryFindMatchingConstructor( private static bool TryFindPreferredConstructor( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType, Type?[] argumentTypes, - [NotNullWhen(true)] ref ConstructorInfo? matchingConstructor, - [NotNullWhen(true)] ref int?[]? parameterMap) + [NotNullWhen(true)] out ConstructorInfo? matchingConstructor, + [NotNullWhen(true)] out int?[]? parameterMap) { bool seenPreferred = false; + matchingConstructor = null; + parameterMap = null; + foreach (ConstructorInfo? constructor in instanceType.GetConstructors()) { if (constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false)) @@ -642,5 +717,204 @@ private static void ThrowMarkedCtorDoesNotTakeAllProvidedArguments() { throw new InvalidOperationException(SR.Format(SR.MarkedCtorMissingArgumentTypes, nameof(ActivatorUtilitiesConstructorAttribute))); } + +#if NET8_0_OR_GREATER // Use the faster ConstructorInvoker which also has alloc-free APIs when <= 4 parameters. + private static object ReflectionFactoryServiceOnlyFixed( + ConstructorInvoker invoker, + Type[] parameterTypes, + Type declaringType, + IServiceProvider serviceProvider) + { + Debug.Assert(parameterTypes.Length >= 1 && parameterTypes.Length <= FixedArgumentThreshold); + Debug.Assert(FixedArgumentThreshold == 4); + + if (serviceProvider is null) + ThrowHelperArgumentNullExceptionServiceProvider(); + + object? arg1 = null; + object? arg2 = null; + object? arg3 = null; + object? arg4 = null; + + switch (parameterTypes.Length) + { + case 4: + arg4 = GetService(serviceProvider, parameterTypes[3], declaringType, false); + goto case 3; + case 3: + arg3 = GetService(serviceProvider, parameterTypes[2], declaringType, false); + goto case 2; + case 2: + arg2 = GetService(serviceProvider, parameterTypes[1], declaringType, false); + goto case 1; + case 1: + arg1 = GetService(serviceProvider, parameterTypes[0], declaringType, false); + break; + } + + return invoker.Invoke(arg1, arg2, arg3, arg4); + } + + private static object ReflectionFactoryServiceOnlySpan( + ConstructorInvoker invoker, + Type[] parameterTypes, + Type declaringType, + IServiceProvider serviceProvider) + { + if (serviceProvider is null) + ThrowHelperArgumentNullExceptionServiceProvider(); + + object?[] arguments = new object?[parameterTypes.Length]; + for (int i = 0; i < parameterTypes.Length; i++) + { + arguments[i] = GetService(serviceProvider, parameterTypes[i], declaringType, false); + } + + return invoker.Invoke(arguments.AsSpan()); + } + + private static object ReflectionFactoryCanonicalFixed( + ConstructorInvoker invoker, + FactoryParameterContext[] parameters, + Type declaringType, + IServiceProvider serviceProvider, + object?[]? arguments) + { + Debug.Assert(parameters.Length >= 1 && parameters.Length <= FixedArgumentThreshold); + Debug.Assert(FixedArgumentThreshold == 4); + + if (serviceProvider is null) + ThrowHelperArgumentNullExceptionServiceProvider(); + + object? arg1 = null; + object? arg2 = null; + object? arg3 = null; + object? arg4 = null; + + switch (parameters.Length) + { + case 4: + ref FactoryParameterContext parameter4 = ref parameters[3]; + arg4 = ((parameter4.ArgumentIndex != -1) + // Throws a NullReferenceException if arguments is null. Consistent with expression-based factory. + ? arguments![parameter4.ArgumentIndex] + : GetService( + serviceProvider, + parameter4.ParameterType, + declaringType, + parameter4.HasDefaultValue)) ?? parameter4.DefaultValue; + goto case 3; + case 3: + ref FactoryParameterContext parameter3 = ref parameters[2]; + arg3 = ((parameter3.ArgumentIndex != -1) + ? arguments![parameter3.ArgumentIndex] + : GetService( + serviceProvider, + parameter3.ParameterType, + declaringType, + parameter3.HasDefaultValue)) ?? parameter3.DefaultValue; + goto case 2; + case 2: + ref FactoryParameterContext parameter2 = ref parameters[1]; + arg2 = ((parameter2.ArgumentIndex != -1) + ? arguments![parameter2.ArgumentIndex] + : GetService( + serviceProvider, + parameter2.ParameterType, + declaringType, + parameter2.HasDefaultValue)) ?? parameter2.DefaultValue; + goto case 1; + case 1: + ref FactoryParameterContext parameter1 = ref parameters[0]; + arg1 = ((parameter1.ArgumentIndex != -1) + ? arguments![parameter1.ArgumentIndex] + : GetService( + serviceProvider, + parameter1.ParameterType, + declaringType, + parameter1.HasDefaultValue)) ?? parameter1.DefaultValue; + break; + } + + return invoker.Invoke(arg1, arg2, arg3, arg4); + } + + private static object ReflectionFactoryCanonicalSpan( + ConstructorInvoker invoker, + FactoryParameterContext[] parameters, + Type declaringType, + IServiceProvider serviceProvider, + object?[]? arguments) + { + if (serviceProvider is null) + ThrowHelperArgumentNullExceptionServiceProvider(); + + object?[] constructorArguments = new object?[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + ref FactoryParameterContext parameter = ref parameters[i]; + constructorArguments[i] = ((parameter.ArgumentIndex != -1) + // Throws a NullReferenceException if arguments is null. Consistent with expression-based factory. + ? arguments![parameter.ArgumentIndex] + : GetService( + serviceProvider, + parameter.ParameterType, + declaringType, + parameter.HasDefaultValue)) ?? parameter.DefaultValue; + } + + return invoker.Invoke(constructorArguments.AsSpan()); + } + + private static object ReflectionFactoryDirect( + ConstructorInvoker invoker, + IServiceProvider serviceProvider, + object?[]? arguments) + { + if (serviceProvider is null) + ThrowHelperArgumentNullExceptionServiceProvider(); + + if (arguments is null) + ThrowHelperNullReferenceException(); //AsSpan() will not throw NullReferenceException. + + return invoker.Invoke(arguments.AsSpan()); + } + + /// + /// For consistency with the expression-based factory, throw NullReferenceException. + /// + [DoesNotReturn] + private static void ThrowHelperNullReferenceException() + { + throw new NullReferenceException(); + } +#elif NETSTANDARD2_1_OR_GREATER || NETCOREAPP + private static object ReflectionFactoryCanonical( + ConstructorInfo constructor, + FactoryParameterContext[] parameters, + Type declaringType, + IServiceProvider serviceProvider, + object?[]? arguments) + { + if (serviceProvider is null) + ThrowHelperArgumentNullExceptionServiceProvider(); + + object?[] constructorArguments = new object?[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + ref FactoryParameterContext parameter = ref parameters[i]; + constructorArguments[i] = ((parameter.ArgumentIndex != -1) + // Throws a NullReferenceException if arguments is null. Consistent with expression-based factory. + ? arguments![parameter.ArgumentIndex] + : GetService( + serviceProvider, + parameter.ParameterType, + declaringType, + parameter.HasDefaultValue)) ?? parameter.DefaultValue; + } + + return constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, constructorArguments, culture: null); + } +#endif // NET8_0_OR_GREATER } } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ActivatorUtilitiesTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ActivatorUtilitiesTests.cs index 860768f0e4612c..dcc847052b2547 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ActivatorUtilitiesTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ActivatorUtilitiesTests.cs @@ -195,7 +195,7 @@ public void TypeActivatorRethrowsOriginalExceptionFromConstructor(CreateInstance CreateInstance(createFunc, provider: serviceProvider)); var ex2 = Assert.Throws(() => - CreateInstance(createFunc, provider: serviceProvider, args: new[] { new FakeService() })); + CreateInstance(createFunc, provider: serviceProvider, args: new object[] { new FakeService() })); // Assert Assert.Equal(nameof(ClassWithThrowingEmptyCtor), ex1.Message); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ActivatorUtilitiesTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ActivatorUtilitiesTests.cs index 4c065b61bb2856..7572e6977a4c49 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ActivatorUtilitiesTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ActivatorUtilitiesTests.cs @@ -94,7 +94,8 @@ public void TypeActivatorThrowsOnNullProvider() public void FactoryActivatorThrowsOnNullProvider() { var f = ActivatorUtilities.CreateFactory(typeof(ClassWithA), new Type[0]); - Assert.Throws(() => f(serviceProvider: null, null)); + Exception ex = Assert.Throws(() => f(serviceProvider: null, null)); + Assert.Contains("serviceProvider", ex.ToString()); } [Fact] @@ -179,7 +180,7 @@ public void CreateInstance_ClassWithABC_MultipleCtorsWithSameLength_ThrowsAmbigu } [Fact] - public void CreateFactory_CreatesFactoryMethod() + public void CreateFactory_CreatesFactoryMethod_4Types_3Injected() { var factory1 = ActivatorUtilities.CreateFactory(typeof(ClassWithABCS), new Type[] { typeof(B) }); var factory2 = ActivatorUtilities.CreateFactory(new Type[] { typeof(B) }); @@ -194,9 +195,42 @@ public void CreateFactory_CreatesFactoryMethod() Assert.IsType(factory1); Assert.IsType(item1); + ClassWithABCS obj = (ClassWithABCS)item1; + Assert.NotNull(obj.A); + Assert.NotNull(obj.B); + Assert.NotNull(obj.C); + Assert.NotNull(obj.S); Assert.IsType>(factory2); Assert.IsType(item2); + + Assert.NotNull(item2.A); + Assert.NotNull(item2.B); + Assert.NotNull(item2.C); + Assert.NotNull(item2.S); + } + + [Fact] + public void CreateFactory_CreatesFactoryMethod_5Types_5Injected() + { + // Inject 5 types which is a threshold for whether fixed or Span<> invoker args are used by reflection. + var factory = ActivatorUtilities.CreateFactory(Type.EmptyTypes); + + var services = new ServiceCollection(); + services.AddSingleton(new A()); + services.AddSingleton(new B()); + services.AddSingleton(new C()); + services.AddSingleton(new S()); + services.AddSingleton(new Z()); + using var provider = services.BuildServiceProvider(); + ClassWithABCSZ item = factory(provider, null); + + Assert.IsType>(factory); + Assert.NotNull(item.A); + Assert.NotNull(item.B); + Assert.NotNull(item.C); + Assert.NotNull(item.S); + Assert.NotNull(item.Z); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -365,6 +399,7 @@ internal class A { } internal class B { } internal class C { } internal class S { } + internal class Z { } internal class ClassWithABCS : ClassWithABC { @@ -373,6 +408,12 @@ internal class ClassWithABCS : ClassWithABC public ClassWithABCS(A a, C c, S s) : this(a, null, c, s) { } } + internal class ClassWithABCSZ : ClassWithABCS + { + public Z Z { get; } + public ClassWithABCSZ(A a, B b, C c, S s, Z z) : base(a, b, c, s) { Z = z; } + } + internal class ClassWithABC_FirstConstructorWithAttribute : ClassWithABC { [ActivatorUtilitiesConstructor]