PerRequestLifetimeManager和Task.Factory.StartNew - Unity的依赖注入

时间:2014-11-02 10:12:05

标签: asp.net-mvc dependency-injection inversion-of-control unity-container ioc-container

如何使用PerRequestLifeTimeManager管理新任务? 我应该在新任务中创建另一个容器吗?(我不想将PerRequestLifeTimeManager更改为PerResolveLifetimeManager / HierarchicalLifetimeManager)

    [HttpPost]
    public ActionResult UploadFile(FileUploadViewModel viewModel)
    {
        var cts = new CancellationTokenSource();
        CancellationToken cancellationToken = cts.Token;
        Task.Factory.StartNew(() =>
        {
            // _fileService =  DependencyResolver.Current.GetService<IFileService>();
            _fileService.ProcessFile(viewModel.FileContent);

        }, cancellationToken);
    }

1 个答案:

答案 0 :(得分:3)

您应该阅读this article about DI in multi-threaded applications。虽然它是为不同的DI库编写的,但您可以找到大多数适用于DI概念的信息。引用一些重要部分:

  

依赖注入会强制您将所有依赖项连接在一起   应用程序中的单个位置:组合根。这意味着   应用程序中有一个地方知道如何   服务行为,它们是否是线程安全的,以及它们应该如何   有线。没有这种集中化,这种知识就会分散   在整个代码库中,很难改变行为   服务。

     

在多线程应用程序中,每个线程都应该拥有自己的对象   图形。这意味着您通常应该打电话   [解析&lt; T&gt;()]一次在线程的开头   执行以获取处理该线程的根对象(或   请求)。容器将构建一个包含所有根的对象图   对象的依赖关系。其中一些依赖将是单身人士;   在所有线程之间共享。其他依赖可能是暂时的;一个   每个依赖项创建一个新实例。其他依赖可能是   特定于线程,特定于请求或具有其他一些生活方式。该   应用程序代码本身并不知道依赖项的方式   注册,这就是应该的方式。

     

在一个开头建立一个新的对象图的建议   线程,在手动启动新(背景)线程时也保持。   虽然您可以将数据传递给其他线程,但您不应该传递   容器控制对其他线程的依赖。在每个新的   线程,您应该再次询问容器的依赖项。什么时候   你开始将依赖从一个线程传递到另一个线程,那些   部分代码必须知道传递它们是否安全   依赖于。例如,那些依赖项是线程安全的吗?   在某些情况下分析这可能是微不足道的,但会阻止你   从现在开始,用其他实现来改变这些依赖关系   必须记住你的代码中有一个地方   发生了,您需要知道传递哪些依赖项。您   再次分散这些知识,使其更难推理   关于DI配置的正确性并使其更容易   以导致并发问题的方式错误配置容器。

因此,您不应该在应用程序代码本身中旋转新线程。你绝对不应该创建一个新的容器实例,因为这会导致各种性能问题;每个应用程序通常只有一个容器实例。

相反,您应该将此基础结构逻辑拖入组合根目录,这样可以简化控制器的代码。您的控制器代码不应超过这个:

[HttpPost]
public ActionResult UploadFile(FileUploadViewModel viewModel)
{
    _fileService.ProcessFile(viewModel.FileContent);
}

另一方面,您不希望更改IFileService实现,因为执行多线程不应该关注它。相反,我们需要一些基础结构逻辑,我们可以放在控制器和文件服务之间,而不必知道这一点。他们这样做的方法是为文件服务实现代理类并将其放在Composition Root中:

private sealed class AsyncFileServiceProxy : IFileService {
    private readonly ILogger logger;
    private readonly Func<IFileService> fileServiceFactory;
    public AsyncFileServiceProxy(ILogger logger, Func<IFileService> fileServiceFactory)
    {
        this.logger = logger;
        this.fileServiceFactory = fileServiceFactory;
    }

    void IFileService.ProcessFile(FileContent content) {
        // Run on a new thread
        Task.Factory.StartNew(() => {
            this.BackgroundThreadProcessFile(content);
        });
    }

    private void BackgroundThreadProcessFile(FileContent content) {
        // Here we run on a different thread and the
        // services should be requested on this thread.
        var fileService = this.fileServiceFactory.Invoke();

        try {
            fileService.ProcessFile(content);
        } 
        catch (Exception ex) {
            // logging is important, since we run on a
            // different thread.
            this.logger.Log(ex);
        }
    }
}

这个类是一个很小的基础结构逻辑,允许在后台线程上处理文件。剩下的唯一事情是配置容器以注入我们的AsyncFileServiceProxy而不是真正的文件服务实现。有多种方法可以做到这一点。这是一个例子:

container.RegisterType<ILogger, YourLogger>();
container.RegisterType<RealFileService>();
container.RegisterType<Func<IFileService>>(() => container.Resolve<RealFileService>(),
    new ContainerControlledLifetimeManager());
container.RegisterType<IFileService, AsyncFileServiceProxy>();

然而,这个方程中缺少一部分,这就是如何处理范围生活方式,例如按要求生活方式。由于您在后台线程上运行东西,因此没有HTTPContext,这基本上意味着您需要启动一些“范围”来模拟请求(因为您的后台线程基本上是它自己的新请求)。然而,这是我对Unity的知识停止的地方。我对Simple Injector和Simple Injector非常熟悉,你可以使用混合生活方式(将每个请求的生活方式与终生范围的生活方式混合在一起)来解决这个问题,并在这样的范围内明确地将呼叫包裹到BackgroundThreadProcessFile 。我想Unity中的解决方案与此非常接近,但遗憾的是我没有足够的Unity知识向您展示如何。希望其他人可以对此发表评论,或者添加一个额外的答案来解释如何在Unity中执行此操作。