События – это объекты, которые получают уведомления о некотором действии в разрабатываемом ПО и могут запускать реакции на это действие. Разработчик может определить эти действия, добавив к событию обработчик. Разберем в этом материале само понятие событий в .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 и написали самостоятельно, реализовав паттерн Наблюдатель. В разных ситуациях может быть удобно использовать разные варианты, но полезно знать и об альтернативных возможностях.
