Skip to content

Commit

Permalink
PredicateContext.Consumer not longer returns null, but instead throws…
Browse files Browse the repository at this point in the history
… an exception. Fixes #694.
  • Loading branch information
dotnetjunkie committed May 21, 2020
1 parent 739c4d7 commit b4c82c0
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 15 deletions.
58 changes: 52 additions & 6 deletions src/SimpleInjector.Tests.Unit/RegisterConditionalTests.cs
Expand Up @@ -21,7 +21,7 @@ public void RegisterConditionalNonGeneric_AllowOverridingRegistrations_NotSuppor
var container = ContainerFactory.New();

container.Options.AllowOverridingRegistrations = true;

// Act
Action action = () => container.RegisterConditional(typeof(ILogger), typeof(NullLogger),
Lifestyle.Singleton, c => true);
Expand Down Expand Up @@ -1708,24 +1708,70 @@ public void ContainerUncontrolledCollections_RegisteredAsConditional_ResolvesExp
Assert.AreEqual(expected: "4, 5, 6", actual: string.Join(", ", intService.Dependency));
}

// See #698
[TestMethod]
public void GetInstance_ConditionalRegistrationAsRootType_PredicateContextConsumerPropertyIsNull()
public void GetInstance_ConditionalRegistrationAsRootTypeUsingConsumerProperty_ThrowsExpectedException()
{
// Arrange
var container = new Container();

PredicateContext context = null;
InjectionConsumerInfo consumer = null;

container.RegisterConditional<ILogger, NullLogger>(c => { context = c; return true; });

// Act
container.GetInstance<ILogger>();

// Act
Action action = () => consumer = context.Consumer;

// Assert
Assert.IsNotNull(context, "PredicateContext should not be null.");
Assert.IsNull(context.Consumer,
"When requesint a root type, the Consumer property should be null.");

AssertThat.ThrowsWithExceptionMessageContains<InvalidOperationException>(@"
Calling the PredicateContext.Consumer property for a conditional registration that is
requested directly from the container is not supported. ILogger is requested directly from the
container opposed to it being injected into another class, which causes this exception. If
ILogger needs to be requested directly (e.g. by calling container.GetInstance<ILogger>()),
check the PredicateContext.HasConsumer property inside the predicate to determine whether
PredicateContext.Consumer can be called, e.g. container.RegisterConditional(typeof(ILogger),
typeof(NullLogger), c => c.HasConsumer ? c.Consumer.ImplementationType == typeof(MyConsumer)
: true). Only call PredicateContext.Consumer when PredicateContext.HasConsumer returns true."
.TrimInside(),
action);
}

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

PredicateContext context = null;

container.RegisterConditional<ILogger, NullLogger>(c => { context = c; return true; });

// Act
container.GetInstance<ILogger>();

// Assert
Assert.IsFalse(context.HasConsumer, "PredicateContext.HasConsumer should be false.");
}

[TestMethod]
public void GetInstance_ConditionalRegistrationInjected_HasConsumerReturnsTrue()
{
// Arrange
var container = ContainerFactory.New();

PredicateContext context = null;

container.RegisterConditional<ILogger, NullLogger>(c => { context = c; return true; });

// Act
container.GetInstance<ServiceDependingOn<ILogger>>();

// Assert
Assert.IsTrue(context.HasConsumer, "PredicateContext.HasConsumer should be true.");
}

// Regression in v4.5.2. See #734
Expand Down
38 changes: 30 additions & 8 deletions src/SimpleInjector/PredicateContext.cs
Expand Up @@ -13,14 +13,14 @@ namespace SimpleInjector
/// <summary>
/// An instance of this type will be supplied to the <see cref="Predicate{T}"/>
/// delegate that is that is supplied to the
/// <see cref="Container.RegisterConditional(System.Type, System.Type, Lifestyle, Predicate{PredicateContext})">RegisterConditional</see>
/// overload that takes this delegate. This type contains information about the open generic service that
/// <see cref="Container.RegisterConditional(Type, Type, Lifestyle, Predicate{PredicateContext})">RegisterConditional</see>
/// overload that takes this delegate. This type contains information about the service that
/// is about to be created and it allows the user to examine the given instance to decide whether this
/// implementation should be created or not.
/// </summary>
/// <remarks>
/// Please see the
/// <see cref="Container.RegisterConditional(System.Type, System.Type, Lifestyle, Predicate{PredicateContext})">Register</see>
/// <see cref="Container.RegisterConditional(Type, Type, Lifestyle, Predicate{PredicateContext})">Register</see>
/// method for more information.
/// </remarks>
[DebuggerDisplay(nameof(PredicateContext) + " ({" + nameof(PredicateContext.DebuggerDisplay) + ", nq})")]
Expand Down Expand Up @@ -91,12 +91,34 @@ internal PredicateContext(InstanceProducer producer, InjectionConsumerInfo consu
public bool Handled { get; }

/// <summary>
/// Gets the contextual information of the consuming component that directly depends on the resolved
/// service. This property will return null in case the service is resolved directly from the container.
/// Gets the contextual information of the consuming component that directly depends on the registered
/// service. This property will never return null, but instead throw an exception when the service is
/// requested directly from the container.
/// </summary>
/// <value>The <see cref="InjectionConsumerInfo"/> or null.</value>
public InjectionConsumerInfo? Consumer =>
this.consumer != InjectionConsumerInfo.Root ? this.consumer : null;
/// <value>The <see cref="InjectionConsumerInfo"/>.</value>
/// <exception cref="InvalidOperationException">Thrown when the service described by this instance
/// is requested directly from the container, opposed to being injected into a consumer.</exception>
public InjectionConsumerInfo Consumer
{
get
{
if (!this.HasConsumer)
{
throw new InvalidOperationException(
StringResources.CallingPredicateContextConsumerOnDirectResolveIsNotSupported(this));
}

return this.consumer;
}
}

/// <summary>
/// Gets a value indicating whether the resolved service is injected into a consumer or is requested
/// directly from the container.
/// </summary>
/// <value>True when the service is injected into a consumer; false when it is requested directly
/// from the container</value>
public bool HasConsumer => this.consumer != InjectionConsumerInfo.Root;

[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode",
Justification = "This method is called by the debugger.")]
Expand Down
27 changes: 26 additions & 1 deletion src/SimpleInjector/StringResources.cs
Expand Up @@ -11,6 +11,7 @@ namespace SimpleInjector
using System.Reflection;
using SimpleInjector.Advanced;
using SimpleInjector.Diagnostics;
using SimpleInjector.Diagnostics.Analyzers;

/// <summary>Internal helper for string resources.</summary>
internal static class StringResources
Expand Down Expand Up @@ -112,6 +113,30 @@ internal static string ContainerCanNotBeUsedAfterDisposal(Type type, string? sta
NoteThatTypeLookalikesAreFound(serviceType, lookalikes),
NoCollectionRegistrationExists(false, serviceType));

internal static string CallingPredicateContextConsumerOnDirectResolveIsNotSupported(
PredicateContext context) =>
Format(
"Calling the {0} property for a conditional registration that is requested directly from " +
"the container is not supported. {1} is requested directly from the container opposed to it " +
"being injected into another class, which causes this exception. If {1} needs to be " +
"requested directly (e.g. by calling container.GetInstance<{2}>()), check the {3} property " +
"inside the predicate to determine whether {0} can be called{4}. Only call {0} when {3} " +
"returns true.",
nameof(PredicateContext) + "." + nameof(PredicateContext.Consumer),
context.ServiceType.TypeName(),
context.ServiceType.CSharpFriendlyName(),
nameof(PredicateContext) + "." + nameof(PredicateContext.HasConsumer),
CallingPredicateContextConsumerOnDirectResolveExample(context));

private static string CallingPredicateContextConsumerOnDirectResolveExample(PredicateContext context) =>
context.ImplementationType == null
? string.Empty
: Format(
", e.g. container.RegisterConditional(typeof({0}), typeof({1}), " +
"c => c.HasConsumer ? c.Consumer.ImplementationType == typeof(MyConsumer) : true)",
context.ServiceType.CSharpFriendlyName(),
context.ImplementationType.CSharpFriendlyName());

internal static string KnownImplementationTypeShouldBeAssignableFromExpressionType(
Type knownImplementationType, Type currentExpressionType) =>
Format(
Expand Down Expand Up @@ -1279,7 +1304,7 @@ private static string BuildAssemblyLocationMessage(Type serviceType, Type duplic

private static string TypeName(this Type type) => type.ToFriendlyName(UseFullyQualifiedTypeNames);

private static string CSharpFriendlyName(Type type) =>
private static string CSharpFriendlyName(this Type type) =>
Types.ToCSharpFriendlyName(type, UseFullyQualifiedTypeNames);

private static string Format(string format, params object?[] args) =>
Expand Down

0 comments on commit b4c82c0

Please sign in to comment.