Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supplying an injected Func<Scope, T> decoratee factory with a container-less Scope throws NullReferenceException #704

Closed
dotnetjunkie opened this issue May 1, 2019 · 1 comment

Comments

Projects
None yet
1 participant
@dotnetjunkie
Copy link
Collaborator

commented May 1, 2019

Simple Injector allows decorators to be constructed in three different ways:

  • A decorator can accept the decoratee as constructor argument
  • A decorator can accept a decoratee factory as constructor argument
  • A decorator can accept a scoped decoratee factory as constructor argument

Simple Injector will generate the Func<T> and Func<Scope, T> factory methods and injects them into the constructor of the decorator. A Func<Scope, T> delegate, however, can only function correctly when it is supplied with a Scope that:

  • Contains a Container instance (currently, a Scope instance can be constructed without Container, #701 fixes that)
  • The Container instance is the same instance that has created the object graph the decorator consists of

If the above conditions are not met, weird things happen:

  • A NullReferenceException is thrown when a container-less Scope instance is provided.
  • A confusing "X is registered as 'Scoped' lifestyle, but the instance is requested outside the context of an active (Scoped) scope" exception is thrown when the Scope's container is a different one.

The following tests demonstrate the problem:

public class ScopedPluginProxy : IPlugin
{
    public readonly Func<Scope, IPlugin> Factory;
    public ScopedPluginProxy(Func<Scope, IPlugin> factory) => this.Factory = factory;
}

[TestMethod]
public void Test1()
{
    // Arrange
    var container = new Container();
    container.Options.DefaultScopedLifestyle = ScopedLifestyle.Flowing;

    container.Register<IPlugin, PluginImpl>(Lifestyle.Scoped);
    container.RegisterDecorator<IPlugin, ScopedPluginProxy>(Lifestyle.Singleton);

    var proxy = (ScopedPluginProxy)container.GetInstance<IPlugin>();
    Func<Scope, IPlugin> factory = proxy.Factory;

    var containerlessScope = new Scope();

    // Act
    Action action = () => factory(containerlessScope);

    // Assert
    AssertThat.ThrowsWithExceptionMessageContains<InvalidOperationException>(
        "For scoped decoratee factories to function, they have to be supplied with a Scope " +
        "instance that references the Container for which the object graph has been built. " +
        "But the Scope instance, provided to this Func<Scope, IPlugin> delegate does not " +
        "belong to any container. Please ensure the supplied Scope instance is created " +
        "using the constructor overload that accepts a Container instance.",
        action);
}

[TestMethod]
public void Test2()
{
    // Arrange
    var container = new Container();
    container.Options.DefaultScopedLifestyle = ScopedLifestyle.Flowing;

    container.Register<IPlugin, PluginImpl>(Lifestyle.Scoped);
    container.RegisterDecorator<IPlugin, ScopedPluginProxy>(Lifestyle.Singleton);

    var proxy = (ScopedPluginProxy)container.GetInstance<IPlugin>();
    Func<Scope, IPlugin> factory = proxy.Factory;

    var scopeFromAnotherContainer = new Scope(new Container());

    // Act
    Action action = () => factory(scopeFromAnotherContainer);

    // Assert
    AssertThat.ThrowsWithExceptionMessageContains<InvalidOperationException>(
        "For scoped decoratee factories to function, they have to be supplied with a Scope " +
        "instance that references the Container for which the object graph has been built. " +
        "But the Scope instance, provided to this Func<Scope, IPlugin> delegate references " +
        "a different Container instance.",
        action);
}

[TestMethod]
public void Test3()
{
    // Arrange
    var container = new Container();
    container.Options.DefaultScopedLifestyle = ScopedLifestyle.Flowing;

    container.Register<IPlugin, PluginImpl>(Lifestyle.Scoped);
    container.RegisterDecorator<IPlugin, ScopedPluginProxy>(Lifestyle.Singleton);

    var proxy = (ScopedPluginProxy)container.GetInstance<IPlugin>();
    Func<Scope, IPlugin> factory = proxy.Factory;

    var validScope = new Scope(container);

    // Act
    var plugin1 = factory(validScope);
    var plugin2 = factory(validScope);
    var plugin3 = factory(new Scope(container));

    // Assert
    AssertThat.IsInstanceOfType(typeof(PluginImpl), plugin1);
    Assert.AreSame(plugin1, plugin2, "Instance is not scoped but Transient.");
    Assert.AreNotSame(plugin2, plugin3, "Instance is not scoped but Singleton.");
}

@dotnetjunkie dotnetjunkie added this to the v4.6 milestone May 1, 2019

@dotnetjunkie

This comment has been minimized.

Copy link
Collaborator Author

commented May 1, 2019

bug-704 branch created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.