在 .NET Controller 中返回 API 响应的最佳实践

作者:微信公众号:【架构师老卢】
2-4 17:28
27

想象这样一个场景:你正不知疲倦地致力于创建一个后端 API 响应系统,结果却出现了一个令人头疼的 bug,比如 “错误 500 内部服务器错误”。当它是 HTTP 响应给出的错误时,实际上它并非毫无头绪。你所需要了解的,是系统内部的工作原理,以便处理这些意外情况。如果你不想让这类响应破坏你的辛勤努力,那么这篇文章就很适合你。

在编程中,像字符串(string)、列表(List)和整数(int)等最基本的返回类型十分常用。然而,当从 API 返回响应时,需求就不止于这些简单类型了。除了通常从数据库获取的数据之外,API 响应一般还包括数据库中的数据、动态视图、指示请求成功与否的状态码,以及导致请求失败的错误类型。为满足这些需求,每种编程语言都为处理 API 响应的函数提供了一套独特的返回类型。现在,一个函数可以返回两种类型的响应:

  • 强类型响应:这些响应返回特定的数据类型,比如 IEnumerable<T>
  • 通用响应:这些响应返回诸如 IActionResultActionResult<T> 这样的基础类型。

为了更好地理解,让我们以根据用户请求返回客户信息为例,返回的可以是单个客户,也可以是客户列表。

public OkObjectResult GetCustomer()
{
    return new OkObjectResult(this.customer);
}

在这里,我们没有使用控制器通常返回的更通用的响应类型。相反,这段代码与 OkObjectResult 类紧密耦合,OkObjectResultIActionResult 的一种具体实现。虽然 OkObjectResult 会返回带有指定对象的 200 OK 响应,但它缺乏灵活性。例如,如果客户对象为空,而我们想要返回 NotFound() 响应,就需要更改该方法的返回类型。为解决这类情况,我们通常会使用更通用的返回类型,如 ActionResultIActionResult,它们在处理各种响应类型时具有更大的灵活性。所以新代码看起来会是这样:

public ActionResult GetCustomer()
{
    if (this.customer == null)
        return NotFound();
    return Ok(this.customer);
}

ActionResult 及相关接口的作用

  • ActionResult:这是一个具体类,实现了 IActionResult 接口并提供了额外功能。它作为返回结果的便捷基类,可以直接包装其他 IActionResult 实现和对象。它还支持类型安全的响应,允许你在单个方法中返回特定类型或结果,这一点我们稍后会探讨。
  • IActionResult:它代表通用的 HTTP 响应,为从动作方法返回各种类型的结果提供了灵活性。当返回 IActionResult 的不同实现,如 OkObjectResultNotFoundObjectResult 时,它特别有用。使用 IActionResult 的上述代码如下:
public IActionResult GetCustomer()
{
    if (this.customer == null)
        return NotFound();
    return Ok(this.customer);
}

现在,我们只是更改了 GetCustomer 函数的返回类型,但代码主体保持不变。那么,区别在哪里呢?就返回 HTTP 响应而言,区别不大,除非你想返回类型安全的响应,并支持不同的结果类型,这时我们会使用 ActionResult<T>。这里的 T 代表可以从 API 返回的特定实体。

IActionResult 将动作方法与特定的结果类型解耦,作为 HTTP 响应的抽象表示。这意味着它致力于遵守生成 API 响应的契约,而无需确定响应的具体类型或格式。相比之下,ActionResult 是 HTTP 响应的具体实现,包含额外功能。当与 <T> 一起使用时,它还确保以特定格式返回数据。

public ActionResult<CustDto> GetCustomer([FromQuery] int id)
{
    var QueryCustomer = _service.GetItem(id);
    if (QueryCustomer == null)
        return NotFound(); 
    
    return _mapper.Map<CustDto>(QueryCustomer); // 将数据库响应映射到 DTO  
}

如你所见,当客户为空时,我们返回 NotFound() 响应;如果数据库返回一个对象,则返回普通的客户 DTO。这里,ActionResult<T> 会自动将 DTO 包装在 OkObjectResult 中。这里可能会有个疑问,为什么没有 IActionResult<T> 呢?这是因为它不是泛型的,没有附加类型参数 <T>,并且返回对象或结果的功能已由 ActionResult 涵盖。

数据传输对象(DTO,Data Transfer Object)是一个单独的讨论话题,如果你想了解更多关于这个话题的内容,请发表评论。

3. JSON 响应

它只是以正确的内容类型返回 JSON 数据。以下是代码示例:

public JsonResult GetCustomer()
{
    return Json(new { Name = "Mike", Age = 30, Gender = "Male" });
}

Json 方法创建一个 JsonResult 对象,该对象会自动将 HTTP 响应的内容类型设置为 application/json,并将提供的对象序列化为 JSON 字符串。不过,我们也可以借助 IActionResultActionResult<T> 返回 JSON 响应。

4. 文件/内容结果

这些是专门的返回类型,用于直接将文件和内容作为 HTTP 响应发送,对于下载文件或返回原始文本很有用。

public FileResult DownloadPdf()
{
    var CustomerData = System.IO.File.ReadAllBytes("Files/sample.pdf");
    return File(CustomerData, "application/pdf", "sample.pdf");
}

这个函数发送一个 PDF 文件供下载。类似地,你可以使用 ContentResult 发送动态 HTML 或返回可由浏览器渲染的图像文件。FileResultContentResult 都实现了 IActionResult,所以即使我们将返回类型更改为 IActionResult,同时保持整个代码不变,它仍然可以正常工作。

异步返回类型

到目前为止,我们讨论的是如何从.NET API 返回 HTTP 结果,但这可能很耗时,并会阻塞程序的执行。现在,让我们探讨如何异步处理这个问题。

异步与同步的区别

在 HTTP 响应方法中使用 asyncawait 对于执行非阻塞操作至关重要。这使得服务器在等待诸如数据库查询、API 请求或文件处理等长时间运行的任务完成时,可以处理其他请求,而不是占用资源。因此,它提高了 API 的可扩展性和响应性。以下是一个示例:

public async Task<IActionResult> GetUserAsync([FromQuery] int id)
{
    var customer = await _dbContext.Customers.FindAsync(id);

    if (customer == null)
        return NotFound();

    return Ok(customer);
}

这个函数对数据库进行异步调用,并根据客户的 Id 返回客户信息。如我们所见,函数的返回类型仅从 IActionResult 变为了 Task<IActionResult>。那么,Task 到底是什么呢?它表示一个不返回结果的异步操作。当执行不需要特定返回值的异步任务时会使用它。另一方面,Task<T>Task 的泛型版本,表示返回结果的异步操作。类型参数 T 表示操作完成后将返回的数据类型。

每当一个服务或方法返回 TaskTask<T> 时,控制器必须标记为 async 并使用 await 以确保非阻塞行为。当将 TaskActionResult 以及特定返回类型结合使用时,代码如下所示:

public async Task<ActionResult<CustDto>> GetCustomerAsync([FromQuery] int id)
{
    var customer = await _dbContext.Customers.FindAsync(id);
    
    if (customer == null)
        return NotFound(); 
    
    var customerDto = _mapper.Map<CustDto>(customer); // 将客户映射到 DTO
    return Ok(customerDto);
}

在异步函数中,返回类型是 TaskValueTaskValueTaskTask 的轻量级替代方案,对于结果可以立即同步获得的情况(例如从缓存中获取)提供优化。

public async ValueTask<ActionResult<CustDto>> GetCustomerAsync([FromQuery] int id)
{
    var customer = await _dbContext.Customers.FindAsync(id);
    if (customer == null)
        return new ValueTask<ActionResult<CustDto>>(NotFound());
    
    var customerDto = _mapper.Map<CustDto>(customer); // 将客户映射到 DTO
    return new ValueTask<ActionResult<CustDto>>(Ok(customerDto));
}

在这种情况下,唯一需要修改的是 ValueTask 需要将结果包装在其构造函数中。除此之外,逻辑保持不变,ValueTask 为结果可同步获得的场景提供了潜在的性能优势。

理解.NET 控制器中的不同返回类型对于构建高效且可维护的 API 至关重要。从 IActionResultActionResult<T> 到像 TaskValueTask 这样的异步返回类型,每种类型都有其特定的用例。ActionResult<T> 提供类型安全性和灵活性,而 TaskValueTask 有助于优化异步操作的性能。根据你的需求选择合适的返回类型,可以提高 API 的响应性、可扩展性和整体效率。

相关留言评论
昵称:
邮箱:
阅读排行