使用 Either<L, R> 约束通用参数

Constraining a generic param with Either<L, R>

我正在使用 MediatR。请求装饰成这样

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

我决定尝试在我的应用程序(LanguageExt 库)中使用函数式编程,所以我的请求现在看起来像这样

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

我正在尝试使用函数 Either<L, R>,以便我可以从以下层次结构切换

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

对此

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

在针对 WebApplication 注册我的路由时,我可以简单地编写以下内容

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

使用以下扩展名

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

AsHttpResultAsync() return 基于成功响应的相关 HTTP 状态,或者我收到的 ErrorResponse 类型作为结果...

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

这一切都很好地联系在一起。 request/response 组合在任何地方都不会出错(因为 IRequest<TResponse>),这可以用作通用约束,因为我可以使用 Either<,> 作为包含在 [=23 中的约束=] (where TRequest : IRequest<Either<ErrorResponse, TResponse>>, where TResponse: SuccessResponse).

但我现在的问题是我正在尝试将 FluentValidation 添加到我的管道中。触发以下内容是因为我指定了确切的响应类型 SignUpResponse。

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

//但这不是我需要的。我需要的是一个开放的泛型,这样它将针对所有请求类型执行,就像这样...

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

但这不起作用,因为 Either 是一个结构,因此未定义为 Either<in L, in R>,因此不能从 Either<ErrorResponse, SuccessResponse> 类型转换为 Either<ErrorResponse, SignUpResponse> ,意味着 is 签入 MediatR 将 return false 并且我的中间件不会被调用。

我试着喜欢功能性的,但它正在反击我,它似乎是一个更好的战士。

答案是(不出所料)我需要用其他调用包装调用而不是使用 MediatR 管道。所以我创建了一个 IDispatcher 接口。

Program.cs...

app.AddUserUseCases();

用户的路由设置...

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 分机...

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 界面...

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

Dispatcher 实施,经过验证...

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

最后,AsHttpResultAsync 扩展将其转换为 HTTP 响应...

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

命令处理程序可以这样写...

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
    }
}