8

我正在使用 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>();
}

我现在需要运行一些后台线程,并从关于线程的 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:

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

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

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生命周期的文档中了解到的那样,使用了以下代码:

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

因此,将以上内容总结为问题:

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

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

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

谢谢,

克里斯

4

2 回答 2

7

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

有了这个,你需要的是以下内容。

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

在 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. 这不是最可靠的做事方式,但它是我向您展示如何做到这一点的最简单方法。

如果我们在命令处理程序/服务层中遇到问题并且不想保存 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);
        }
    });
}

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

于 2012-06-15T23:45:03.777 回答
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) { }
    }
}

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

于 2012-08-10T10:26:03.863 回答