使用简单注入器在 C# 中实现域事件处理程序模式
Implementing Domain Event Handler pattern in C# with Simple Injector
我正在尝试使用 Simple Injector 在 C# 中实现域事件模式。
我已将我的代码简化为一个文件,该文件可以 运行 作为控制台应用程序,并已排除简单注入器代码以明确此问题的目的。
我遇到的问题是每个事件都可以有多个事件处理程序并且可以引发多个事件但是我想限制我的 Dispatcher 只处理实现 IEvent
接口的事件所以我把对我的 Dispatch 方法的限制。
这导致了关于如何从 Simple Injector 获取实例的问题,因为每次调用 Dispatch
方法时 TEvent
的类型为 IEvent
(如我所料)但是我需要获取传入的事件类型,以便从 Simple Injector 获取相关的处理程序。
希望我的代码能更好地解释这一点:
interface IEvent
{
}
interface IEventHandler<T> where T : IEvent
{
void Handle(T @event);
}
class StandardEvent : IEvent
{
}
class AnotherEvent : IEvent
{
}
class StandardEventHandler : IEventHandler<StandardEvent>
{
public void Handle(StandardEvent @event)
{
Console.WriteLine("StandardEvent handled");
}
}
class AnotherEventHandler : IEventHandler<AnotherEvent>
{
public void Handle(AnotherEvent @event)
{
Console.WriteLine("AnotherEvent handled");
}
}
这是我的调度员:
static class Dispatcher
{
// I need to get the type of @event here so I can get the registered instance from the
// IoC container (SimpleInjector), however TEvent is of type IEvent (as expected).
// What I need to do here is Get the registered instance from Simple Injector for each
// Event Type i.e. Container.GetAllInstances<IEventHandler<StandardEvent>>()
// and Container.GetAllInstances<IEventHandler<AnotherEvent>>()
public static void Dispatch<TEvent>(TEvent @event) where TEvent : IEvent
{
}
}
class PlainOldObject
{
public ICollection<IEvent> Events = new List<IEvent>
{
new StandardEvent(),
new AnotherEvent()
};
}
class StandAlone
{
static void Main(string[] args)
{
var poco = new PlainOldObject();
foreach (var @event in poco.Events)
{
Dispatcher.Dispatch(@event);
}
}
}
我已经在 Dispatch 方法中评论了我的问题所在。有人知道我应该如何解决这个问题吗?
此致,
加里
您需要的解决方案有点取决于 Dispatcher
的消费者调用事件的方式。如果消费者在编译时总是知道事件的确切类型,则可以使用上面显示的通用 Dispatch<TEvent>(TEvent)
方法。在那种情况下,Dispatcher
的实施将非常简单。
如果另一方面,消费者可能并不总是知道确切的类型,而只是简单地使用 IEvent
接口,那么 Dispatch<TEvent>(TEvent)
中的泛型类型参数就变得无用了,你会更好关闭定义 Dispatch(IEvent)
方法。这使得实现更加复杂,因为您将需要使用反射来解决这个问题。
另请注意,最好引入 IEventDispatcher
抽象。不要从代码中调用静态 class。甚至 Udi Dahan(很久以前最初 described such static class)现在也认为这是一种反模式。相反,将 IEventDispatcher
抽象注入需要事件调度的 classes。
如果所有消费者都使用编译时已知的事件类型,您的实现将如下所示:
public interface IEventDispatcher
{
void Dispatch<TEvent>(TEvent @event) where TEvent : IEvent;
}
private sealed class Dispatcher : IEventDispatcher
{
private readonly Container container;
public Dispatcher(Container container) {
this.container = container;
}
public void Dispatch<TEvent>(TEvent @event) where TEvent : IEvent {
if (@event == null) throw new ArgumentNullException("event");
var handlers = this.container.GetAllInstances<IEventHandler<TEvent>>();
foreach (var handler in handlers) {
handler.Handle(@event);
}
}
}
如果另一方面,事件类型未知,您可以使用以下代码:
public interface IEventDispatcher
{
void Dispatch(IEvent @event);
}
private sealed class Dispatcher : IEventDispatcher
{
private readonly Container container;
public Dispatcher(Container container) {
this.container = container;
}
public void Dispatch(IEvent @event) {
if (@event == null) throw new ArgumentNullException("event");
Type handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType());
var handlers = this.container.GetAllInstances(handlerType);
foreach (dynamic handler in handlers) {
handler.Handle((dynamic)@event);
}
}
}
请注意,与使用 .NET 反射相比,使用动态关键字有一些不明显的优势 API。例如,当使用 dynamic 调用处理程序的 Handle
方法时,从句柄抛出的任何异常都会直接冒泡。另一方面,当使用 MethodInfo.Invoke
时,异常将被包装在一个新的异常中。这使得捕获和调试更加困难。
您的事件处理程序可以按如下方式注册:
container.RegisterCollection(typeof(IEventHandler<>), listOfAssembliesToSearch);
要使用 SimpleInjector 并动态注入域事件,您可以执行以下操作:
在 SI 注册中
_container.Register(typeof(IDomainEventHandler<>), new[] { typeof(IDomainEventHandler<>).Assembly});
然后创建一个活动
public class PolicyAddressChangedEvent : IDomainEvent
{
public Address NewAddress { get; }
public Address OriginalAddress { get; }
public PolicyAddressChangedEvent(Address oldBillingAddress, Address newbillingAddress)
{
OriginalAddress = oldBillingAddress;
NewAddress = newbillingAddress;
}
}
然后为事件创建处理程序
public class PolicyAddressChangeHandler : IDomainEventHandler<PolicyAddressChangedEvent>
{
private readonly ILoggingService _loggingService;
public PolicyAddressChangeHandler(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public void Handle(PolicyAddressChangedEvent domainEvent)
{
_loggingService.Info("New policy address recorded", new Dictionary<string, object> { { "new address", domainEvent.NewAddress } }, "FrameworkSample");
//this could be event hub, queues, or signalR messages, updating a data warehouse, sending emails, or even updating other domain contexts
}
}
现在,要在使用简单注入器创建 IDomainEventDistpatcher 时注入正确的注入器,您可以使用工厂注入器。这是获取所有类型并能够动态查找它们的关键。通过这样做,我们将 Func 注入到 DomainEventDispatcher 中。
_container.RegisterSingleton<IDomainEventDispatcher>(() =>
{
return new DomainEventDispatcher(type => _container.GetInstance(type));
});
现在在 DomainEventDispatcher 中我们有
public class DomainEventDispatcher : IDomainEventDispatcher
{
private readonly Func<Type, object> _handlerLookup;
public DomainEventDispatcher(Func<Type, object> handlerLookup)
{
_handlerLookup = handlerLookup;
}
public void Dispatch(IDomainEvent domainEvent)
{
Type handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
var handler = GetHandler(handlerType);
if (handler != null)
{
handler.Handle((dynamic)domainEvent);
}
}
private dynamic GetHandler(Type filterType)
{
try
{
object handler = _handlerLookup.Invoke(filterType);
return handler;
}
catch (Exception)
{
return null;
}
}
}
这现在获取 IDomainEvent 并创建正确的类型并根据提供的 Func 进行查找。
这样更好,因为现在我们不强制依赖 class 来了解我们正在使用的 DI 实现。与上面史蒂文的回答非常相似(有一些小周),只是想也能提供一个完整的例子。
这是使用缓存委托的更快版本。第一次调用后没有动态也没有反射。使用默认 Microsoft DI 中的 IServiceProvider
,但可以轻松更改。也可以使用表达式树,但它们会占用更多内存:
public class EventDispatcherService : IEventDispatcher
{
private static readonly ConcurrentDictionary<Type, IEnumerable<Func<object, Task>>> HandlersCache
= new ConcurrentDictionary<Type, IEnumerable<Func<object, Task>>>();
private static readonly Type HandlerType = typeof(IEventHandler<>);
private static readonly MethodInfo MakeDelegateMethod = typeof(EventDispatcherService)
.GetMethod(nameof(MakeDelegate), BindingFlags.Static | BindingFlags.NonPublic);
private static readonly Type OpenGenericFuncType = typeof(Func<,>);
private static readonly Type TaskType = typeof(Task);
private readonly IServiceProvider serviceProvider;
public EventDispatcherService(IServiceProvider serviceProvider)
=> this.serviceProvider = serviceProvider;
public async Task Dispatch(IDomainEvent domainEvent)
{
var eventHandlers = HandlersCache.GetOrAdd(domainEvent.GetType(), eventType =>
{
var eventHandlerType = HandlerType.MakeGenericType(eventType);
var makeDelegate = MakeDelegateMethod.MakeGenericMethod(eventType);
var funcType = OpenGenericFuncType.MakeGenericType(eventType, TaskType);
return this.serviceProvider
.GetServices(eventHandlerType)
.Select(handler => handler
.GetType()
.GetMethod("Handle")
.CreateDelegate(funcType, handler))
.Select(handlerDelegateConcrete => (Func<object, Task>)makeDelegate
.Invoke(null, new object[] { handlerDelegateConcrete }))
.ToList();
});
foreach (var eventHandler in eventHandlers)
{
await eventHandler(domainEvent);
}
}
private static Func<object, Task> MakeDelegate<T>(Func<T, Task> action)
=> value => action((T)value);
}
这是事件处理程序接口:
public interface IEventHandler<in TEvent>
where TEvent : IDomainEvent
{
Task Handle(TEvent domainEvent);
}
我正在尝试使用 Simple Injector 在 C# 中实现域事件模式。
我已将我的代码简化为一个文件,该文件可以 运行 作为控制台应用程序,并已排除简单注入器代码以明确此问题的目的。
我遇到的问题是每个事件都可以有多个事件处理程序并且可以引发多个事件但是我想限制我的 Dispatcher 只处理实现 IEvent
接口的事件所以我把对我的 Dispatch 方法的限制。
这导致了关于如何从 Simple Injector 获取实例的问题,因为每次调用 Dispatch
方法时 TEvent
的类型为 IEvent
(如我所料)但是我需要获取传入的事件类型,以便从 Simple Injector 获取相关的处理程序。
希望我的代码能更好地解释这一点:
interface IEvent
{
}
interface IEventHandler<T> where T : IEvent
{
void Handle(T @event);
}
class StandardEvent : IEvent
{
}
class AnotherEvent : IEvent
{
}
class StandardEventHandler : IEventHandler<StandardEvent>
{
public void Handle(StandardEvent @event)
{
Console.WriteLine("StandardEvent handled");
}
}
class AnotherEventHandler : IEventHandler<AnotherEvent>
{
public void Handle(AnotherEvent @event)
{
Console.WriteLine("AnotherEvent handled");
}
}
这是我的调度员:
static class Dispatcher
{
// I need to get the type of @event here so I can get the registered instance from the
// IoC container (SimpleInjector), however TEvent is of type IEvent (as expected).
// What I need to do here is Get the registered instance from Simple Injector for each
// Event Type i.e. Container.GetAllInstances<IEventHandler<StandardEvent>>()
// and Container.GetAllInstances<IEventHandler<AnotherEvent>>()
public static void Dispatch<TEvent>(TEvent @event) where TEvent : IEvent
{
}
}
class PlainOldObject
{
public ICollection<IEvent> Events = new List<IEvent>
{
new StandardEvent(),
new AnotherEvent()
};
}
class StandAlone
{
static void Main(string[] args)
{
var poco = new PlainOldObject();
foreach (var @event in poco.Events)
{
Dispatcher.Dispatch(@event);
}
}
}
我已经在 Dispatch 方法中评论了我的问题所在。有人知道我应该如何解决这个问题吗?
此致, 加里
您需要的解决方案有点取决于 Dispatcher
的消费者调用事件的方式。如果消费者在编译时总是知道事件的确切类型,则可以使用上面显示的通用 Dispatch<TEvent>(TEvent)
方法。在那种情况下,Dispatcher
的实施将非常简单。
如果另一方面,消费者可能并不总是知道确切的类型,而只是简单地使用 IEvent
接口,那么 Dispatch<TEvent>(TEvent)
中的泛型类型参数就变得无用了,你会更好关闭定义 Dispatch(IEvent)
方法。这使得实现更加复杂,因为您将需要使用反射来解决这个问题。
另请注意,最好引入 IEventDispatcher
抽象。不要从代码中调用静态 class。甚至 Udi Dahan(很久以前最初 described such static class)现在也认为这是一种反模式。相反,将 IEventDispatcher
抽象注入需要事件调度的 classes。
如果所有消费者都使用编译时已知的事件类型,您的实现将如下所示:
public interface IEventDispatcher
{
void Dispatch<TEvent>(TEvent @event) where TEvent : IEvent;
}
private sealed class Dispatcher : IEventDispatcher
{
private readonly Container container;
public Dispatcher(Container container) {
this.container = container;
}
public void Dispatch<TEvent>(TEvent @event) where TEvent : IEvent {
if (@event == null) throw new ArgumentNullException("event");
var handlers = this.container.GetAllInstances<IEventHandler<TEvent>>();
foreach (var handler in handlers) {
handler.Handle(@event);
}
}
}
如果另一方面,事件类型未知,您可以使用以下代码:
public interface IEventDispatcher
{
void Dispatch(IEvent @event);
}
private sealed class Dispatcher : IEventDispatcher
{
private readonly Container container;
public Dispatcher(Container container) {
this.container = container;
}
public void Dispatch(IEvent @event) {
if (@event == null) throw new ArgumentNullException("event");
Type handlerType = typeof(IEventHandler<>).MakeGenericType(@event.GetType());
var handlers = this.container.GetAllInstances(handlerType);
foreach (dynamic handler in handlers) {
handler.Handle((dynamic)@event);
}
}
}
请注意,与使用 .NET 反射相比,使用动态关键字有一些不明显的优势 API。例如,当使用 dynamic 调用处理程序的 Handle
方法时,从句柄抛出的任何异常都会直接冒泡。另一方面,当使用 MethodInfo.Invoke
时,异常将被包装在一个新的异常中。这使得捕获和调试更加困难。
您的事件处理程序可以按如下方式注册:
container.RegisterCollection(typeof(IEventHandler<>), listOfAssembliesToSearch);
要使用 SimpleInjector 并动态注入域事件,您可以执行以下操作:
在 SI 注册中
_container.Register(typeof(IDomainEventHandler<>), new[] { typeof(IDomainEventHandler<>).Assembly});
然后创建一个活动
public class PolicyAddressChangedEvent : IDomainEvent
{
public Address NewAddress { get; }
public Address OriginalAddress { get; }
public PolicyAddressChangedEvent(Address oldBillingAddress, Address newbillingAddress)
{
OriginalAddress = oldBillingAddress;
NewAddress = newbillingAddress;
}
}
然后为事件创建处理程序
public class PolicyAddressChangeHandler : IDomainEventHandler<PolicyAddressChangedEvent>
{
private readonly ILoggingService _loggingService;
public PolicyAddressChangeHandler(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public void Handle(PolicyAddressChangedEvent domainEvent)
{
_loggingService.Info("New policy address recorded", new Dictionary<string, object> { { "new address", domainEvent.NewAddress } }, "FrameworkSample");
//this could be event hub, queues, or signalR messages, updating a data warehouse, sending emails, or even updating other domain contexts
}
}
现在,要在使用简单注入器创建 IDomainEventDistpatcher 时注入正确的注入器,您可以使用工厂注入器。这是获取所有类型并能够动态查找它们的关键。通过这样做,我们将 Func 注入到 DomainEventDispatcher 中。
_container.RegisterSingleton<IDomainEventDispatcher>(() =>
{
return new DomainEventDispatcher(type => _container.GetInstance(type));
});
现在在 DomainEventDispatcher 中我们有
public class DomainEventDispatcher : IDomainEventDispatcher
{
private readonly Func<Type, object> _handlerLookup;
public DomainEventDispatcher(Func<Type, object> handlerLookup)
{
_handlerLookup = handlerLookup;
}
public void Dispatch(IDomainEvent domainEvent)
{
Type handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
var handler = GetHandler(handlerType);
if (handler != null)
{
handler.Handle((dynamic)domainEvent);
}
}
private dynamic GetHandler(Type filterType)
{
try
{
object handler = _handlerLookup.Invoke(filterType);
return handler;
}
catch (Exception)
{
return null;
}
}
}
这现在获取 IDomainEvent 并创建正确的类型并根据提供的 Func 进行查找。
这样更好,因为现在我们不强制依赖 class 来了解我们正在使用的 DI 实现。与上面史蒂文的回答非常相似(有一些小周),只是想也能提供一个完整的例子。
这是使用缓存委托的更快版本。第一次调用后没有动态也没有反射。使用默认 Microsoft DI 中的 IServiceProvider
,但可以轻松更改。也可以使用表达式树,但它们会占用更多内存:
public class EventDispatcherService : IEventDispatcher
{
private static readonly ConcurrentDictionary<Type, IEnumerable<Func<object, Task>>> HandlersCache
= new ConcurrentDictionary<Type, IEnumerable<Func<object, Task>>>();
private static readonly Type HandlerType = typeof(IEventHandler<>);
private static readonly MethodInfo MakeDelegateMethod = typeof(EventDispatcherService)
.GetMethod(nameof(MakeDelegate), BindingFlags.Static | BindingFlags.NonPublic);
private static readonly Type OpenGenericFuncType = typeof(Func<,>);
private static readonly Type TaskType = typeof(Task);
private readonly IServiceProvider serviceProvider;
public EventDispatcherService(IServiceProvider serviceProvider)
=> this.serviceProvider = serviceProvider;
public async Task Dispatch(IDomainEvent domainEvent)
{
var eventHandlers = HandlersCache.GetOrAdd(domainEvent.GetType(), eventType =>
{
var eventHandlerType = HandlerType.MakeGenericType(eventType);
var makeDelegate = MakeDelegateMethod.MakeGenericMethod(eventType);
var funcType = OpenGenericFuncType.MakeGenericType(eventType, TaskType);
return this.serviceProvider
.GetServices(eventHandlerType)
.Select(handler => handler
.GetType()
.GetMethod("Handle")
.CreateDelegate(funcType, handler))
.Select(handlerDelegateConcrete => (Func<object, Task>)makeDelegate
.Invoke(null, new object[] { handlerDelegateConcrete }))
.ToList();
});
foreach (var eventHandler in eventHandlers)
{
await eventHandler(domainEvent);
}
}
private static Func<object, Task> MakeDelegate<T>(Func<T, Task> action)
=> value => action((T)value);
}
这是事件处理程序接口:
public interface IEventHandler<in TEvent>
where TEvent : IDomainEvent
{
Task Handle(TEvent domainEvent);
}