如何配置Simple Injector以在ASP.NET MVC中运行后台线程

我正在使用Simple Injector来管理我注入的依赖项的生命周期(在本例中为UnitOfWork ),我很高兴有一个单独的装饰器而不是我的服务或命令处理程序,在保存和处理之后,在编写业务时使代码更容易逻辑层(我遵循本博文中概述的架构)。

通过在构造根容器的构造过程中使用Simple Injector MVC NuGet包和以下代码,上面的工作完美(并且非常容易),如果图中存在多个依赖项,则相同的实例将全部注入 – 完美的entity framework模型上下文。

 private static void InitializeContainer(Container container) { container.RegisterPerWebRequest(); // register all other interfaces with: // container.Register(); } 

我现在需要运行一些后台线程,并从Simple Injector 文档中了解可以代理命令的线程 ,如下所示:

 public sealed class TransactionCommandHandlerDecorator : ICommandHandler { private readonly ICommandHandler handlerToCall; private readonly IUnitOfWork unitOfWork; public TransactionCommandHandlerDecorator( IUnitOfWork unitOfWork, ICommandHandler decorated) { this.handlerToCall = decorated; this.unitOfWork = unitOfWork; } public void Handle(TCommand command) { this.handlerToCall.Handle(command); unitOfWork.Save(); } } 

ThreadedCommandHandlerProxy:

 public class ThreadedCommandHandlerProxy : ICommandHandler { Func<ICommandHandler> instanceCreator; public ThreadedCommandHandlerProxy( Func<ICommandHandler> creator) { this.instanceCreator = creator; } public void Handle(TCommand command) { Task.Factory.StartNew(() => { var handler = this.instanceCreator(); handler.Handle(command); }); } } 

但是,从这个线程示例文档中我可以看到工厂被使用,如果我将工厂引入我的命令和服务层,事情会变得混乱和不一致,因为我将为不同的服务提供不同的保存方法(一个容器处理保存,其他实例化的工厂内服务处理保存和处理) – 您可以看到没有任何工厂的服务代码框架是多么清晰和简单:

 public class BusinessUnitCommandHandlers : ICommandHandler, ICommandHandler { private IBusinessUnitService businessUnitService; private IInvoiceService invoiceService; public BusinessUnitCommandHandlers( IBusinessUnitService businessUnitService, IInvoiceService invoiceService) { this.businessUnitService = businessUnitService; this.invoiceService = invoiceService; } public void Handle(AddBusinessUnitCommand command) { businessUnitService.AddCompany(command.name); } public void Handle(DeleteBusinessUnitCommand command) { invoiceService.DeleteAllInvoicesForCompany(command.ID); businessUnitService.DeleteCompany(command.ID); } } public class BusinessUnitService : IBusinessUnitService { private readonly IUnitOfWork unitOfWork; private readonly ILogger logger; public BusinessUnitService(IUnitOfWork unitOfWork, ILogger logger) { this.unitOfWork = unitOfWork; this.logger = logger; } void IBusinessUnitService.AddCompany(string name) { // snip... let container call IUnitOfWork.Save() } void IBusinessUnitService.DeleteCompany(int ID) { // snip... let container call IUnitOfWork.Save() } } public class InvoiceService : IInvoiceService { private readonly IUnitOfWork unitOfWork; private readonly ILogger logger; public BusinessUnitService(IUnitOfWork unitOfWork, ILogger logger) { this.unitOfWork = unitOfWork; this.logger = logger; } void IInvoiceService.DeleteAllInvoicesForCompany(int ID) { // snip... let container call IUnitOfWork.Save() } } 

有了上面我的问题开始形成,正如我从ASP .NET PerWebRequest生命周期的文档中所理解的,使用以下代码:

 public T GetInstance() { var context = HttpContext.Current; if (context == null) { // No HttpContext: Let's create a transient object. return this.instanceCreator(); } object key = this.GetType(); T instance = (T)context.Items[key]; if (instance == null) { context.Items[key] = instance = this.instanceCreator(); } return instance; } 

以上工作正常,每个HTTP请求都会有一个有效的HttpContext.Current ,但是如果我使用ThreadedCommandHandlerProxy启动一个新线程,它将创建一个新线程,并且该线程中将不再存在HttpContext

由于HttpContext在每次后续调用时都为null,因此注入服务构造函数的所有对象实例都是新的且唯一的,与每个Web请求的正常HTTP相反,其中对象作为所有服务中的同一实例正确共享。

所以总结以上问题:

无论是从HTTP请求创建还是通过新线程,我将如何获取构造的对象和注入的公共项?

在命令处理程序代理中由一个线程管理UnitOfWork是否有任何特殊注意事项? 如何确保在处理程序执行后保存并处理它?

如果我们在命令处理程序/服务层中遇到问题并且不想保存UnitOfWork ,那么我们只是抛出exception吗? 如果是这样,是否有可能在全局级别捕获它,或者我们是否需要在处理程序装饰器或代理中的trycatchcatch每个请求的exception?

谢谢,

克里斯

让我首先警告,如果您希望在Web应用程序中异步执行命令,您可能需要退后一步,看看您要实现的目标。 在后台线程上启动处理程序之后,Web应用程序总是存在被回收的风险。 当ASP.NET应用程序被回收时,所有后台线程都将被中止。 将命令发布到(事务性)队列并让后台服务选择它们可能会更好。 这可以确保命令不会“丢失”。 并且还允许您在处理程序未成功成功时重新执行命令。 它还可以帮助您免于进行一些令人讨厌的注册(无论您选择哪种DI框架,您都可能拥有这种注册),但这可能只是一个侧面问题。 如果您确实需要运行处理程序异步,至少尝试最小化您运行异步的处理程序数。

除此之外,您需要的是以下内容。

正如您所指出的,由于您正在异步运行(某些)命令处理程序,因此您无法使用每个Web请求生活方式。 您将需要一个混合解决方案,它在每个Web请求和“其他”之间进行混合。 其他东西最有可能是一生的范围 。 由于几个原因,这些混合解决方案没有内置扩展。 首先,它是一个非常奇特的function,没有多少人需要。 其次,你可以将任何两种或三种生活方式混合在一起,这几乎是混合动力的无穷无尽的组合。 最后,(很容易)自己注册。

在Simple Injector 2中,添加了Lifestyle类,它包含一个CreateHybrid方法,允许组合任何两种生活方式来创建新的生活方式。 这是一个例子:

 var hybridLifestyle = Lifestyle.CreateHybrid( () => HttpContext.Current != null, new WebRequestLifestyle(), new LifetimeScopeLifestyle()); 

您可以使用这种混合生活方式来注册工作单元:

 container.Register(hybridLifestyle); 

由于您将工作单元注册为Per Lifetime Scope,因此必须为某个线程显式创建和处置Lifetime Scope。 最简单的方法是将其添加到ThreadedCommandHandlerProxy 。 这不是最简单的做事方式,但这是向我展示如何做到这一点的最简单方法。

如果我们在命令处理程序/服务层中遇到问题并且不想保存UnitOfWork,那么我们只是抛出exception吗?

典型的做法是抛出exception。 事实上,这是例外的一般规则:

如果你的方法不能做它的名字承诺它可以,抛出。 – >

命令处理程序应该忽略它执行的上下文的方式,并且你想要的最后一件事是区分它是否应该抛出exception。 所以投掷是你最好的选择。 然而,当在后台线程上运行时,你最好捕获该exception,因为如果你没有捕获它,.NET将终止整个AppDomain。 在Web应用程序中,这意味着AppDomain回收,这意味着您的Web应用程序(或至少该服务器)将在短时间内脱机。

另一方面,您也不希望丢失任何exception信息,因此您应该记录该exception,并且可能希望使用该exception记录该命令的序列化表示,以便您可以看到传入了哪些数据。添加到ThreadedCommandHandlerProxy.Handle方法,它看起来像这样:

 public void Handle(TCommand command) { string xml = this.commandSerializer.ToXml(command); Task.Factory.StartNew(() => { var logger = this.container.GetInstance(); try { using (container.BeginTransactionScope()) { // must be created INSIDE the scope. var handler = this.instanceCreator(); handler.Handle(command); } } catch (Exception ex) { // Don't let the exception bubble up, // because we run in a background thread. this.logger.Log(ex, xml); } }); } 

我警告过,异步运行处理程序可能不是最好的主意。 但是,由于您正在应用此命令/处理程序模式,因此您可以在以后切换到使用排队,而无需更改应用程序中的单行代码。 这只是编写某种QueueCommandHandlerDecorator (它将命令序列化并将其发送到队列)并改变组合根中的连接方式,你很高兴(当然还有)忘记实现从队列中执行命令的服务。 换句话说,这个SOLID设计的优点在于实现这些function与应用程序的大小不变。

使用方便的命令处理程序装饰器可以克服在ASP.NET中运行后台线程的部分问题 :

 public class AspNetSafeBackgroundCommandHandlerDecorator : ICommandHandler, IRegisteredObject { private readonly ICommandHandler decorated; private readonly object locker = new object(); public AspNetSafeBackgroundCommandHandlerDecorator( ICommandHandler decorated) { this.decorated = decorated; } public void Handle(T command) { HostingEnvironment.RegisterObject(this); try { lock (this.locker) { this.decorated.Handle(command); } } finally { HostingEnvironment.UnregisterObject(this); } } void IRegisteredObject.Stop(bool immediate) { // Ensure waiting till Handler finished. lock (this.locker) { } } } 

当您在命令处理程序和ThreadedCommandHandlerProxy之间放置此装饰器时,您将确保在此类命令运行时(在正常情况下)AppDomain未被卸载。