Как стать автором
Обновить

Использование библиотеки MediatR при реализации бизнес-логики в проектах, реализуемых на базе .NET

Время на прочтение10 мин
Количество просмотров4.7K

На просторах интернета появились библиотеки, позволяющие упростить и ускорить построение бизнес‑логики разрабатываемого приложения. Одна из таких библиотек — MediatR. В данной статье я хочу описать небольшой пример из реального проекта. Проект, web приложение, предназначен для автоматизации некоторого бизнес‑процесса. В рамках данного проекта была реализована задача по согласованию, где использовался инструментарий библиотеки MediatR. Я не буду уделять особого внимания моментам, связанным с установкой и настройкой данной библиотеки в проекте, выделю только то, что необходимо для решения нашей задачи.

Описание библиотеки MediatR

Несколько строк хочу посвятить описанию самой библиотеки. MediatR — это библиотека с открытым кодом, которая помогает реализовать поведенческий шаблон проектирования «Посредник» (Mediator). Шаблон «Посредник» способствует организации взаимодействия множества объектов без создания ссылок друг на друга, то есть отпадает необходимость в передаче объектам информации друг о друге. Это порождает слабую связанность, которая оказывает положительный эффект при изменении или доработках объектов. MediatR получил широкое применение при проектировании и разработке микросервисов. Ссылка на исходный код проекта библиотек располагается здесь. Бинарный код MediatR можно найти на сайте Nuget. В состав библиотеки входит обширный инструментарий для реализации логики разрабатываемых приложений, я остановлюсь на обработчиках и опишу способ их применения.

Разработка задачи

Вернемся к задаче. Внесу некоторое уточнение по технологиям, используемым в проекте. Проект использует ASP.NET MVC и Unity — библиотеку внедрения зависимостей.

Рисунок 1.
Рисунок 1.

Еще раз напомню, что в рамках данного проекта реализуется подзадача по согласованию неких событий, не будем углубляться в подробности, каких. В согласовании принимают участие несколько участников. После успешного согласования все ответственные должны получить оповещение о том, что согласование завершено, например, в виде электронного письма. Для большей наглядности изобразим алгоритм задачи в виде схемы, которая изображена на рисунке 1. Здесь возникает вопрос, как используемая библиотека поможет нам в реализации данного процесса? При изучении содержимого библиотеки MediatR, в ее составе можно обнаружить интерфейсы, которые используются при создании пользовательских классов. Рассмотрим только те, которые мы будем использовать при написании функционала для нашей задачи:

  1. IRequestHandler<IRequest,TResponse> интерфейс для реализации класса обработчика.

  2. IRequest интерфейс, который применяется при описании объекта запроса, используемого классом обработчика.

  3. IRequestPreProcessor — интерфейс для описания класса, который будет выполнятся перед вызовом класса обработчика.

  4. IRequestPostProcessor<TRequest, TResult> интерфейс для описания класса, который будет выполнятся после вызова класса обработчика.

Тут прослеживается некоторая логика поведения. При запросе основного обработчика (IRequestHandler) логика библиотеки MediatR запускает еще несколько вспомогательных классов обработки, которые наследуются от интерфейсов IRequestPreProcessor и IRequestPostProcessor. Первый вызывается перед запуском, а второй после завершения выполнения кода основного обработчика. Это правило поведения будет очень полезно при реализации задачи.

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

  1. StartHandler – запускает согласование.

  2. ApproveHadler – обрабатывает подтверждение со стороны участника согласования.

  3. RejectHandler – обрабатывает отклонение со стороны участника согласования.

Как я уже упоминал, все классы будут наследоваться от интерфейса:

IRequestHandler<IRequest,TResponse>

Реализуем их:

.. 
public class StartHandler : IRequestHandler<StartRequest, string>
{
     public Task<string> Handle(
                                StartRequest request, 
                                CancellationToken cancellationToken)
     {
          ..
     }
}
..
..
public class ApproveHandler : IRequestHandler<ApproveRequest, string>
{
     public Task<string> Handle(
                               ApproveRequest request, 
                               CancellationToken cancellationToken)
     {
           ..
     }
}
..
..
public class RejectHandler : IRequestHandler<RejectRequest, string>
{
     public Task<string> Handle(
                                RejectLRequest request,
                                CancellationToken cancellationToken)
     {
             ..
     }
    }
..

Объекты запроса StartRequest, ApproveRequest и RejectRequest опишем ниже.

..
public class StartRequest : BaseRequest 
{
}
..
public class ApproveRequest  : BaseRequest
{
   public string Comments { get; set; }

   public IConfig Iconfig { get; set; }
}
..
public class RejectRequest : BaseRequest
{
   public string Comments { get; set; }
}
..
public class BaseRequest : IRequest<string>
{
   public IUnitOfWork IUnitOfWork { get; set; }
   public IUserContext IUserContext { get; set; }
   public int? EventID { get; set; }
}
..

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

Приведу пример таких пре‑обработчиков и пост‑обработчиков.

..
public class GenericRequestPreProcessorBehavior<TRequest> : 
                        IRequestPreProcessor<TRequest>
{
    public Task Process(TRequest request, CancellationToken token)
    {
    //код предобработки, вызывается перед запуском основного обработчика
      ..
    }
}
..
public class GenericRequestPostProcessorBehavior<TRequest, TResult> :
                                  IRequestPostProcessor<TRequest, TResult>
{
    public Task Process(TRequest request, TResult result)
    {
            //постобработка после того, как согласование было запущено
            if (request is StartRequest)
            {
               //код для обработки после запуска согласования
                ..
            }
                ..
            //постобработка после отклонения мероприятия
            if (request is RejectRequest) {
                //код для обработки после отклонения согласования 
                //одним из участников
                ..
            }
                ..
            //постобработка после завершения согласования
            if (request is ApproveRequest) {
                //код для обработки после подтверждения одним 
                //из участников согласования
                ..
            }
            ..
            return Task.FromResult<TResult>(result);
        }
    }

Библиотека MediatR очень хорошо работает с различными типами IoC‑контейнеров. Здесь можно найти примеры настройки MediatR и различных библиотек, реализующих внедрение зависимостей. Как я упоминал выше, в нашем проекте используется библиотека Unity. Основной момент, на который нужно обратить внимание — это регистрация в контейнере внедрения зависимостей для наших основных обработчиков и вспомогательных обработчиков. Если этого не сделать, то функционал работать не будет. Приведу часть кода, где описана регистрация в IoC контейнере классов обработчиков.

..
public static class UnityConfig
{
    private static UnityContainer container;
    
    public static void RegisterComponents()
    {
        container = new UnityContainer();
        …
        StringBuilder sb = new StringBuilder();

        container.RegisterInstance<TextWriter>(new StringWriter(sb))
                    .RegisterMediator(new HierarchicalLifetimeManager())
                    .RegisterMediatorHandlers(
                        Assembly.GetAssembly(typeof(StartRequest)));

        container.RegisterInstance<TextWriter>(new StringWriter(sb))
                    .RegisterMediator(new HierarchicalLifetimeManager())
                    .RegisterMediatorHandlers(
                        Assembly.GetAssembly(typeof(RejectRequest)));

         container.RegisterInstance<TextWriter>(new StringWriter(sb))
                   .RegisterMediator(new HierarchicalLifetimeManager())
                   .RegisterMediatorHandlers(
                        Assembly.GetAssembly(typeof(ApproveRequest)));
            ...
         container.RegisterType(
                            typeof(IRequestPreProcessor<>),
                            typeof(GenericRequestPreProcessorBehavior<>),
                           "GenericRequestPreProcessorBehavior");
         container.RegisterType(
                           typeof(IRequestPostProcessor<,>),
                           typeof(GenericRequestPostProcessorBehavior<,>),
                           "GenericRequestPostProcessorBehavior");
           ...
          DependencyResolver.SetResolver(
                            new UnityDependencyResolver(container));
        }
    }

Нужно отметить, что используемые методы:

public static IUnityContainer RegisterMediator(
                   this IUnityContainer container, 
                   LifetimeManager lifetimeManager)
..

и

public static IUnityContainer RegisterMediatorHandlers(
                      this IUnityContainer container, 
                       Assembly assembly)
..

являются расширениями для контейнера внедрения зависимостей Unity и не являются частью библиотеки. Пример их реализации можно найти на просторах интернета, причем вариант реализации может зависеть от версии библиотеки MediatR и версии Framework .NET, поэтому я не буду приводить примеры реализации этих расширений в данной статье.   

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

..
/// <summary>
/// Контроллер для запуска, подтверждения и отмены согласования
/// </summary>
public class AgreementController : BaseController
{
     public AgreementController(IUnitOfWork IUnitOfWork) 
                                : base(IUnitOfWork)
     {
      }
     /// <summary>
     /// Запуск процесса согласования для события
     /// </summary>
     /// <param name=”id">идентификатор события</param>
     /// <returns>Task<ActionResult></returns>
     public Task<ActionResult> Start(int id)
     {
        string error = string.Empty;
        var eventId = id;
        var userContext = CurrentUserContext;

        return Task.Run<ActionResult>(async() => {
        try
        {
         
          var response = await mediator.Send(
                    new StartRequest() {
                        IUnitOfWork = this.IUnitOfWork,
                        IUserContext = userContext,
                        EventID = eventId });

           if (!response.StartsWith("OK")) {
                return Json(new JsonMessageView() { 
                            Status = "ERROR", 
                            Message = response},
                       JsonRequestBehavior.AllowGet);
             }

             return Json(new JsonMessageView() { 
                           Status= "OK", 
                           Message= $"{eventId}",
                        JsonRequestBehavior.AllowGet);

          }
          catch (Exception ex)
          {
                error = ex.Message;
          }
          return Json(new JsonMessageView(){ 
                        Status = "ERROR", 
                        Message = $"ОШИБКА. {error}"},
                     JsonRequestBehavior.AllowGet);
            });

        }
      
     /// <summary>
     /// Согласовать
     /// </summary>
     /// <param name=”id">идентификатор события</param>
     /// <param name="comments">комментарий</param>
     /// <returns>Task<ActionResult></returns>
     public async Task<ActionResult> Approve(int id, string comments)
     {
        string error = string.Empty;
        int eventId = id;
        var userContext = CurrentUserContext;
        return await Task.Run<ActionResult>(async () => {
           try
           {
              var response = await mediator.Send(new ApproveRequest()
              {
                   IUnitOfWork = this.IUnitOfWork,
                   IUserContext = userContext,
                   EventID = eventId,
                   Comments = comments ?? string.Empty,
                   IConfig = new AppConfiguration()
               });

               if (!response.StartsWith("OK"))
               {
                   return Json(new JsonMessageView() { 
                                    Status = "ERROR", 
                                    Message = response},
                             JsonRequestBehavior.AllowGet);
                    }

               return Json(new JsonMessageView() { 
                                Status = "OK", 
                                Message = $"{eventId}"},
                           JsonRequestBehavior.AllowGet);

          }
          catch (Exception ex)
          {
              error = ex.Message;
           }
           return Json(new JsonMessageView() { 
                              Status = "ERROR", 
                              Message = $"ОШИБКА. {error}"},
                       JsonRequestBehavior.AllowGet);
        });
     }

     /// <summary>
     /// Отклонить
     /// </summary>
     /// <param name="id">идентификатор события</param>
     /// <param name="comments">комментарий</param>
     /// <returns>Task<ActionResult></returns>
     public async Task<ActionResult> Reject(int id,string сomments)
     {
         string error = string.Empty;
         int eventId = id;
         var userContext = CurrentUserContext;
         return await Task.Run<ActionResult>(async() => {
             try
             {

                 var response = await mediator.Send(new RejectRequest()
                 {
                     IUnitOfWork = this.IUnitOfWork,
                     IUserContext = userContext,
                     EventID = eventId,
                     Comments = comments ?? string.Empty
                 });

                 if (!response.StartsWith("OK"))
                 {
                     return Json(new JsonMessageView() { 
                                       Status = "ERROR", 
                                       Message = response},
                                 JsonRequestBehavior.AllowGet);
                 }

                 return Json(new JsonMessageView() { 
                                   Status = "OK", 
                                   Message = $"{eventId}" },
                           JsonRequestBehavior.AllowGet);

             }
             catch (Exception ex)
             {
                 error = ex.Message;
             }
             return Json(new JsonMessageView() { 
                              Status = "ERROR", 
                              Message = $"ОШИБКА. {error}"},
                         JsonRequestBehavior.AllowGet);
         });
     }
  ..
 }

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

var response = await mediator.Send(new ApproveRequest()
              {
                   IUnitOfWork = this.IUnitOfWork,
                   IUserContext = userContext,
                   EventID = eventId,
                   Comments = comments ?? string.Empty,
                   IConfig = new AppConfiguration()
               });
Рисунок 2
Рисунок 2

запускает обработчики, которые выполняются в последовательности, указанной на рисунке 2.  Сначала будет вызван метод Process(TRequest request, CancellationToken token) класса GenericRequestPreProcessorBehavior. Затем запустится метод Handle(ApproveRequest request, CancellationToken cancellationToken) основного обработчика  ApproveHandler, где находится основная логика обработки согласования. После выполнения этого метода выполнится метод Process(TRequest request, TResult result) класса GenericRequestPostProcessorBehavior. В данном методе выполняется проверка, что все участники вынесли свое решение. Если условие выполнено, то нужно запустить процедуру оповещения всех ответственных сотрудников о том, что согласование завершено.

Положительные и отрицательные стороны использования библиотеки

Рассмотрим, какие бонусы мы получим.  К положительным моментам использования библиотеки можно отнести:

  1. Уменьшения связанности между объектами.

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

  3. Позволяет с меньшими затратами и, не ломая текущую архитектуру проекта, создать расширение и доработку функционала.

К недостаткам использования данной библиотеки можно отнести:

  1. Сложность настраивания и подключения библиотек к проекту.

  2. Сложность реализации функционала. Описание дополнительных вспомогательных классов, которые нужны при написании обработчиков.

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

Заключение

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

Дополнительные ссылки:

  1. Адрес проекта библиотеки https://github.com/jbogard/MediatR

  2. Адрес библиотеки сайта nuget https://www.nuget.org/packages/MediatR/

  3. Примеры использования различных библиотек по внедрению зависимостей — https://github.com/jbogard/MediatR/wiki

Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+3
Комментарии19

Публикации

Информация

Сайт
hr.auriga.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия