jbogard / MediatR

Simple, unambitious mediator implementation in .NET
Apache License 2.0
11.14k stars 1.18k forks source link

Generic Requests and Request Handlers #822

Closed TheMagnificent11 closed 1 year ago

TheMagnificent11 commented 1 year ago

I'm trying to use generics to create an abstract query request handler for aggregate roots in a domain-driven design architecture.

I've got the following classes to setup a Mediatr request.

However, I get a compiler error when using ISender.Send because it thinks my response object is a plain old object instead a QueryResult<T>.

Do you know what I'm doing wrong?

public interface IQuery<T> : IRequest<T>
    where T : class
{
    Guid CorrelationId { get; }
}
public abstract class BaseResult
{
    protected BaseResult(ResultStatus status, Dictionary<string, List<string>>? errors)
    {
        this.Status = status;
        this.Errors = errors ?? new Dictionary<string, List<string>>();
        this.IsSuccess = status == ResultStatus.Success;
    }

    public bool IsSuccess { get; private set; }
    public ResultStatus Status { get; private set; }
    public Dictionary<string, List<string>> Errors { get; private set; }

    ...
}
public class QueryResult<T> : BaseResult
    where T : class
{
    private QueryResult(T? data, ResultStatus status, Dictionary<string, List<string>>? errors)
        : base(status, errors)
    {
        this.Data = data;
    }

    public T? Data { get; }

    ...
}
public class AggregateRootQuery<TAggregate, TDto>
    : IRequest<QueryResult<IEnumerable<TDto>>>, IQuery<IEnumerable<TDto>>
    where TAggregate : class, IAggregateRoot // IAggregate root is a marker interface to only allow queries to start at the aggregate root when using my EF core DB context wrapper
    where TDto : class
{
    public AggregateRootQuery(Guid correlationId)
    {
        this.CorrelationId = correlationId;
    }

    public Guid CorrelationId { get; }
}
public abstract class BaseAggregateRootQueryHandler<TAggregate, TDto> : IRequestHandler<AggregateRootQuery<TAggregate, TDto>, QueryResult<IEnumerable<TDto>>>
    where TAggregate : class, IAggregateRoot
    where TDto : class
{
    protected BaseAggregateRootQueryHandler(IDbContext dbContext, IMapper mapper, ILogger logger)
    {
        this.DbContext = dbContext;
        this.Mapper = mapper;
        this.Logger = logger;
    }

    protected IDbContext DbContext { get; }
    protected IMapper Mapper { get; }
    protected ILogger Logger { get; }

    public async Task<QueryResult<IEnumerable<TDto>>> Handle(AggregateRootQuery<TAggregate, TDto> request, CancellationToken cancellationToken)
    {
        var entities = await this.ApplyFilter(this.DbContext.AggregateRoot<TAggregate>())
            .ToArrayAsync(cancellationToken);

        var dtos = this.MapToDataTransferObjects(entities);

        this.Logger.Debug("{Count} {EntityType} read from database", entities.Length, nameof(TAggregate));

        return QueryResult<IEnumerable<TDto>>.Success(dtos);
    }

    protected abstract IQueryable<TAggregate> ApplyFilter(IQueryable<TAggregate> source);

    protected virtual IEnumerable<TDto> MapToDataTransferObjects(IEnumerable<TAggregate> source)
    {
        return this.Mapper.Map<IEnumerable<TDto>>(source);
    }
}

Usage

        var result = await this.Mediator.Send(new AggregateRootQuery<Domain.Order, OrdrDto>(Guid.NewGuid()));

        // `IsSuccess` and `Data` have compiler errors because it thinks `result` is an object and not a `QueryResult<IEnumberable<OrderDto>>`
        if (!result.IsSuccess || result.Data == null)
        {
            // Error handing
        }
TheMagnificent11 commented 1 year ago

Closing because I've created a question of Stack Overflow instead