Skip to content

Commit f58cee2

Browse files
authored
ActivatorUtilities not depending on ctor order for creating instances (#75846)
1 parent 54c4a4b commit f58cee2

File tree

7 files changed

+511
-64
lines changed

7 files changed

+511
-64
lines changed

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs

Lines changed: 90 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,35 @@ public static object CreateInstance(
3131
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType,
3232
params object[] parameters)
3333
{
34-
int bestLength = -1;
35-
bool seenPreferred = false;
34+
if (provider == null)
35+
{
36+
throw new ArgumentNullException(nameof(provider));
37+
}
3638

37-
ConstructorMatcher bestMatcher = default;
39+
if (instanceType.IsAbstract)
40+
{
41+
throw new InvalidOperationException(SR.CannotCreateAbstractClasses);
42+
}
3843

39-
if (!instanceType.IsAbstract)
44+
IServiceProviderIsService? serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
45+
// if container supports using IServiceProviderIsService, we try to find the longest ctor that
46+
// (a) matches all parameters given to CreateInstance
47+
// (b) matches the rest of ctor arguments as either a parameter with a default value or as a service registered
48+
// if no such match is found we fallback to the same logic used by CreateFactory which would only allow creating an
49+
// instance if all parameters given to CreateInstance only match with a single ctor
50+
if (serviceProviderIsService != null)
4051
{
52+
int bestLength = -1;
53+
bool seenPreferred = false;
54+
55+
ConstructorMatcher bestMatcher = default;
56+
bool multipleBestLengthFound = false;
57+
4158
foreach (ConstructorInfo? constructor in instanceType.GetConstructors())
4259
{
4360
var matcher = new ConstructorMatcher(constructor);
4461
bool isPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false);
45-
int length = matcher.Match(parameters);
62+
int length = matcher.Match(parameters, serviceProviderIsService);
4663

4764
if (isPreferred)
4865
{
@@ -61,19 +78,37 @@ public static object CreateInstance(
6178
{
6279
bestLength = length;
6380
bestMatcher = matcher;
81+
multipleBestLengthFound = false;
82+
}
83+
else if (bestLength == length)
84+
{
85+
multipleBestLengthFound = true;
6486
}
6587

6688
seenPreferred |= isPreferred;
6789
}
90+
91+
if (bestLength != -1)
92+
{
93+
if (multipleBestLengthFound)
94+
{
95+
throw new InvalidOperationException(SR.Format(SR.MultipleCtorsFoundWithBestLength, instanceType, bestLength));
96+
}
97+
98+
return bestMatcher.CreateInstance(provider);
99+
}
68100
}
69101

70-
if (bestLength == -1)
102+
Type?[] argumentTypes = new Type[parameters.Length];
103+
for (int i = 0; i < argumentTypes.Length; i++)
71104
{
72-
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.";
73-
throw new InvalidOperationException(message);
105+
argumentTypes[i] = parameters[i]?.GetType();
74106
}
75107

76-
return bestMatcher.CreateInstance(provider);
108+
FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructorInfo, out int?[] parameterMap);
109+
var constructorMatcher = new ConstructorMatcher(constructorInfo);
110+
constructorMatcher.MapParameters(parameterMap, parameters);
111+
return constructorMatcher.CreateInstance(provider);
77112
}
78113

79114
/// <summary>
@@ -92,7 +127,7 @@ public static ObjectFactory CreateFactory(
92127
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType,
93128
Type[] argumentTypes)
94129
{
95-
FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo? constructor, out int?[]? parameterMap);
130+
FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructor, out int?[] parameterMap);
96131

97132
ParameterExpression? provider = Expression.Parameter(typeof(IServiceProvider), "provider");
98133
ParameterExpression? argumentArray = Expression.Parameter(typeof(object[]), "argumentArray");
@@ -152,8 +187,7 @@ private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
152187
object? service = sp.GetService(type);
153188
if (service == null && !isDefaultParameterRequired)
154189
{
155-
string? message = $"Unable to resolve service for type '{type}' while attempting to activate '{requiredBy}'.";
156-
throw new InvalidOperationException(message);
190+
throw new InvalidOperationException(SR.Format(SR.UnableToResolveService, type, requiredBy));
157191
}
158192
return service;
159193
}
@@ -202,7 +236,7 @@ private static Expression BuildFactoryExpression(
202236

203237
private static void FindApplicableConstructor(
204238
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType,
205-
Type[] argumentTypes,
239+
Type?[] argumentTypes,
206240
out ConstructorInfo matchingConstructor,
207241
out int?[] matchingParameterMap)
208242
{
@@ -212,8 +246,7 @@ private static void FindApplicableConstructor(
212246
if (!TryFindPreferredConstructor(instanceType, argumentTypes, ref constructorInfo, ref parameterMap) &&
213247
!TryFindMatchingConstructor(instanceType, argumentTypes, ref constructorInfo, ref parameterMap))
214248
{
215-
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.";
216-
throw new InvalidOperationException(message);
249+
throw new InvalidOperationException(SR.Format(SR.CtorNotLocated, instanceType));
217250
}
218251

219252
matchingConstructor = constructorInfo;
@@ -223,7 +256,7 @@ private static void FindApplicableConstructor(
223256
// Tries to find constructor based on provided argument types
224257
private static bool TryFindMatchingConstructor(
225258
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType,
226-
Type[] argumentTypes,
259+
Type?[] argumentTypes,
227260
[NotNullWhen(true)] ref ConstructorInfo? matchingConstructor,
228261
[NotNullWhen(true)] ref int?[]? parameterMap)
229262
{
@@ -233,7 +266,7 @@ private static bool TryFindMatchingConstructor(
233266
{
234267
if (matchingConstructor != null)
235268
{
236-
throw new InvalidOperationException($"Multiple constructors accepting all given argument types have been found in type '{instanceType}'. There should only be one applicable constructor.");
269+
throw new InvalidOperationException(SR.Format(SR.MultipleCtorsFound, instanceType));
237270
}
238271

239272
matchingConstructor = constructor;
@@ -253,7 +286,7 @@ private static bool TryFindMatchingConstructor(
253286
// Tries to find constructor marked with ActivatorUtilitiesConstructorAttribute
254287
private static bool TryFindPreferredConstructor(
255288
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type instanceType,
256-
Type[] argumentTypes,
289+
Type?[] argumentTypes,
257290
[NotNullWhen(true)] ref ConstructorInfo? matchingConstructor,
258291
[NotNullWhen(true)] ref int?[]? parameterMap)
259292
{
@@ -289,7 +322,7 @@ private static bool TryFindPreferredConstructor(
289322

290323
// Creates an injective parameterMap from givenParameterTypes to assignable constructorParameters.
291324
// Returns true if each given parameter type is assignable to a unique; otherwise, false.
292-
private static bool TryCreateParameterMap(ParameterInfo[] constructorParameters, Type[] argumentTypes, out int?[] parameterMap)
325+
private static bool TryCreateParameterMap(ParameterInfo[] constructorParameters, Type?[] argumentTypes, out int?[] parameterMap)
293326
{
294327
parameterMap = new int?[constructorParameters.Length];
295328

@@ -336,39 +369,48 @@ public ConstructorMatcher(ConstructorInfo constructor)
336369
_parameterValues = new object?[_parameters.Length];
337370
}
338371

339-
public int Match(object[] givenParameters)
372+
public int Match(object[] givenParameters, IServiceProviderIsService serviceProviderIsService)
340373
{
341-
int applyIndexStart = 0;
342-
int applyExactLength = 0;
343-
for (int givenIndex = 0; givenIndex != givenParameters.Length; givenIndex++)
374+
for (int givenIndex = 0; givenIndex < givenParameters.Length; givenIndex++)
344375
{
345376
Type? givenType = givenParameters[givenIndex]?.GetType();
346377
bool givenMatched = false;
347378

348-
for (int applyIndex = applyIndexStart; givenMatched == false && applyIndex != _parameters.Length; ++applyIndex)
379+
for (int applyIndex = 0; applyIndex < _parameters.Length; applyIndex++)
349380
{
350381
if (_parameterValues[applyIndex] == null &&
351382
_parameters[applyIndex].ParameterType.IsAssignableFrom(givenType))
352383
{
353384
givenMatched = true;
354385
_parameterValues[applyIndex] = givenParameters[givenIndex];
355-
if (applyIndexStart == applyIndex)
356-
{
357-
applyIndexStart++;
358-
if (applyIndex == givenIndex)
359-
{
360-
applyExactLength = applyIndex;
361-
}
362-
}
386+
break;
363387
}
364388
}
365389

366-
if (givenMatched == false)
390+
if (!givenMatched)
367391
{
368392
return -1;
369393
}
370394
}
371-
return applyExactLength;
395+
396+
// confirms the rest of ctor arguments match either as a parameter with a default value or as a service registered
397+
for (int i = 0; i < _parameters.Length; i++)
398+
{
399+
if (_parameterValues[i] == null &&
400+
!serviceProviderIsService.IsService(_parameters[i].ParameterType))
401+
{
402+
if (ParameterDefaultValue.TryGetDefaultValue(_parameters[i], out object? defaultValue))
403+
{
404+
_parameterValues[i] = defaultValue;
405+
}
406+
else
407+
{
408+
return -1;
409+
}
410+
}
411+
}
412+
413+
return _parameters.Length;
372414
}
373415

374416
public object CreateInstance(IServiceProvider provider)
@@ -382,7 +424,7 @@ public object CreateInstance(IServiceProvider provider)
382424
{
383425
if (!ParameterDefaultValue.TryGetDefaultValue(_parameters[index], out object? defaultValue))
384426
{
385-
throw new InvalidOperationException($"Unable to resolve service for type '{_parameters[index].ParameterType}' while attempting to activate '{_constructor.DeclaringType}'.");
427+
throw new InvalidOperationException(SR.Format(SR.UnableToResolveService, _parameters[index].ParameterType, _constructor.DeclaringType));
386428
}
387429
else
388430
{
@@ -411,16 +453,27 @@ public object CreateInstance(IServiceProvider provider)
411453
return _constructor.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: _parameterValues, culture: null);
412454
#endif
413455
}
456+
457+
public void MapParameters(int?[] parameterMap, object[] givenParameters)
458+
{
459+
for (int i = 0; i < _parameters.Length; i++)
460+
{
461+
if (parameterMap[i] != null)
462+
{
463+
_parameterValues[i] = givenParameters[(int)parameterMap[i]!];
464+
}
465+
}
466+
}
414467
}
415468

416469
private static void ThrowMultipleCtorsMarkedWithAttributeException()
417470
{
418-
throw new InvalidOperationException($"Multiple constructors were marked with {nameof(ActivatorUtilitiesConstructorAttribute)}.");
471+
throw new InvalidOperationException(SR.Format(SR.MultipleCtorsMarkedWithAttribute, nameof(ActivatorUtilitiesConstructorAttribute)));
419472
}
420473

421474
private static void ThrowMarkedCtorDoesNotTakeAllProvidedArguments()
422475
{
423-
throw new InvalidOperationException($"Constructor marked with {nameof(ActivatorUtilitiesConstructorAttribute)} does not accept all given argument types.");
476+
throw new InvalidOperationException(SR.Format(SR.MarkedCtorMissingArgumentTypes, nameof(ActivatorUtilitiesConstructorAttribute)));
424477
}
425478
}
426479
}

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Resources/Strings.resx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,31 @@
140140
<value>Implementation type cannot be '{0}' because it is indistinguishable from other services registered for '{1}'.</value>
141141
<comment>{0} = implementation type, {1} = service type</comment>
142142
</data>
143+
<data name="MultipleCtorsMarkedWithAttribute" xml:space="preserve">
144+
<value>Multiple constructors were marked with {0}.</value>
145+
<comment>{0} = attribute used with ActivatorUtilities</comment>
146+
</data>
147+
<data name="MarkedCtorMissingArgumentTypes" xml:space="preserve">
148+
<value>Constructor marked with {0} does not accept all given argument types.</value>
149+
<comment>{0} = attribute used with ActivatorUtilities</comment>
150+
</data>
151+
<data name="CannotCreateAbstractClasses" xml:space="preserve">
152+
<value>Instances of abstract classes cannot be created.</value>
153+
</data>
154+
<data name="MultipleCtorsFoundWithBestLength" xml:space="preserve">
155+
<value>Multiple constructors for type '{0}' were found with length {1}.</value>
156+
<comment>{0} = instance type, {1} = best length</comment>
157+
</data>
158+
<data name="UnableToResolveService" xml:space="preserve">
159+
<value>Unable to resolve service for type '{0}' while attempting to activate '{1}'.</value>
160+
<comment>{0} = service type, {1} = required by</comment>
161+
</data>
162+
<data name="CtorNotLocated" xml:space="preserve">
163+
<value>A suitable constructor for type '{0}' 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.</value>
164+
<comment>{0} = instance type</comment>
165+
</data>
166+
<data name="MultipleCtorsFound" xml:space="preserve">
167+
<value>Multiple constructors accepting all given argument types have been found in type '{0}'. There should only be one applicable constructor.</value>
168+
<comment>{0} = instance type</comment>
169+
</data>
143170
</root>

0 commit comments

Comments
 (0)