【问题标题】:Where to place AsyncScopedLifestyle when using Simple Injector使用 Simple Injector 时放置 AsyncScopedLifestyle 的位置
【发布时间】:2019-06-03 16:09:07
【问题描述】:

我正在编写一个在呼叫中心使用的应用程序。每当电话打到工作站时,我都需要创建一组对象(可能大约 30 个)。我希望这些对象仅在通话期间存在,因为它们包含状态,并且我认为创建新对象比尝试在每次通话时重置它们的状态更有意义。在创建这些对象时,它必须执行一些异步活动,例如为其他应用程序建立多个套接字并向它们发送消息。当电话结束时,它必须做更多的异步操作,例如发送结束通话消息,然后关闭套接字。

我一直在研究 Simple Injector 的 AsyncScopedLifestyle 功能。这是我认为如何使用它的简化示例:

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();
        }
    }
}

不过,我不确定我是否将其放置在正确的位置。我假设这个工厂类必须存在于我的 Composition Root 中,因为它引用了一个特定的 DI 容器。但对我而言,Composition Root 仅用于组合对象图,并且通常它没有使用这些对象的逻辑,就像上面的代码那样。

如何在一个地方创建一组对象并在另一个地方使用它们,然后在它们的工作完成时销毁它们?

【问题讨论】:

    标签: c# dependency-injection async-await ioc-container simple-injector


    【解决方案1】:

    我假设这个工厂类必须存在于我的组合根目录中,因为它引用了一个特定的 DI 容器。

    当然。只有Composition Root 应该引用容器。这通常意味着您需要引入抽象。这允许您实现依赖于您的合成根内部的 DI 容器的适配器逻辑,而应用程序代码可以依赖于它的抽象。

    但对我来说,组合根仅用于组合对象图,通常它没有使用这些对象的逻辑,就像上面的代码那样。

    您应该避免将业务逻辑放在组合根中。组合根是应用程序基础设施的一部分。不过,这并不意味着它只能组成对象图。允许调用创建的对象图。如果不允许对组合对象图进行操作,将很难做任何有用的事情。

    如何在一个地方创建一组对象并在另一个地方使用它们,然后在它们的工作完成时销毁它们?

    要解决这个问题,你应该尝试从不同的角度来解决这个问题。理想情况下,您的 Composition Root 应该只对 GetInstance 进行一次调用,并对已解析的对象进行一次方法调用。这通常可以通过将所有代码包装在一个新类中来实现。解析的依赖项将被提升为新类中的构造函数参数。

    当你将这种技术应用到你的代码中时,你最终会得到这样的结果:

    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();        
        }
    }
    

    在上面的代码中,我做了一个简单的一对一转换。每个解析都成为构造函数依赖项。这可能并非在所有情况下都是正确的。例如,我可以想象您想从容器中解析多个 Socket 实例。但是请注意,Socket 在我看来更像是运行时数据。您可能需要考虑避免使用 Container 来解析此类对象(正如在 this article 中提到的那样)。

    这个新的CallExecutor 类可以完全定义为应用程序代码;不依赖于 DI 容器。 CallTaskFactory 的剩余代码非常短,可以在您的 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();
            }
        }
    }
    

    然而,CallExecutor 的引入确实导致所有依赖项都被急切地创建。这可能看起来效率低下,但不应该是这样,因为object composition should be fast,所以你可以compose your graphs with confidence。与 Simple Injector 结合使用时,您几乎不会遇到性能问题,因为 Simple Injector 可以在瞬间轻松创建数千个对象。

    【讨论】:

    • 我喜欢 CallExecutor 类,但即便如此,看到 5 个构造函数参数似乎太多了,而这只是我在此处发布的简化代码。真实版本有大约 30 个组件,并且每次添加新的呼叫处理功能时都会不断变化。我可以创建一个包含所有这些组件的参数类,因此构造函数只有一个参数吗?或者,CallExecutor 构造函数可以采用 IEnumerable 调用组件,每个调用组件都有一个 StartAsync 和一个 StopAsync 方法?
    • 大类是一种设计味道;违反单一责任原则。恢复到服务定位器反模式将无济于事。解决方案是找到能够对抗这种“不断变化”的类的正确设计。开放/封闭原则将我们推向可以添加新功能的设计,而无需更改现有代码。这不是一件容易的事,但值得实现的目标。
    • 是的,违反 SRP 确实是我问题的症结所在。但我无法避免这样一个事实,即当呼叫开始时需要发生 30 种不同的事情——这只是系统要求。我考虑让它们成为事件,以便我可以根据需要添加更多,但它们中的大多数是异步的,我需要等待它们的任务,因为如果它们中的任何一个失败,电话需要结束。
    • 尝试使用聚合服务或组合。
    • 我尝试使用复合材料,结果很接近。但我努力确保组件以正确的顺序执行。例如,通过套接字发送消息 A 需要在发送消息 B 之前发生,因此这些组件必须按顺序执行。但是其他组件与套接字无关,所以我想并行执行。我最终创建了一个 Workflow 类,它主要并行执行组件,但可以告诉组件何时需要等待另一个组件完成才能执行。你觉得这种技术有什么问题吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多