diff --git a/src/Shared/ActivatorUtilities/ActivatorUtilities.cs b/src/Shared/ActivatorUtilities/ActivatorUtilities.cs new file mode 100644 index 0000000000..e2553ced1a --- /dev/null +++ b/src/Shared/ActivatorUtilities/ActivatorUtilities.cs @@ -0,0 +1,429 @@ +// 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 System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; + +#if ActivatorUtilities_In_DependencyInjection +using Microsoft.Extensions.Internal; + +namespace Microsoft.Extensions.DependencyInjection +#else +namespace Microsoft.Extensions.Internal +#endif +{ + /// + /// Helper code for the various activator services. + /// + +#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 + static class ActivatorUtilities + { + private static readonly MethodInfo GetServiceInfo = + GetMethodInfo>((sp, t, r, c) => GetService(sp, t, r, c)); + + /// + /// Instantiate a type with constructor arguments provided directly and/or from an . + /// + /// The service provider used to resolve dependencies + /// The type to activate + /// Constructor arguments not provided by the . + /// An activated object of type instanceType + 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 constructor in instanceType + .GetTypeInfo() + .DeclaredConstructors + .Where(c => !c.IsStatic && c.IsPublic)) + { + var matcher = new ConstructorMatcher(constructor); + var isPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false); + var length = matcher.Match(parameters); + + if (isPreferred) + { + if (seenPreferred) + { + ThrowMultipleCtorsMarkedWithAttributeException(); + } + + if (length == -1) + { + ThrowMarkedCtorDoesNotTakeAllProvidedArguments(); + } + } + + if (isPreferred || bestLength < length) + { + bestLength = length; + bestMatcher = matcher; + } + + seenPreferred |= isPreferred; + } + } + + if (bestMatcher == null) + { + 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); + } + + return bestMatcher.CreateInstance(provider); + } + + /// + /// Create a delegate that will instantiate a type with constructor arguments provided directly + /// and/or from an . + /// + /// The type to activate + /// + /// The types of objects, in order, that will be passed to the returned function as its second parameter + /// + /// + /// A factory that will instantiate instanceType using an + /// and an argument array containing objects matching the types defined in argumentTypes + /// + public static ObjectFactory CreateFactory(Type instanceType, Type[] argumentTypes) + { + FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructor, out int?[] parameterMap); + + var provider = Expression.Parameter(typeof(IServiceProvider), "provider"); + var argumentArray = Expression.Parameter(typeof(object[]), "argumentArray"); + var factoryExpressionBody = BuildFactoryExpression(constructor, parameterMap, provider, argumentArray); + + var factoryLamda = Expression.Lambda>( + factoryExpressionBody, provider, argumentArray); + + var result = factoryLamda.Compile(); + return result.Invoke; + } + + /// + /// Instantiate a type with constructor arguments provided directly and/or from an . + /// + /// The type to activate + /// The service provider used to resolve dependencies + /// Constructor arguments not provided by the . + /// An activated object of type T + public static T CreateInstance(IServiceProvider provider, params object[] parameters) + { + return (T)CreateInstance(provider, typeof(T), parameters); + } + + + /// + /// Retrieve an instance of the given type from the service provider. If one is not found then instantiate it directly. + /// + /// The type of the service + /// The service provider used to resolve dependencies + /// The resolved service or created instance + public static T GetServiceOrCreateInstance(IServiceProvider provider) + { + return (T)GetServiceOrCreateInstance(provider, typeof(T)); + } + + /// + /// Retrieve an instance of the given type from the service provider. If one is not found then instantiate it directly. + /// + /// The service provider + /// The type of the service + /// The resolved service or created instance + public static object GetServiceOrCreateInstance(IServiceProvider provider, Type type) + { + return provider.GetService(type) ?? CreateInstance(provider, type); + } + + private static MethodInfo GetMethodInfo(Expression expr) + { + var mc = (MethodCallExpression)expr.Body; + return mc.Method; + } + + private static object GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired) + { + var service = sp.GetService(type); + if (service == null && !isDefaultParameterRequired) + { + var message = $"Unable to resolve service for type '{type}' while attempting to activate '{requiredBy}'."; + throw new InvalidOperationException(message); + } + return service; + } + + private static Expression BuildFactoryExpression( + ConstructorInfo constructor, + int?[] parameterMap, + Expression serviceProvider, + Expression factoryArgumentArray) + { + var constructorParameters = constructor.GetParameters(); + var constructorArguments = new Expression[constructorParameters.Length]; + + for (var i = 0; i < constructorParameters.Length; i++) + { + var constructorParameter = constructorParameters[i]; + var parameterType = constructorParameter.ParameterType; + var hasDefaultValue = ParameterDefaultValue.TryGetDefaultValue(constructorParameter, out var defaultValue); + + if (parameterMap[i] != null) + { + constructorArguments[i] = Expression.ArrayAccess(factoryArgumentArray, Expression.Constant(parameterMap[i])); + } + else + { + var parameterTypeExpression = new Expression[] { serviceProvider, + Expression.Constant(parameterType, typeof(Type)), + Expression.Constant(constructor.DeclaringType, typeof(Type)), + Expression.Constant(hasDefaultValue) }; + constructorArguments[i] = Expression.Call(GetServiceInfo, parameterTypeExpression); + } + + // Support optional constructor arguments by passing in the default value + // when the argument would otherwise be null. + if (hasDefaultValue) + { + var defaultValueExpression = Expression.Constant(defaultValue); + constructorArguments[i] = Expression.Coalesce(constructorArguments[i], defaultValueExpression); + } + + constructorArguments[i] = Expression.Convert(constructorArguments[i], parameterType); + } + + return Expression.New(constructor, constructorArguments); + } + + private static void FindApplicableConstructor( + Type instanceType, + Type[] argumentTypes, + out ConstructorInfo matchingConstructor, + out int?[] parameterMap) + { + 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) + { + continue; + } + + if (TryCreateParameterMap(constructor.GetParameters(), argumentTypes, out int?[] tempParameterMap)) + { + if (matchingConstructor != null) + { + throw new InvalidOperationException($"Multiple constructors accepting all given argument types have been found in type '{instanceType}'. There should only be one applicable constructor."); + } + + matchingConstructor = constructor; + parameterMap = tempParameterMap; + } + } + + 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) + { + 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. + // Returns true if each given parameter type is assignable to a unique; otherwise, false. + private static bool TryCreateParameterMap(ParameterInfo[] constructorParameters, Type[] argumentTypes, out int?[] parameterMap) + { + parameterMap = new int?[constructorParameters.Length]; + + for (var i = 0; i < argumentTypes.Length; i++) + { + var foundMatch = false; + var givenParameter = argumentTypes[i].GetTypeInfo(); + + for (var j = 0; j < constructorParameters.Length; j++) + { + if (parameterMap[j] != null) + { + // This ctor parameter has already been matched + continue; + } + + if (constructorParameters[j].ParameterType.GetTypeInfo().IsAssignableFrom(givenParameter)) + { + foundMatch = true; + parameterMap[j] = i; + break; + } + } + + if (!foundMatch) + { + return false; + } + } + + return true; + } + + private class ConstructorMatcher + { + private readonly ConstructorInfo _constructor; + private readonly ParameterInfo[] _parameters; + private readonly object[] _parameterValues; + private readonly bool[] _parameterValuesSet; + + public ConstructorMatcher(ConstructorInfo constructor) + { + _constructor = constructor; + _parameters = _constructor.GetParameters(); + _parameterValuesSet = new bool[_parameters.Length]; + _parameterValues = new object[_parameters.Length]; + } + + public int Match(object[] givenParameters) + { + var applyIndexStart = 0; + var applyExactLength = 0; + for (var givenIndex = 0; givenIndex != givenParameters.Length; givenIndex++) + { + var givenType = givenParameters[givenIndex]?.GetType().GetTypeInfo(); + var givenMatched = false; + + for (var applyIndex = applyIndexStart; givenMatched == false && applyIndex != _parameters.Length; ++applyIndex) + { + if (_parameterValuesSet[applyIndex] == false && + _parameters[applyIndex].ParameterType.GetTypeInfo().IsAssignableFrom(givenType)) + { + givenMatched = true; + _parameterValuesSet[applyIndex] = true; + _parameterValues[applyIndex] = givenParameters[givenIndex]; + if (applyIndexStart == applyIndex) + { + applyIndexStart++; + if (applyIndex == givenIndex) + { + applyExactLength = applyIndex; + } + } + } + } + + if (givenMatched == false) + { + return -1; + } + } + return applyExactLength; + } + + public object CreateInstance(IServiceProvider provider) + { + for (var index = 0; index != _parameters.Length; index++) + { + if (_parameterValuesSet[index] == false) + { + var value = provider.GetService(_parameters[index].ParameterType); + if (value == null) + { + if (!ParameterDefaultValue.TryGetDefaultValue(_parameters[index], out var defaultValue)) + { + throw new InvalidOperationException($"Unable to resolve service for type '{_parameters[index].ParameterType}' while attempting to activate '{_constructor.DeclaringType}'."); + } + else + { + _parameterValues[index] = defaultValue; + } + } + else + { + _parameterValues[index] = value; + } + } + } + + try + { + return _constructor.Invoke(_parameterValues); + } + catch (TargetInvocationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + // The above line will always throw, but the compiler requires we throw explicitly. + throw; + } + } + } + + 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/src/Shared/ActivatorUtilities/ActivatorUtilitiesConstructorAttribute.cs b/src/Shared/ActivatorUtilities/ActivatorUtilitiesConstructorAttribute.cs new file mode 100644 index 0000000000..67ffa13f6f --- /dev/null +++ b/src/Shared/ActivatorUtilities/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/Shared/ActivatorUtilities/ObjectFactory.cs b/src/Shared/ActivatorUtilities/ObjectFactory.cs new file mode 100644 index 0000000000..517247811e --- /dev/null +++ b/src/Shared/ActivatorUtilities/ObjectFactory.cs @@ -0,0 +1,25 @@ +// 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 +{ + + /// + /// The result of . + /// + /// The to get service arguments from. + /// Additional constructor arguments. + /// The instantiated type. +#if ActivatorUtilities_In_DependencyInjection + public +#else + internal +#endif + delegate object ObjectFactory(IServiceProvider serviceProvider, object[] arguments); +} \ No newline at end of file diff --git a/src/Shared/ActivatorUtilities/sharedsources.props b/src/Shared/ActivatorUtilities/sharedsources.props new file mode 100644 index 0000000000..b35fe34b10 --- /dev/null +++ b/src/Shared/ActivatorUtilities/sharedsources.props @@ -0,0 +1,8 @@ + + + + true + $(ContentTargetFolders)\cs\netstandard1.0\ + + + diff --git a/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs b/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs new file mode 100644 index 0000000000..a71bad37b1 --- /dev/null +++ b/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs @@ -0,0 +1,47 @@ +// 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 System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + internal class ParameterDefaultValue + { + public static bool TryGetDefaultValue(ParameterInfo parameter, out object defaultValue) + { + bool hasDefaultValue; + var tryToGetDefaultValue = true; + defaultValue = null; + + try + { + hasDefaultValue = parameter.HasDefaultValue; + } + catch (FormatException) when (parameter.ParameterType == typeof(DateTime)) + { + // Workaround for https://github.com/dotnet/corefx/issues/12338 + // If HasDefaultValue throws FormatException for DateTime + // we expect it to have default value + hasDefaultValue = true; + tryToGetDefaultValue = false; + } + + if (hasDefaultValue) + { + if (tryToGetDefaultValue) + { + defaultValue = parameter.DefaultValue; + } + + // Workaround for https://github.com/dotnet/corefx/issues/11797 + if (defaultValue == null && parameter.ParameterType.IsValueType) + { + defaultValue = Activator.CreateInstance(parameter.ParameterType); + } + } + + return hasDefaultValue; + } + } +}