Search code examples
c#dependency-injectionsimple-injectorconstructor-injection

Context Based Dependency Injection in Multi-Threaded Application


I have a Service running on a server which listens to a Message Que. When a message is received, a new Thread is started and the message is passed to that Thread for processing.

I have defined an interface which provides access the current user for consumption in various classes used for the message processing:

public interface IUserContext {
    User CurrentUser { get; }
}

This user will likely change from message to message.

My question is how do I register an implementation of IUserContext in SimpleInjector so that the correct User, contained in the incoming message, is properly returned by the CurrentUser property?

In my Asp.Net application this was accomplished by the following:

container.Register<IUserContext>(() => {
    User user = null;
    try {
         user = HttpContext.Current?.Session[USER_CONTEXT] as IUser;
    }
    catch { }

    return new UserContext(user);
});

I would imagine this would be accomplished using Lifetime Scoping, but I can't define that in a static class and set the User in each thread, because it could corrupt another process. This is my best guess at the implementation?

 public static Func<User> UserContext { get; set; }

Then in my code in the new Thread:

using (container.BeginLifetimeScope()) {
    .....
    var user = GetUserContext(message);
    UserContextInitializer.UserContext = () => new UserContext(user);
    .....
}

Then registration would look something like this:

container.Register<IUserContext>(() => UserContextInitializer.UserContext);

Thread Safety aside, Is this the correct approach to implement this in SimpleInjector? Is there another pattern which would be more correct?


Solution

  • Let's start with your ASP.NET-specific IUserContext registration:

    container.Register<IUserContext>(() => {
        User user = null;
        try {
             user = HttpContext.Current?.Session[USER_CONTEXT] as IUser;
        }
        catch { }
    
        return new UserContext(user);
    });
    

    This registration is problematic, because the UserContext component depends on the availability of runtime data, while, as described here, the creation of object graphs should be separated from runtime data and runtime data should flow through the system.

    In other words, you should rewrite your UserContext class to the following:

    public class AspNetUserContext : IUserContext
    {
        User CurrentUser => (User)HttpContext.Current.Session[USER_CONTEXT];
    }
    

    This allows this ASP.NET-specific IUserContext implementation to be registered as follows:

    container.RegisterInstance<IUserContext>(new AspNetUserContext());
    

    Of course, the previous does not solve the problem in your Windows Service, but the previous does lay the foundation for the solution.

    For the Windows Service you need a custom implementation (an adapter) as well:

    public class ServiceUserContext : IUserContext
    {
        User CurrentUser { get; set; }
    }
    

    This implementation is much simpler and here ServiceUserContext's CurrentUser property is a writable property. This solves your problem elegantly, because you can now do the following:

    // Windows Service Registration:
    container.Register<IUserContext, ServiceUserContext>(Lifestyle.Scoped);
    container.Register<ServiceUserContext>(Lifestyle.Scoped);
    
    // Code in the new Thread:
    using (container.BeginLifetimeScope())
    {
        .....
        var userContext = container.GetInstance<ServiceUserContext>();
    
        // Set the user of the scoped ServiceUserContext
        userContext.CurrentUser = GetUserContext(message);
    
        var handler = container.GetInstance<IHandleMessages<SomeMessage>>();
    
        handler.Handle(message);
    
        .....
    }
    

    Here, the solution is, as well, the separation of creation of object graphs and the use of runtime data. In this case, the runtime data is provided to the object graph after construction (i.e. using userContext.CurrentUser = GetUserContext(message)).