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.