0

I am using MediatR. Requests are decorated like so

public record GetUserInfoQuery(Guid id) : IRequest<GetUserInfoResponse>;
public record GetUserInfoResponse(Guid id, string Name);

I have decided to try to use functional programming in my app (LanguageExt library), so my request now looks like this

public record GetUserInfoQuery(Guid id) : IRequest<Either<ErrorResponse, GetUserInfoResponse>>;
public record GetUserInfoResponse(Guid id, string Name);

I'm trying to use the functional Either<L, R> so I can switch from the following hierarchy

Response (has properties like ValidationErrors etc) <|-- SignUpResponse

To this

          ---- SuccessResponse <|--- SignUpResponse
        / 
Response
        \
          ---- ErrorResponse <|--- (BadRequestResponse / ConflictResponse / etc)

When registering my routes against WebApplication I can simply write the following

app.MapApiRequest<GetUserInfoQuery, GetUserInfoResponse>("/some/url");

which uses the following extension

public static WebApplication MapApiRequest<TRequest, TResponse>(this WebApplication app, string url)
    where TRequest : IRequest<Either<ErrorResponse, TResponse>>
    where TResponse : SuccessResponse
{
    app.MapPost(
        url,
        ([FromBody] TRequest request, [FromServices] IMediator mediator) => mediator.Send(request).AsHttpResultAsync());
    return app;
}

The AsHttpResultAsync() returns the relevant HTTP status based on the successful response, or the type of ErrorResponse I receive in as the result...

public static class EitherExtensions
{
    public static IResult AsHttpResult<TError, TSuccess>(this Either<TError, TSuccess> source)
        where TError : ErrorResponse
        where TSuccess : SuccessResponse
    {
        ArgumentNullException.ThrowIfNull(source);
        IResult json =
            source.Match(
                Right: x => Results.Json(x),
                Left: x =>
                    x switch
                    {
                        ConflictResponse x => Results.Conflict(x),
                        BadRequestResponse x => Results.BadRequest(x),
                        UnauthorizedResponse _ => Results.Unauthorized(),
                        _ => Results.StatusCode(500)
                    });
        return json;
    }

    public static async Task<IResult> AsHttpResultAsync<TError, TSuccess>(this Task<Either<TError, TSuccess>> source)
        where TError : ErrorResponse
        where TSuccess : SuccessResponse
    =>
        (await source).AsHttpResult();
}

This ties everything up nicely. There is nowhere I can get the request/response combination wrong (because of IRequest<TResponse>), and this works as a generic constraint because I am able to use Either<,> as my constraint wrapped in Mediator.IRequest<> (where TRequest : IRequest<Either<ErrorResponse, TResponse>>, where TResponse: SuccessResponse).

But my problem now is that I am trying to add FluentValidation to my pipeline. The following gets triggered because I am specifying the exact response type SignUpResponse.

public class MediatRValidatingMiddleware<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<TResponse>, IRequest<Either<ErrorResponse, SignUpResponse>>

//But that is not what I need. What I need is an open generic so it will execute for all request types, like so...

where TRequest : IRequest<TResponse>, IRequest<Either<ErrorResponse, SignUpResponse>>

But this doesn't work because Either<L, R> is a struct and so isn't define as Either<in L, in R> and so cannot be typecast from Either<ErrorResponse, SuccessResponse> to Either<ErrorResponse, SignUpResponse>, meaning the is check in MediatR will return false and my middleware won't get called.

I'm trying to like functional, but it is fighting me back and it seems to be a better fighter.

Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • Might be a stupid question, but shouldn't that be `where TRequest : IRequest, IRequest>`? – Camilo Terevinto Feb 04 '22 at 10:12
  • 1
    *"[FP] is fighting me back"* Or perhaps it's the frameworks you've chosen to use that are fighting you back... – Mark Seemann Feb 04 '22 at 10:43
  • 1
    FWIW, [C#'s `in` keyword is a specialisation of the more general concept of a covariant functor](https://blog.ploeh.dk/2021/10/25/functor-variance-compared-to-cs-notion-of-variance). Since [Either is a (bi)functor](https://blog.ploeh.dk/2019/01/14/an-either-functor), you may be able to write the necessary mapping code yourself, if MediatR gives you an extensibility point for it, that is. – Mark Seemann Feb 04 '22 at 10:46
  • Circling back to my first comment, there's nothing in FP that inherently prevents you from doing things like this, but the frameworks in question may come with built-in assumptions (about how you'd want to use them) that fit poorly with the FP way of doing things. – Mark Seemann Feb 04 '22 at 10:48

1 Answers1

0

The answer was (unsurprisingly) that I needed to wrap the calls with other calls rather than use the MediatR pipeline. So I created an IDispatcher interface.

In Program.cs...

app.AddUserUseCases();

The route setup for users...

public static class UserUseCases
{
    public static WebApplication AddUserUseCases(this WebApplication app) => app
        .MapApiRequest<SignUpCommand, SignUpResponse>("/api/v1/users/sign-up")
        .MapApiRequest<SignInCommand, SignInResponse>("/api/v1/users/sign-in");
}

MapApiRequest extension...

public static class WebApplicationExtensions
{
    public static WebApplication MapApiRequest<TRequest, TResponse>(this WebApplication app, string url)
        where TRequest : IRequest<Either<ErrorResponse, TResponse>>
        where TResponse : SuccessResponse
    {
        app.MapPost(
            url,
            ([FromBody] TRequest request, [FromServices] IDispatcher<TRequest, TResponse> dispatcher)
            => 
                dispatcher.ExecuteAsync(request).AsHttpResultAsync());
        return app;
    }
}

IDispatcher interface...

public interface IDispatcher<TRequest, TResponse>
    where TRequest : IRequest<Either<ErrorResponse, TResponse>>
{
    ValueTask<Either<ErrorResponse, TResponse>> ExecuteAsync(TRequest request, CancellationToken cancellationToken = default);
}

Dispatcher implementation, with validation...

public class Dispatcher<TRequest, TResponse> : IDispatcher<TRequest, TResponse>
    where TRequest : IRequest<Either<ErrorResponse, TResponse>>
{
    private readonly IMediator Mediator;
    private readonly ImmutableArray<IValidator<TRequest>> Validators;

    public Dispatcher(IMediator mediator, IEnumerable<IValidator<TRequest>> validators)
    {
        ArgumentNullException.ThrowIfNull(validators);
        Mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        Validators = validators.ToImmutableArray();
    }

    public async ValueTask<Either<ErrorResponse, TResponse>> ExecuteAsync(
        TRequest request,
        CancellationToken cancellationToken = default)
    {
        var errors = ImmutableArray.Create<ValidationError>();
        var context = new ValidationContext<TRequest>(request);
        foreach (IValidator<TRequest> validator in Validators)
        {
            var validationResult = await validator.ValidateAsync(context, cancellationToken);
            if (!validationResult.IsValid)
            {
                errors = errors.AddRange(
                    validationResult.Errors.
                        Select(x => new ValidationError(Key: x.PropertyName, Message: x.ErrorMessage)));
            }

            if (cancellationToken.IsCancellationRequested)
                break;
        }
        if (errors.Any())
            return new BadRequestResponse(errors);

        try
        {
            Either<ErrorResponse, TResponse> mediatorResult = await Mediator.Send(request, cancellationToken);
            return mediatorResult;
        }
        catch (DbConflictException)
        {
            return new ConflictResponse();
        }
        catch (DbUniqueIndexViolationException e)
        {
            return new ConflictResponse($"{e.PropertyName} must be unique");
        }
        catch
        {
            return new UnexpectedErrorResponse();
        }
    }
}

And finally, the AsHttpResultAsync extension to convert it to an HTTP response...

public static class EitherExtensions
{
    public static IResult AsHttpResult<TResponse>(this Either<ErrorResponse, TResponse> source)
        where TResponse : SuccessResponse
    {
        ArgumentNullException.ThrowIfNull(source);
        IResult json =
            source.Match(
                Right: x => Results.Json(x),
                Left: response =>
                    response switch
                    {
                        BadRequestResponse x => Results.BadRequest(x),
                        ConflictResponse x => Results.Conflict(x),
                        UnauthorizedResponse _ => Results.Unauthorized(),
                        UnexpectedErrorResponse _ => Results.StatusCode(500),
                        _ => throw new NotImplementedException()
                    });
        return json;
    }

    public static async ValueTask<IResult> AsHttpResultAsync<TResponse>(this ValueTask<Either<ErrorResponse, TResponse>> source)
        where TResponse: SuccessResponse
    =>
        (await source).AsHttpResult();
}

A command handler can then be written like so...

public class SignUpCommandHandler : IRequestHandler<SignUpCommand, Either<ErrorResponse, SignUpResponse>>
{
    public async Task<Either<ErrorResponse, SignUpResponse>> Handle(
        SignUpCommand request,
        CancellationToken cancellationToken)
    {
        if (........) return new BadRequestResponse(.....); fail
        return new SignUpResponse(); // success
    }
}
Peter Morris
  • 20,174
  • 9
  • 81
  • 146