如何使用 MediatR 正确实现结果对象程序流



我没有对程序流使用异常,而是尝试使用基于 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. 如何约束中介管道行为以可能处理泛型或非泛型结果响应?

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

这就是我想出的开始,但感觉非常"臭">

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.

我要掉进兔子洞了吗?有没有更好的方法可以做到这一点?

尝试这样的事情的原因

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

非常感谢,

我要掉进兔子洞了吗?有没有更好的方法可以做到这一点?

是的,是的。

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

<小时 />

1。类型滥用

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

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

这不是应该如何处理结果对象。结果对象包含其中的信息。它不应该根据其类型进行占卜。

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

<小时 />

2.空结果

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

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

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

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

ResultResult<T>分开的唯一真正原因是,您可以避免使用未初始化的T值(通常null)的Result<T>。因此,让我们重新评估这个决定:我们是否应该在这里避免null值?

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

这里的最终结论是,您可以安全地删除Result并重构使用它的任何代码,以实际使用具有未初始化TResult<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,使用未初始化的T将不良结果实例化为Result<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 };
}
  • 让您的中介请求返回其特定的Result<MyFoo>类型,即使没有要返回的实际值
// inside your Mediatr request
return Result<BlobLink>.Success(myBlobLink);
return Result<BlobLink>.Failure("This is an error message");
  • 删除尝试设置响应的中介管道行为。它不属于那里。
  • 让控制器操作根据它收到的结果决定要返回的内容。
  • 使用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)方法中,该方法为你做这件事(例如,在你所有的api控制器派生的基MyController : ApiController类中);但要注意,你的可重用代码可能很难返回上下文相关的错误状态。

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

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

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

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

您不需要在此处抛出异常,您的结果对象专门存在以返回错误的结果,而不必诉诸异常。只需检查结果对象的成功状态,并返回与结果对象状态对应的相应 HTTP 响应。

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

最新更新