使用 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
}
}
我正在使用 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>>
但这不起作用,因为 EitherEither<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
}
}