Как разработать систему событий и обработчиков событий, используя универсальный код на C#?

Я хочу создать систему событий в своей серверной части 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, поскольку их общие типы теряются во время выполнения, а приведение типов во время выполнения с определенными событиями приводит к появлению нулевых ссылок и других видов исключений.

I am getting events at runtime from some source что такое some source? это очередь или поток событий или просто все происходит в sync?
sa-es-ir 14.07.2024 08:18

@sa-es-ir RabbitMQ. Получение контента оттуда и десериализация его в один из производных классов IEvent. Десериализация в порядке, я получаю правильные производные экземпляры во время выполнения.

idchlife 14.07.2024 08:28

Тогда, пожалуйста, не создавайте свою собственную реализацию pub/sub, я предлагаю использовать Masstransit, который также имеет интеграцию с RabbitMQ, Masstransit.io/documentation/concepts/messages

sa-es-ir 14.07.2024 08:34

@sa-es-ir спасибо за ваш вклад! Я знаю о МассТранзите. Меня интересует, как мне сейчас добиться того, что я описал в теме. Какие у меня есть варианты без использования внешних готовых решений?

idchlife 14.07.2024 12:02

вы говорите, что отражение происходит медленно, но мы говорим о мс. почему вы беспокоитесь об оптимизации, если собираетесь писать все самостоятельно, а не использовать проверенные библиотеки?

qujck 16.07.2024 12:42

@qujck, спасибо и тебе за твой вклад! В настоящее время я использую RabbitMQ для простой системы событий и хочу сделать это с нуля, чтобы понять, как я могу добиться этого с помощью дженериков. Если я не часто использую отражение или злоупотребляю памятью, все должно быть хорошо. Это не ракетостроительная задача. Дженерики и их преодоление — единственная проблема, которая у меня есть. Нет ничего плохого в том, чтобы думать о скорости размышления, когда пытаетесь найти решение самостоятельно. Это правильная попытка, и не все следует делать с помощью внешних библиотек. Тем более не понимая, как они работают.

idchlife 17.07.2024 15:06

«Поэтому я не знаю их точного типа». - десериализуется ли событие в какой-то конкретный тип события?

Guru Stron 17.07.2024 21:03

@GuruStron да! Я использую полиморфизм System.Text.Json. Есть базовое событие. На самом деле я добился того, чего хотел, используя действия для регистрации обработчиков событий. Из-за особенности ковариации дженериков C# я не могу передать свой обработчик событий здесь и там и сохранить его общий аргумент события в сигнатуре метода (он потерян, приведение к TestEventHandler к IEventHandler<IEvent> возвращает значение null). Поэтому мне нужно было сделать все в 1 методе регистрации обработчика событий. Выглядит некрасиво, грязно, но работает. Надеюсь, вы, ребята, предложите что-нибудь получше :D

idchlife 17.07.2024 21:11
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
8
66
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основная идея состоит в том, чтобы использовать отражение только для создания обработчиков и словаря от типа к обработчику. Что-то в этом роде (с использованием деревьев выражений):

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 19.07.2024 15:20

@idchlife был рад помочь! Обратите внимание, что этот метод может иметь некоторые ограничения — например, AFAIK, он не будет работать с Native AOT.

Guru Stron 19.07.2024 15:40

Другие вопросы по теме