我正在研究某种EventSourcing架构,我的应用程序中有两个主要概念-events
和handlers
。
事件示例:
class NewRecordCreated: EventMessage {...}
有些处理程序看起来像:
class WriteDBHandler: IEventHandler<NewRecordCreated>, IEventHandler<RecordUpdated> {
public void Handle(NewRecordCreated eventMessage) {...}
public void Handle(RecordUpdated eventMessage) {...}
}
此外,我还有队列协议的自定义实现,它将事件调度到适当的处理程序。所以,基本上在应用程序启动时,我解析程序集,并根据类型创建事件和处理程序之间的映射。
因此,当我实际将事件调度到处理程序时,我会根据事件类型获取处理程序的类型链,比如var handlerChain = [typeof(WriteDbHandler), typeof(LogHandler), typeof(ReadModelUpdateHandler)]
,对于每个处理程序,我需要调用它的实例,然后将其强制转换为适当的接口(IEventHandler<>
),然后调用Handle
方法。
但我不能转换为通用接口,因为这是不可能的。我考虑了实现非通用版本接口的选项,但每次添加额外的方法实现对我来说似乎很不愉快,尤其是在没有任何真正原因的情况下
我考虑动态调用或反射,但这两种变体似乎都存在性能问题。也许你可以给我一些合适的替代方案?
使用反射
您可以使用反射来获得对需要调用的方法的引用,而不是尝试强制转换为IEventHandler<>
。下面的代码就是一个很好的例子。为了简洁起见,它简化了"队列协议",但它应该充分说明您需要进行的反射
class MainClass
{
public static void Main(string [] args)
{
var a = Assembly.GetExecutingAssembly();
Dictionary<Type, List<Type>> handlerTypesByMessageType = new Dictionary<Type, List<Type>>();
// find all types in the assembly that implement IEventHandler<T>
// for some value(s) of T
foreach (var t in a.GetTypes())
{
foreach (var iface in t.GetInterfaces())
{
if (iface.GetGenericTypeDefinition() == typeof(IEventHandler<>))
{
var messageType = iface.GetGenericArguments()[0];
if (!handlerTypesByMessageType.ContainsKey(messageType))
handlerTypesByMessageType[messageType] = new List<Type>();
handlerTypesByMessageType[messageType].Add(t);
}
}
}
// get list of events
var messages = new List<EventMessage> {
new NewRecordCreated("one"),
new RecordUpdated("two"),
new RecordUpdated("three"),
new NewRecordCreated("four"),
new RecordUpdated("five"),
};
// process all events
foreach (var msg in messages)
{
var messageType = msg.GetType();
if (!handlerTypesByMessageType.ContainsKey(messageType))
{
throw new NotImplementedException("No handlers for that type");
}
if (handlerTypesByMessageType[messageType].Count < 1)
{
throw new NotImplementedException("No handlers for that type");
}
// look up the handlers for the message type
foreach (var handlerType in handlerTypesByMessageType[messageType])
{
var handler = Activator.CreateInstance(handlerType);
// look up desired method by name and parameter type
var handlerMethod = handlerType.GetMethod("Handle", new Type[] { messageType });
handlerMethod.Invoke(handler, new object[]{msg});
}
}
}
}
我编译了这个并在我的机器上运行,得到了我认为正确的结果。
使用运行时代码生成
如果反射速度不够快,您可以为每个输入消息类型动态编译代码并执行它。System.Reflection.Emit
命名空间具有实现这一点的功能。您可以定义一个动态方法(不要与dynamic
关键字混淆,后者是其他东西),并发出一个sequence-if-IL操作码,该操作码将按顺序运行列表中的每个处理程序。
public static Dictionary<Type, Action<EventMessage>> GenerateHandlerDelegatesFromTypeLists(Dictionary<Type, List<Type>> handlerTypesByMessageType)
{
var handlersByMessageType = new Dictionary<Type, Action<EventMessage>>();
foreach (var messageType in handlerTypesByMessageType.Keys)
{
var handlerTypeList = handlerTypesByMessageType[messageType];
if (handlerTypeList.Count < 1)
throw new NotImplementedException("No handlers for that type");
var method =
new DynamicMethod(
"handler_" + messageType.Name,
null,
new [] { typeof(EventMessage) });
var gen = method.GetILGenerator();
foreach (var handlerType in handlerTypeList)
{
var handlerCtor = handlerType.GetConstructor(new Type[0]);
var handlerMethod =
handlerType.GetMethod("Handle", new Type[] { messageType });
// create an object of the handler type
gen.Emit(OpCodes.Newobj, handlerCtor);
// load the EventMessage passed as an argument
gen.Emit(OpCodes.Ldarg_0);
// call the handler object's Handle method
gen.Emit(OpCodes.Callvirt, handlerMethod);
}
gen.Emit(OpCodes.Ret);
var del = (Action<EventMessage>)method.CreateDelegate(
typeof(Action<EventMessage>));
handlersByMessageType[messageType] = del;
}
}
然后,您不需要用handlerMethod.Invoke(handler, new object[]{msg})
调用处理程序,而只需像调用其他委托一样,用handlersByMessageType[messageType](msg)
调用委托。
此处列出完整代码。
实际的代码生成是在GenerateHandlerDelegatesFromTypeLists
方法中完成的。它实例化一个新的DynamicMethod
,获取其关联的ILGenerator
,然后依次为每个处理程序发出操作码。对于每个处理程序类型,它将实例化该处理程序类型的新对象,将事件消息加载到堆栈中,然后在处理程序对象上执行该消息类型的Handle
方法。当然,这是假设处理程序类型都有零个参数构造函数。不过,如果需要将参数传递给构造函数,则必须对其进行大量修改。
还有其他方法可以进一步加快速度。如果您放宽了为每条消息创建一个新处理程序对象的要求,那么您可以在生成代码的同时创建对象并加载它们。在这种情况下,将gen.Emit(OpCodes.Newobj, handlerCtor)
替换为gen.Emit(OpCodes.Ldobj, handlerObjectsByType[handlerType])
。这会给您带来两个好处:1.你正在避免在每条消息上进行分配2.当您填充handlerObjectsByType
字典时,您可以任意方式实例化对象。您甚至可以将构造函数与参数或工厂方法一起使用。