Как избежать дублирования кода в обработчиках запросов MediatR?

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

Пример: у меня есть абстрактный класс Entity, который определяет свойство ID. Все сущности наследуются от этого класса.

public abstract class Entity
{
    public long Id { get; private set; }

    protected Entity(long id)
    {
        Id = id;
    }
    ...
}

Затем для каждой сущности я хочу создать запрос GetById. Один из этих запросов выглядит так:

public class GetUserByIdQuery : IRequest<UserDto>
{
    public long UserId { get; set; }
    public class Handler : IRequestHandler<GetUserByIdQuery, UserDto>
    {
        private readonly IRepository<User> repository;
        private readonly IMapper mapper;

        public Handler(IUnitOfWork unitOfWork, IMapper mapper)
        {
            repository = unitOfWork.GetRepository<User>();
            this.mapper = mapper;
        }
        public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
        {
            var user = await repository.FindAsync(request.UserId, null, cancellationToken);
            if (user is null)
            {
                throw new EntityNotFoundException();
            }

            return mapper.Map<UserDto>(user);
        }
    }

}

Проблема в том, что этот класс выглядит одинаково для всех сущностей. Без CQRS у меня, наверное, было бы что-то вроде этого:

public class EntityFinder<TEntity, TDto> where TEntity : Entity
{
    private readonly IRepository<TEntity> repository;
    private readonly IMapper mapper;

    public EntityFinder(IUnitOfWork unitOfWork, IMapper mapper)
    {
        repository = unitOfWork.GetRepository<TEntity>();
        this.mapper = mapper;
    }
    public async Task<TDto> GetByIdAsync(long id)
    {
        var entity = await repository.FindAsync(id);
        if (entity is null)
        {
            throw new EntityNotFoundException();
        }

        return mapper.Map<TDto>(entity);
    }
}

Я попытался сделать что-то подобное с общим запросом и обработчиком, но у MediatR возникли проблемы с поиском обработчика (даже когда я пытался зарегистрировать его вручную в контейнере DI).

Как лучше всего избежать такого дублирования?


person Gur Galler    schedule 05.03.2020    source источник
comment
В последнее время я тоже погружаюсь в Mediatr, и одна из вещей, к которой я пытаюсь приспособиться, - это ощущение дублирования кода. Изучая некоторые переговоры, проведенные Джимми Богардом (сопровождающим библиотеки), кажется, что он рассматривает это как преимущество до некоторой степени, потому что это означает, что при изменении команды или запроса изменяется только один экземпляр, без риска для другие. Это также позволяет, например, иметь разные входные и выходные DTO. С нетерпением жду того, что скажут другие, более опытные.   -  person Tyler Hundley    schedule 05.03.2020
comment
Также см. Некоторые заметки Джимми по этому поводу здесь   -  person Tyler Hundley    schedule 05.03.2020
comment
@TylerHundley, я не согласен с этим. Предположим, у нас есть 100 таких запросов. И хочу его расширить, добавить логирование при загрузке данных. Теперь мне потребуется гораздо больше времени, чтобы копировать / вставлять код повсюду. Затем уходит много времени на тестирование всех запросов. Так что для меня дублирующийся код затрудняет расширение, а не приносит никакой пользы.   -  person Alex - Tin Le    schedule 06.03.2020
comment
@ Alex-TinLe Ярмарка очков. Я бы сказал, что сквозные проблемы, такие как ведение журнала, должны решаться через сам Mediatr через Behaviors, когда это возможно. Я думаю, что сокращение дублирования - в целом хорошая цель, хотя полезно спросить, действительно ли код дублируется (они всегда будут полностью отражать друг друга) или пока он просто такой же (т.е. вместо использования одного DTO для ввода и вывода используйте разные DTO для каждого, поскольку в будущем потребности могут измениться для одного). Я думаю, что Джимми дает несколько хороших мыслей в статье, опубликованной в моем предыдущем комментарии.   -  person Tyler Hundley    schedule 06.03.2020


Ответы (1)


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

public class EntityFinder<TEntity, TDto> where TEntity : Entity
{ ... // Same as your code }

public class GetUserByIdQuery : IRequest<UserDto>
{
    public long UserId { get; set; }
    public class Handler : IRequestHandler<GetUserByIdQuery, UserDto>, EntityFinder<User, UserDto>
    {
        public Handler(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        { }
        public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
            => await base.GetByIdAsync(request.UserId);
    }

}
person Alex - Tin Le    schedule 05.03.2020
comment
Как мы можем создать базовый обработчик команд. Собственно, я хочу включить / отключить запись. Я не хочу дублировать свой код - person Velkumar; 04.05.2021