使用dynamic
实现双重调度:
public interface IDomainEvent {}
public class DomainEventDispatcher
{
private readonly List<Delegate> subscribers = new List<Delegate>();
public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : IDomainEvent
{
subscribers.Add(subscriber);
}
public void Publish<TEvent>(TEvent domainEvent) where TEvent : IDomainEvent
{
foreach (Action<TEvent> subscriber in subscribers.OfType<Action<TEvent>>())
{
subscriber(domainEvent);
}
}
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (IDomainEvent domainEvent in domainEvents)
{
// Force double dispatch - bind to runtime type.
Publish(domainEvent as dynamic);
}
}
}
public class ProcessCompleted : IDomainEvent { public string Name { get; set; } }
在大多数情况下都适用:
var dispatcher = new DomainEventDispatcher();
dispatcher.Subscribe((ProcessCompleted e) => Console.WriteLine("Completed " + e.Name));
dispatcher.PublishQueue(new [] { new ProcessCompleted { Name = "one" },
new ProcessCompleted { Name = "two" } });
完成一个
完成了两次
但是如果子类对调度代码不可见,则会导致运行时错误:
public static class Bomb
{
public static void Subscribe(DomainEventDispatcher dispatcher)
{
dispatcher.Subscribe((Exploded e) => Console.WriteLine("Bomb exploded"));
}
public static IDomainEvent GetEvent()
{
return new Exploded();
}
private class Exploded : IDomainEvent {}
}
// ...
Bomb.Subscribe(dispatcher); // no error here
// elsewhere, much later...
dispatcher.PublishQueue(new [] { Bomb.GetEvent() }); // exception
RuntimeBinderException
类型'object'不能用作泛型类型或方法'DomainEventDispatcher.Publish(TEvent)'中的类型参数'TEvent'
这是一个人为的例子;一个更现实的事件将是另一个集会内部的事件。
如何防止此运行时异常?如果这不可行,我如何在Subscribe
方法中检测到这种情况并快速失败?
编辑:消除动态广播的解决方案是可以接受的,只要它们不需要知道所有子类的访问者风格的类。
答案 0 :(得分:2)
如何防止此运行时异常?
你真的不能,这是dynamic
的本质。
如果这不可行,我如何在
Subscribe
方法中检测到这种情况并快速失败?
您可以在添加订阅者之前检查typeof(TEvent).IsPublic
。
那就是说,我不确定你是否真的需要dynamic
进行双重调度。如果subscribers
是Dictionary<Type, List<Action<IDomainEvent>>>
并且您根据Publish(IDomainEvent domainEvent)
在domainEvent.GetType()
中查找了订阅者,该怎么办?
答案 1 :(得分:1)
我没有试图找出动态调用失败的原因,而是集中精力提供一个可行的解决方案,因为我理解合同的方式,你有一个有效的订阅者,因此你应该能够调用它。
幸运的是,有一些非动态的基于呼叫的解决方案。
通过反思调用Publish
方法:
private static readonly MethodInfo PublishMethod = typeof(DomainEventDispatcher).GetMethod("Publish"); // .GetMethods().Single(m => m.Name == "Publish" && m.IsGenericMethodDefinition);
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var publish = PublishMethod.MakeGenericMethod(domainEvent.GetType());
publish.Invoke(this, new[] { domainEvent });
}
}
通过反思调用subscriber
:
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var eventType = typeof(Action<>).MakeGenericType(domainEvent.GetType());
foreach (var subscriber in subscribers)
{
if (eventType.IsAssignableFrom(subscriber.GetType()))
subscriber.DynamicInvoke(domainEvent);
}
}
}
通过预编译的缓存委托调用Publish
方法:
private static Action<DomainEventDispatcher, IDomainEvent> CreatePublishFunc(Type eventType)
{
var dispatcher = Expression.Parameter(typeof(DomainEventDispatcher), "dispatcher");
var domainEvent = Expression.Parameter(typeof(IDomainEvent), "domainEvent");
var call = Expression.Lambda<Action<DomainEventDispatcher, IDomainEvent>>(
Expression.Call(dispatcher, "Publish", new [] { eventType },
Expression.Convert(domainEvent, eventType)),
dispatcher, domainEvent);
return call.Compile();
}
private static readonly Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>> publishFuncCache = new Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>>();
private static Action<DomainEventDispatcher, IDomainEvent> GetPublishFunc(Type eventType)
{
lock (publishFuncCache)
{
Action<DomainEventDispatcher, IDomainEvent> func;
if (!publishFuncCache.TryGetValue(eventType, out func))
publishFuncCache.Add(eventType, func = CreatePublishFunc(eventType));
return func;
}
}
public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
var publish = GetPublishFunc(domainEvent.GetType());
publish(this, domainEvent);
}
}
使用编译的System.Linq.Expressions
按需创建和缓存代理。
到目前为止,这种方法应该是最快的。它也是最接近动态调用实现的,它的不同之处在于:)
答案 2 :(得分:0)
您所要做的就是将发布方法更改为:
foreach(var subscriber in subscribers)
if(subscriber.GetMethodInfo().GetParameters().Single().ParameterType == domainEvent.GetType())
subscriber.DynamicInvoke(domainEvent);
<强>更新强>
您还必须将呼叫更改为
Publish(domainEvent); //Remove the as dynamic
这样您就不必更改Publish的签名
我更喜欢我的另一个答案: C# subscribe to events based on parameter type?
更新2
关于你的问题
我很好奇为什么这个动态调用适用于我的原始版本 一个失败。
请记住,动态不是特殊类型
基本上编译器:
1)用对象替换它
2)重构代码到更复杂的代码
3)删除编译时检查(这些检查在运行时完成)
如果您尝试替换
Publish(domainEvent as dynamic);
与
Publish(domainEvent as object);
您将收到相同的消息,但这次是在编译时。 错误消息是不言自明的:
类型'object'不能用作类型参数'TEvent' 泛型类型或方法'DomainEventDispatcher.Publish(TEvent)'
作为最后一点。
动态是针对特定场景设计的,99.9%的时间不需要它,您可以使用静态类型代码替换它。
如果您认为自己需要它(如上述情况),那么您可能做错了
答案 3 :(得分:-1)
由于您的SUMIFS
方法已经具有泛型类型,因此您可以轻松进行更改:
Subscribe
如果您缺少发布和订阅方的编译时类型信息,您仍然可以消除动态转换。见Expression building example.