Tipos de valor devuelto de acción del controlador de la API web de ASP.NET Core

Por Scott Addie

Vea o descargue el código de ejemplo (cómo descargarlo)

ASP.NET Core ofrece las siguientes opciones relativas a los tipos de valor devuelto de acción del controlador de la API web:

En este documento se explica cuándo resulta más adecuado usar cada tipo de valor devuelto.

Tipo específico

La acción más sencilla devuelve un tipo de datos primitivo o complejo (por ejemplo, string o un tipo de objeto personalizado). Consideremos la siguiente acción, que devuelve una colección de objetos Product personalizados:

[HttpGet]
public List<Product> Get() =>
    _repository.GetProducts();

Sin condiciones conocidas de las que haya que protegerse durante la ejecución de la acción, bastaría con que se devolviera un tipo específico. La acción anterior no acepta parámetros, por lo que no se necesita ninguna validación de restricciones de parámetros.

Cuando son posibles varios tipos de valor devuelto, es habitual mezclar un tipo de valor devuelto con el tipo de valor devuelto primitivo ActionResult o complejo. IActionResult o ActionResult <T> son necesarios para dar cabida a este tipo de acción. En este documento se proporcionan varios ejemplos de varios tipos de valor devuelto.

Devolver IEnumerable <T> o IAsyncEnumerable<T>

En ASP.net Core 2.2 y versiones anteriores, la devolución de IEnumerable<T> en una acción da como resultado la iteración de la colección sincrónica por parte del serializador. El resultado es el bloqueo de llamadas y una posibilidad de colapso del grupo de subprocesos. Por poner un ejemplo, imagine que se usa Entity Framework (EF) Core para las necesidades de acceso a los datos de la API Web. El tipo de valor devuelto de la acción siguiente se enumera sincrónicamente durante la serialización:

public IEnumerable<Product> GetOnSaleProducts() =>
    _context.Products.Where(p => p.IsOnSale);

Para evitar la enumeración sincrónica y las esperas por bloqueo en la base de datos en ASP.NET Core 2.2 y versiones anteriores, invoque ToListAsync:

public async Task<IEnumerable<Product>> GetOnSaleProducts() =>
    await _context.Products.Where(p => p.IsOnSale).ToListAsync();

En ASP.net Core 3,0 y versiones posteriores, que una acción devuelva IAsyncEnumerable<T>:

  • Ya no da como resultado iteraciones sincrónicas.
  • Es tan eficaz como devolver IEnumerable<T>.

Tanto ASP.NET Core 3.0 como las versiones posteriores almacenan en búfer el resultado de la siguiente acción antes de proporcionarlo al serializador:

public IEnumerable<Product> GetOnSaleProducts() =>
    _context.Products.Where(p => p.IsOnSale);

Considere la posibilidad de declarar el tipo de valor devuelto de la signatura de la acción como IAsyncEnumerable<T> para garantizar la iteración asincrónica. En última instancia, el modo de iteración se basa en el tipo concreto subyacente que se va a devolver. MVC almacena en búfer automáticamente cualquier tipo concreto que implemente IAsyncEnumerable<T>.

Considere la siguiente acción, que devuelve los registros de producto con precio de venta como IEnumerable<Product>:

[HttpGet("syncsale")]
public IEnumerable<Product> GetOnSaleProducts()
{
    var products = _repository.GetProducts();

    foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}

El IAsyncEnumerable<Product> equivalente de la acción anterior es:

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _repository.GetProductsAsync();

    await foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}

Las dos acciones anteriores no suponen ningún bloqueo a partir de ASP.NET Core 3.0.

Tipo IActionResult

El tipo de valor devuelto IActionResult resulta adecuado cuando existen varios tipos de valor devuelto ActionResult posibles en una acción. Los tipos ActionResult representan varios códigos de estado HTTP. Cualquier clase no abstracta derivada de ActionResult se considera un tipo de valor devuelto válido. Algunos tipos de valor devueltos comunes en esta categoría son BadRequestResult (400), NotFoundResult (404) y OkObjectResult (200). Como alternativa, se pueden usar métodos de conveniencia en la clase ControllerBase para devolver tipos ActionResult de una acción. Por ejemplo, return BadRequest(); es una forma abreviada de return new BadRequestResult();.

Dado que hay varios tipos de valor devuelto y rutas de acceso en este tipo de acción, es necesario usar el atributo [ProducesResponseType] . Este atributo genera detalles de respuesta más pormenorizados relativos a las páginas de ayuda de API web generadas por herramientas como Swagger. [ProducesResponseType] indica los tipos conocidos y los códigos de estado HTTP que la acción va a devolver.

Acción sincrónica

Veamos la siguiente acción sincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return Ok(product);
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return Ok(product);
}

En la acción anterior:

  • Se devuelve un código de estado 404 cuando el producto representado por id no existe en el almacén de datos subyacente. El método de conveniencia NotFound se invoca como una abreviatura para return new NotFoundResult();.
  • Se devuelve un código de estado 200 con el objeto Product cuando existe el producto. El método de conveniencia Ok se invoca como una abreviatura para return new OkObjectResult(product);.

Acción asincrónica

Veamos la siguiente acción asincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[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);
}
[HttpPost]
[Consumes("application/json")]
[ProducesResponseType(typeof(Product), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync([FromBody] Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    await _repository.AddProductAsync(product);

    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

En la acción anterior:

  • Se devuelve un código de estado 400 cuando la descripción del producto contiene "XYZ Widget". El método de conveniencia BadRequest se invoca como una abreviatura para return new BadRequestResult();.
  • Al crear un producto, el método de conveniencia CreatedAtAction genera un código de estado 201. Una alternativa a la llamada a CreatedAtAction es return new CreatedAtActionResult(nameof(GetById), "Products", new { id = product.Id }, product);. En esta ruta de acceso de código, el objeto Product se proporciona en el cuerpo de la respuesta. Se proporciona un encabezado de respuesta Location que contiene la dirección URL del producto recién creada.

Por ejemplo, el siguiente modelo indica que las solicitudes deben incluir las propiedades Name y Description. Si no se proporcionan Name y Description en la solicitud, se producirá un error en la validación de los modelos.

public class Product
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string Description { get; set; }
}

Si se aplica el atributo ASP.NET Core 2.1 o posterior, los errores de validación del modelo producen un código de [ApiController] estado 400. Para obtener más información, consulte Respuestas HTTP 400 automáticas.

ActionResult frente a IActionResult

En la sección siguiente se compara ActionResult con IActionResult

Tipo <T> ActionResult

ASP.NET Core incluye el tipo de valor devuelto ActionResult <T> para las acciones del controlador de API web. Permite devolver un tipo que se deriva de ActionResult o bien un tipo específico. ActionResult<T> reporta las siguientes ventajas con frente al tipo IActionResult:

  • La [ProducesResponseType] propiedad del atributo se puede Type excluir. Por ejemplo, [ProducesResponseType(200, Type = typeof(Product))] se simplifica a [ProducesResponseType(200)]. En su lugar, el tipo de valor devuelto esperado de la acción se infiere desde T en ActionResult<T>.
  • Los operadores de conversión implícitos admiten la conversión tanto de T como de ActionResult en ActionResult<T>. T se convierte en ObjectResult, lo que significa que return new ObjectResult(T); se ha simplificado para return T;.

C# no admite operadores de conversión implícitos en las interfaces. Por consiguiente, la conversión de la interfaz a un tipo concreto es necesaria para usar ActionResult<T>. Por ejemplo, el uso de IEnumerable en el siguiente ejemplo no funciona:

[HttpGet]
public ActionResult<IEnumerable<Product>> Get() =>
    _repository.GetProducts();

Una opción para corregir el código anterior es devolver _repository.GetProducts().ToList();.

La mayoría de las acciones tiene un tipo de valor devuelto específico. Se pueden producir condiciones inesperadas que durante la ejecución de una acción, en cuyo caso no se devuelve el tipo específico. Por ejemplo, puede suceder que el parámetro de entrada de una acción genere un error de validación de modelos. En tal caso, lo normal es que se devuelva el tipo ActionResult correspondiente en lugar del tipo específico.

Acción sincrónica

Veamos una acción sincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[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;
}

En la acción anterior:

  • Se devuelve un código de estado 404 cuando el producto no existe en la base de datos.
  • Se devuelve un código de estado 200 con el objeto Product correspondiente cuando existe el producto. Antes de ASP.NET Core 2.1, la línea return product; tenía que ser return Ok(product);.

Acción asincrónica

Veamos una acción asincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[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);
}

En la acción anterior:

  • El entorno de ejecución de ASP.NET Core devuelve un código de estado 400 (BadRequest) en los casos siguientes:
    • Se [ApiController] ha aplicado el atributo y se produce un error en la validación del modelo.
    • La descripción del producto contiene "Widget XYZ".
  • Al crear un producto, el método CreatedAtAction genera un código de estado 201. En esta ruta de acceso de código, el objeto Product se proporciona en el cuerpo de la respuesta. Se proporciona un encabezado de respuesta Location que contiene la dirección URL del producto recién creada.

Recursos adicionales