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

标签 c# .net asp.net-mvc-3 dependency-injection simple-injector

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

通过在构建组合根容器期间使用 Simple Injector MVC NuGet 包和以下代码,以上代码完美(并且非常容易)工作,如果图中存在多个依赖项,则同一实例将注入(inject)所有- 非常适合 Entity Framework 模型上下文。

private static void InitializeContainer(Container container)
{
    container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>();
    // register all other interfaces with:
    // container.Register<Interface, Implementation>();
}

我现在需要运行一些后台线程并从 Simple Injector 了解 documentation on threads该命令可以代理如下:

public sealed class TransactionCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> handlerToCall;
    private readonly IUnitOfWork unitOfWork;

    public TransactionCommandHandlerDecorator(
        IUnitOfWork unitOfWork, 
        ICommandHandler<TCommand> decorated)
    {
        this.handlerToCall = decorated;
        this.unitOfWork = unitOfWork;
    }

    public void Handle(TCommand command)
    {
         this.handlerToCall.Handle(command);
         unitOfWork.Save();
    }
}

ThreadedCommandHandlerProxy:

public class ThreadedCommandHandlerProxy<TCommand>
    : ICommandHandler<TCommand>
{
    Func<ICommandHandler<TCommand>> instanceCreator;

    public ThreadedCommandHandlerProxy(
        Func<ICommandHandler<TCommand>> creator)
    {
        this.instanceCreator = creator;
    }

    public void Handle(TCommand command)
    {
        Task.Factory.StartNew(() =>
        {
            var handler = this.instanceCreator();
            handler.Handle(command);
        });
    }
} 

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

public class BusinessUnitCommandHandlers :
    ICommandHandler<AddBusinessUnitCommand>,
    ICommandHandler<DeleteBusinessUnitCommand>
{
    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()
    }
}

根据我从 documentation on ASP .NET PerWebRequest lifetimes 中了解到的情况,我的问题开始形成了,使用了以下代码:

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,因此注入(inject)到服务构造函数中的所有对象实例都将是新的且唯一的,这与正常的 HTTP 每个 Web 请求相反,其中对象被正确共享为所有服务中的相同实例。

所以把上面的总结成问题:

无论是从 HTTP 请求还是通过新线程创建的,我将如何构建对象并注入(inject)公共(public)项目?

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

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

谢谢,

克里斯

最佳答案

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

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

正如您所指出的,由于您正在异步运行(一些)命令处理程序,因此您不能对它们使用每个网络请求的生活方式。您将需要一个混合解决方案,该解决方案在每个 Web 请求和“其他”之间混合。其他东西很可能是 per lifetime scope .由于几个原因,这些混合解决方案没有内置扩展。首先,它是一个非常奇特的功能,没有多少人需要。其次,你可以将任意两种或三种生活方式混合在一起,这样就几乎是无穷无尽的混合体。最后,自行注册非常(非常)容易。

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

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

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

container.Register<IUnitOfWork, DiUnitOfWork>(hybridLifestyle);

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

If we had a problem within the command-handler/service-layer and didn't want to save the UnitOfWork, would we simply throw an exception?

典型的做法是抛出异常。这实际上是异常(exception)的一般规则:

If your method can't do what it's name promises it can, throw. ->

命令处理程序应该不知道它是如何执行的上下文,你最不想做的就是区分它是否应该抛出异常。所以 throw 是你最好的选择。然而,当在后台线程上运行时,您最好捕获该异常,因为如果您不捕获它,.NET 将杀死整个 AppDomain。在 Web 应用程序中,这意味着 AppDomain 回收,这意味着您的 Web 应用程序(或至少该服务器)将在短时间内脱机。

另一方面,您也不希望丢失任何异常信息,因此您应该记录该异常,并且可能想要记录带有该异常的该命令的序列化表示,以便您可以查看传递了哪些数据in. 当添加到 ThreadedCommandHandlerProxy.Handle 时方法,它看起来像这样:

public void Handle(TCommand command)
{
    string xml = this.commandSerializer.ToXml(command);    

    Task.Factory.StartNew(() =>
    {
        var logger = 
            this.container.GetInstance<ILogger>();

        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<T> 的问题(它序列化命令并将其发送到队列)并改变组合根中的连接方式,你就可以开始了(当然不要忘记实现从队列中执行命令的服务) .换句话说,这种 SOLID 设计的优点在于,实现这些功能对于应用程序的大小是不变的。

关于c# - 如何配置 Simple Injector 以在 ASP.NET MVC 中运行后台线程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11041601/

相关文章:

c# - 内置类型的依赖注入(inject)

c# - 如何使用 asp.net mvc 和单元测试组织代码和测试

c# - 通过索引/键引用时列表与字典

c# - 如何使用 Task.Run(Action<T>)

asp.net-mvc - Telerik MVC 网格未正确分组

c# - 在 ResourceDictionary 中添加 .cs?

c# - 可空对象必须在 Select Linq 中具有值

c# - 自引用构造函数。这个图案有味道吗?

c# - .NET 解决方案 - 许多项目与一个项目

c# - RESTful API 中的向后兼容性