Я хочу создать систему событий в своей серверной части ASP.NET Core, используя дженерики для упрощения создания и регистрации обработчиков.
Основная проблема, с которой я столкнулся при попытке использовать несколько решений, — это общие конфликты при попытке сохранить обработчики событий получения и выполнения.
У меня есть база для таких событий:
internal interface IEvent
{
// Required to get with which tag/key event is stored in an external system
public static abstract string GetEventName();
}
public class TestEvent : IEvent
{
public static string GetEventName() => "test_event";
}
И база для обработчиков событий вроде этой:
public interface IEventHandler
{
}
public interface IEventHandler<TEvent> : IEventHandler where TEvent : IEvent
{
public void HandleEvent(TEvent e);
}
public class TestEventHandler : IEventHandler<TestEvent>
{
public void HandleEvent(TestEvent e)
{
// Doing something
}
}
Я хочу иметь возможность легко зарегистрироваться event handlers
в сервисном контейнере или где-то еще, чтобы они загружались автоматически. Прописать их в каком-нибудь EventHandlerFactory
или EventHandlerRegistrator
тоже хорошо, но это не меняет основной проблемы.
Я получаю events
во время выполнения из какого-то источника, они десериализуются из полезной нагрузки JSON.
Поэтому я не знаю их точного типа.
Я могу определить их тип во время выполнения с помощью отражения, но этого будет недостаточно, поскольку мне нужна высокая пропускная способность обработки событий, а отражение (если я не ошибаюсь) происходит медленно.
У меня также возникают проблемы с хранением event handlers
, поскольку их общие типы теряются во время выполнения, а приведение типов во время выполнения с определенными событиями приводит к появлению нулевых ссылок и других видов исключений.
@sa-es-ir RabbitMQ. Получение контента оттуда и десериализация его в один из производных классов IEvent. Десериализация в порядке, я получаю правильные производные экземпляры во время выполнения.
Тогда, пожалуйста, не создавайте свою собственную реализацию pub/sub, я предлагаю использовать Masstransit
, который также имеет интеграцию с RabbitMQ, Masstransit.io/documentation/concepts/messages
@sa-es-ir спасибо за ваш вклад! Я знаю о МассТранзите. Меня интересует, как мне сейчас добиться того, что я описал в теме. Какие у меня есть варианты без использования внешних готовых решений?
вы говорите, что отражение происходит медленно, но мы говорим о мс. почему вы беспокоитесь об оптимизации, если собираетесь писать все самостоятельно, а не использовать проверенные библиотеки?
@qujck, спасибо и тебе за твой вклад! В настоящее время я использую RabbitMQ для простой системы событий и хочу сделать это с нуля, чтобы понять, как я могу добиться этого с помощью дженериков. Если я не часто использую отражение или злоупотребляю памятью, все должно быть хорошо. Это не ракетостроительная задача. Дженерики и их преодоление — единственная проблема, которая у меня есть. Нет ничего плохого в том, чтобы думать о скорости размышления, когда пытаетесь найти решение самостоятельно. Это правильная попытка, и не все следует делать с помощью внешних библиотек. Тем более не понимая, как они работают.
«Поэтому я не знаю их точного типа». - десериализуется ли событие в какой-то конкретный тип события?
@GuruStron да! Я использую полиморфизм System.Text.Json. Есть базовое событие. На самом деле я добился того, чего хотел, используя действия для регистрации обработчиков событий. Из-за особенности ковариации дженериков C# я не могу передать свой обработчик событий здесь и там и сохранить его общий аргумент события в сигнатуре метода (он потерян, приведение к TestEventHandler к IEventHandler<IEvent> возвращает значение null). Поэтому мне нужно было сделать все в 1 методе регистрации обработчика событий. Выглядит некрасиво, грязно, но работает. Надеюсь, вы, ребята, предложите что-нибудь получше :D
Основная идея состоит в том, чтобы использовать отражение только для создания обработчиков и словаря от типа к обработчику. Что-то в этом роде (с использованием деревьев выражений):
public static class Helper
{
private static readonly Dictionary<Type, IEventHandler> Handlers = new()
{
{typeof(TestEvent), new TestEventHandler()}
};
// not an Action<IEvent> due to static abstract member of the interface
private static Dictionary<Type, Action<object>> HandlerFuncs = new()
{
{typeof(TestEvent), CreateHandler<TestEvent>()}
};
public static void Handle(IEvent @event) => HandlerFuncs[@event.GetType()](@event);
private static Action<object> CreateHandler<T>()
{
var genericMethod = typeof(Helper)
.GetMethod(nameof(Helper.HandleGeneric), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(typeof(T));
var parameterExpression = Expression.Parameter(typeof(object));
var expression = Expression.Lambda<Action<object>>(Expression.Call(genericMethod, parameterExpression), parameterExpression);
return expression.Compile();
}
private static void HandleGeneric<T>(object @event) where T : class, IEvent
{
var eventHandler = Handlers[typeof(T)] as IEventHandler<T>;
eventHandler.HandleEvent((T)@event);
}
}
Звонок выглядит так: Helper.Handle(new TestEvent());
.
И Handlers
, и HandlerFuncs
можно создать с помощью отражения и/или поставщика услуг, я использую словари, создаваемые вручную, только в демонстрационных целях. Вы также можете использовать стороннюю библиотеку, например Scrutor, чтобы уменьшить количество отражений, которые необходимо писать вручную.
Его можно относительно легко обновить для работы с DI. Например:
public class HandlerInvoker
{
private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, object>> handlerFuncs = new();
private readonly IServiceProvider _serviceProvider;
public HandlerInvoker(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void HandleEvent(IEvent e)
{
var handler = handlerFuncs.GetOrAdd(e.GetType(), CreateHandler);
handler(_serviceProvider, e);
}
private static Action<IServiceProvider, object> CreateHandler(Type t)
{
var genericMethod = typeof(HandlerInvoker)
.GetMethod(nameof(HandleGeneric), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(t);
var p1 = Expression.Parameter(typeof(IServiceProvider));
var p2 = Expression.Parameter(typeof(object));
var expression = Expression.Lambda<Action<IServiceProvider, object>>(Expression.Call(genericMethod, p1, p2), p1, p2);
return expression.Compile();
}
private static void HandleGeneric<T>(IServiceProvider sp, object @event) where T : class, IEvent
{
var eventHandler =sp.GetRequiredService<IEventHandler<T>>();
eventHandler.HandleEvent((T)@event);
}
}
И пример использования:
var services = new ServiceCollection();
services.AddTransient<IEventHandler<TestEvent>, TestEventHandler>();
services.AddTransient<HandlerInvoker>();
var sp = services.BuildServiceProvider();
var handlerInvoker = sp.GetService<HandlerInvoker>();
handlerInvoker.HandleEvent(new TestEvent());
В зависимости от варианта использования деревья отражений и выражений могут быть заменены генераторами источников.
Компиляция выражений — это ФАНТАСТИЧЕСКАЯ техника, о которой я даже отдаленно не подозревал. Замечательный! Спасибо!
@idchlife был рад помочь! Обратите внимание, что этот метод может иметь некоторые ограничения — например, AFAIK, он не будет работать с Native AOT.
I am getting events at runtime from some source
что такоеsome source
? это очередь или поток событий или просто все происходит вsync
?