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在处理程序装饰或代理?
谢谢,
克里斯
让我首先警告,如果您希望在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设计的优点是实现这些功能与应用程序的大小不变.
使用方便的命令处理程序装饰器可以克服在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未卸载.