Search code examples
c#oopdomain-driven-designcqrsmediatr

How to avoid code duplication in MediatR request handlers?


I am playing around with CQRS and the MediatR library, trying to learn some of the best practices. One problem I have is code duplication in Command/Query handlers. I would like to know what is the best way to share logic between handlers.

Example: I have an abstract Entity class that defines an ID property. All entities inherit from that class.

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

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

Then for every entity, I want to create a GetById query. One of those queries looks like so:

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);
        }
    }

}

The problem is that this class looks exactly the same for all the entities. Without CQRS I would probably have something like this:

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);
    }
}

I tried doing something similar with a generic query and handler but MediatR had trouble finding the handler (even when I tried registering it manually to the DI container).

What is the best way to avoid such duplication?


Solution

  • Can you try below code. This way, you reuse the loading code, and at the same time, provide an end point to handle the request.

    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);
        }
    
    }