dan*_*wig 5 multithreading asynchronous dependency-injection entity-framework-4.1 simple-injector
使用简单的喷油器与下面介绍的命令模式和这里所描述的查询模式.对于其中一个命令,我有2个处理程序实现.第一个是同步执行的"正常"实现:
public class SendEmailMessageHandler
: IHandleCommands<SendEmailMessageCommand>
{
public SendEmailMessageHandler(IProcessQueries queryProcessor
, ISendMail mailSender
, ICommandEntities entities
, IUnitOfWork unitOfWork
, ILogExceptions exceptionLogger)
{
// save constructor args to private readonly fields
}
public void Handle(SendEmailMessageCommand command)
{
var emailMessageEntity = GetThisFromQueryProcessor(command);
var mailMessage = ConvertEntityToMailMessage(emailMessageEntity);
_mailSender.Send(mailMessage);
emailMessageEntity.SentOnUtc = DateTime.UtcNow;
_entities.Update(emailMessageEntity);
_unitOfWork.SaveChanges();
}
}
Run Code Online (Sandbox Code Playgroud)
另一个就像一个命令装饰器,但显式包装前一个类在一个单独的线程中执行命令:
public class SendAsyncEmailMessageHandler
: IHandleCommands<SendEmailMessageCommand>
{
public SendAsyncEmailMessageHandler(ISendMail mailSender,
ILogExceptions exceptionLogger)
{
// save constructor args to private readonly fields
}
public void Handle(SendEmailMessageCommand command)
{
var program = new SendAsyncEmailMessageProgram
(command, _mailSender, _exceptionLogger);
var thread = new Thread(program.Launch);
thread.Start();
}
private class SendAsyncEmailMessageProgram
{
internal SendAsyncEmailMessageProgram(
SendEmailMessageCommand command
, ISendMail mailSender
, ILogExceptions exceptionLogger)
{
// save constructor args to private readonly fields
}
internal void Launch()
{
// get new instances of DbContext and query processor
var uow = MyServiceLocator.Current.GetService<IUnitOfWork>();
var qp = MyServiceLocator.Current.GetService<IProcessQueries>();
var handler = new SendEmailMessageHandler(qp, _mailSender,
uow as ICommandEntities, uow, _exceptionLogger);
handler.Handle(_command);
}
}
}
Run Code Online (Sandbox Code Playgroud)
有一段时间,simpleinjector对我大吼大叫,告诉我它发现了2个实现IHandleCommands<SendEmailMessageCommand>.我发现以下工作,但不确定它是否是最佳/最佳方式.我想显式注册这个接口以使用Async实现:
container.RegisterManyForOpenGeneric(typeof(IHandleCommands<>),
(type, implementations) =>
{
// register the async email handler
if (type == typeof(IHandleCommands<SendEmailMessageCommand>))
container.Register(type, implementations
.Single(i => i == typeof(SendAsyncEmailMessageHandler)));
else if (implementations.Length < 1)
throw new InvalidOperationException(string.Format(
"No implementations were found for type '{0}'.",
type.Name));
else if (implementations.Length > 1)
throw new InvalidOperationException(string.Format(
"{1} implementations were found for type '{0}'.",
type.Name, implementations.Length));
// register a single implementation (default behavior)
else
container.Register(type, implementations.Single());
}, assemblies);
Run Code Online (Sandbox Code Playgroud)
我的问题:这是正确的方式,还是有更好的东西?例如,我想重用Simpleinjector抛出的所有其他实现的现有异常,而不是必须在回调中显式抛出它们.
更新回复史蒂文的答案
我已将我的问题更新为更明确.我以这种方式实现它的原因是因为作为操作的一部分,该命令在成功发送之后更新在db实体上System.Nullable<DateTime>调用的属性.SentOnUtcMailMessage
的ICommandEntities和IUnitOfWork都是由一个实体框架中实现DbContextclass.The DbContext每HTTP上下文注册,使用这里所描述的方法:
container.RegisterPerWebRequest<MyDbContext>();
container.Register<IUnitOfWork>(container.GetInstance<MyDbContext>);
container.Register<IQueryEntities>(container.GetInstance<MyDbContext>);
container.Register<ICommandEntities>(container.GetInstance<MyDbContext>);
Run Code Online (Sandbox Code Playgroud)
RegisterPerWebRequestsimpleinjector wiki中扩展方法的默认行为是在HttpContextnull为空时注册一个瞬态实例(它将在新启动的线程中).
var context = HttpContext.Current;
if (context == null)
{
// No HttpContext: Let's create a transient object.
return _instanceCreator();
...
Run Code Online (Sandbox Code Playgroud)
这就是Launch方法使用服务定位器模式获取单个实例的原因DbContext,然后将其直接传递给同步命令处理程序构造函数.为了使_entities.Update(emailMessageEntity)和_unitOfWork.SaveChanges()行工作,两者必须使用相同的DbContext实例.
注意:理想情况下,发送电子邮件应由单独的投票工作人员处理.这个命令基本上是一个队列清算所.数据库中的EmailMessage实体已具有发送电子邮件所需的所有信息.此命令只是从数据库中获取未发送的一个,发送它,然后记录该操作的DateTime.这样的命令可以通过从不同的进程/应用程序进行轮询来执行,但我不会接受这个问题的答案.现在,当某种http请求事件触发它时,我们需要启动此命令.
确实有更简单的方法可以做到这一点.例如BatchRegistrationCallback,您可以使用该OpenGenericBatchRegistrationExtensions.GetTypesToRegister方法,而不是像在上一个代码段中那样注册.此方法由方法在内部使用RegisterManyForOpenGeneric,并允许您在将返回的类型发送到RegisterManyForOpenGeneric重载之前对其进行过滤:
var types = OpenGenericBatchRegistrationExtensions
.GetTypesToRegister(typeof(IHandleCommands<>), assemblies)
.Where(t => !t.Name.StartsWith("SendAsync"));
container.RegisterManyForOpenGeneric(
typeof(IHandleCommands<>),
types);
Run Code Online (Sandbox Code Playgroud)
但我认为对您的设计进行一些更改会更好.当您将异步命令处理程序更改为通用装饰器时,您将完全删除该问题.这样的通用装饰器可能如下所示:
public class SendAsyncCommandHandlerDecorator<TCommand>
: IHandleCommands<TCommand>
{
private IHandleCommands<TCommand> decorated;
public SendAsyncCommandHandlerDecorator(
IHandleCommands<TCommand> decorated)
{
this.decorated = decorated;
}
public void Handle(TCommand command)
{
// WARNING: THIS CODE IS FLAWED!!
Task.Factory.StartNew(
() => this.decorated.Handle(command));
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,这个装饰器是有缺陷的,因为我稍后会解释的原因,但是为了教育,让我们继续这样做.
使此类型通用,允许您将此类型重用于多个命令.因为这种类型是通用的,所以RegisterManyForOpenGeneric会跳过这个(因为它不能猜测泛型类型).这允许您注册装饰器,如下所示:
container.RegisterDecorator(
typeof(IHandleCommands<>),
typeof(SendAsyncCommandHandler<>));
Run Code Online (Sandbox Code Playgroud)
但是,在你的情况下,你不希望这个装饰器被包裹在所有处理程序周围(就像之前的注册一样).有一个RegisterDecorator带谓词的重载,它允许你指定何时应用这个装饰器:
container.RegisterDecorator(
typeof(IHandleCommands<>),
typeof(SendAsyncCommandHandlerDecorator<>),
c => c.ServiceType == typeof(IHandleCommands<SendEmailMessageCommand>));
Run Code Online (Sandbox Code Playgroud)
应用此谓词后,SendAsyncCommandHandlerDecorator<T>将仅应用于IHandleCommands<SendEmailMessageCommand>处理程序.
另一个选项(我更喜欢)是注册该版本的封闭通用SendAsyncCommandHandlerDecorator<T>版本.这使您不必指定谓词:
container.RegisterDecorator(
typeof(IHandleCommands<>),
typeof(SendAsyncCommandHandler<SendEmailMessageCommand>));
Run Code Online (Sandbox Code Playgroud)
然而,正如我所指出的,给定装饰器的代码是有缺陷的,因为你应该总是在新线程上构建一个新的依赖图,并且永远不会将依赖从线程传递给线程(原始装饰器所做的).本文中有关此内容的更多信息:如何在多线程应用程序中使用依赖项注入.
所以答案实际上更复杂,因为这个通用装饰器应该是一个代替原始命令处理程序的代理(或者甚至可能是包装处理程序的装饰器链).此代理必须能够在新线程中构建新的对象图.这个代理看起来像这样:
public class SendAsyncCommandHandlerProxy<TCommand>
: IHandleCommands<TCommand>
{
Func<IHandleCommands<TCommand>> factory;
public SendAsyncCommandHandlerProxy(
Func<IHandleCommands<TCommand>> factory)
{
this.factory = factory;
}
public void Handle(TCommand command)
{
Task.Factory.StartNew(() =>
{
var handler = this.factory();
handler.Handle(command);
});
}
}
Run Code Online (Sandbox Code Playgroud)
虽然Simple Injector没有内置支持来解析Func<T>工厂,但这些RegisterDecorator方法是例外.这样做的原因是,在没有框架支持的情况下,使用Func依赖项注册装饰器会非常繁琐.换句话说,当SendAsyncCommandHandlerProxy使用RegisterDecorator方法注册时,Simple Injector将自动注入一个Func<T>可以创建装饰类型的新实例的委托.由于代理仅重新启用(单例)工厂(并且是无状态的),我们甚至可以将其注册为单例:
container.RegisterSingleDecorator(
typeof(IHandleCommands<>),
typeof(SendAsyncCommandHandlerProxy<SendEmailMessageCommand>));
Run Code Online (Sandbox Code Playgroud)
显然,您可以将此注册与其他RegisterDecorator注册混合使用.例:
container.RegisterManyForOpenGeneric(
typeof(IHandleCommands<>),
typeof(IHandleCommands<>).Assembly);
container.RegisterDecorator(
typeof(IHandleCommands<>),
typeof(TransactionalCommandHandlerDecorator<>));
container.RegisterSingleDecorator(
typeof(IHandleCommands<>),
typeof(SendAsyncCommandHandlerProxy<SendEmailMessageCommand>));
container.RegisterDecorator(
typeof(IHandleCommands<>),
typeof(ValidatableCommandHandlerDecorator<>));
Run Code Online (Sandbox Code Playgroud)
这个注册用任何命令处理程序包装TransactionalCommandHandlerDecorator<T>,可选地用异步代理装饰它,并且总是用一个包装它ValidatableCommandHandlerDecorator<T>.这允许您同步地执行验证(在同一个线程上),并且当验证成功时,在新线程上旋转处理命令,在该线程上的事务中运行.
由于某些依赖项是按Web请求注册的,这意味着当没有Web请求时,它们将获得一个新的(瞬态)实例异常,这就是它在Simple Injector中实现的方式(就像这样)当你启动一个新线程来运行代码时).当您使用EF实现多个接口时DbContext,这意味着Simple Injector将为每个构造函数注入的接口创建一个新实例,正如您所说,这将是一个问题.
您需要重新配置DbContext,因为纯Per Web请求不会.有几种解决方案,但我认为最好是制作混合PerWebRequest/PerLifetimeScope实例.您需要Per Lifetime Scope扩展包.另请注意,它也是Per Web Request的扩展包,因此您不必使用任何自定义代码.完成此操作后,您可以定义以下注册:
container.RegisterPerWebRequest<DbContext, MyDbContext>();
container.RegisterPerLifetimeScope<IObjectContextAdapter,
MyDbContext>();
// Register as hybrid PerWebRequest / PerLifetimeScope.
container.Register<MyDbContext>(() =>
{
if (HttpContext.Current != null)
return (MyDbContext)container.GetInstance<DbContext>();
else
return (MyDbContext)container
.GetInstance<IObjectContextAdapter>();
});
Run Code Online (Sandbox Code Playgroud)
更新 简单注射器2现在具有生活方式的明确概念,这使得先前的注册更容易.因此,建议以下注册:
var hybrid = Lifestyle.CreateHybrid(
lifestyleSelector: () => HttpContext.Current != null,
trueLifestyle: new WebRequestLifestyle(),
falseLifestyle: new LifetimeScopeLifestyle());
// Register as hybrid PerWebRequest / PerLifetimeScope.
container.Register<MyDbContext, MyDbContext>(hybrid);
Run Code Online (Sandbox Code Playgroud)
由于Simple Injector只允许注册一次类型(它不支持键控注册),因此无法使用PerWebRequest生活方式和PerLifetimeScope生活方式注册MyDbContext.所以我们不得不作弊,所以我们进行两次注册(每种生活方式一次)并选择不同的服务类型(DbContext和IObjectContextAdapter).服务类型并不重要,除了MyDbContext必须从该服务类型实现/继承(MyDbContext如果方便的话,随意在您的实现虚拟接口).
除了这两个注册,我们需要第三次注册,一个映射,让我们能够恢复正常的生活方式.这是Register<MyDbContext>根据操作是否在HTTP请求中执行而获得正确实例的.
您AsyncCommandHandlerProxy必须开始新的生命周期范围,其操作如下:
public class AsyncCommandHandlerProxy<T>
: IHandleCommands<T>
{
private readonly Func<IHandleCommands<T>> factory;
private readonly Container container;
public AsyncCommandHandlerProxy(
Func<IHandleCommands<T>> factory,
Container container)
{
this.factory = factory;
this.container = container;
}
public void Handle(T command)
{
Task.Factory.StartNew(() =>
{
using (this.container.BeginLifetimeScope())
{
var handler = this.factory();
handler.Handle(command);
}
});
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,容器是作为依赖项添加的AsyncCommandHandlerProxy.
现在,任何MyDbContext在HttpContext.Currentnull 时解析的实例都将获得Per Lifetime Scope实例而不是新的瞬态实例.
| 归档时间: |
|
| 查看次数: |
2202 次 |
| 最近记录: |