Pull to refresh

Comments 34

Спасибо за статью! Хотел бы добавить, что несколько обработчиков разных запросов можно аналогично с сервисами объединять в один класс, чтобы выносить общую логику в приватные методы, тут нет никаких ограничений и рефакторинг доступен. И сам медиатор используется часто для разделения команд и запросов при использвовании CQRS.

В коде контроллера вы нигде не ссылаетесь на класс CreateToDoItemHandler. Это означает, что вы можете двигать этот класс в пределах одной сборки как вам угодно, и вам не потребуется ничего изменять в вашем контроллере.

Сделайте интерфейс для сервиса и точно так же можно двигать куда угодно реализацию. Соглашусь с Вами, что тут никаких преимуществ mediatr не даёт, а наоборот, усложняет навигацию. Перейти к реализации интерфейса из контроллера - один клик. Найти реализацию обработчика команды - n кликов.

Следующее отличие более важно. Видите ли, ваш обработчик запросов - это класс. И весь этот класс ответственен за выполнение единственной операции

Конечно, мы можем использовать наследование и вынести всё, что нужно, в базовый класс

Это означает, что сервис может содержать множество различных методов, ответственных за разные операции

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

Так что тут тоже преимуществ никаких.

Например, вы можете написать декоратор для логирования так

Имхо, это проще сделать на уровне middleware, потому что будет проще и очевиднее для любого человека в команде.

Это может быть важно, если вы посылаете запросы изнутри обработчиков запросов

Поэтому убедитесь, что вы не создаёте две отдельные транзакции

Вот в этом сама суть mediatr, что он делает многие вещи неявными (в плохом смысле). Он может упасть в рантайме, при попытке зарезолвить обработчик, если я забыл его зарегать. Он не скажет мне, что для события, которое я бросил, нет обработчика. С его использованием можно легко заполучить циклические зависимости, т.е. я могу из сервиса1 вызывать команду, которую обрабатывает сервис2 и вызывает команду, которую обрабатывает сервис1 и т.д. В случае явных вызовов у меня это упадёт где нибудь на этапе резолва.

Ещё у него есть "замечательный" функционал валидации, который тоже выполняется неявно где то там. Ну т.е. команда может содержать поле string с некоторыми правилами (непустное, например). Делаем валидацию, она что то проверяет и всё. В самом обработчике я надеюсь, что кто то где то там проверил всё, но я в этом не могу быть уверенным. Встречал вопрос, "нужно ли писать тесты, что валидатор вызвался или нет"?

Дабы не выглядело как вброс, я так и не увидел ответ на вопрос, какую реальную проблему решает mediatr? Тот же CQRS спокойно реализуется и без mediatr. Взамен он лишает Вас поддержки компилятора, навигации по коду, заставляет держать в голове неявный контекст и позволяет писАть более запутанный код.

Соглашусь, что проблем эта штука приносит больше, чем решает, больше похоже на хайп. Например,

var toDoItemId = await _mediator.Send(createToDoItemRequest);

Выбор вызываемого метода определяется типом параметра. Если у меня контроллер для работы с каким-то обьектом, и 5 разных действий - мне придётся сделать 5 классов (одинаковое содержимое, разные названия)?!

А если аргумент - просто целое число, и таких методов много, как тогда?

В вашем приложении будет несколько описаний той работы, которую ему нежно выполнять (например, создать запись ToDo, изменить имя пользователя и т. д.).

Опечатка?

Подход не нов, что-то похожее видел в servicestack

Прежде всего, MediatR отделяет обработчики запросов от самих запросов.

Я считаю это неудобным.

Но обработчики запросов - отдельные классы. Конечно, мы можем использовать наследование и вынести всё, что нужно, в базовый класс.

Не наследование, надо использовать DI.

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

    [Route("register")]
    [ApiController]
    public class RegisterController
    {        
        [ApiVersion("1.0")]
        [HttpPost("email")]
        public async Task<RegisterByEmail.RegisterByEmailResponse> RegisterByEmail([FromBody] RegisterByEmail.RegisterByEmailRequest request, [FromServices] RegisterByEmail.Handler handler)
        {
            var result = await handler.Handle(request); // это можно улучшить и писать этот бойлерплейт, но считайте что это просто пример
            return result;
        }
		...
        
    }

    public class RegisterByEmail
    {
        public class RegisterByEmailRequest : IRequest<RegisterByEmailResponse>, IValidateable, IHaveUserId, IRequestData
        {
            public string Email { get; set; }
            public string Password { get; set; }
            ...
        }

        public class RegisterByEmailResponse : IErrorable<bool>
        {
            public bool HasError { get; set; }
            public string Message { get; set; }
            public ErrorCode ErrorCode { get; set; }
            public string[] Errors { get; set; }
            public bool Data { get; set; }
        }

        private class Validator : AbstractValidator<RegisterByEmailRequest>
        {
            public Validator()
            {
                RuleFor(field => field.UserId).NotNull().NotEmpty().NotEqual(UserId.Default());
                RuleFor(field => field.Email).NotEmpty().EmailAddress().MaximumLength(Settings.StringMaxLength);
                RuleFor(field => field.Password).NotEmpty().MinimumLength(Settings.PasswordMinLength).MaximumLength(Settings.StringMaxLength);
                ...
            }
        }

        public class Handler : IRequestHandler<RegisterByEmailRequest, RegisterByEmailResponse>
        {
            private readonly IAccountManager _accountManager;
            private readonly ApplicationDbContext _dbContext;
            private readonly ILogger _logger;
            ...

            public Handler(ITracer tracer, ApplicationDbContext dbContext, ...)
            {
                _tracer = tracer;
                _dbContext = dbContext;
                ...
            }

            public async Task<RegisterByEmailResponse> Handle(RegisterByEmailRequest request)
            {
                ...
                return result;
            }
        }
    }
А в каком проекте должен тогда находиться класс RegisterByEmail?

Обычно, Requests & Responses лежат где-то в отдельном проекте контрактов, а сама реализация в виде Handler лежит уже там где нужно. Как поступать в таком случае?

Рискну предположить что у вас в солюшене отдельный проект с веб-контроллерами, отдельный проект с бизнес-логикой, и т.д. Отсюда и проблема куда что положить.

Я так делал когда создавал монолиты. С переходом на небольшие микросервисы это всё кладу в один проект, просто в разных директориях. На хабре была статья про feature-per-folder. Впрочем, есть некоторые исключения для доменых моделей и инфраструктуры, но что касается Requests & Responses - они лежат в том же проекте где и контроллеры и хендлеры.

Что касается контрактов, по swagger спецификации генерируется клиент (C# и/или Typescript) для микросервиса и да, он является отдельным проектом, который импортят все кто хотят работать с этим микросервисом.

//----------------------
// <auto-generated>
//     Generated using the NSwag toolchain v13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------

namespace Client.Account.V1_0
{
    using System = global::System;

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v12.0.0.0))")]
    public partial interface IAccountClient_V1_0
    {
      System.Threading.Tasks.Task<RegisterByEmailResponse> RegisterEmailAsync(string api_version = null, RegisterByEmailRequest body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));    
      ...
    }
    
...    

[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v12.0.0.0)")]
    public partial class RegisterByEmailRequest 
    {
        [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Email { get; set; }
    
        [Newtonsoft.Json.JsonProperty("password", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Password { get; set; }
    
       ...
    
    
    }

Для простых проектов Mediatr является оверхедом.

Для сложных, использующих out-of-process запросы, - непригоден по причине того, что выполняет запросы in-process. В этом случае лучше сразу брать что-то наподобие MassTransit.

Есть ли золотая середина, где Mediatr окажется к месту? Возможно.

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


Да, можно сделать интерфейс сервиса и отдать контроллеру.
Один интерфейс, ещё один, потом ещё парочку.
Или сказать всем контроллерам "работай только через этот сервис" и "используя вот эти DTO (request)".
А вот реализации уже могут лежать где угодно.
С одной стороны свобода действий. Но с другой — вероятность хаоса (хендлеры разбросаны повсюду). Поэтому важно правильно организовать процесс. У нас примерно так:


Features
|-- Queries
    |-- AppQuery
        |-- AppQuery.Handler <-- вложенный приватный класс
|-- Commands
    |-- AppCommand
        |-- AppCommand.Handler <-- вложенный приватный класс

В таком варианте CTRL+CLICK по запросу и сразу виден его хендлер.
Но подходит только для схемы "один запрос, один хендлер".
Ну и плюс ивенты. Бывает удобно.


А ещё может понадобиться для всех хендлеров добавить одну общую обработку. Логгирование как пример, но может какой-то обработчик ошибок или спец. валидатор.


В общем штука интересная для проектов с кучей контроллеров.
Для проектов попроще добавляет больше boilerplate кода.

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

Можно, но зачем?

Что бы меньше связей отслеживать при изменениях.
Можно провести аналогию с микросервисами, которые общаются через брокер (RabbitMq, Kafka и т.п.).
Один микросервис постит сообщение (запрос или команду) в брокер, не зная кто его будет обрабатывать. А обрабатывать его будет "кто-то". И вот этого "кто-то" можно в любой момент поменять. Даже никаких контрактов соблюдать не надо. Главное уметь обрабатывать запрос.

А можете в виде проблемы сформулировать? Т.е. проблема в том, что при изменении (чего?) нужно отслеживать больше связей?

Даже никаких контрактов соблюдать не надо

Вот это странно звучит. Мне кажется, аналогия с брокерами не совсем уместна тут

Ну простой пример, первый пришедший в голову.
Имеем данные и сервис-валидатор, описанный контрактом:


Пример сервиса
public class SomeData
{
    public int SomeValue { get; set; }
}

public interface IValidatorService
{
    public IEnumerable<string> Validate(SomeData someData);
}

internal class ValidatorService : IValidatorService
{
    public IEnumerable<string> Validate(SomeData someData)
    {
        if (someData.SomeValue != 10)
        {
            return new[]
            {
                "Something must have gone very wrong!", 
            };
        }

        return Enumerable.Empty<String>();
    }
}

Ничего необычного. Используем тоже по простому:


Пример использования
void Test(IValidatorService validator)
{
    var badData = new SomeData
    {
        SomeValue = 20,
    };

    validator.Validate(badData).Dump("Bad");

    var goodData = new SomeData
    {
        SomeValue = 10,
    };

    validator.Validate(goodData).Dump("Good");
}

И теперь нам понадобилось сделать валидацию асинхронной (ну не подумал никто об этом заранее, бывает).
Для этого нам надо переделать и контракт и реализацию и все места, где сервис используется.
В случае с MediatR мы бы просто вызвали бы "какой-то" обработчик, передав ему SomeData. Никаких контрактов сервиса не нарушаем (его просто нет, либо он внутренний для хендлера). Ничего по коду меняем. Более того, можно по окончанию валидации запустить какой-нибудь общий ивент.

Какой то у Вас надуманный пример)

Как часто Вы на асинхронные операции переделываете? Ну, максимум один раз.

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

А я вот приведу реальный пример, когда я бросал событие через mediatr, но забыл зарегать реализацию. (Сразу скажу, что такая ситуация может повториться при условном мерже или рефакторинге) и медиатр ничего не сказал. Ну т.е. это логично, наверное, но странно

Какой то у Вас надуманный пример)
Как часто Вы на асинхронные операции переделываете? Ну, максимум один раз.
Т.е. по факту плюс только в том, что заставляет везде task в качестве результата использовать. Сейчас, думаю, это практически везде по дефолту так. Есть реальные примеры?)

Достаточно взять любую ситуацию с необходимостью изменения контракта.


А я вот приведу реальный пример, когда я бросал событие через mediatr, но забыл зарегать реализацию. (Сразу скажу, что такая ситуация может повториться при условном мерже или рефакторинге) и медиатр ничего не сказал. Ну т.е. это логично, наверное, но странно

Эмм. Вот прямо сейчас в текущем проекте проверил. Убрал хендлер.


[18:43:49 ERR] An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Handler was not found for request of type MediatR.IRequestHandler`2[MedService.API.Logic.Features.Auth.Commands.SignInCommand,MedService.API.Contracts.Auth.SignInResponse]. Register your handlers with the container. See the samples in GitHub for examples.

А Вы событие бросили или команду выполняете?

Я про событие бросал, через publish, если не ошибаюсь уже,

Я понимаю, что специфика событий такова, что обработчиков может не быть.

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

Ну на мой взгляд это не корректно сравнивать — событие и вызов сервиса. Как вы одно другим заменяете?

Просто, медиатр как раз отвязывает нас от вызова сервиса, разве Вы не это изначально говорили?

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

Достаточно взять любую ситуацию с необходимостью изменения контракта

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

Просто, медиатр как раз отвязывает нас от вызова сервиса, разве Вы не это изначально говорили?

Да, но в Вашем примере речь про ивент, а не вызов сервиса.

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

Ну от ситуации зависит. Всё же это разные вещи.

Да, но в Вашем примере речь про ивент, а не вызов сервиса.

Потому что в обработчике команды у меня теперь диссонанс. Вызвать команду? Звучит странно, команду из обработчика команды, или можно? И насколько быстро мне станет тяжело понимать, что происходит?
Событие? Нужно убедиться, что оно перехватиться и это подходит только в том случае, если я допускаю ошибки в обработчике события, т.к. исключения, брошенные в процессе обработки события игнорятся - ну специфика такая, что событие бросил и забыл. Вызвать сервис? А на кой мне медиатор, чтоб только в контроллере его юзать?

Я выше упустил Ваш поинт про "Достаточно взять любую ситуацию с необходимостью изменения контракта", давайте к нему, чтоб выяснить всё таки, какие преимущества даёт mediatr и какие проблемы решает?
Если меняются входные параметры - в любом случае менять и вызов и реализацию. Если меняется возвращаемый результат - аналогично. А что ещё в контракт входит?

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

Событие? Нужно убедиться, что оно перехватиться и это подходит только в том случае, если я допускаю ошибки в обработчике события, т.к. исключения, брошенные в процессе обработки события игнорятся - ну специфика такая, что событие бросил и забыл. Вызвать сервис? А на кой мне медиатор, чтоб только в контроллере его юзать?

Опять же, от ситуации зависит. Думаю где-то вызвать команду вполне себе. Где-то ивент послать. Где запрос. Где-то сервис.

Нельзя так взять и сказать что лучше.

События они несколько про другое. Это больше асинхронное исполнение. Их не надо ждать. Как раз буквально "бросил и забыл".

Я выше упустил Ваш поинт про "Достаточно взять любую ситуацию с необходимостью изменения контракта", давайте к нему, чтоб выяснить всё таки, какие преимущества даёт mediatr и какие проблемы решает?

Если меняются входные параметры - в любом случае менять и вызов и реализацию. Если меняется возвращаемый результат - аналогично. А что ещё в контракт входит?

В принципе да, согласен. Пример в этом плане не подходящий.

В моём случае мне показалось проще использовать его для разделения функциональности. Не внедряя в контроллеры сервисы и разделяя функции по хендлерам потом проще в этом всём ориентироваться. Не создавать один сервис с кучей методов и использовать его в контроллере, а создавать на каждую функцию свой хендлер. Отделяя их по фичам.

"Проблема" которую он решил для меня - разделение функциональности по файлам.

"Проблема" которую он решил для меня - разделение функциональности по файлам.

Эта проблема решается партиал классом. Если разделять надо именно по файлам.

Не создавать один сервис с кучей методов и использовать его в контроллере

Ну так получается один контроллер с кучей методов. Как бы хрен редьки не слаще. Или я чего-то недопонял?

Взяли явный код и заменили неявным, кастанули заклинание «SOLID», которое даёт +10 от аргумента «говнокод» и все вроде бы хорошо. Но есть проблемы. Вернее нет проблем, которые бы нуждались в отдельном инструменте такого масштаба. В язык уже встроен шустрый механизм динамической диспетчеризации сообщений - vtbl, таблица виртуальных методов, которой хватает в 90% случаев. Ещё в .NET есть MulticastDelegate, который закроет минимум половину от оставшихся 10%. Ещё есть multiple dispatch через dynamic, что закроет ещё существенный кусок.

MediatR даёт нам тормозную, но более мощную реализацию vtbl. Ради оставшихся 10% проще написать свой велосипед на 50 строк, использовать middleware, написать action filter или просто хардкодить. Любое из этих решений проще поддерживать.

Как раз хотел спросить про производительность. Он действительно тормозной?

Тормоза понятие относительно, MediatR точно медленнее чем vtbl, важно это или нет, зависит от контекста.

А как кидать доменные события без медиатора или его аналога?

Ну, есть способ делать это с помощью dependency container. Для каждого события создаётся свой интерфейс-слушатель (какой-нибудь ISomeEventHandler). Все, кто хочет слушать это событие, реализуют этот интерфейс и регистрируются в контейнере. Когда кому-то нужно сгенерировать данное событие, он выгребает из контейнера всё, что реализует данный интерфейс, и передаёт им данные события.

Да, но ведь тогда получится тот же mediatr только в виде велосипеда, ведь он делает то же самое - регистрирует хендлеры в DI, а затем резолвит их от туда

Да. Но у вас больше контроля за тем, как это всё работает. Но в конце концов решать вам, что использовать.

Есть старое развесистое WinForms приложение, надо сделать в нем что-то типа: в форме Settings изменили установки и другие формы, в которых эти установки используются отреагировали. Оставим пока невозможность всегда реагировать без рестарта и т.д., предположим всегда можно.

Хотел использовать Reactive , но подумал, что можно MediatR notifications. Подскажите, возможно ли MediatR notifications.

Я думаю, что отправка уведомлений с помощью MediatR будет для вас проще, чем с использованием Reactive. В случае Reactive вам необходимо будет и в местах отправки уведомлений, и в местах подписки на них иметь ссылки на объекты Reacitve (например, на ISubject). В случае MediatR нужно только иметь ссылку на IMediator и только в местах отправки уведомлений.

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

Sign up to leave a comment.

Articles