c# - traves - Manera efectiva de invocar el método de interfaz genérico en tiempo de ejecución
metodo invoke c# (1)
Estoy trabajando en algún tipo de arquitectura de EventSourcing y tengo 2 conceptos principales en mi aplicación: events
y handlers
.
Ejemplo de eventos:
class NewRecordCreated: EventMessage {...}
Y hay algunos controladores parece:
class WriteDBHandler: IEventHandler<NewRecordCreated>, IEventHandler<RecordUpdated> {
public void Handle(NewRecordCreated eventMessage) {...}
public void Handle(RecordUpdated eventMessage) {...}
}
Y también tengo una implementación personalizada del protocolo de cola que envía eventos a los controladores adecuados. Básicamente, en el inicio de la aplicación analizo el ensamblaje y creo una asignación entre el evento y los manejadores según los tipos.
Así que cuando estoy distribuyendo eventos a los manejadores basándome en el tipo de evento obteniendo los tipos de cadena de manejador - algo como var handlerChain = [typeof(WriteDbHandler), typeof(LogHandler), typeof(ReadModelUpdateHandler)]
y para cada uno de esos manejadores necesito invocar es una instancia, luego lo lanzó a la interfaz adecuada ( IEventHandler<>
) y luego invocó el método Handle
.
Pero no puedo convertir a la interfaz genérica, ya que no es posible. Pienso en las opciones de implementar una versión no genérica de la interfaz, pero me parece bastante desagradable agregar una implementación de método extra cada vez, especialmente si no hay razones reales para ello.
Pienso en la invocación dinámica o la reflexión, pero ambas variantes parecen tener problemas de rendimiento. ¿Tal vez podrías aconsejarme algunas alternativas adecuadas?
Usando la reflexión
En lugar de tratar de convertir a IEventHandler<>
, puede usar el reflejo para obtener una referencia al método que necesita invocar. El siguiente código es un buen ejemplo. Simplifica el "protocolo de cola" en aras de la brevedad, pero debe ilustrar suficientemente la reflexión que debe hacer.
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});
}
}
}
}
Compilé esto y lo ejecuté en mi máquina y obtuve lo que creo que son los resultados correctos.
Uso de generación de código en tiempo de ejecución
Si la reflexión no es lo suficientemente rápida para sus propósitos, puede compilar el código sobre la marcha para cada tipo de mensaje de entrada y ejecutarlo. El System.Reflection.Emit
nombres System.Reflection.Emit
tiene facilidades para hacer justamente eso. Puede definir un método dinámico (que no debe confundirse con la palabra clave dynamic
, que es otra cosa), y emitir una secuencia si IL codifica que ejecutará cada controlador en la lista en secuencia.
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;
}
}
Luego, en lugar de invocar a los manejadores con handlerMethod.Invoke(handler, new object[]{msg})
, simplemente llama al delegado como cualquier otro, con handlersByMessageType[messageType](msg)
.
Listado de código completo aquí .
La generación del código real se realiza en el método GenerateHandlerDelegatesFromTypeLists
. DynamicMethod
un nuevo DynamicMethod
, obtiene su ILGenerator
asociado y, a su ILGenerator
, emite ILGenerator
para cada manejador. Para cada tipo de controlador, creará una instancia de un nuevo objeto de ese tipo de controlador, cargará el mensaje de evento en la pila y luego ejecutará el método Handle
para ese tipo de mensaje en el objeto controlador. Por supuesto, esto supone que todos los tipos de controlador tienen constructores de cero parámetros. Sin embargo, si necesita pasar argumentos a los constructores, tendrá que modificarlo considerablemente.
Hay otras formas de acelerar esto aún más. Si relaja el requisito de crear un nuevo objeto controlador con cada mensaje, entonces podría simplemente crear los objetos al generar el código y cargarlos. En ese caso, reemplace gen.Emit(OpCodes.Newobj, handlerCtor)
con gen.Emit(OpCodes.Ldobj, handlerObjectsByType[handlerType])
. Eso le da dos beneficios: 1. está evitando una asignación en cada mensaje 2. puede instanciar los objetos de la forma que desee cuando handlerObjectsByType
diccionario handlerObjectsByType
. Incluso puede usar constructores con parámetros o métodos de fábrica.