如何配置 Simple Injector 以在 ASP.NET MVC 中运行后台线程 [英] How to configure Simple Injector to run background threads in ASP.NET MVC

查看:10
本文介绍了如何配置 Simple Injector 以在 ASP.NET MVC 中运行后台线程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am using Simple Injector to manage the lifetime of my injected dependencies (in this case UnitOfWork), and I am very happy as having a separate decorator rather than my service or command handler looking after saving and disposing makes code a lot easier when writing business logic layers (I follow the architecture that is outlined in this blog post).

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

The above is working perfectly (and very easily) by using the Simple Injector MVC NuGet package and the following code during the construction of the composition root container, if more than one dependency exists in the graph the same instance is injected across all - perfect for Entity Framework model context.

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

我现在需要运行一些后台线程并从 Simple Injector 中理解 有关线程的文档 可以按如下方式代理命令:

I now need to run some background threads and understand from Simple Injector documentation on threads that commands can be proxied as follows:

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:

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

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

However, from this threading sample documentation I can see factories are used, if I introduce factories to my commands and service layer things will get confused and inconsistent as I will have different saving methodologies for different services (one container handles saving, other instantiated factories within services handle saves and disposing) - you can see how clear and simple the service code skeleton is without any factories:

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

根据上述内容,我的问题开始形成,正如我从 ASP .NET PerWebRequest 文档中了解到的生命周期,使用如下代码:

With the above my problem starts to form, as I understand from the documentation on ASP .NET PerWebRequest lifetimes, the following code is used:

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 将不再存在于该线程中.

The above works fine for each HTTP request there will be a valid HttpContext.Current, however if I spin-up a new thread with the ThreadedCommandHandlerProxy it will create a new thread and the HttpContext will no longer exist within that thread.

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

Since the HttpContext would be null on each subsequent call, all instances of objects injected into service constructors would be new and unique, the opposite to normal HTTP per web request where objects are shared correctly as the same instance across all services.

所以把以上总结成问题:

So to summarize the above into questions:

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

How would I go about getting the objects constructed and common items injected regardless of whether created from HTTP request or via a new thread?

UnitOfWork 由命令处理程序代理中的线程管理是否有任何特殊考虑?如何确保在处理程序执行后将其保存和处理?

Are there any special considerations for having a UnitOfWork managed by a thread within a command handler proxy? How can one ensure it is saved and disposed of after the handler has executed?

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

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? If so, is it possible to catch this at a global level or do we need to catch the exception per request from within a try-catch in the handler decorator or proxy?

谢谢,

克里斯

推荐答案

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

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

With that out of the way, what you need is the following.

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

As you noted, since you are running (some) command handlers asynchronously, you can't use the per web request lifestyle on them. You will need a hybrid solution, that mixes between per web request and 'something else'. That something else will most likely be a per lifetime scope. There are no built-in extensions for these hybrid solutions, because of a couple reasons. First of all it's quite an exotic feature that not many people need. Second, you can mix any two or three lifestyles together, so that would be almost an endless combination of hybrids. And last, it is (pretty) easy do register this yourself.

在 Simple Injector 2 中,添加了 Lifestyle 类,它包含一个 CreateHybrid 方法,可以将任意两种生活方式结合起来创建新的生活方式.举个例子:

In Simple Injector 2, the Lifestyle class has been added and it contains a CreateHybrid method that allows combining any two lifestyles to create a new lifestyle. Here's an example:

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

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

You can use this hybrid lifestyle to register the unit of work:

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

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

Since you are registering the unit of work as Per Lifetime Scope, you must explicitly create and dispose a Lifetime Scope for a certain thread. The simplest thing to do is to add this to your ThreadedCommandHandlerProxy. This is not the most SOLID way of doing things, but it is the easiest way for me to show you how to do this.

如果我们在命令处理程序/服务层中遇到问题并且不想保存 UnitOfWork,我们会不会简单地抛出一个例外?

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?

典型的做法是抛出异常.这实际上是例外的一般规则:

The typical thing to do is to throw an exception. It's in fact the general rule of exceptions:

如果你的方法不能像它的名字所承诺的那样做,抛出.->

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

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

The command handler should be ignorant about how the context in which it is executed, and the last thing you want is to differentiate in whether it should throw an exception. So throwing is your best option. When running on a background thread however, you'd better catch that exception, since .NET will kill the whole AppDomain, if you don't catch it. In a web application this means a AppDomain recycle, which means you're web application (or at least that server) will be offline for a short period of time.

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

On the other hand, you also don't want to lose any exception information, so you should log that exception, and probably want to log a serialized representation of that command with that exception, so you can see what data was passed in. When added to the ThreadedCommandHandlerProxy.Handle method, the it would look like this:

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>(序列化命令并将其发送到队列)并更改组合根中事物的连接方式的问题,并且您很高兴go(当然不要忘记实现从队列中执行命令的服务).换句话说,这种 SOLID 设计的优点在于,这些功能的实现与应用程序的大小保持一致.

I warned about how running handlers asynchronously might not be the best idea. However, since you are applying this command / handler pattern, you will be able to switch to using queuing later on, without having to alter a single line of code in your application. It's just a matter of writing some sort of QueueCommandHandlerDecorator<T> (which serializes the command and sends it to the queue) and change the way things are wired in your composition root, and you're good to go (and of course don't forget to implement the service that executes commands from the queue). In other words, what's great about this SOLID design is that implementing these features is constant to the size of the application.

这篇关于如何配置 Simple Injector 以在 ASP.NET MVC 中运行后台线程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆