Search code examples
asp.net-web-apiserilogsimple-injector

Simple Injector: Register Conditional vs RegisterSingleton


Context: I am trying to capture multiple events in our api using serilog and the elastic search sink, these events include: GET actions(regular web api flow) as well as login attempts (Owin). I am registering serilog like this:

container.RegisterConditional(
    typeof(ICustomLogger),
    c => typeof(CustomLogger<>).MakeGenericType(
        c.Consumer?.ImplementationType ?? typeof(object)),
    Lifestyle.Singleton,
    c => true);

I am doing this for Owin registration:

app.Use(async (context, next) =>
{
    using (var scope = container.BeginExecutionContextScope())
    {
        await next.Invoke();
    }
});

Then when I call container.Verify(); the logger constructor gets called(as expected). However when I call the logger from my OAuthAuthorizationServerProvider implementation like this: var logger = ObjectFactory.GetInstance<ICustomLogger>(); the constructor gets called again, meaning the singleton is not really a singleton anymore, I am totally aware of the service locator antipattern(don't really have much time to refactor that piece right now), what is interesting is that if I change the logger registration to the following (getting rid of the type piece) then only one instance is created:

container.RegisterSingleton(typeof(ICustomLogger), typeof(CustomLogger))

The reason why I am trying to use the first option is because I want to be able to use .ForContext(typeof(T)); for serilog, and in case you are wondering how I am registering the ObjectFactory piece here it is: Class:

public static class ObjectFactory
{
    public static Container container;

    public static void SetContainer(Container container)
    {
        ObjectFactory.container = container;
    }

    public static T GetInstance<T>() where T : class
    {
        return container.GetInstance<T>();
    }
}

Registration(from my main bootstrapper):

ObjectFactory.SetContainer(container);

So my main question is: Why is only one instance created with RegisterSingleton but multiple are created with RegisterConditional and Lifestyle.Singleton?


Solution

  • The reason multiple instances are created has nothing to do with the fact that you are using your ObjectFactory, but simply because different closed versions of CustomLogger<T> are created:

    • A CustomLogger<object> is created when resolved as root type (by calling GetInstance<ICustomLogger>()
    • A CustomLogger<Service1> is created when ICustomLogger is injected into Service1.

    Because a CustomLogger<object> is a different type than a CustomLogger<Service1>, it is impossible to have just one instance for both types. They both have to be created. This might seem weird, but consider these two classes:

    public class Service1
    {
        public Service1(ICustomLogger logger) { }
    }
    
    public class Service2
    {
        public Service2(ICustomLogger logger) { }
    }
    

    At runtime, considering these two definitions, you want to create object graphs similar to the following:

    var s1 = new Service1(new CustomLogger<Service1>());
    var s2 = new Service2(new CustomLogger<Service2>());
    

    In the above case, the CustomLoggers are not cached and are, therefore, effectively transients. You might try to rewrite this to the following, but that would not be the same:

    ICustomLogger logger = new CustomLogger<Service1>();
    var s1 = new Service1(logger);
    var s2 = new Service2(logger);
    

    Now both service get the same single instance. However, this means that Service2 gets a CustomLogger<Service1> which is not what you configured. You configured the dependency to be this:

    typeof(CustomLogger<>).MakeGenericType(c.Consumer.ImplementationType)
    

    Consequence of this is that when Service2 starts to log, it will look like if the messages are coming from Service1. This would likely be incorrect behavior. Otherwise, you could have simply called RegisterSingleton<ICustomLogger, CustomLogger<Service1>>().

    This all means that the Singleton guarantee for a RegisterConditional that uses an implementation-type factory (as in your example) only holds for the specified closed generic type.

    Note that RegisterConditional isn't the only part in Simple Injector where you see this behavior. The following registrations do have the same effect:

    container.RegisterSingleton(typeof(IFoo<>), typeof(Foo<>));
    
    container.RegisterDecorator(typeof(IFoo<>), typeof(FooDecorator<>), Lifestyle.Singleton);
    

    In both these cases multiple instances of closed generic versions of Foo<T> and FooDecorator<T> can be created, but Simple Injector guarantees that there is only one instance of every closed-generic version of Foo<T>. With RegisterDecorator<T>, however, even that is not guaranteed. Consider this example:

    container.Collection.Append<ILogger, Logger1>(Lifestyle.Singleton);
    container.Collection.Append<ILogger, Logger2>(Lifestyle.Singleton);
    
    container.RegisterDecorator<ILogger, LoggerDecorator>(Lifestyle.Singleton);
    

    In this case, a collection of ILogger components is registered where each ILogger element will be wrapped with a LoggerDecorator. Because a LoggerDecorator can only depend on either Logger1 or Logger2, but not both, Simple Injector will have to create a new LoggerDecorator instance for each element. If the LoggerDecorator would be cached as a true Singleton, this would be the result:

    private static ILogger logger = new LoggerDecorator(new Logger1());
    
    private static loggers = new[] { logger, logger };
    

    As there is only 1 single LoggerDecorator, this decorator depends on Logger1, which means that the collection of loggers only has two elements that both point to the same Logger1 instance. There is no Logger2.

    This information is reflected in the following Simple Injector documentation sections: