Skip to content

Commit

Permalink
Container.Options.ResolveUnregisteredConcreteTypes setting added to d…
Browse files Browse the repository at this point in the history
…isable creation of unregistered concrete types. Fixes #377.
  • Loading branch information
dotnetjunkie committed Mar 21, 2019
1 parent 3618e01 commit cf8c9ce
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 22 deletions.
32 changes: 32 additions & 0 deletions src/SimpleInjector.Tests.Unit/ContainerOptionsTests.cs
Expand Up @@ -936,6 +936,38 @@ public void LifestyleSelectionBehavior_DefaultImplementation_RedirectsToDefaultL
// Assert
Assert.AreSame(Lifestyle.Singleton, lifestyle);
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_ByDefault_True()
{
// Arrange
var expectedValue = true;

var container = new Container();

// Act
var actualValue = container.Options.ResolveUnregisteredConcreteTypes;

// Assert
Assert.AreEqual(expectedValue, actualValue);
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_ChangedAfterContainerIsLocked_ThrowsAnException()
{
// Arrange
var container = new Container();

container.GetInstance<ConcreteCommand>();

// Act
Action action = () => container.Options.ResolveUnregisteredConcreteTypes = false;

// Assert
AssertThat.ThrowsWithExceptionMessageContains<InvalidOperationException>(
"The container can't be changed after the first call",
action);
}

private static PropertyInfo GetProperty<T>(Expression<Func<T, object>> propertySelector)
{
Expand Down
121 changes: 120 additions & 1 deletion src/SimpleInjector.Tests.Unit/ResolveUnregisteredTypeEventTests.cs
Expand Up @@ -290,7 +290,7 @@ public void GetInstance_EventRegisteredThatThrowsException_ThrowsAnDescriptiveEx
{
e.Register(() => { throw new Exception(); });
};

// Act
Action action = () => container.GetInstance<IUserRepository>();

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

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToFalse_DoesNotAllowUnregisteredConcreteRootTypesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = false;

// Add a dummy registration.
container.RegisterInstance(new FakeTimeProvider());

// Act
Action action = () => container.GetInstance<ConcreteCommand>();

// Assert
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(@"
No registration for type ConcreteCommand could be found and an implicit registration
could not be made. Note that the container's Options.ResolveUnregisteredConcreteTypes
option is set to 'false'. This disallows the container to construct this unregistered
concrete type."
.TrimInside(),
action);
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToFalse_DoesNotAllowUnregisteredConcreteDependenciesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = false;

container.Register<ServiceDependingOn<ConcreteCommand>>();

// Act
Action action = () => container.GetInstance<ServiceDependingOn<ConcreteCommand>>();

// Assert
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(@"
The constructor of type ServiceDependingOn<ConcreteCommand> contains
the parameter with name 'dependency' and type ConcreteCommand that is not
registered. Please ensure ConcreteCommand is registered, or change the constructor of
ServiceDependingOn<ConcreteCommand>. Note that the container's
Options.ResolveUnregisteredConcreteTypes option is set to 'false'. This disallows the
container to construct this unregistered concrete type."
.TrimInside(),
action);
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToNever_DoesAllowRegisteredConcreteRootTypesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = false;

// Add a dummy registration.
container.Register<ConcreteCommand>();

// Act
container.GetInstance<ConcreteCommand>();
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToNever_DoesAllowRegisteredConcreteDependenciesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = false;

container.Register<ConcreteCommand>();
container.Register<ServiceDependingOn<ConcreteCommand>>();

// Act
container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToTrue_DoesAllowUnregisteredConcreteRootTypesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = true;

// Act
container.GetInstance<ConcreteCommand>();
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToTrue_DoesAllowRegisteredConcreteDependenciesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = true;

// Act
container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
}

[TestMethod]
public void ResolveUnregisteredConcreteTypes_SetToFalseWithUnregisteredTypeHandlingType_DoesAllowUnregisteredConcreteDependenciesToBeResolved()
{
// Arrange
var container = new Container();
container.Options.ResolveUnregisteredConcreteTypes = false;

// Using unregistered type resolution re-enable the 'Always' behavior.
container.ResolveUnregisteredType += (s, e) =>
{
if (!e.Handled && !e.UnregisteredServiceType.IsAbstract)
{
e.Register(container.Options.LifestyleSelectionBehavior
.SelectLifestyle(e.UnregisteredServiceType)
.CreateRegistration(e.UnregisteredServiceType, container));
}
};

// Act
container.GetInstance<ServiceDependingOn<ConcreteCommand>>();
}

public class CompositeService<T> where T : struct
{
public CompositeService(Nullable<T>[] dependencies)
Expand Down
1 change: 1 addition & 0 deletions src/SimpleInjector/Container.Common.cs
Expand Up @@ -478,6 +478,7 @@ internal void ThrowParameterTypeMustBeRegistered(InjectionTargetInfo target)
{
throw new ActivationException(
StringResources.ParameterTypeMustBeRegistered(
this,
target,
this.GetNumberOfConditionalRegistrationsFor(target.TargetType),
this.ContainsOneToOneRegistrationForCollectionType(target.TargetType),
Expand Down
30 changes: 15 additions & 15 deletions src/SimpleInjector/Container.Resolving.cs
Expand Up @@ -270,6 +270,9 @@ internal Action<object> GetInitializer(Type implementationType, Registration con
return this.GetInstanceProducerForType(serviceType, consumer, buildProducer);
}

internal bool IsConcreteConstructableType(Type concreteType) =>
this.Options.IsConstructableType(concreteType, out string errorMesssage);

private Action<T> GetInitializer<T>(Type implementationType, Registration context)
{
Action<T>[] initializersForType = this.GetInstanceInitializersFor<T>(implementationType, context);
Expand Down Expand Up @@ -613,7 +616,8 @@ private InstanceProducer BuildEmptyCollectionInstanceProducerForEnumerable(Type
InjectionConsumerInfo context)
where TConcrete : class
{
if (this.IsConcreteConstructableType(typeof(TConcrete), context))
if (this.Options.ResolveUnregisteredConcreteTypes
&& this.IsConcreteConstructableType(typeof(TConcrete)))
{
return this.GetOrBuildInstanceProducerForConcreteUnregisteredType(typeof(TConcrete), () =>
{
Expand All @@ -630,8 +634,11 @@ private InstanceProducer BuildEmptyCollectionInstanceProducerForEnumerable(Type
private InstanceProducer TryBuildInstanceProducerForConcreteUnregisteredType(Type type,
InjectionConsumerInfo context)
{
if (type.IsAbstract() || type.IsValueType() || type.ContainsGenericParameters() ||
!this.IsConcreteConstructableType(type, context))
if (!this.Options.ResolveUnregisteredConcreteTypes
|| type.IsAbstract()
|| type.IsValueType()
|| type.ContainsGenericParameters()
|| !this.IsConcreteConstructableType(type))
{
return null;
}
Expand Down Expand Up @@ -681,13 +688,6 @@ private InstanceProducer BuildEmptyCollectionInstanceProducerForEnumerable(Type
return producer;
}

private bool IsConcreteConstructableType(Type concreteType, InjectionConsumerInfo context)
{
string errorMesssage;

return this.Options.IsConstructableType(concreteType, out errorMesssage);
}

// We're registering a service type after 'locking down' the container here and that means that the
// type is added to a copy of the registrations dictionary and the original replaced with a new one.
// This 'reference swapping' is thread-safe, but can result in types disappearing again from the
Expand Down Expand Up @@ -762,13 +762,13 @@ private void ThrowMissingInstanceProducerException(Type serviceType)

private void ThrowNotConstructableException(Type concreteType)
{
string exceptionMessage;

// Since we are at this point, we know the concreteType is NOT constructable.
this.Options.IsConstructableType(concreteType, out exceptionMessage);
// At this point we know the concreteType is either NOT constructable or
// Options.ResolveUnregisteredConcreteTypes is configured to not return the type.
this.Options.IsConstructableType(concreteType, out string exceptionMessage);

throw new ActivationException(
StringResources.ImplicitRegistrationCouldNotBeMadeForType(concreteType, this.HasRegistrations)
StringResources.ImplicitRegistrationCouldNotBeMadeForType(
this, concreteType, this.HasRegistrations)
+ " " + exceptionMessage);
}
}
Expand Down
30 changes: 28 additions & 2 deletions src/SimpleInjector/ContainerOptions.cs
@@ -1,7 +1,7 @@
#region Copyright Simple Injector Contributors
/* The Simple Injector is an easy-to-use Inversion of Control library for .NET
*
* Copyright (c) 2013-2015 Simple Injector Contributors
* Copyright (c) 2013-2019 Simple Injector Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction, including
Expand All @@ -26,7 +26,6 @@ namespace SimpleInjector
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using SimpleInjector.Advanced;
Expand Down Expand Up @@ -77,6 +76,9 @@ public class ContainerOptions
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private ScopedLifestyle defaultScopedLifestyle;

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private bool resolveUnregisteredConcreteTypes = true;

internal ContainerOptions(Container container)
{
Requires.IsNotNull(container, nameof(container));
Expand Down Expand Up @@ -132,6 +134,30 @@ public bool UseFullyQualifiedTypeNames
set { StringResources.UseFullyQualifiedTypeNames = value; }
}

/// <summary>
/// Gets or sets a value indicating whether the container should resolve unregistered concrete types.
/// The default value is <code>true</code>. Consider changing the value to <code>false</code> to prevent
/// accidental creation of types you haven't registered explicitly.
/// </summary>
/// <value>The value indicating whether the container should resolve unregistered concrete types.</value>
/// <exception cref="InvalidOperationException">
/// Thrown when this container instance is locked and can not be altered.
/// </exception>
public bool ResolveUnregisteredConcreteTypes
{
get
{
return this.resolveUnregisteredConcreteTypes;
}

set
{
this.Container.ThrowWhenContainerIsLockedOrDisposed();

this.resolveUnregisteredConcreteTypes = value;
}
}

/// <summary>
/// Gets or sets the constructor resolution behavior. By default, the container only supports types
/// that have a single public constructor.
Expand Down
33 changes: 29 additions & 4 deletions src/SimpleInjector/StringResources.cs
Expand Up @@ -242,31 +242,38 @@ internal static string DiagnosticWarningsReported(IList<DiagnosticResult> errors
nameof(Container.Collection),
nameof(ContainerCollectionRegistrator.Append));

internal static string ParameterTypeMustBeRegistered(InjectionTargetInfo target, int numberOfConditionals,
bool hasRelatedOneToOneMapping, bool hasRelatedCollectionMapping, Type[] skippedDecorators,
internal static string ParameterTypeMustBeRegistered(
Container container,
InjectionTargetInfo target,
int numberOfConditionals,
bool hasRelatedOneToOneMapping,
bool hasRelatedCollectionMapping,
Type[] skippedDecorators,
Type[] lookalikes) =>
target.Parameter != null
? string.Format(CultureInfo.InvariantCulture,
"The constructor of type {0} contains the parameter with name '{1}' and type {2} that " +
"is not registered. Please ensure {2} is registered, or change the constructor of {0}.{3}{4}{5}{6}{7}",
"is not registered. Please ensure {2} is registered, or change the constructor of {0}.{3}{4}{5}{6}{7}{8}",
target.Member.DeclaringType.TypeName(),
target.Name,
target.TargetType.TypeName(),
GetAdditionalInformationAboutExistingConditionalRegistrations(target, numberOfConditionals),
DidYouMeanToDependOnNonCollectionInstead(hasRelatedOneToOneMapping, target.TargetType),
DidYouMeanToDependOnCollectionInstead(hasRelatedCollectionMapping, target.TargetType),
NoteThatSkippedDecoratorsWereFound(target.TargetType, skippedDecorators),
NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(container, target.TargetType),
NoteThatTypeLookalikesAreFound(target.TargetType, lookalikes, numberOfConditionals))
: string.Format(CultureInfo.InvariantCulture,
"Type {0} contains the property with name '{1}' and type {2} that is not registered. " +
"Please ensure {2} is registered, or change {0}.{3}{4}{5}{6}{7}",
"Please ensure {2} is registered, or change {0}.{3}{4}{5}{6}{7}{8}",
target.Member.DeclaringType.TypeName(),
target.Name,
target.TargetType.TypeName(),
GetAdditionalInformationAboutExistingConditionalRegistrations(target, numberOfConditionals),
DidYouMeanToDependOnNonCollectionInstead(hasRelatedOneToOneMapping, target.TargetType),
DidYouMeanToDependOnCollectionInstead(hasRelatedCollectionMapping, target.TargetType),
NoteThatSkippedDecoratorsWereFound(target.TargetType, skippedDecorators),
NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(container, target.TargetType),
NoteThatTypeLookalikesAreFound(target.TargetType, lookalikes, numberOfConditionals));

internal static string TypeMustHaveASinglePublicConstructorButItHasNone(Type serviceType) =>
Expand Down Expand Up @@ -336,6 +343,16 @@ internal static string TypeMustNotContainInvalidInjectionTarget(InjectionTargetI
serviceType.TypeName(),
ContainerHasNoRegistrationsAddition(containerHasRegistrations));

internal static string ImplicitRegistrationCouldNotBeMadeForType(
Container container,
Type serviceType,
bool containerHasRegistrations) =>
string.Format(CultureInfo.InvariantCulture,
"No registration for type {0} could be found and an implicit registration could not be made.{1}{2}",
serviceType.TypeName(),
NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(container, serviceType),
ContainerHasNoRegistrationsAddition(containerHasRegistrations));

internal static string DefaultScopedLifestyleCanNotBeSetWithLifetimeScoped() =>
string.Format(CultureInfo.InvariantCulture,
"{0} can't be set with the value of {1}.{2}.",
Expand Down Expand Up @@ -975,6 +992,14 @@ private static string BuildRegistrationName(Tuple<Type, Type, InstanceProducer>
}
}

private static string NoteThatConcreteTypeCanNotBeResolvedDueToConfiguration(
Container container, Type serviceType) =>
container.IsConcreteConstructableType(serviceType)
&& !container.Options.ResolveUnregisteredConcreteTypes
? " Note that the container's Options.ResolveUnregisteredConcreteTypes option is set " +
"to 'false'. This disallows the container to construct this unregistered concrete type."
: string.Empty;

private static string BuildAssemblyLocationMessage(Type serviceType, Type duplicateAssemblyLookalike)
{
string serviceTypeLocation = GetAssemblyLocationOrNull(serviceType);
Expand Down

0 comments on commit cf8c9ce

Please sign in to comment.