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