【译】ASP.NET Core Web API的返回类型
原文链接:传送门。
ASP.NET Core为Web API控制器动作方法返回类型提供了如下几个选择:
这篇文章解释了什么时候最适合使用各个类型。
指定类型(Specific type)
最简单的API会返回原生的或者复杂的数据类型(比如,string 或者自定义对象类型)。考虑如下的Action方法,其返回了一个自定义的Product对象的集合。
[HttpGet] public List<Product> Get() => _repository.GetProducts();
在程序的执行过程中,如果没有可知的条件来破坏安全,便可以返回一个特定的类型。前面的Action方法没有接收任何参数,因此不需要任何参数约束验证。
当有可能具有多个返回类型时,通常的做法是将一个ActionResult 返回类型与原生的或者复杂的返回类型混合起来。IActionResult 或者 ActionResult<T> 都可以搭配这种类型的Action。
这篇文章也会提供多个返回类型的几个示例。
返回IEnumerable<T> 或者 IAsyncEnumerable<T>
在ASP.NET Core 2.2及之前的版本中,从一个Action中返回 IEnumerable<T> 会导致序列化器的同步集合迭代。其结果便会阻塞调用以及潜在的资源池匮乏。为了演示这个,假设我们将使用EF Core来实现Web API的数据访问需求。如下的Action的返回类型在序列化时是同步枚举的。
public IEnumerable<Product> GetOnSaleProducts() => _context.Products.Where(p => p.IsOnSale);
在ASP.NET Core 2.2及之前的版本中,为了避免同步枚举以及在数据库中的阻塞等待,我们可以调用ToListAsync。
public async Task<IEnumerable<Product>> GetOnSaleProducts() => await _context.Products.Where(p => p.IsOnSale).ToListAsync();
在ASP.NET Core 3.0及后续的版本中,可直接从Action 中返回IAsyncEnumerable<T>。其将会:
- 不会导致同步迭代
- 变得和返回IEnumerable<T> 一样高效。
ASP.NET Core 3.0及后续的版本在将如下结果提供给序列化器之前,会将其缓存起来:
public IEnumerable<Product> GetOnSaleProducts() => _context.Products.Where(p => p.IsOnSale);
可以考虑将Action方法签名的返回类型声明为 IAsyncEnumerable<T> 来保证同步迭代。最终,其迭代模式会基于返回的底层具体类型。MVC会自动缓存任何实现了IAsyncEnumerable<T>的具体类型。
考虑如下Action,其将sale-priced Product 记录作为一个IEnumerable<Product>来返回。
[HttpGet("syncsale")] public IEnumerable<Product> GetOnSaleProducts() { var products = _repository.GetProducts(); foreach (var product in products) { if (product.IsOnSale) { yield return product; } } }
上述Action的 IAsyncEnumerable<Product> 等价版本如下:
[HttpGet("asyncsale")] public async IAsyncEnumerable<Product> GetOnSaleProductsAsync() { var products = _repository.GetProductsAsync(); await foreach (var product in products) { if (product.IsOnSale) { yield return product; } } }
在ASP.NET Core 3.0中,上述两个Action都是非阻塞的。
IActionResult返回类型
当在一个Action方法中有可能具有多个ActionResult返回类型时,我们便可以使用 IActionResult 返回类型。ActionResult 类型表示了各种各样的HTTP状态码。继承自ActionResult 的任何非抽象类型均描述了一个有效的返回类型。在这个类别中,一些通用的返回类型是: BadRequestResult (400),NotFoundResult (404)以及OkObjectResult (200)。同样的,我峨我们可以在Action中使用ControllerBase 类的一些便捷方法来返回ActionResult类型。举例来说,return BadRequest(),是 return new BadRequestResult()的一种简便写法。
因为在这种类型的Action中会有多个返回类型,我们便可以使用 [ProducesResponseType]
属性。这个属性为由工具(比如Swagger)生成的Web API帮助文档页产生更具描述性的响应详细。[ProducesResponseType]
标识了由Action返回的确知的类型以及状态码。
同步Action
考虑如下同步Action,其有两种可能的返回类型。
[HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public IActionResult GetById(int id) { if (!_repository.TryGetProduct(id, out var product)) { return NotFound(); } return Ok(product); }
在上述Action中:
- 当由id表示的Product并不存在于底层的数据存储中的时候,其便会返回一个404状态码。调用NotFound 方法来作为
return new NotFoundResult();
.的便捷方式。 - 当Product确实存在时,便会返回一个200状态码以及Product对象。我们调用Ok 方法来作为return new OkObjectResult(product);的一种便捷写法。
异步Action
考虑如下异步Action,其具有两个可能的返回类型。
[HttpPost] [Consumes(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<IActionResult> CreateAsync(Product product) { if (product.Description.Contains("XYZ Widget")) { return BadRequest(); } await _repository.AddProductAsync(product); return CreatedAtAction(nameof(GetById), new { id = product.Id }, product); }
在上述Action中:
- 当产品描述包含“XYZ Widget”时,便会返回一个400状态码。我们调用BadRequest 方法来作为 return new BadRequestResult();的一种便捷写法。
- 当创建一个Product时,CreatedAtAction 方法便会产生一个201状态码。调用CreatedAtAction 的另一种替代方案是返回:return new CreatedAtActionResult(nameof(GetById), "Products", new { id = product.Id }, product);。在这个代码路径中,Product 对象提供在响应体中。而响应头Location则包含了新创建的Product对象的URL。
举个例子,如下的Model表明请求必须包含Name和Description属性。在请求中如果不能提供Name或者Descrption属性将会导致模型验证失败。
public class Product { public int Id { get; set; } [Required] public string Name { get; set; } [Required] public string Description { get; set; } }
在ASP.NET Core 2.1及后续版本中,如果应用了[ApiController] 属性,模型验证错误会导致400错误状态码。更多信息,请查看 Automatic HTTP 400 responses。
ActionResult<T>类型
ASP.NET Core 2.1 为Web API控制器Action方法引入了ActionResult<T> 返回类型。它使你能够返回一个继承自 ActionResult 的类型或者返回一个特定的类型。相比于IActionResult类型来说,ActionResult<T> 提供了如下优势:
-
[ProducesResponseType] 属性的Type属性可以被排除。举个例子[ProducesResponseType(200, Type = typeof(Product))] 可以被简化为
[ProducesResponseType(200)]。Action期望的类型将会从
ActionResult<T> 中的T中推断出来。
- 隐式转化操作符(Implicit cast operators )支持
T
和ActionResult 到ActionResult<T>的转化。T转化为ObjectResult,其意味着
return new ObjectResult(T);可以简化为return T;。
C#不支持接口上的隐式转化操作符。因此,为了使用ActionResult<T> 我们必须要将接口转化为具体的类型。举个例子 ,在如下的代码示例中使用
IEnumerable
是不会工作的。
[HttpGet] public ActionResult<IEnumerable<Product>> Get() => _repository.GetProducts();
修正上述方法的一个途径便是返回_repository.GetProducts().ToList();。
大部分Action都有特定的返回类型。在Action的执行中可能会发生非期望的情形,在这种情况下不会返回特定的类型。举个例子,一个Action的输入参数可能没有通过模型验证。在这种情况下,通常会返回一个合适的ActionResult类型而不是一个特定的类型。
同步Action
考虑一个同步的Action,其具有两种可能的返回类型。
[HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<Product> GetById(int id) { if (!_repository.TryGetProduct(id, out var product)) { return NotFound(); } return product; }
在上述Action中:
- 当Product不存在于数据库中时,便返回一个404状态码。
- 当Product确实存在时候便会随着相关的Product对象返回一个200状态码。在ASP.NET Core之前的版本中,
return product;
将会是return Ok(product);。
异步Action
考虑一个异步的Action,其具有两种可能的返回类型。
[HttpPost] [Consumes(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<ActionResult<Product>> CreateAsync(Product product) { if (product.Description.Contains("XYZ Widget")) { return BadRequest(); } await _repository.AddProductAsync(product); return CreatedAtAction(nameof(GetById), new { id = product.Id }, product); }
在上述Action中;
- ASP.NET Core运行时会返回一个400状态码,当
- 应用了
[ApiController]特性并且模型验证失败。
- Product描述信息包含 “XYZ Widget”。
- 应用了
- 当创建一个product 时,CreatedAtAction 方法会生成一个201状态码。在这个代码路径中,
Product
对象被提供在响应体中,而响应头 Location则包含了新创建的Product的URL。
额外资源