如何使用 MediatR 正确实现 Result 对象程序流程?

Kon*_*262 3 c# mediatr asp.net-core-webapi

我没有使用程序流的异常,而是尝试根据 MediatR 中讨论的想法使用自定义 Result对象。我这里有一个非常简单的例子......

public class Result 
{
    public HttpStatusCode StatusCode { get; set; }
    public string Error { get; set; }

    public Result(HttpStatusCode statusCode)
    {
        StatusCode = statusCode;
    }

    public Result(HttpStatusCode statusCode, string error)
    {
        StatusCode = statusCode;
        Error = error;
    }
}

public class Result<TContent> : Result
{
    public TContent Content { get; set; }

    public Result(TContent content) : base(HttpStatusCode.OK)
    {
        Content = content;
    }
}
Run Code Online (Sandbox Code Playgroud)

这个想法是,任何失败都将使用非通用版本,而成功将使用通用版本。

我遇到了以下设计问题......

  1. 如果响应可能是通用的或非通用的,我应该指定什么作为控制器方法的返回类型?

例如...

[HttpGet("{id}")]
public async Task<ActionResult<Result<string>>> Get(Guid id)
{
    return await _mediator.Send(new GetBlobLink.Query() { TestCaseId = id });
}
Run Code Online (Sandbox Code Playgroud)

如果我只是返回类型的验证失败,这将不起作用Result

  1. 如何限制 mediatr 管道行为以处理通用或非通用结果响应?

例如,如果结果成功,我只想返回Content通用版本的Result. 如果失败,我想返回结果对象。

这是我想出的开始,但感觉非常“臭”

public class RestErrorBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public IHttpContextAccessor _httpContextAccessor { get; set; }
    public RestErrorBehaviour(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var response = await next();
        
        if (response.GetType().GetGenericTypeDefinition() == typeof(Result<>))
        {
            _httpContextAccessor.HttpContext.Response.StatusCode = 200;

            //How do I return whatever the value of Content on the response is here?
        }
        if (response is Result)
        {
            _httpContextAccessor.HttpContext.Response.StatusCode = 400;

            return response;
        } 
        
        return response;
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 序列化可能是一个挑战。如果有一个用例需要Result<>成功请求返回完整对象怎么办 - 我不一定总是想显示"error": null.

我是不是掉进兔子洞了?有一个更好的方法吗?

尝试这样的事情的原因

  • 瘦身控制器
  • 对 API 请求的格式良好的 json 响应
  • 避免验证的异常控制流(因此避免需要使用.net core中间件来处理和格式化请求异常)。

非常感谢,

Fla*_*ter 13

我是不是掉进兔子洞了?有一个更好的方法吗?

是的,是的。

这里有几个问题。有些是技术困难,有些则源于采取了错误的方法。我想在解决这些问题之前单独解决这些问题,因此解决方案(及其背后的推理)更有意义。


1. 类型滥用

Result通过尝试将类型(类型本身)用作数据来滥用您的类型(及其通用变体):

if (response.GetType().GetGenericTypeDefinition() == typeof(Result<>))
Run Code Online (Sandbox Code Playgroud)

这不是结果对象应该被处理的方式。结果对象包含其内部的信息。不应该根据它的类型来占卜。

我会回到这一点,但首先我想回答你的直接问题。这两个问题将在本答案中进一步解决。


2、结果为空

注意:虽然并非每个默认值都是null(例如结构、基元),但对于本答案的其余部分,我将把它称为一个null值,以保持简单。

您在尝试同时容纳基本类型和泛型Result类型时遇到了一个问题,您已经注意到这并不容易完成。重要的是要认识到这个问题是您自己造成的:您正在努力管理您有意创建的两种不同类型。

当您将对象向下转换为基本类型,然后心里想“我真的很想知道派生类型是什么以及它包含什么”时,那么您正在处理多态性滥用。这里的经验法则是,当您需要了解/访问派生类型时,您不应该将其向下转换为其基类型。

在处理自己造成的问题时,首先要解决的问题是重新评估您是否应该首先这样做(导致问题的事情)。

Result分离and的唯一真正原因Result<T>是这样可以避免 a 具有Result<T>未初始化的T值(通常是null)。因此,让我们重新评估这个决定:我们应该null从一开始就避免使用价值观吗?

我的答案是否定的。null这里是一个有意义的值,它表明没有值。因此我们不应该避免它,因此我们分离ResultResult<T>类型的基础变得(双关语)无效。

这里的最终结论是,您可以安全地删除Result和重构任何使用它的代码,以实际使用 aResult<T>和 uninitialized T,但我将进一步讨论具体的解决方案。


3. 设置响应值

_httpContextAccessor.HttpContext.Response.StatusCode = 200;

//How do I return whatever the value of Content on the response is here?
Run Code Online (Sandbox Code Playgroud)

我不清楚您为什么要尝试在 Mediatr 管道行为中设置响应的值。返回的值由控制器操作决定。这已经在您的代码中:

[HttpGet("{id}")]
public async Task<ActionResult<Result<string>>> Get(Guid id)
{
    return await _mediator.Send(new GetBlobLink.Query() { TestCaseId = id });
}
Run Code Online (Sandbox Code Playgroud)

这是设置返回值的类型和返回值本身的地方。

Mediatr 管道是您业务逻辑的一部分,而不是 REST API 的一部分。我认为您已经通过 (a) 将 HTTP 状态代码放入 Mediatr 结果对象中以及 (b) 让 Mediatr 管道设置 HTTP 响应本身来混淆该行。

相反,您的控制器应该决定 HTTP 响应对象是什么。使用结果类时,通常的方法是:

public async Task<IActionResult> Get(Guid id)
{
    var result = await GetResult(id);

    if(result.IsSuccess)
        return Ok(result.Value);
    else
        // return a bad result
}
Run Code Online (Sandbox Code Playgroud)

请注意,我没有定义糟糕的结果应该是什么。处理不良结果的正确方法是结合上下文。可能是一般的服务器错误,权限问题,找不到引用的资源,...它通常需要您了解不良结果的原因,这需要解析结果对象。

这太上下文化了,无法定义可重用的 - 这正是为什么你的控制器操作应该是决定正确的“坏”响应的原因。

另请注意,我使用了IActionResult. 这允许您返回任何您想要的结果,这通常是有益的,因为它允许您返回不同的“好”和“坏”结果,例如:

if(result.IsSuccess)
    return Ok(result.Value);             // type: ActionResult<Foo>
else
    return BadRequest(result.Errors);    // type: ActionResult<string[]>
Run Code Online (Sandbox Code Playgroud)

我提出的解决方案

  • 完全删除Result,实例化不良结果,就像Result<T>未初始化的T.
  • 不要在此处使用 HTTP 状态代码。您的结果类应该只包含业务逻辑,而不包含表示逻辑。
  • 为您的结果类提供一个布尔属性来指示成功,而不是尝试通过基本/通用结果类型来预测它。
  • 与您发布的问题无关,但很好的提示:允许返回多个错误消息。这样做可能与上下文相关。
  • 为了强制错误的结果不包含值,而好的结果必须包含值,请隐藏构造函数并依赖更具描述性的静态方法
public class Result<T>
{
    public bool IsSuccess { get; private set; }
    public T Value { get; private set; }
    public string[] Errors { get; private set; }

    private Result() { }

    public static Result<T> Success(T value) => new Result<T>() { IsSuccess = true, Value = value };
    public static Result<T> Failure(params string[] errors) => new Result<T>() { IsSuccess = false, Errors = errors };
}
Run Code Online (Sandbox Code Playgroud)
  • 让您的 Mediatr 请求返回其特定Result<MyFoo>类型,即使没有实际值可返回
// inside your Mediatr request

return Result<BlobLink>.Success(myBlobLink);

return Result<BlobLink>.Failure("This is an error message");
Run Code Online (Sandbox Code Playgroud)
  • 删除尝试设置响应的 Mediatr 管道行为。它不属于那里。
  • 让控制器操作根据收到的结果决定返回什么。
  • 使用IActionResult, 允许您的代码返回它想要的任何内容 - 因为您特别希望根据收到的结果返回不同的内容。
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id)
{
    Result<BlobLink> result = await _mediator.Send(new GetBlobLink.Query() { TestCaseId = id });

    if(result.IsSuccess && result.Value != null)
        return Ok(result.Value);
    else if(result.IsSuccess && result.Value == null)
        return NotFound();
    else
        return BadRequest(result.Errors);
}
Run Code Online (Sandbox Code Playgroud)

这只是一个基本示例,说明如何根据返回的对象获得多个 http 响应状态。根据具体的控制器操作,相关的返回语句可能会发生变化。

请注意,此示例将任何失败结果视为错误请求,而不是内部服务器错误。我通常设置 REST API 来使用异常过滤器捕获所有未处理的异常,这最终会返回内部服务器错误(除非对于某种异常类型有更相关的 http 状态代码)。

如何(以及是否)区分内部服务器错误和错误请求错误不是您问题的重点。为了有一个具体的例子,我向您展示了一种方法,其他方法仍然存在。


您的担忧

  • 瘦身控制器

细长的控制器很好,但这里需要绘制合理的线条。您为适应更瘦的控制器而编写的代码比使用稍微不那么瘦的控制器会带来更多的问题。

如果您愿意,您可以if success通过将每个控制器操作传递给ActionResult HandleResult<T>(Result<T> result)为您执行此操作的通用方法来避免检查每个控制器操作(例如,在MyController : ApiController所有 api 控制器派生的基类中);但请注意,可重用代码可能很难返回上下文相关的错误状态。

这就是权衡:可重用性很好,但代价是处理特定案例和定制响应变得更加困难。

  • 对 API 请求的格式良好的 json 响应

您的ActionResult内容及其内容将始终被序列化,因此这并不是真正需要担心的问题。

  • 避免验证的异常控制流(因此避免需要使用.net core中间件来处理和格式化请求异常)。

您不需要在这里抛出异常,您的结果对象专门存在用于返回错误结果,而无需诉诸异常。只需检查结果对象的成功状态,并返回与结果对象的状态相对应的适当的 HTTP 响应。

话虽这么说,例外总是可能的。几乎不可能保证不会抛出任何异常。尽可能避免异常是好的,但不要跳过实际处理可能引发的任何异常 - 即使您不希望出现任何异常。