Search code examples
c#.netgenericscqrsmediatr

Generic CQRS Query handler with custom return type


I am trying to build a generic query handler using the MediatR (v8) library. Lets jump to the code: First of all I have an abstract query class like this:

public abstract class Query<TQueryResult> : IRequest<TQueryResult>
{
    public Guid Id { get; } = Guid.NewGuid();

    public DateTime Timestamp { get; }

    protected Query()
    {
        Timestamp = DateTime.Now;
    }
}

From the corresponding query handler I would like to return a Result wrapper object, which looks as the following:

public class Result<T> 
{
    public T Payload { get;  }
    public string FailureReason { get;  }

    public bool IsSuccess => FailureReason == null;

    public Result(T payload)
    {
        Payload = payload;
    }

    public Result(string failureReason)
    {
        FailureReason = failureReason;
    }

    public static Result<T> Success(T payload)
        => new Result<T>(payload);

    public static Result<T> Failure(string reason)
        => new Result<T>(reason);

    public static implicit operator bool(Result<T> result) => result.IsSuccess;

}

And last but not least, lets see the query handler:

public abstract class AbstractQueryHandler<TQuery, TQueryResult, TResultValue> : IRequestHandler<TQuery, TQueryResult>
    where TQuery : Query<TQueryResult>
    where TQueryResult : class
{

    public Task<TQueryResult> Handle(TQuery request, CancellationToken cancellationToken)
    {
        try
        {
            return HandleQuery(request);
        }
        catch (Exception e)
        {
            return Task.FromResult(Result<TResultValue>.Failure(GetFailureMessage(e)) as TQueryResult);
        }
    }

    public abstract Task<TQueryResult> HandleQuery(TQuery request);

    private static string GetFailureMessage(Exception e)
    {
        return "There was an error while executing query: \r\n" + e.Message;
    }
}

To be honest I am not pleased with this solution due to the three type parameters I have in the query handler. Let's see some corresponding tests to reveal my concerns regarding. First the test-helper objects:

public class ExampleDto 
{
    public string Name { get; set; }
}

public class BasicQuery : Query<Result<ExampleDto>>
{

}

public class BasicQueryHandler : AbstractQueryHandler<BasicQuery, Result<ExampleDto>, ExampleDto>
{
    public override Task<Result<ExampleDto>> HandleQuery(BasicQuery request)
    {
        return Task.FromResult(Result<ExampleDto>.Success(new ExampleDto() { Name = "Result Name" }));
    }
}

And then the test:

    [Fact]
    public async Task GivenBasicQuery_whenHandle_thenSuccessResultWithPayload()
    {
        var handler = new BasicQueryHandler();

        var result = await handler.Handle(new BasicQuery(), CancellationToken.None);

        Check.That(result.IsSuccess).IsTrue();
        Check.That(result.Payload.Name).IsEqualToValue("Result Name");
    }

As you can see in the BasicQueryHandler there is some kind of duplication when declaring the three types, namely <BasicQuery, Result<ExampleDto>, ExampleDto>. It seems really fishy to me. I also tried many other possibilities, checking articles and SO questions/answers on the internet but could not come up with a cleaner solution with. What am I doing wrong? Is it possible to reduce the number of type parameters (of query handler) to 2? Thanks in advance for you help!


Solution

  • Basically, I moved the Result<> from the type parameter in the class declaration to the method declaration. I also removed the interfaces for clarity (you didn't share the definitions anyway).

    public abstract class AbstractQueryHandler<TQuery, TQueryResult>
        where TQuery : Query<TQueryResult>
        where TQueryResult : class, new()
    {
    
        public Task<Result<TQueryResult>> Handle(TQuery request, CancellationToken cancellationToken)
        {
            try
            {
                return HandleQuery(request);
            }
            catch (Exception e)
            {
                return Task.FromResult(Result<TQueryResult>.Failure(new TQueryResult(), GetFailureMessage(e)));
            }
        }
    
        public abstract Task<Result<TQueryResult>> HandleQuery(TQuery request);
    
        private static string GetFailureMessage(Exception e)
        {
            return "There was an error while executing query: \r\n" + e.Message;
        }
    }
    
    public class BasicQueryHandler : AbstractQueryHandler<BasicQuery, ExampleDto>
    {
        public override Task<Result<ExampleDto>> HandleQuery(BasicQuery request)
        {
            return Task.FromResult(Result<ExampleDto>.Success(new ExampleDto() { Name = "Result Name" }));
        }
    }
    

    Note that since you need to new up a TQueryResult in the exception path, the new() keyword had to be added to the generic constraint.