Search code examples
c#dependency-injectionasync-awaitioc-containersimple-injector

Where to place AsyncScopedLifestyle when using Simple Injector


I am writing an application that is used at a call center. Whenever a phone call lands on a workstation, I need to create a set of objects (maybe about 30). I want those objects only to exist for the duration of the phone call because they contain state, and I think it makes more sense to create new objects than to try and reset their state each time a call lands. At the time of creation of these objects, it must perform some async activity such as establishing multiple sockets to other applications and sending messages to them. When the phone call ends, it must do more async operations such as sending end call messages and then closing the sockets.

I have been looking into the AsyncScopedLifestyle functionality of Simple Injector. Here is a simplified example of how I think I would use it:

class CallTaskFactory
{
    private readonly Container Container;

    public CallTaskFactory(Container container)
    {
        Container = container;
    }

    public async Task CreateCallTask()
    {
        using (Scope scope = AsyncScopedLifestyle.BeginScope(Container))
        {
            // Get the socket's destination
            SocketDestinationProvider socketDestProvider =
                Container.GetInstance<SocketDestinationProvider>();

            EndPoint ep = await socketDestProvider.GetSocketDestination();

            // Now create a socket and connect to that destination
            Socket socket = Container.GetInstance<Socket>();
            await socket.ConnectAsync(ep);

            // Send a simple message on the socket
            var Sender1 = Container.GetInstance<MessageSender1>();
            await Sender1.SendStartMessage();

            // Send another message, and the response tells us whether we need
            // to create some object that does something on a timer
            var Sender2 = Container.GetInstance<MessageSender2>();
            var Response = await Sender2.SendStartMessageAndAwaitResponse();
            if (Response.Result)
            {
                Container.GetInstance<ClassThatChecksSomethingOnATimer>();
            }

            // The call stays active until the socket closes
            TaskCompletionSource<int> Completion = new TaskCompletionSource<int>();
            socket.Closed += (sender, e) => { Completion.TrySetResult(0); };
            await Completion.Task;

            // Clean up
            await Sender2.SendStopMessage();
            await Sender1.SendStopMessage();
            await socket.DisconnectAsync();
        }
    }
}

I am not sure I am putting it in the correct place, though. I assume this factory class would have to exist inside my Composition Root, because it references a specific DI container. But to me the Composition Root is for composing an object graph only, and generally it doesn't have logic that uses those objects, as the above code does.

How can I create a set of objects in one place and consume them in another, and then destroy them when their work is done?


Solution

  • I assume this factory class would have to exist inside my composition root, because it references a specific DI container.

    Absolutely. Only the Composition Root should reference the container. This typically means you need to introduce an abstraction. This allows you to implement the adapter logic that depends on the DI Container inside your Composition Root, while the application code can take a dependency on its abstraction.

    But to me the composition root is for composing an object graph only, and generally it doesn't have logic that uses those objects, as the above code does.

    You should prevent placing business logic inside the Composition Root. The Composition Root is part of the applicaiton's infrastructure. That doesn't mean, though, that it can only compose object graphs. It is allowed to invoke the created object graph. It would be pretty hard to do anything useful if it wasn't allowed to act on the composed object graph.

    How can I create a set of objects in one place and consume them in another, and then destroy them when their work is done?

    To solve this problem, you should try to approach this from a different angle. Your Composition Root should ideally only have a single call to GetInstance and a single method call on the resolved object(s). This can generally be achieved by wrapping all the code in a new class. The resolved dependencies will be promoted to constructor arguments in the new class.

    When you apply this technique to your code, you'd end up with something like this:

    public class CallExecutor {
        ...        
        public CallExecutor(
            SocketDestinationProvider socketDestinationProvider, Socket socket,
            MessageSender1 messageSender1, MessageSender2 messageSender2,
            ClassThatChecksSomethingOnATimer checker)
        {
            this.socketDestinationProvider = socketDestinationProvider;
            this.socket = socket;
            this.messageSender1 = messageSender1;
            this.messageSender2 = messageSender2;
            this.checker = checker;
        }
        
        public async Task Call()
        {
            EndPoint ep = await this.socketDestProvider.GetSocketDestination();
            await this.socket.ConnectAsync(ep);
            await this.sender1.SendStartMessage();
            var response = await this.sSender2.SendStartMessageAndAwaitResponse();
            if (response.Result)
            {
                this.checker.DoSomething(...)
            }
    
            TaskCompletionSource<int> Completion = new TaskCompletionSource<int>();
            socket.Closed += (sender, e) => { Completion.TrySetResult(0); };
            await Completion.Task;
    
            await this.sender2.SendStopMessage();
            await this.sender1.SendStopMessage();
            await this.socket.DisconnectAsync();        
        }
    }
    

    In the above code, I did a simple one-to-one convertion. Every resolve became a constructor dependency. This might not be correct in all cases. I can imagine, for instance, that you want to resolve multiple Socket instances from the container. Do note, however, that a Socket seems more like runtime data to me. You might want to consider to refrain using the Container to resolve objects like these (as is alluded to in this article).

    This new CallExecutor class can be defined completely as application code; there's no dependency on the DI Container. The remaining code of the CallTaskFactory is so short, that it can easily be implemented inside your Composition Root:

    class CallTaskFactory : ICallTaskFactory
    {
        private Container container;
        
        public CallTaskFactory(Container container)
        {
            this.container = container;
        }
    
        public async Task CreateCallTask()
        {
            using (AsyncScopedLifestyle.BeginScope(this.container))
            {
                await this.container.GetInstance<CallExecutor>().Call();
            }
        }
    }
    

    The introduction of the CallExecutor, however, did cause all dependencies to be created eagerly. This might seem inefficient, but shouldn't be the case, because object composition should be fast, so you can compose your graphs with confidence. When done this in combination with Simple Injector, you will hardly ever get into performance trouble, because Simple Injector can easily create thousands of objects in a split second.