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

g18*_*18c 8 .net c# dependency-injection asp.net-mvc-3 simple-injector

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

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

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

我现在需要运行一些后台线程并从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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
        });
    }
} 
Run Code Online (Sandbox Code Playgroud)

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

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

有了上面我的问题开始形成,正如我从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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

所以总结以上问题:

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

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

如果我们在命令处理程序/服务层内遇到问题并且不想保存UnitOfWork,我们只是抛出异常吗?如果是的话,是有可能抓住这个在全球层面,还是我们需要捕捉每个请求的例外,从内try- catch在处理程序装饰或代理?

谢谢,

克里斯

Ste*_*ven 7

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

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

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

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

var hybridLifestyle = Lifestyle.CreateHybrid(
    () => HttpContext.Current != null,
    new WebRequestLifestyle(),
    new LifetimeScopeLifestyle());
Run Code Online (Sandbox Code Playgroud)

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

container.Register<IUnitOfWork, DiUnitOfWork>(hybridLifestyle);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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


Ste*_*ven 5

使用方便的命令处理程序装饰器可以克服在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) { }
    }
}
Run Code Online (Sandbox Code Playgroud)

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