SimpleInjector unexpected behaviour of explicitly registered singleton collection

117 Views Asked by At

In SimpleInjector documentation it is said:

Simple Injector preserves the lifestyle of instances that are returned from an injected IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyCollection<T> and IReadOnlyList<T> instance. In reality you should not see the the injected IEnumerable<T> as a collection of instances; you should consider it a stream of instances. Simple Injector will always inject a reference to the same stream (the IEnumerable<T> or ICollection<T> itself is a singleton) and each time you iterate the IEnumerable<T>, for each individual component, the container is asked to resolve the instance based on the lifestyle of that component.

After reading this I expected that after I've registered all IFoo and IFoo<T> as singletons, IEnumerable<IFoo> will be singleton itself and it enumeration will always lead to the same objects.

But this piece of code doesn't work like that:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using SimpleInjector;

class Program
{
    interface IFoo{}
    interface IFoo<T> : IFoo{}
    class Foo : IFoo<Foo>{}

    class Consumer1
    {
        public IFoo<Foo> FooInstance { get; }
        public Consumer1(IFoo<Foo> fooInstance){FooInstance = fooInstance;}
    }

    class Consumer2
    {
        public IFoo<Foo> FooInstance { get; }
        public Consumer2(IFoo<Foo> fooInstance){FooInstance = fooInstance;}
    }

    class Consumer3
    {
        public IEnumerable<IFoo> FooIntances { get; }
        public Consumer3(IEnumerable<IFoo> fooIntances){FooIntances = fooIntances;}
    }

    static void Main(string[] args)
    {
        var cont = new Container();

        var types = Assembly.GetExecutingAssembly()
                            .GetTypes()
                            .Where(x =>
                                    typeof(IFoo<>).IsAssignableFrom(x) ||
                                    typeof(IFoo).IsAssignableFrom(x))
                            .Where(x => !x.IsInterface);

        foreach (var type in types)
        {
            var genericFoo = type.GetInterfaces().Single(x=>x.IsGenericType);
            var genericArgs = genericFoo.GenericTypeArguments;
            var closedGenericFoo = typeof(IFoo<>).MakeGenericType(genericArgs);

            var reg = Lifestyle.Singleton.CreateRegistration(type, cont);

            cont.AddRegistration(closedGenericFoo, reg);
            cont.AddRegistration(typeof(IFoo), reg);
        }

        cont.RegisterCollection<IFoo>(new[] {Assembly.GetExecutingAssembly()});

        var cons1 = cont.GetInstance<Consumer1>();
        var cons2 = cont.GetInstance<Consumer2>();
        var cons3_1 = cont.GetInstance<Consumer3>();
        var cons3_2 = cont.GetInstance<Consumer3>();

        // Expected: true | Actually: true
        Console.WriteLine($"cons1.FooInstance == cons2.FooInstance : {cons1.FooInstance == cons2.FooInstance}"); 
        // Expected: true | Actually: true
        Console.WriteLine($"cons3_1.FooInstances == cons3_2.FooInstances : {cons3_1.FooIntances == cons3_2.FooIntances}");
        // Expected: true | Actually: false
        Console.WriteLine($"cons3_1.FooIntances.First() == cons1.FooInstance : {cons3_1.FooIntances.First() == cons1.FooInstance}");
        // Expected: true | Actually: false
        Console.WriteLine($"cons3_1.FooIntances.First() == cons3_2.FooIntances.First() : {cons3_1.FooIntances.First() == cons3_2.FooIntances.First()}");

        Console.ReadKey();
    }
}

What I'm trying to achieve:

  1. single Foo implementation registered to both IFoo<Foo> and to IFoo (partially works)
  2. single stream of IFoo which is IEnumerable<IFoo> (it works)
  3. Iterating of IEnumerable above should give me same Foo implementation (doesn't work)

Is it possible?

1

There are 1 best solutions below

4
On BEST ANSWER

What's happening here is the following:

The call to cont.RegisterCollection<IFoo>(new[] {Assembly.GetExecutingAssembly()}) results in the following registration:

cont.RegisterCollection<IFoo>(new[] { typeof(Foo) });

When the collection is resolved for the first time, the container will look up the registration for Foo to make sure it will reuse that registration and thus its lifestyle.

With your registration however, no registration for Foo can be founds. There are registrations however for IFoo<Foo> and IFoo but what Simple Injector is concerned, those are different registrations.

As a result of this, Simple Injector will create a last-minute registration for Foo on your behalf and the default lifestyle that is used by Simple Injector is Transient.

This is why although you already registered Foo twice (once as IFoo<Foo> and once as IFoo), the element of the collection will still be transient.

You would have noticed this problem if you had called Container.Verify() at the end of the registration process. Simple Injector detects these kinds of misconfigurations for you. You should always call Container.Verify().

To solve this, you can change your configuration to the following:

static void Main(string[] args)
{
    var container = new Container();

    // GetTypesToRegister can do the assembly scanning for you.
    IEnumerable<Type> types = container.GetTypesToRegister(typeof(IFoo<>), 
        new[] { Assembly.GetExecutingAssembly() });

    // Here we create a list of Registration instance once.
    Registration[] regs = (
        from type in types
        select Lifestyle.Singleton.CreateRegistration(type, container))
        .ToArray();

    // Here we register the registrations as one-to-one mapping.
    foreach (var reg in regs)
    {
        Type closedGenericFoo = reg.ImplementationType.GetInterfaces()
            .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IFoo<>))
            .Single();

        container.AddRegistration(closedGenericFoo, reg);
    }

    // Here we make a one-to-many mapping between IFoo and the registrations.
    container.RegisterCollection<IFoo>(regs);

    container.Verify();

    ...
}