首页 > 解决方案 > 如何使用 MediatR 正确实现 Result 对象程序流程?

问题描述

我没有在程序流中使用异常,而是尝试使用基于 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;
    }
}

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

我在以下设计问题上遇到了麻烦...

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

例如...

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

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

  1. 如何约束 mediatr 管道行为以潜在地处理通用或非通用结果响应?

例如,如果结果是成功的,我只想ContentResult. 如果失败,我想返回结果对象。

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

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;
    }
}
  1. 序列化可能是一个挑战。如果有一个用例需要Result<>为成功的请求返回完整的对象——我不一定总是想显示"error": null

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

尝试这样的事情的原因

非常感谢,

标签: c#asp.net-core-webapimediatr

解决方案


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

是的,是的。

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


1.类型滥用

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

if (response.GetType().GetGenericTypeDefinition() == typeof(Result<>))

这不是结果对象应该被处理的方式。结果对象包含其中的信息。它不应该根据它的类型来预测。

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


2.空结果

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

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

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

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

分离Resultand的唯一真正原因Result<T>是您可以避免 aResult<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?

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

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

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

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
}

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

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

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

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

我提出的解决方案

  • 完全删除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 };
}
  • 让您的 Mediatr 请求返回它们的特定Result<MyFoo>类型,即使没有要返回的实际值
// inside your Mediatr request

return Result<BlobLink>.Success(myBlobLink);

return Result<BlobLink>.Failure("This is an error message");
  • 删除尝试设置响应的 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);
}

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

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

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


您的担忧

  • 瘦控制器

瘦控制器很好,但这里有合理的线条可以绘制。你为适应更瘦的控制器而编写的代码引入了比稍微不那么瘦的控制器更多的问题。

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

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

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

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

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

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

话虽如此,例外总是可能的。几乎不可能保证永远不会抛出异常。尽可能避免异常是好的,但不要跳过实际处理任何可能引发的异常——即使你不期望任何异常。


推荐阅读