mardi 3 mars 2020

Avoid dynamic invocation when processing a bus event that implements a generic interface

I have recently completed a course about Microservices and RabbitMQ using ASP.NET Core and noticed that event processing uses dynamic invocation. The code is the following (only relevant parts retained ):

public sealed class RabbitMqBus : IEventBus
{
    private readonly IMediator _mediator;
    private readonly Dictionary<string, List<Type>> _handlers;
    private readonly List<Type> _eventTypes;
}

public void Subscribe<TEvent, THandler>() where TEvent : Event where THandler : IEventHandler<TEvent>
{
    string eventName = typeof(TEvent).Name;
    var handlerType = typeof(THandler);

    if (!_eventTypes.Contains(typeof(TEvent)))
        _eventTypes.Add(typeof(TEvent));

    if (!_handlers.ContainsKey(eventName))
        _handlers.Add(eventName, new List<Type>());

    if (_handlers[eventName].Any(s => s == handlerType))
        throw new ArgumentException($"Handler type {handlerType.Name} is already registered for {eventName}");

    _handlers[eventName].Add(handlerType);

    StartBasicConsume<TEvent>();
}

private async Task ConsumerReceived(object sender, BasicDeliverEventArgs e)
{
    string eventName = e.RoutingKey;
    string message = Encoding.UTF8.GetString(e.Body);

    try
    {
        await ProcessEvent(eventName, message);
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception);
        throw;
    }
}

private async Task ProcessEvent(string eventName, string message)
{
    if (_handlers.ContainsKey(eventName))
    {
        using var scope = _serviceScopeFactory.CreateScope();

        var subscriptions = _handlers[eventName];
        foreach (var subscription in subscriptions)
        {
            var handler = scope.ServiceProvider.GetService(subscription);
            if (handler == null)
                continue;

            //TODO: check if this can be made typed and avoid messy dynamic invocation at the end
            Type eventType = _eventTypes.SingleOrDefault(t => t.Name == eventName);
            object @event = JsonConvert.DeserializeObject(message, eventType);
            Type concreteType = typeof(IEventHandler<>).MakeGenericType(eventType);
            await (Task) concreteType.GetMethod("Handle").Invoke(handler, new[] {@event});
        }
    }
}

public interface IEventHandler
{
}

public interface IEventHandler<in TEvent>: IEventHandler
    where TEvent: Event
{
    Task Handle(TEvent @event);
}

My issue is with the code following the TODO because it relies on reflection and dynamic invocation of a method with a clear name ("Handle").

My first improvement is eliminating the magic string through nameof:

Type eventType = _eventTypes.SingleOrDefault(t => t.Name == eventName);
Event @event = (Event) JsonConvert.DeserializeObject(message, eventType);
Type concreteType = typeof(IEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod(nameof(IEventHandler<Event>.Handle)).Invoke(handler, new object[] { @event });

However, reflection is still used. I understand that this is required because Handle is defined as a generic method (part of a generic interface) and the received event is dynamically constructed.

Is there a way to refactor this code to avoid reflection?





Aucun commentaire:

Enregistrer un commentaire