События – это объекты, которые получают уведомления о некотором действии в разрабатываемом ПО и могут запускать реакции на это действие. Разработчик может определить эти действия, добавив к событию обработчик. Разберем в этом материале само понятие событий в .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 и написали самостоятельно, реализовав паттерн Наблюдатель. В разных ситуациях может быть удобно использовать разные варианты, но полезно знать и об альтернативных возможностях.
Комментарии (8)
Prikalel
23.04.2024 08:56nice
спасибо за статью но хотелось бы пж и ответы на тонкие вопросы типа "сожрёт ли gc объект вышедший из употребления но на евент которого до сих пор висит обработчик," в ту же оперу "надо ли снимать все обработчики с объекта чтобы его сожрал gc".
ну и например где в итоге обработчики выполняться будут ? в том же потоке и выполнение последующих команд приостановится пока все обработчики не завершат свое выполнение?
или "а есть ли лимит на кол-во обработчиков " ну и в придачу можно ли сделать event в static class-е чтобы ему не надо было при invoke передавать this.
за медиатор спасибо конечно, но стоит упомянуть что без DI медиатор не сработает и есть ли альтернативы которые не используют DI но предоставляют все те же плюшки.
IrinaMakovetskaya Автор
23.04.2024 08:56+2Отвечу на вопросы по пунктам:
да, GC удалит объект в котором есть евент с обработчиком, так как это объект ссылается на обработчик, а не наоборот.
Вполне естественно, что не асинхронный обработчик будет выполняться в том же потоке, до остальных действий, мало того, если в обработчике произойдет не обработанное исключение - до остальных команд дело так и не дойдет. Если обработчик асинхронный - можно запустить его без ожидания выполнения, но проблема в том, что при удалении объекта - обработчики, которые не успели выполниться, не выполненятся.
Так как ссылка на следующий обработчик добавляется в конец предыдущего - явного ограничения количества обработчиков нет.
Объявить событие в статическом классе возможно, тогда в Invoke надо передавать null
MediatR использовался в нашем проекте, где DI был необходим (ссылка на nuget есть в статье), и рассматривался именно как альтернатива обычным евентам, если нужно уменьшить количество зависимостей
andrey_stepanov1
23.04.2024 08:56Например, у нас в руках была сахарница с синтаксическим сахаром, и мы сахар из нее рассыпали прямо в на наш язык программирования...
А если серьезно: круто было сделать что-то вроде блок-схемы для принятия решения, что использовать ивенты, делегаты или MediatR.
Prikalel
23.04.2024 08:56она будет одинакова для всех этих счастьей.
другое дело что ты можешь, например, в handler в MediatR через конструктор передать любой сервис и таким образом избавиться от внедрения там где это не нужно, а в случае с ивентами придётся руками создать оба объекта и один с другим связать.
Okunev_PY
23.04.2024 08:56Намешали пресное с солёным, да и пример с сахарницей как и любые за уши притянутые с бургерами, ничего не объяснил, а только жути нагнал.
Паттерн наблюдатель никакого не имеет отношения к событиям. Это разные вещи, и зауши притягивать MediatR и его реализацию к событиям net вообще не стоит.
Статья вообще не расскрывает сути как устроены события, что такое просто deligate и чем он отличаеться от multicast delegate.
Про вызов событий не верно написано, в каком потоке будет вызвано события зависит от того как вызываеться событие. Invoke вызовет в потоке в котором вызываеться событие. BeginInvoke сделает вызов с переключение контекста на сторону обработчика.
Если не отписываться от событий то GC такой объект не удалит, для него это зависшие ссылки.
В общем можно долго продолжать, но посыл я думаю понятен.
ryanl
23.04.2024 08:56но посыл я думаю понятен.
Понятно только то, что в вашем посте больше ерунды написано чем здравой критики.
brn
Нет, это уже обязательно.
https://learn.microsoft.com/en-us/dotnet/csharp/modern-events
IrinaMakovetskaya Автор
Видимо Вы имели ввиду: теперь не обязательно. Спасибо за замечание, действительно обязательность наследования от System.EventArgs убрали начиная с .NET Framework 4.5