Autor en Google+
Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

12 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 3 de julio de 2018
ASP.NET Core MVC Uno de los objetivos de ASP.NET Core ha sido siempre servir como infraestructura para la creación de servicios HTTP o APIs web, por lo que ya desde las primeras versiones se incluían funcionalidades específicamente diseñadas para facilitar esta tarea.

ASP.NET Core 2.1 continúa profundizando en esa línea e incluye entre sus novedades el nuevo atributo [ApiController], un decorador aplicable a controladores que los identifica como puntos de entrada de APIS, aplicando de forma automática una serie de convenciones bastante útiles a la hora de crear este tipo de componentes:
[ApiController]
[Route("api/[controller]")]
public class ValuesController : ControllerBase
{
    ...
}
Fijaos que, a diferencia de ASP.NET Web API (.NET Framework), se ha optado por utilizar un atributo en lugar de emplear herencia (en aquél framework existía la clase ApiController).
A continuación veremos qué debemos tener en cuenta a la hora de aplicar este atributo a nuestros controladores y qué convenciones son las que estaremos asumiendo al utilizarlo.

Obligatoriedad de usar rutas explícitas

El atributo [Route] es obligatorio La aplicación de [ApiController] a un controlador hace obligatorio el uso de rutado por atributos hacia sus acciones. Es decir, los controladores decorados con [ApiController] no serán accesibles utilizando rutas por convenciones, como las definidas en el método Configure() de la clase Startup. De no ser así, se lanzará una excepción durante el arranque de la aplicación y no se podrá ejecutar.

Por tanto, este atributo vendrá siempre acompañado del correspondiente [Route], ya sea a nivel de controlador, o en sus acciones.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    ...
}

Inferencia del origen de parámetros

Al decorar un controlador con [ApiController], el binder usará el siguiente criterio, en orden de aplicación, para determinar desde dónde obtener los valores para los parámetros de las acciones:
  • Los parámetros de tipo complejo (clases, básicamente) se obtendrán desde el cuerpo de la petición. Sería equivalente a decorar el parámetro con [FromBody] en versiones anteriores a ASP.NET Core 2.1, por lo que, como ocurría anteriormente, sólo puede existir un parámetro de este tipo en las acciones.
     
  • Los valores para los parámetros de tipo IFormFile e IFormFileCollection serán tomados desde los valores recibidos como campos de formulario. Equivalente a usar [FromForm] en versiones anteriores.
     
  • Los parámetros cuyo nombre coincida con un valor presente en la ruta de la petición serán tomados desde ella, es decir, como si estuvieran decorados con [FromRoute].
     
  • El resto de parámetros se asumirá que proceden de la query string, equivalente a aplicar FromQuery] sobre ellos.
Por ejemplo, en una acción código como la siguiente, el valor del parámetro id se obtendrá desde la ruta de la petición, Invoice desde el cuerpo de la petición, y returnUrl desde la query string, todo ello sin necesidad de especificarlo expresamente:
[ApiController]
[Route("api/[controller]")]
public class InvoiceController: ControllerBase 
{
    [HttpPost("update/{id}")]
    public ActionResult<InvoiceCreationResult> Update(int id, InvoiceInfo invoice, string returnUrl)
    {
        ...
    }
}

Errores HTTP 400 automáticos cuando el estado del modelo es inválido

Cuando entra una petición dirigida hacia una acción perteneciente a un controlador decorado con [ApiController], el binder cargará sus parámetros y aplicará automáticamente las validaciones asociadas a los datos de entrada. Como es habitual, el resultado de dicha validación lo tendremos en la propiedad ModelState.IsValid.

Pues bien, por defecto, el framework comprobará todos los objetos recibidos, y retornará un error HTTP 400 cuando se produzcan errores de validación. A continuación puedes ver un ejemplo de acción que devuelve el mismo objeto que recibe como parámetro, y el resultado obtenido de algunas peticiones:
public class Friend
{
    [Required]
    public string Name { get; set; }
    [Range(0, 120)]
    public int Age { get; set; }
}

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    [HttpPost]
    public ActionResult<Friend> Friend(Friend f)
    {
        return f;
    }
}
PeticiónRespuesta
Enviamos un objeto correcto:
POST https://localhost:44399/api/test/ HTTP/1.1
Host: localhost:44399
Content-Length: 34
Content-Type: application/json

{
   name: "John",
   age: 34
}
HTTP 200, con el mismo objeto recibido:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 24

{"name":"John","age":34}
Objeto con contenidos inválidos en los campos:
POST https://localhost:44399/api/test/ HTTP/1.1
Host: localhost:44399
Content-Length: 31
Content-Type: application/json

{
   name: "",
   age: 200
}
HTTP 400 con el resultado de las validaciones incorrectas:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 91

{
   "Age":["The field Age must be between 0 and 120."],
   "Name":["The Name field is required."]
}
Ni siquiera enviamos un objeto:
POST https://localhost:44399/api/test/ HTTP/1.1
Host: localhost:44399
Content-Length: 0
HTTP 400 indicando un error en el objeto:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 33

{"":["The input was not valid."]}

Personalización o desactivación de algunos comportamientos

Aunque las convenciones asociadas al uso de [ApiController] son bastante razonables, si por cualquier motivo queremos desactivar alguno de los comportamientos por defecto, podemos hacerlo desde el método ConfigureServices() de la clase Startup.

En el siguiente código muestra cómo desactivarlas todas (ya, mucho sentido no tendría, pero me vale como ejemplo ;))
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = false;
    options.SuppressInferBindingSourcesForParameters = false;
    options.SuppressModelStateInvalidFilter = false;
});
Ojo, que estas líneas deben incluirse después de haber añadido los servicios de MVC con services.AddMvc(), porque de lo contrario no funcionará.
También podemos configurar el tipo de resultado que se retornará cuando se detecte que el estado del modelo es inválido. Por ejemplo, en el siguiente código se establece como respuesta un resultado del tipo NotFoundResult(), lo que provocará que nuestros clientes obtengan un HTTP 404 como respuesta a una petición con datos inválidos:
services.Configure<ApiBehaviorOptions>(options =>
{
    ...
    options.InvalidModelStateResponseFactory = context => new NotFoundResult();
});
¡Y eso es todo! Espero que lo que hemos visto os sea de utilidad, y gracias a estas pequeñas novedades os resulte más sencillo el desarrollo de APIs HTTP con ASP.NET Core :)

Publicado en Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

2 Comentarios:

Anónimo dijo...

Como siempre, estás a la última. Nosotros ya hemos comenzado a aplicar 2.1 por estas razones, no sin haber perdido un considerable aunque pequeño tiempo creando filtros que ya han añadido por defecto, como el BadRequest que describes.

Ctrr+A => supr... Y a continuar para adelante.

Solo añadir que ya es compatible los despliegues de CI/DI en AWS de proyectos .Net Core 2.1

Un cordial saludo José.

José M. Aguilar dijo...

Muchas gracias, figura!

Veo que vais a todo trapo, a ver si un día nos vemos y me ponéis al día :)

Un abrazo!

Artículos relacionados: