События в .NET: стандартная реализация, альтернативы, и причем тут сахар

—

от автора

События – это объекты, которые получают уведомления о некотором действии в разрабатываемом ПО и могут запускать реакции на это действие. Разработчик может определить эти действия, добавив к событию обработчик. Разберем в этом материале само понятие событий в .NET и разные способы работы с ними.

Объясним на сахаре

Если говорить простым языком, то можно провести аналогию с просыпанным сахаром. Например, у нас в руках была сахарница, и мы сахар из нее рассыпали – это событие. Что делать, если рассыпался сахар? Идти за веником, чтобы убрать – это и есть обработчик события. Этот обработчик с предназначенным для него действием «сидит у нас в голове». Даже если сахар мы никогда не просыпали, мы все равно знаем, что веником его можно будет убрать. 

Обработчик может меняться. Например, мы покупаем пылесос – теперь у нас в голове меняется обработчик для того же события: мы пойдем не за веником, а за пылесосом (один обработчик мы удалили, другой добавили). Изменение обработчика не влияет на событие. Обработчика может и не быть вовсе – тогда человек будет просто ходить по просыпанному сахару.

О событиях в C#

В C# события существуют с самого начала. Например, при создании элементарного приложения Web Forms, для обработки нажатия на кнопку нужно добавить конструкцию вида

MyButton.Click += new EventHandler(this.MyBtn_Click);

Сlick – это и есть событие, которое уже было добавлено разработчиками в класс Button, а MyBtn_Click – это обработчик, написанный программистом. 

Сейчас события используются реже, но возможность создавать и использовать их сохранилась. Так как же это устроено изнутри?

Первое, что хочется отметить: такой физической сущности как событие нет. Потому что событие будет являться делегатом, ссылкой на метод, который мы можем определить, а можем и не определить в дальнейшем. То есть по сути существует лишь обработчик события, и это вызывает некоторую путаницу в формулировках.

Основная суть добавления событий – внести возможность различной обработки события для объекта в разных частях программы, а также – возможность добавить обработчик к объекту. Потом можно изменить, добавить новый или убрать текущий обработчик в другом месте при работе с тем же объектом.

В C# события – это отдельный тип членов класса, обозначаемый ключевым словом event. Наряду со свойствами и параметрами.

Класс, который реализует событие, должен содержать следующую конструкцию:

event тип_делегата имя_события;
  • тип_делегата указывает на прототип вызываемого метода (или методов);

  • имя_события – конкретный объект объявляемого события.

Добавление обработчика события производится с помощью операции += :

имя_события += обработчик_события;

Разберем добавление обработки событий на примере логирования. Здесь, и в дальнейшем, мы будем использовать консольное приложение .Net 7.0, C# 11.

class Program {     static void Main()     {         EmailService emailService = new EmailService(emailFrom: "hr@disney.com");          // Добавляем обработчик события         emailService.MailSent += LogToConsole;          emailService.SendMail(emailTo: "Milo.Murphy@disney.com", subject: "Welcome to Disney", body: "Today is your first day...");     }      // Обработчик, который логирует в консоль отправленное сообщение     static void LogToConsole(MailSentEventArgs eventArgs)         => Console.WriteLine(             $"[Консоль] Письмо с темой '{eventArgs.Subject}' отправлено с адреса '{eventArgs.EmailFrom}' на адрес '{eventArgs.EmailTo}': {eventArgs.Body}"); }  // Класс в котором реализовано событие class EmailService {     private readonly string _emailFrom;      public EmailService(string emailFrom)     {         _emailFrom = emailFrom;     }      // Объявляем событие     public event MailSentEventHandler? MailSent;      public void SendMail(string emailTo, string subject, string body)     {         // Отправляем письмо пользователю...         // Send(_emailFrom, emailTo, subject, body);          // Вызываем метод запуска события         var eventArgs = new MailSentEventArgs         {             EmailFrom = _emailFrom,             EmailTo = emailTo,             Subject = subject,             Body = body         };         OnMailSent(eventArgs);     }      // Используем метод для запуска события     protected virtual void OnMailSent(MailSentEventArgs eventArgs)     {         MailSentEventHandler? mailSentHandler = MailSent;         if (mailSentHandler != null)         {             mailSentHandler(eventArgs);         }     } }  // Объявляем тип события public delegate void MailSentEventHandler(MailSentEventArgs eventArgs);  public record MailSentEventArgs {     public string? EmailFrom { get; init; }     public string? EmailTo { get; init; }     public string? Subject { get; init; }     public string? Body { get; init; } } 

Мы добавили событие MailSent в класс EmailService и добавили обработчик этого события LogToConsole.

Результат:

[Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day...

Добавление и удаление обработчиков

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

Цепочка обработчиков реализуется через Delegate.Combine. То есть физически каждый делегат содержит ссылку на реализацию, внутри которой ссылка на следующую реализацию. Обработчик можно удалить в процессе.

static void Main()     {         EmailService emailService = new EmailService(emailFrom: "hr@disney.com");          // Добавляем обработчик события         emailService.MailSent += LogToConsole;         emailService.MailSent += LogToFile;          emailService.SendMail(emailTo: "Milo.Murphy@disney.com", subject: "Welcome to Disney", body: "Today is your first day...");          emailService.MailSent -= LogToFile;         emailService.SendMail(emailTo: "Milo.Murphy@disney.com", subject: "Welcome to Disney", body: "Today is your first day...");     }      // Обработчик, который логирует в консоль отправленное сообщение     static void LogToConsole(MailSentEventArgs eventArgs)         => Console.WriteLine(             $"[Консоль] Письмо с темой '{eventArgs.Subject}' отправлено с адреса '{eventArgs.EmailFrom}' на адрес '{eventArgs.EmailTo}': {eventArgs.Body}");      // Обработчик, который логирует в файл отправленное сообщение     static void LogToFile(MailSentEventArgs eventArgs)         => Console.WriteLine(             $"[Файл] Письмо с темой '{eventArgs.Subject}' отправлено с адреса '{eventArgs.EmailFrom}' на адрес '{eventArgs.EmailTo}': {eventArgs.Body}"); 

Результат:

[Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... [Файл] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... [Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day...

Если нужно выполнить какие-то действия при добавлении или удалении обработчика, можно переписать функции add/remove у делегата. Мы добавим логирование к действиям добавления/удаления обработчика.

private MailSentEventHandler? mailSent; public event MailSentEventHandler MailSent { add { mailSent += value; Console.WriteLine($"Обработчик {value.Method.Name} добавлен"); } remove { Console.WriteLine($"Обработчик {value.Method.Name} удален"); mailSent -= value; } } 

Результат:

Обработчик LogToConsole добавлен Обработчик LogToFile добавлен [Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... [Файл] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... Обработчик LogToFile удален [Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day.. 

Если в обработчике произойдет ошибка – ошибка будет на уровне приложения. Если добавить try catch при вызове обработчика, то в случае, если произойдет ошибка в первом обработчике, последующие обработчики выполняться не будут. Поэтому следует предусмотреть это в каждом обработчике.

static void LogToConsole(MailSentEventArgs eventArgs) { try { throw new Exception("Ошибка записи"); } catch (Exception e) { Console.WriteLine(e); } }

EventHandler

В .NET существует делегат EventHandler, предназначенный как раз для объявления события и принимающий определенный тип входных параметров.

Классы, которые мы собираемся использовать для хранения информации, передаваемой обработчику события, должны наследоваться от типа System.EventArgs. При этом имя типа желательно заканчивать словом EventArgs.

Начиная с .NET Framework 4.5 наследованиe аргументов от System.EventArgs стало не обязательным, но в примере мы его оставим, это никак не повлияет на результат.

Создадим тип параметров, используемых обработчиками:

public class MailSentEventArgs : EventArgs { public string? EmailFrom { get; init; } public string? EmailTo { get; init; } public string? Subject { get; init; } public string? Body { get; init; } }

И тогда событие может быть объявлено как

public event EventHandler<MailSentEventArgs>? MailSent;

При этом обработчик должен принимать параметры object sender и MailSentEventArgs args. Где sender – текущий элемент класса, в котором определен event, a args – передаваемые обработчику данные. Обработчик может быть использован для событий в разных классах, поэтому разумнее принимать экземпляр типа object, а не конкретного типа. Так как в этом случае в метод обработчика могут приходить данные от разных событий, но с одинаковыми параметрами.

То есть обработчики будут выглядеть так:

public static void LogToConsole(object? sender, MailSentEventArgs args)         => Console.WriteLine(             $"[Консоль] Письмо с темой '{args.Subject}' отправлено с адреса '{args.EmailFrom}' на адрес '{args.EmailTo}': {args.Body}");        public static void LogToFile(object? sender, MailSentEventArgs args)         => Console.WriteLine(             $"[Файл] Письмо с темой '{args.Subject}' отправлено с адреса '{args.EmailFrom}' на адрес '{args.EmailTo}': {args.Body}")

А вызов, поскольку событие представляет собой делегат, например, так:

MailSent?.Invoke(this, eventArgs);

Порой необходимо передать внутрь функции какие-то данные, а порой действие будет выполняться независимо от внешних данных – тогда передавать внутрь функции ничего не нужно. В случаях, когда не нужно передавать в обработчик никаких данных, мы можем воспользоваться EventArgs.Empty.  То есть объявление события не будет указывать тип аргумента:

public event EventHandler? MailSent;

Обработчик при этом должен принимать object sender и EventArgs:

static void LogToConsole(object? sender, EventArgs eventArgs) => Console.WriteLine("[Консоль] Отправлено письмо");

а вызов будет выглядеть так:

MailSent?.Invoke(this, EventArgs.Empty);

AsyncEventHandler

Обработка делегатов может быть асинхронной. С ее помощью получается лучше распределить ресурсы компьютера и потоки обработки данных – это полезно в случаях, когда операция находится в режиме ожидания достаточно длительное время и мы выигрываем в скорости от перераспределения потоков.

Подключив к проекту Microsoft.VisualStudio.Threading, получаем возможность сделать обработку делегатов асинхронной.

Тогда объявлять событие мы будем так:

public event AsyncEventHandler<MailSentEventArgs>? MailSent;

Обработчик будет выглядеть так:

static Task LogToConsoleAsync(object? sender, MailSentEventArgs args) { Console.WriteLine( $"[Консоль] Письмо с темой '{args.Subject}' отправлено с адреса '{args.EmailFrom}' на адрес '{args.EmailTo}': {args.Body}"); return Task.CompletedTask; }

а вызываться так:

await MailSent?.InvokeAsync(this, new MailSentEventArgs());

Реализация событий компилятором

Несколько слов о представлении событий в IL-кодe. При компиляции кроме объявления события также создаются два метода add и remove: они реализуют конструкции += и –=.  

Для наглядности можно скомпилировать и декомпилировать обратно наш код. Тогда станет видно, что добавляет компилятор. Прогнав таким образом наш первоначальный код здесь мы получим следующий код в месте создания события.

public event MailSentEventHandler MailSent {     [NullableContext(2)]     [CompilerGenerated]     add     {         MailSentEventHandler mailSentEventHandler = this.MailSent;         while (true)         {             // берем текущий делегат             MailSentEventHandler mailSentEventHandler2 = mailSentEventHandler;              // добавляем к текущему делегату новый             MailSentEventHandler value2 = (MailSentEventHandler)Delegate                                          .Combine(mailSentEventHandler2, value);              // сравниваем MailSent и mailSentEventHandler2             // и, если они равны, заменяем MailSent на value2             // а в mailSentEventHandler записываем исходное значение MailSent             mailSentEventHandler = Interlocked              .CompareExchange(ref this.MailSent, value2, mailSentEventHandler2);              // если предыдущая операция выполнилась успешно - заканчиваем             // это нужно для безопасной многопоточной работы - если в             // параллельном потоке у события изменится список делегатов,             // то while запустится повторно и добавит новый делегат к уже            // обновленной цепочке делегатов             if ((object)mailSentEventHandler == mailSentEventHandler2)             {                 break;             }         }     }     [NullableContext(2)]     [CompilerGenerated]     remove     {         MailSentEventHandler mailSentEventHandler = this.MailSent;         while (true)         {             // берем текущий делегат             MailSentEventHandler mailSentEventHandler2 = mailSentEventHandler;              // удаляем из него value             MailSentEventHandler value2 = (MailSentEventHandler)Delegate                                           .Remove(mailSentEventHandler2, value);              // сравниваем MailSent и mailSentEventHandler2             // и, если они равны, заменяем MailSent на value2             // а в mailSentEventHandler записываем исходное значение MailSent                         mailSentEventHandler = Interlocked              .CompareExchange(ref this.MailSent, value2, mailSentEventHandler2);              // если предыдущая операция выполнилась успешно - заканчиваем             if ((object)mailSentEventHandler == mailSentEventHandler2)             {                 break;             }         }     } }

Паттерн «Наблюдатель»

Реализация событий укладывается в паттерн «Наблюдатель», суть которого в наличии одного наблюдаемого объекта и нескольких наблюдателей. Если возвращаться к аналогии с сахаром, в рамках паттерна сахарница будет наблюдаемым объектом, а человек, который рассыпал сахар или просто находился рядом – наблюдателем. Наблюдателей может быть больше одного (кто-то с веником, а кто-то с пылесосом). Далее, когда рассыпается сахар, сначала один наблюдатель делает свои действия, а другой следом – свои.

Паттерн «Наблюдатель» можно реализовать через добавления наблюдателей как реализаций делегата, а можно – через добавление наблюдателей в список, хранящийся в наблюдаемом объекте.

Ниже приведен пример реализации этого паттерна без использования событий.

class Program {     static void Main()     {         EmailService emailService = new EmailService("hr@disney.com");          // Добавляем обработчик события         var consoleObserver = new ConsoleObserver(emailService);         var fileObserver = new FileObserver(emailService);          emailService.SendMail(             emailTo: "Milo.Murphy@disney.com",             subject: "Welcome to Disney",             body: "Today is your first day...");          fileObserver.StopObserve();          emailService.SendMail(             emailTo: "Milo.Murphy@disney.com",             subject: "Welcome to Disney",             body: "Today is your first day...");     } }  interface IObserver {     void Update(string emailFrom, string emailTo, string subject, string body); }  interface IObservable {     void RegisterObserver(IObserver o);     void RemoveObserver(IObserver o); }  class EmailService : IObservable {     private readonly List<IObserver> _observers;     private readonly string _emailFrom;      public EmailService(string emailFrom)     {         _emailFrom = emailFrom;         _observers = new List<IObserver>();     }      public void RegisterObserver(IObserver o)     {         _observers.Add(o);     }      public void RemoveObserver(IObserver o)     {         _observers.Remove(o);     }      protected void MailSent(string emailTo, string subject, string body)     {         foreach (IObserver o in _observers)         {             o.Update(_emailFrom, emailTo, subject, body);         }     }      public void SendMail(string emailTo, string subject, string body)     {         // Отправить письмо пользователю         //Send(_emailFrom, emailTo, subject, body);          // Запускаем методы наблюдателей         MailSent(emailTo, subject, body);     } }  class ConsoleObserver : IObserver {     IObservable? _stock;     public ConsoleObserver(IObservable obs)     {         _stock = obs;         _stock.RegisterObserver(this);     }     public void Update(         string emailFrom,         string emailTo,         string subject,         string body)     {         Console.WriteLine($"[Консоль] Письмо с темой '{subject}' отправлено с адреса '{emailFrom}' на адрес '{emailTo}': {body}");     }      public void StopObserve()     {         if (_stock is null)         {             return;         }         _stock.RemoveObserver(this);         _stock = null;     } }  class FileObserver : IObserver {     IObservable? _stock;     public FileObserver(IObservable obs)     {         _stock = obs;         _stock.RegisterObserver(this);     }      public void Update(         string emailFrom,         string emailTo,         string subject,         string body)     {         Console.WriteLine($"[Файл] Письмо с темой '{subject}' отправлено с адреса '{emailFrom}' на адрес '{emailTo}': {body}");     }      public void StopObserve()     {         if (_stock is null)         {             return;         }         _stock.RemoveObserver(this);         _stock = null;     } }

Результат:

[Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... [Файл] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... [Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day...

Минусы и плюсы такой реализации паттерна «Наблюдатель»

Минусы

  • в этой реализации нам пришлось самим писать интерфейсы, а для событий есть прописанные интерфейсы и классы, которыми остается только воспользоваться;

  • более высокий порог вхождения – нужно разобраться с работой паттерна;

  • под каждое событие придется писать свой набор интерфейсов из-за различий данных события, передаваемых в метод IObserver.Update (либо аналогично событиям использовать тип object, но тогда теряется наглядность);

  • при этом код стал более отлаживаемым – весь код доступен для редактирования, и более прозрачным – можно пройтись по реализации и в точности посмотреть работу (в событиях, например, не очевидно, что будет, если случится эксепшен в одном из цепочки делегатов).

Плюсы

  • подобную реализацию можно использовать также в языках, где нет делегатов – например, С++;

  • можно обеспечить распараллеливание реакции наблюдателей;

  • можно отловить случившийся эксепшн в цепочке и обработать в зависимости от логики задачи;

  • точные контракты: есть конкретные методы с конкретным набором параметров под нужное событие, а не набор параметров вида «object sender, EventArgs e».

MediatR

В библиотеке MediatR из коробки есть своя реализация событий. Это не существующие в C# события, но есть некоторое сходство.

Как следует из названия, MediatR – это реализация паттерна «Посредник». Суть паттерна в создании прослойки между частями кода. Это нужно в случае наличия большого количества связей между объектами – есть вероятность запутать логику реализации. 

«Посредник» ограничивает объекты от явных ссылок друг на друга, уменьшая количество взаимосвязей. Основной принцип реализации в том, что мы создаем объект и можем добавить обработчики для него. При этом достаточно использовать у типа отправляемого объекта интерфейс IRequest  или INotification и указать у обработчика интерфейс, связанный с типом объекта. Тогда MediatR вызовет нужный обработчик при выполнении команды Send для интерфейса IRequest и Publish для интерфейса INotification, в который будет передан объект.

Обычно при работе с библиотекой MediatR используются интерфейсы IRequest и IRequestHandler. Тип, используемый медиатором, должен быть унаследован от IRequest<TResponse>, где TResponse – результат обработки запроса, а обработчик должен поддерживать интерфейс IRequestHandler<TRequest, TResponse>, где TRequest – созданный нами тип с интерфейсом IRequest. Но нужно помнить, что обработчик здесь может быть только один.

Для случая множества обработчиков в MediatR был создан интерфейс INotification. И, соответственно, обработчики должны поддерживать интерфейс INotificationHandler<TRequest>.

Рассмотрим пример (необходимо установить nuget-пакет MediatR и nuget-пакет Microsoft.Extensions.DependencyInjection):

using System.Reflection; using MediatR; using Microsoft.Extensions.DependencyInjection;  class Program {     static async Task Main()     {         var serviceCollection = new ServiceCollection()             .AddMediatR(cfg =>          cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))             .BuildServiceProvider();          var mediator = serviceCollection.GetRequiredService<IMediator>();         EmailService emailService = new EmailService(mediator, "hr@disney.com");          await emailService.SendMailToUserAsync(             emailTo: "Milo.Murphy@disney.com",             subject: "Welcome to Disney",             body: "Today is your first day...");     } }  class MailSentRequest : INotification {     public string? EmailFrom { get; init; }     public string? EmailTo { get; init; }     public string? Subject { get; init; }     public string? Body { get; init; } }  class ConsoleHandler : INotificationHandler<MailSentRequest> {     public Task Handle(         MailSentRequest request,         CancellationToken cancellationToken) { Console.WriteLine( $"[Консоль] Письмо с темой '{request.Subject}' отправлено с адреса '{request.EmailFrom}' на адрес '{request.EmailTo}': {request.Body}"); return Task.CompletedTask; } }  class FileHandler : INotificationHandler<MailSentRequest> {     public Task Handle(         MailSentRequest request,         CancellationToken cancellationToken) { Console.WriteLine( $"[Файл] Письмо с темой '{request.Subject}' отправлено с адреса '{request.EmailFrom}' на адрес '{request.EmailTo}': {request.Body}"); return Task.CompletedTask; } }  class EmailService {     private readonly string _emailFrom;     private readonly IMediator _mediator;      public EmailService(IMediator mediator, string emailFrom)     {         _mediator = mediator;         _emailFrom = emailFrom;     }      public async Task SendMailToUserAsync(         string emailTo,         string subject,         string body)     {         // Отправить письмо пользователю         //Send(_emailFrom, emailTo, subject, body);         // Вызываем метод запуска события         await MailSentAsync(_emailFrom, emailTo, subject, body);     }      protected async Task MailSentAsync(string emailFrom, string emailTo, string subject, string body)     {         var request = new MailSentRequest         {             EmailFrom = emailFrom,             EmailTo = emailTo,             Subject = subject,             Body = body         };         await _mediator.Publish(request);     } } 

Результат:

[Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... [Файл] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day...

Плюсы и минусы использования MediatR:

Плюс

  • использование MediatR уменьшает количество зависимостей, что будет плюсом при большом количестве объектов и связей между ними.

Минусы

  • классы хэндлеров помечаются не используемыми (это можно исправить, навесив атрибут [UsedImplicitly]);

  • нельзя «по щелчку» перейти к реализации;

  • не всегда очевидно, какие хэндлеры будут вызваны при вызове медиатора;

  • не получится во время выполнения программы добавить/удалить обработчик;

  • невозможно (или сложно) установить четкий порядок вызова обработчика. Он больше подходит для вызова независимых друг от друга обработчиков.

Есть способ обойти второй минус из перечисленных. Нужно реализацию запроса и обработчик поместить в один partial-класс:

public partial class MailSent {   public class Request : INotification   { public string? EmailFrom { get; init; } public string? EmailTo { get; init; } public string? Subject { get; init; } public string? Body { get; init; }   } }  public partial class MailSent {   public class ConsoleHandler : INotificationHandler<Request>   {     public Task Handle( Request request, CancellationToken cancellationToken) { Console.WriteLine( $"[Консоль] Письмо с темой '{request.Subject}' отправлено с адреса '{request.EmailFrom}' на адрес '{request.EmailTo}': {request.Body}"); return Task.CompletedTask; }   } }  public partial class MailSent {   public class FileHandler : INotificationHandler<Request>   {     public Task Handle( Request request, CancellationToken cancellationToken) { Console.WriteLine( $"[Файл] Письмо с темой '{request.Subject}' отправлено с адреса '{request.EmailFrom}' на адрес '{request.EmailTo}': {request.Body}"); return Task.CompletedTask; }   } } 

тогда при создании запроса нужно указывать оба класса:

protected async Task MailSentAsync( string emailFrom, string emailTo, string subject, string body) { var request = new MailSent.Request { EmailFrom = emailFrom, EmailTo = emailTo, Subject = subject, Body = body }; await _mediator.Publish(request); }

При попытке перейти «по щелчку» к классу MailSent нам будет предложен выбор перейти к запросу или к реализации.

 Дополнительные возможности

В MediatR есть удобная реализация поведения конвейера (pipeline behavior). Для этого используется интерфейс IPipelineBehavior<TRequest, TResponse>.

services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MailSentBehavior<,>));     class MailSentBehavior<TRequest, TResponse>      : IPipelineBehavior<TRequest, TResponse>  {       public async Task<TResponse> Handle(             TRequest request,             RequestHandlerDelegate<TResponse> next,             CancellationToken cancellationToken)       {             try             {                   Console.WriteLine($"Перед запуском {typeof(TRequest).Name}");                   return await next();             }             finally             {                   Console.WriteLine($"После запуска {typeof(TRequest).Name}");             }     }   }

Так, можно добавить какую-то обработку для каждого вызова Handle из классов унаследованных от IRequestHandler. Например, логирование, на примере которого мы рассматривали реализацию обработки событий.

К сожалению, мы можем использовать PipelineBehavior только для  IRequest и не можем для INotification.

Чтобы реализовать подобную функциональность для событий нужно переопределить NotificationPublisher.

class Program {     static async Task Main()     {         var serviceCollection = new ServiceCollection()             .AddMediatR(config =>         {            config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());            config.NotificationPublisher = new MailSentPublisher();            config.NotificationPublisherType = typeof(MailSentPublisher);         })             .BuildServiceProvider();          var mediator = serviceCollection.GetRequiredService<IMediator>();         EmailService emailService =                 new EmailService(mediator: mediator, emailFrom: "hr@disney.com");          await emailService.SendMailToUserAsync( emailTo: "Milo.Murphy@disney.com", subject: "Welcome to Disney", body: "Today is your first day...");     } }  class MailSentPublisher : INotificationPublisher {     public async Task Publish( IEnumerable<NotificationHandlerExecutor> handlerExecutors, INotification notification, CancellationToken cancellationToken)     {         foreach (var handler in handlerExecutors)         {             try             {                 Console.WriteLine( $"Перед запуском {handler.HandlerInstance.GetType().Name}");                 await handler.HandlerCallback(notification, cancellationToken) .ConfigureAwait(false);             }             catch (Exception e)             {                 Console.WriteLine($"Произошла ошибка {e.Message}");             }             finally             {                 Console.WriteLine( $"После запуска {handler.HandlerInstance.GetType().Name}");             }         }     } } 

Результат:

Перед запуском ConsoleHandler [Консоль] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... После запуска ConsoleHandler Перед запуском FileHandler [Файл] Письмо с темой 'Welcome to Disney' отправлено с адреса 'hr@disney.com' на адрес 'Milo.Murphy@disney.com': Today is your first day... После запуска FileHandler

Так, мы добавили некую обработку до и после запуска каждого обработчика события, а еще обеспечили выполнение следующих, даже если в предыдущем возникла ошибка.

Вместо заключения

Мы рассмотрели несколько вариантов реализации обработчиков Событий: с помощью стандартных средств C#, с помощью средств библиотеки MediatR и написали самостоятельно, реализовав паттерн Наблюдатель. В разных ситуациях может быть удобно использовать разные варианты, но полезно знать и об альтернативных возможностях.


ссылка на оригинал статьи https://habr.com/ru/articles/809467/