Skip to content

Commit cf8c9ce

Browse files
committed
Container.Options.ResolveUnregisteredConcreteTypes setting added to disable creation of unregistered concrete types. Fixes #377.
1 parent 3618e01 commit cf8c9ce

File tree

6 files changed

+225
-22
lines changed

6 files changed

+225
-22
lines changed

src/SimpleInjector.Tests.Unit/ContainerOptionsTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,38 @@ public void LifestyleSelectionBehavior_DefaultImplementation_RedirectsToDefaultL
936936
// Assert
937937
Assert.AreSame(Lifestyle.Singleton, lifestyle);
938938
}
939+
940+
[TestMethod]
941+
public void ResolveUnregisteredConcreteTypes_ByDefault_True()
942+
{
943+
// Arrange
944+
var expectedValue = true;
945+
946+
var container = new Container();
947+
948+
// Act
949+
var actualValue = container.Options.ResolveUnregisteredConcreteTypes;
950+
951+
// Assert
952+
Assert.AreEqual(expectedValue, actualValue);
953+
}
954+
955+
[TestMethod]
956+
public void ResolveUnregisteredConcreteTypes_ChangedAfterContainerIsLocked_ThrowsAnException()
957+
{
958+
// Arrange
959+
var container = new Container();
960+
961+
container.GetInstance<ConcreteCommand>();
962+
963+
// Act
964+
Action action = () => container.Options.ResolveUnregisteredConcreteTypes = false;
965+
966+
// Assert
967+
AssertThat.ThrowsWithExceptionMessageContains<InvalidOperationException>(
968+
"The container can't be changed after the first call",
969+
action);
970+
}
939971

940972
private static PropertyInfo GetProperty<T>(Expression<Func<T, object>> propertySelector)
941973
{

src/SimpleInjector.Tests.Unit/ResolveUnregisteredTypeEventTests.cs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ public void GetInstance_EventRegisteredThatThrowsException_ThrowsAnDescriptiveEx
290290
{
291291
e.Register(() => { throw new Exception(); });
292292
};
293-
293+
294294
// Act
295295
Action action = () => container.GetInstance<IUserRepository>();
296296

@@ -611,6 +611,125 @@ public void ResolveUnregisteredType_Always_IsExpectedToBeCached()
611611
Assert.AreEqual(1, callCount, "The result of ResolveUnregisteredType is expected to be cached.");
612612
}
613613

614+
[TestMethod]
615+
public void ResolveUnregisteredConcreteTypes_SetToFalse_DoesNotAllowUnregisteredConcreteRootTypesToBeResolved()
616+
{
617+
// Arrange
618+
var container = new Container();
619+
container.Options.ResolveUnregisteredConcreteTypes = false;
620+
621+
// Add a dummy registration.
622+
container.RegisterInstance(new FakeTimeProvider());
623+
624+
// Act
625+
Action action = () => container.GetInstance<ConcreteCommand>();
626+
627+
// Assert
628+
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(@"
629+
No registration for type ConcreteCommand could be found and an implicit registration
630+
could not be made. Note that the container's Options.ResolveUnregisteredConcreteTypes
631+
option is set to 'false'. This disallows the container to construct this unregistered
632+
concrete type."
633+
.TrimInside(),
634+
action);
635+
}
636+
637+
[TestMethod]
638+
public void ResolveUnregisteredConcreteTypes_SetToFalse_DoesNotAllowUnregisteredConcreteDependenciesToBeResolved()
639+
{
640+
// Arrange
641+
var container = new Container();
642+
container.Options.ResolveUnregisteredConcreteTypes = false;
643+
644+
container.Register<ServiceDependingOn<ConcreteCommand>>();
645+
646+
// Act
647+
Action action = () => container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
648+
649+
// Assert
650+
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(@"
651+
The constructor of type ServiceDependingOn<ConcreteCommand> contains
652+
the parameter with name 'dependency' and type ConcreteCommand that is not
653+
registered. Please ensure ConcreteCommand is registered, or change the constructor of
654+
ServiceDependingOn<ConcreteCommand>. Note that the container's
655+
Options.ResolveUnregisteredConcreteTypes option is set to 'false'. This disallows the
656+
container to construct this unregistered concrete type."
657+
.TrimInside(),
658+
action);
659+
}
660+
661+
[TestMethod]
662+
public void ResolveUnregisteredConcreteTypes_SetToNever_DoesAllowRegisteredConcreteRootTypesToBeResolved()
663+
{
664+
// Arrange
665+
var container = new Container();
666+
container.Options.ResolveUnregisteredConcreteTypes = false;
667+
668+
// Add a dummy registration.
669+
container.Register<ConcreteCommand>();
670+
671+
// Act
672+
container.GetInstance<ConcreteCommand>();
673+
}
674+
675+
[TestMethod]
676+
public void ResolveUnregisteredConcreteTypes_SetToNever_DoesAllowRegisteredConcreteDependenciesToBeResolved()
677+
{
678+
// Arrange
679+
var container = new Container();
680+
container.Options.ResolveUnregisteredConcreteTypes = false;
681+
682+
container.Register<ConcreteCommand>();
683+
container.Register<ServiceDependingOn<ConcreteCommand>>();
684+
685+
// Act
686+
container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
687+
}
688+
689+
[TestMethod]
690+
public void ResolveUnregisteredConcreteTypes_SetToTrue_DoesAllowUnregisteredConcreteRootTypesToBeResolved()
691+
{
692+
// Arrange
693+
var container = new Container();
694+
container.Options.ResolveUnregisteredConcreteTypes = true;
695+
696+
// Act
697+
container.GetInstance<ConcreteCommand>();
698+
}
699+
700+
[TestMethod]
701+
public void ResolveUnregisteredConcreteTypes_SetToTrue_DoesAllowRegisteredConcreteDependenciesToBeResolved()
702+
{
703+
// Arrange
704+
var container = new Container();
705+
container.Options.ResolveUnregisteredConcreteTypes = true;
706+
707+
// Act
708+
container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
709+
}
710+
711+
[TestMethod]
712+
public void ResolveUnregisteredConcreteTypes_SetToFalseWithUnregisteredTypeHandlingType_DoesAllowUnregisteredConcreteDependenciesToBeResolved()
713+
{
714+
// Arrange
715+
var container = new Container();
716+
container.Options.ResolveUnregisteredConcreteTypes = false;
717+
718+
// Using unregistered type resolution re-enable the 'Always' behavior.
719+
container.ResolveUnregisteredType += (s, e) =>
720+
{
721+
if (!e.Handled && !e.UnregisteredServiceType.IsAbstract)
722+
{
723+
e.Register(container.Options.LifestyleSelectionBehavior
724+
.SelectLifestyle(e.UnregisteredServiceType)
725+
.CreateRegistration(e.UnregisteredServiceType, container));
726+
}
727+
};
728+
729+
// Act
730+
container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
731+
}
732+
614733
public class CompositeService<T> where T : struct
615734
{
616735
public CompositeService(Nullable<T>[] dependencies)

src/SimpleInjector/Container.Common.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ internal void ThrowParameterTypeMustBeRegistered(InjectionTargetInfo target)
478478
{
479479
throw new ActivationException(
480480
StringResources.ParameterTypeMustBeRegistered(
481+
this,
481482
target,
482483
this.GetNumberOfConditionalRegistrationsFor(target.TargetType),
483484
this.ContainsOneToOneRegistrationForCollectionType(target.TargetType),

src/SimpleInjector/Container.Resolving.cs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ internal InstanceProducer GetRegistrationEvenIfInvalid(Type serviceType, Injecti
270270
return this.GetInstanceProducerForType(serviceType, consumer, buildProducer);
271271
}
272272

273+
internal bool IsConcreteConstructableType(Type concreteType) =>
274+
this.Options.IsConstructableType(concreteType, out string errorMesssage);
275+
273276
private Action<T> GetInitializer<T>(Type implementationType, Registration context)
274277
{
275278
Action<T>[] initializersForType = this.GetInstanceInitializersFor<T>(implementationType, context);
@@ -613,7 +616,8 @@ private InstanceProducer TryBuildInstanceProducerForConcreteUnregisteredType<TCo
613616
InjectionConsumerInfo context)
614617
where TConcrete : class
615618
{
616-
if (this.IsConcreteConstructableType(typeof(TConcrete), context))
619+
if (this.Options.ResolveUnregisteredConcreteTypes
620+
&& this.IsConcreteConstructableType(typeof(TConcrete)))
617621
{
618622
return this.GetOrBuildInstanceProducerForConcreteUnregisteredType(typeof(TConcrete), () =>
619623
{
@@ -630,8 +634,11 @@ private InstanceProducer TryBuildInstanceProducerForConcreteUnregisteredType<TCo
630634
private InstanceProducer TryBuildInstanceProducerForConcreteUnregisteredType(Type type,
631635
InjectionConsumerInfo context)
632636
{
633-
if (type.IsAbstract() || type.IsValueType() || type.ContainsGenericParameters() ||
634-
!this.IsConcreteConstructableType(type, context))
637+
if (!this.Options.ResolveUnregisteredConcreteTypes
638+
|| type.IsAbstract()
639+
|| type.IsValueType()
640+
|| type.ContainsGenericParameters()
641+
|| !this.IsConcreteConstructableType(type))
635642
{
636643
return null;
637644
}
@@ -681,13 +688,6 @@ private static InstanceProducer BuildInstanceProducerForConcreteUnregisteredType
681688
return producer;
682689
}
683690

684-
private bool IsConcreteConstructableType(Type concreteType, InjectionConsumerInfo context)
685-
{
686-
string errorMesssage;
687-
688-
return this.Options.IsConstructableType(concreteType, out errorMesssage);
689-
}
690-
691691
// We're registering a service type after 'locking down' the container here and that means that the
692692
// type is added to a copy of the registrations dictionary and the original replaced with a new one.
693693
// This 'reference swapping' is thread-safe, but can result in types disappearing again from the
@@ -762,13 +762,13 @@ private bool ContainsExplicitRegistrationFor(Type serviceType) =>
762762

763763
private void ThrowNotConstructableException(Type concreteType)
764764
{
765-
string exceptionMessage;
766-
767-
// Since we are at this point, we know the concreteType is NOT constructable.
768-
this.Options.IsConstructableType(concreteType, out exceptionMessage);
765+
// At this point we know the concreteType is either NOT constructable or
766+
// Options.ResolveUnregisteredConcreteTypes is configured to not return the type.
767+
this.Options.IsConstructableType(concreteType, out string exceptionMessage);
769768

770769
throw new ActivationException(
771-
StringResources.ImplicitRegistrationCouldNotBeMadeForType(concreteType, this.HasRegistrations)
770+
StringResources.ImplicitRegistrationCouldNotBeMadeForType(
771+
this, concreteType, this.HasRegistrations)
772772
+ " " + exceptionMessage);
773773
}
774774
}

src/SimpleInjector/ContainerOptions.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#region Copyright Simple Injector Contributors
22
/* The Simple Injector is an easy-to-use Inversion of Control library for .NET
33
*
4-
* Copyright (c) 2013-2015 Simple Injector Contributors
4+
* Copyright (c) 2013-2019 Simple Injector Contributors
55
*
66
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
77
* associated documentation files (the "Software"), to deal in the Software without restriction, including
@@ -26,7 +26,6 @@ namespace SimpleInjector
2626
using System.Collections.Generic;
2727
using System.ComponentModel;
2828
using System.Diagnostics;
29-
using System.Diagnostics.CodeAnalysis;
3029
using System.Linq.Expressions;
3130
using System.Reflection;
3231
using SimpleInjector.Advanced;
@@ -77,6 +76,9 @@ public class ContainerOptions
7776
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
7877
private ScopedLifestyle defaultScopedLifestyle;
7978

79+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
80+
private bool resolveUnregisteredConcreteTypes = true;
81+
8082
internal ContainerOptions(Container container)
8183
{
8284
Requires.IsNotNull(container, nameof(container));
@@ -132,6 +134,30 @@ public bool UseFullyQualifiedTypeNames
132134
set { StringResources.UseFullyQualifiedTypeNames = value; }
133135
}
134136

137+
/// <summary>
138+
/// Gets or sets a value indicating whether the container should resolve unregistered concrete types.
139+
/// The default value is <code>true</code>. Consider changing the value to <code>false</code> to prevent
140+
/// accidental creation of types you haven't registered explicitly.
141+
/// </summary>
142+
/// <value>The value indicating whether the container should resolve unregistered concrete types.</value>
143+
/// <exception cref="InvalidOperationException">
144+
/// Thrown when this container instance is locked and can not be altered.
145+
/// </exception>
146+
public bool ResolveUnregisteredConcreteTypes
147+
{
148+
get
149+
{
150+
return this.resolveUnregisteredConcreteTypes;
151+
}
152+
153+
set
154+
{
155+
this.Container.ThrowWhenContainerIsLockedOrDisposed();
156+
157+
this.resolveUnregisteredConcreteTypes = value;
158+
}
159+
}
160+
135161
/// <summary>
136162
/// Gets or sets the constructor resolution behavior. By default, the container only supports types
137163
/// that have a single public constructor.

src/SimpleInjector/StringResources.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,31 +242,38 @@ internal static string CollectionTypeAlreadyRegistered(Type serviceType) =>
242242
nameof(Container.Collection),
243243
nameof(ContainerCollectionRegistrator.Append));
244244

245-
internal static string ParameterTypeMustBeRegistered(InjectionTargetInfo target, int numberOfConditionals,
246-
bool hasRelatedOneToOneMapping, bool hasRelatedCollectionMapping, Type[] skippedDecorators,
245+
internal static string ParameterTypeMustBeRegistered(
246+
Container container,
247+
InjectionTargetInfo target,
248+
int numberOfConditionals,
249+
bool hasRelatedOneToOneMapping,
250+
bool hasRelatedCollectionMapping,
251+
Type[] skippedDecorators,
247252
Type[] lookalikes) =>
248253
target.Parameter != null
249254
? string.Format(CultureInfo.InvariantCulture,
250255
"The constructor of type {0} contains the parameter with name '{1}' and type {2} that " +
251-
"is not registered. Please ensure {2} is registered, or change the constructor of {0}.{3}{4}{5}{6}{7}",
256+
"is not registered. Please ensure {2} is registered, or change the constructor of {0}.{3}{4}{5}{6}{7}{8}",
252257
target.Member.DeclaringType.TypeName(),
253258
target.Name,
254259
target.TargetType.TypeName(),
255260
GetAdditionalInformationAboutExistingConditionalRegistrations(target, numberOfConditionals),
256261
DidYouMeanToDependOnNonCollectionInstead(hasRelatedOneToOneMapping, target.TargetType),
257262
DidYouMeanToDependOnCollectionInstead(hasRelatedCollectionMapping, target.TargetType),
258263
NoteThatSkippedDecoratorsWereFound(target.TargetType, skippedDecorators),
264+
NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(container, target.TargetType),
259265
NoteThatTypeLookalikesAreFound(target.TargetType, lookalikes, numberOfConditionals))
260266
: string.Format(CultureInfo.InvariantCulture,
261267
"Type {0} contains the property with name '{1}' and type {2} that is not registered. " +
262-
"Please ensure {2} is registered, or change {0}.{3}{4}{5}{6}{7}",
268+
"Please ensure {2} is registered, or change {0}.{3}{4}{5}{6}{7}{8}",
263269
target.Member.DeclaringType.TypeName(),
264270
target.Name,
265271
target.TargetType.TypeName(),
266272
GetAdditionalInformationAboutExistingConditionalRegistrations(target, numberOfConditionals),
267273
DidYouMeanToDependOnNonCollectionInstead(hasRelatedOneToOneMapping, target.TargetType),
268274
DidYouMeanToDependOnCollectionInstead(hasRelatedCollectionMapping, target.TargetType),
269275
NoteThatSkippedDecoratorsWereFound(target.TargetType, skippedDecorators),
276+
NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(container, target.TargetType),
270277
NoteThatTypeLookalikesAreFound(target.TargetType, lookalikes, numberOfConditionals));
271278

272279
internal static string TypeMustHaveASinglePublicConstructorButItHasNone(Type serviceType) =>
@@ -336,6 +343,16 @@ internal static string ImplicitRegistrationCouldNotBeMadeForType(Type serviceTyp
336343
serviceType.TypeName(),
337344
ContainerHasNoRegistrationsAddition(containerHasRegistrations));
338345

346+
internal static string ImplicitRegistrationCouldNotBeMadeForType(
347+
Container container,
348+
Type serviceType,
349+
bool containerHasRegistrations) =>
350+
string.Format(CultureInfo.InvariantCulture,
351+
"No registration for type {0} could be found and an implicit registration could not be made.{1}{2}",
352+
serviceType.TypeName(),
353+
NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(container, serviceType),
354+
ContainerHasNoRegistrationsAddition(containerHasRegistrations));
355+
339356
internal static string DefaultScopedLifestyleCanNotBeSetWithLifetimeScoped() =>
340357
string.Format(CultureInfo.InvariantCulture,
341358
"{0} can't be set with the value of {1}.{2}.",
@@ -975,6 +992,14 @@ private static string NoteThatTypeLookalikesAreFound(Type serviceType, Type[] lo
975992
}
976993
}
977994

995+
private static string NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(
996+
Container container, Type serviceType) =>
997+
container.IsConcreteConstructableType(serviceType)
998+
&& !container.Options.ResolveUnregisteredConcreteTypes
999+
? " Note that the container's Options.ResolveUnregisteredConcreteTypes option is set " +
1000+
"to 'false'. This disallows the container to construct this unregistered concrete type."
1001+
: string.Empty;
1002+
9781003
private static string BuildAssemblyLocationMessage(Type serviceType, Type duplicateAssemblyLookalike)
9791004
{
9801005
string serviceTypeLocation = GetAssemblyLocationOrNull(serviceType);

0 commit comments

Comments
 (0)