Недавно я натолкнулся в нашем коде на использование пакета MediatR. Это заинтересовало меня. Почему я должен использовать MediatR? Какие преимущества он мне предоставляет? Здесь я собираюсь рассмотреть эти вопросы.

Как пользоваться MediatR

На базовом уровне использование MediatR очень просто. Сначала вы устанавливаете NuGet пакет MediatR. В вашем приложении будет несколько описаний той работы, которую ему нужно выполнять (например, создать запись ToDo, изменить имя пользователя и т. д.). Эти описания в MediatR называются запросами (requests). Это обычные классы, реализующие интерфейс IRequest. Он является маркер-интерфейсом без всяких членов.

class CreateToDoItem : IRequest<int>
{
    public string ToDoItemText { get; set; }
}

Эти классы могут не содержать никакой логики. Они представляют собой просто контейнеры данных, необходимых для выполнения операций.

Но что означает параметр T в интерфейсе IRequest? Видите ли, ваши операции могут возвращать некоторые результаты. Например, при создании записи ToDo вам может потребовать получить ID этой записи. Именно для этого и служит параметр T. В нашем случае, мы хотим получить целочисленный идентификатор записи ToDo.

Теперь нам нужен код, который, собственно, и будет выполнять нашу операцию. В MediatR этот код называется обработчиком запроса (request handler). Обработчики запроса должны реализовывать интерфейс IRequestHandler<TRequest, TResponse>, где TRequest должен быть IRequest<TResponse>:

class CreateToDoItemHandler : IRequestHandler<CreateToDoItem, int>
{
    public Task<int> Handle(CreateToDoItem request, CancellationToken cancellationToken)
    {
        ...
    }
}

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

Всё, что осталось нам сделать, это соединить запрос с соответствующим обработчиком. MediatR использует для этого контейнер зависимостей. Если вы разрабатываете приложение ASP.NET Core, вы можете воспользоваться пакетом MediatR.Extensions.Microsoft.DependencyInjection. Но MediatR поддерживает и массу других контейнеров.

services.AddMediatR(typeof(Startup));

Здесь services - экземпляр интерфейса IServiceCollection, который обычно доступен вам в методе ConfigureServices класса Startup. Данная команда сканирует сборку, в которой определён класс Startup, и находит там все обработчики запросов.

Теперь вы можете выполнять ваши запросы. Для этого вам потребуется получить экземпляр интерфейса IMediator. Он регистрируется в вашем контейнере зависимостей той же командой AddMediatR.

var toDoItemId = await mediator.Send(createToDoItemRequest);

Вот и всё. MediatR найдёт соответствующий обработчик запроса, выполнит его и вернёт вам результат.

И здесь мы переходим к главному вопросу.

Зачем мне нужен MediatR?

Давайте представим себе, что у нас есть ASP.NET Core контроллер, который поддерживает операции работы с записями ToDo. Мы сравним, как можно реализовать создание такой записи с использованием MediatR и без него. Вот как выглядит код без MediatR:

[ApiController]
public class ToDoController : ControllerBase
{
    private readonly IToDoService _service;

    public ToDoController(IToDoService service)
    {
        _service = service;
    }

    [HttpPost]
    public async Task CreateToDoItem([FromBody] CreateToDoItem createToDoItemRequest)
    {
        var toDoItemId = await _service.CreateToDoItem(createToDoItemRequest);

        return Ok(toDoItemId);
    }
}

А вот так выглядит реализация, использующая MediatR:

[ApiController]
public class ToDoController : ControllerBase
{
    private readonly IMediator _mediator;

    public ToDoController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task CreateToDoItem([FromBody] CreateToDoItem createToDoItemRequest)
    {
        var toDoItemId = await _mediator.Send(createToDoItemRequest);

        return Ok(toDoItemId);
    }
}

Вы видите здесь какие-нибудь серьёзные преимущества MediatR? Я - нет. На самом деле мне кажется, что версия с MediatR чуть менее читабельна. Она использует обобщённый метод Send вместо более осмысленного CreateToDoItem.

Так зачем же мне использовать MediatR?

Ссылки

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

Но лично я не вижу здесь большого преимущества. Да, вам будет удобнее выполнять некоторые изменения в вашем проекте. Но в то же время вы получите и ряд трудностей. Из кода вашего контроллера вы не можете видеть, кто в действительности выполняет ваш запрос. Чтобы найти обработчик для экземпляра CreateToDoItem, вы должны знать, что такое MediatR, и как он работает. Здесь нет ничего особенно сложного. В конце концов, IToDoService так же не является реализацией обработчика, вам приходится искать классы, реализующие данный интерфейс. Тем не менее у новичка уйдёт больше времени, чтобы понять, что здесь происходит.

Единственная ответственность

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

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

Другими словами, если вы собираетесь отстрелить себе ногу, у вас всё ещё есть масса вариантов, как сделать это.

Декораторы

Но существует ещё одно очень важное преимущество MediatR. Видите ли, все ваши обработчики запросов реализуют один и тот же интерфейс IRequestHandler. Это означает, что вы можете создавать декораторы, применимые ко всем ним. В ASP.NET Core вы можете использовать пакет Scrutor для поддержки декораторов. Например, вы можете написать декоратор для логирования так:

class LoggingDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IRequestHandler<TRequest, TResponse> _handler;
    private readonly Logger _logger;

    public LoggingDecorator(IRequestHandler<TRequest, TResponse> handler,
        Logger logger)
    {
        _handler = handler;
        _logger = logger;
    }

    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
    {
        _logger.Log("Log something here.");

        return _handler.Handle(request, cancellationToken);
    }
}

Теперь зарегистрируйте его:

services.AddMediatR(typeof(Startup));
services.Decorate(typeof(IRequestHandler<,>), typeof(LoggingDecorator<,>));

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

Но зачем вообще связываться со Scrutor? MediatR предоставляет вам ту же функциональность с поведениями конвейера (pipeline behaviors). Создайте класс, реализующий IPipelineBehavior:

class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly Logger _logger;

    public LoggingBehavior(Logger logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next)
    {
        try
        {
            _logger.Log($"Before execution for {typeof(TRequest).Name}");

            return await next();
        }
        finally
        {
            _logger.Log($"After execution for {typeof(TRequest).Name}");
        }
    }
}

Зарегистрируйте его:

services.AddMediatR(typeof(Startup));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

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

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

interface ITransactional { }

class CreateToDoItem : IRequest<int>, ITransactional
    ...

Как применить поведение только к тем запросам, которые помечены ITransactional? Вы можете использовать ограничения на типы:

class TransactionalBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : ITransactional
    ...

Проделать то же самое с декораторами Scrutor нельзя. Если вы реализуете декоратор вот так:

class TransactionalDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>, ITransactional
    ...

вы не сможете использовать его, если у вас есть хотя бы один запрос, не реализующий ITransactional.

Создавая поведения конвейера, помните, что они выполняются при каждом вызове метода Send. Это может быть важно, если вы посылаете запросы изнутри обработчиков запросов:

class CommandHandler : IRequestHandler<Command, string>
{
    private readonly IMediator _mediator;

    public CommandHandler(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task<string> Handle(Command request, CancellationToken cancellationToken)
    {
        ...

        var result = await _mediator.Send(new AnotherCommand(), cancellationToken);

        ...
    }
}

Если и Command, и AnotherCommand помечены с помощью ITransactional, соответствующее поведение TransactionalBehavior будет выполнено дважды. Поэтому убедитесь, что вы не создаёте две отдельные транзакции.

Другая функциональность

MediatR предоставляет вам и другую функциональность. Так он поддерживает механизм уведомлений (notification). Он может оказаться очень полезным, если вы в вашей системе используете доменные события. Все классы событий должны реализовывать маркер-интерфейс INotification. И вы можете создать любое количество обработчиков для конкретного типа событий с помощью интерфейса INotificationHandler. Разница между запросами и уведомлениями следующая. Запрос передаётся только одному единственному обработчику. Уведомления передаются всем зарегистрированным обработчикам для данного типа уведомлений. Также от обработчика запроса вы можете получить некоторый результат его обработки. Уведомления не позволяют получить никакого результата. Используйте метод Publish для рассылки уведомлений.

MediatR также предоставляет механизм обработки исключений. Он довольно изощренный, и вы можете почитать о нём здесь.

Заключение

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

Комментарии (30)


  1. melt
    12.11.2021 20:49
    +1

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


  1. dopusteam
    13.11.2021 09:05
    +1

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

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

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

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

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

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

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

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

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

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

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

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

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

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


  1. TimeCoder
    13.11.2021 09:39
    -1

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

    var toDoItemId = await _mediator.Send(createToDoItemRequest);

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

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


  1. S-type
    13.11.2021 10:39

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

    Опечатка?


  1. RouR
    13.11.2021 13:49

    Подход не нов, что-то похожее видел в 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;
                }
            }
        }
    


    1. Zaphkiel
      14.11.2021 10:38

      А в каком проекте должен тогда находиться класс RegisterByEmail?

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


      1. RouR
        14.11.2021 11:21

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

        Я так делал когда создавал монолиты. С переходом на небольшие микросервисы это всё кладу в один проект, просто в разных директориях. На хабре была статья про 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; }
            
               ...
            
            
            }


  1. koodeer
    13.11.2021 15:59

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

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

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


  1. uhfath
    13.11.2021 16:00

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


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


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

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


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


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


    1. piton_nsk
      13.11.2021 16:33

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

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


      1. uhfath
        13.11.2021 16:37

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


        1. dopusteam
          13.11.2021 16:39

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

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

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


          1. uhfath
            13.11.2021 17:48

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


            Пример сервиса
            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. Никаких контрактов сервиса не нарушаем (его просто нет, либо он внутренний для хендлера). Ничего по коду меняем. Более того, можно по окончанию валидации запустить какой-нибудь общий ивент.


            1. dopusteam
              13.11.2021 18:03

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

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

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

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


              1. uhfath
                13.11.2021 18:46

                Какой то у Вас надуманный пример)
                Как часто Вы на асинхронные операции переделываете? Ну, максимум один раз.
                Т.е. по факту плюс только в том, что заставляет везде 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.


                1. dopusteam
                  13.11.2021 18:47

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


                  1. uhfath
                    13.11.2021 18:49

                    Команду. Через Send().


                    1. dopusteam
                      13.11.2021 18:56

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

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

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


                      1. uhfath
                        13.11.2021 18:58

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


                      1. dopusteam
                        13.11.2021 19:03

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

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

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

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


                      1. uhfath
                        13.11.2021 19:11

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

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

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

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


                      1. dopusteam
                        13.11.2021 19:24

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

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

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


                      1. uhfath
                        13.11.2021 19:56

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

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

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

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

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

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

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

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

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

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


                      1. piton_nsk
                        13.11.2021 22:49

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

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

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

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


  1. andreyverbin
    14.11.2021 20:09

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

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


    1. den_labs
      14.11.2021 20:45

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



      1. andreyverbin
        15.11.2021 09:34

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


  1. win32nipuh
    17.11.2021 11:39

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

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


    1. iakimov Автор
      17.11.2021 17:57

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

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