简体   繁体   English

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

[英]How to configure Simple Injector to run background threads in ASP.NET MVC

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

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

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

I now need to run some background threads and understand from Simple Injector documentation on threads that commands can be proxied as follows: 我现在需要运行一些后台线程并从Simple Injector 文档中了解可以代理命令的线程 ,如下所示:

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

With the above my problem starts to form, as I understand from the documentation on ASP .NET PerWebRequest lifetimes , the following code is used: 有了上面我的问题开始形成,正如我从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;
}

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

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

So to summarize the above into questions: 所以总结以上问题:

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

Are there any special considerations for having a UnitOfWork managed by a thread within a command handler proxy? 在命令处理程序代理中由一个线程管理UnitOfWork是否有任何特殊注意事项? How can one ensure it is saved and disposed of after the handler has executed? 如何确保在处理程序执行后保存并处理它?

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

Thanks, 谢谢,

Chris 克里斯

Let me start of by warning that if you wish to execute commands asynchronously in a web application, you might want to take a step back and look at what you are trying to achieve. 让我首先警告,如果您希望在Web应用程序中异步执行命令,您可能需要退后一步,看看您要实现的目标。 There is always the risk of the web application being recycled just after you started your handler on a background thread. 在后台线程上启动处理程序之后,Web应用程序总是存在被回收的风险。 When a ASP.NET app gets recycled, all background threads will be aborted. 当ASP.NET应用程序被回收时,所有后台线程都将被中止。 It would perhaps be better to publish commands to a (transactional) queue and let a background service pick them up. 将命令发布到(事务性)队列并让后台服务选择它们可能会更好。 This ensures that commands can't 'get lost'. 这可以确保命令不会“丢失”。 And also allows you to re-execute commands when the handler does not succeed successfully. 并且还允许您在处理程序未成功成功时重新执行命令。 It can also save you from having to do some nasty registrations (which you will probably have no matter which DI framework you pick), but this probably just a side issue. 它还可以让您免于进行一些令人讨厌的注册(无论您选择哪种DI框架,您都可能拥有它),但这可能只是一个侧面问题。 And if you do need to run handlers async, at least try to minimize the number of handlers that you run async. 如果您确实需要运行处理程序异步,至少尝试最小化您运行异步的处理程序的数量。

With that out of the way, what you need is the following. 除此之外,您需要的是以下内容。

As you noted, since you are running (some) command handlers asynchronously, you can't use the per web request lifestyle on them. 正如您所指出的,由于您正在异步运行(某些)命令处理程序,因此您无法使用每个Web请求生活方式。 You will need a hybrid solution, that mixes between per web request and 'something else'. 您将需要一种混合解决方案,它可以在每个Web请求和“其他内容”之间进行混合。 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. 最后,(非常)容易自己注册。

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. 在Simple Injector 2中,添加了Lifestyle类,它包含一个CreateHybrid方法,允许组合任何两种生活方式来创建新的生活方式。 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);

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. 由于您将工作单元注册为Per Lifetime Scope,因此必须为某个线程显式创建和处置Lifetime Scope。 The simplest thing to do is to add this to your ThreadedCommandHandlerProxy . 最简单的方法是将它添加到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. 这还不是最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? 如果我们在命令处理程序/服务层中遇到问题并且不想保存UnitOfWork,那么我们只是抛出异常吗?

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. 如果你的方法不能做它的名字承诺它可以,抛出。 -> - >

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. 然而,当在后台线程上运行时,你最好捕获该异常,因为如果你没有捕获它,.NET将终止整个AppDomain。 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. 在Web应用程序中,这意味着AppDomain回收,这意味着您的Web应用程序(或至少该服务器)将在短时间内脱机。

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: 另一方面,您也不希望丢失任何异常信息,因此您应该记录该异常,并且可能希望使用该异常记录该命令的序列化表示,以便您可以看到传入了哪些数据。添加到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);
        }
    });
}

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). 这只是编写某种QueueCommandHandlerDecorator<T> (它将命令序列化并将其发送到队列)并更改组合根中的连接方式的问题,而且你很高兴(当然还有)忘记实现从队列中执行命令的服务。 In other words, what's great about this SOLID design is that implementing these features is constant to the size of the application. 换句话说,这个SOLID设计的优点是实现这些功能与应用程序的大小不变。

Part of the problems of running background threads in ASP.NET can be overcome with a convenient command handler decorator: 使用方便的命令处理程序装饰器可以克服在ASP.NET中运行后台线程的部分问题

public class AspNetSafeBackgroundCommandHandlerDecorator<T>
    : ICommandHandler<T>, IRegisteredObject
{
    private readonly ICommandHandler<T> decorated;

    private readonly object locker = new object();

    public AspNetSafeBackgroundCommandHandlerDecorator(
        ICommandHandler<T> 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) { }
    }
}

When you put this decorator between a command handler and the ThreadedCommandHandlerProxy , you'll ensure that (under normal conditions) AppDomain isn't unloaded while such command is running. 当您将此装饰器放在命令处理程序和ThreadedCommandHandlerProxy ,您将确保在此类命令运行时(在正常情况下)AppDomain未卸载。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM