Saltar al contenido

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

17 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 Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 7 de febrero de 2017
ASP.NET Core MVCDurante el proceso de negociación de contenidos en un API desarrollado con ASP.NET Core MVC, lo habitual es que el output formatter utilizado para componer el resultado enviado de vuelta al cliente sea seleccionado a partir de lo indicado por el cliente en la cabecera Accept de la petición HTTP, pero en este post vamos a ver que esto no tiene por qué ser necesariamente así.

Por ejemplo, supongamos que tenemos una aplicación MVC con un controlador que expone un servicio como el siguiente:
[Route("api/contacts")]
public class ContactsController : Controller
{
    private readonly IContactRepository _repository;

    public ContactsController(IContactRepository repository)
    {
        _repository = repository;
    }

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var contact = _repository.Get(id);
        if (contact == null)
            return NotFound();
        return Ok(contact);
    }
}
Esta acción, que retornaría un objeto de tipo Contact del modelo de nuestra aplicación, podría ser fácilmente utilizada desde otras capas o sistemas, por ejemplo como sigue:
// Petición:
GET http://localhost:2805/api/contacts/1 HTTP/1.1 
Accept: application/json

// Respuesta:
HTTP/1.1 200 OK 
Content-Type: application/json; charset=utf-8  
Content-Length: 51

{"id":1,"name":"John Smith","phone":"998-12-32-12"}
Imaginad ahora que incluimos en nuestra aplicación ASP.NET Core MVC el paquete Microsoft.AspNetCore.Mvc.Formatters.Xml y añadimos el formateador XML en la configuración de servicios:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddXmlSerializerFormatters();
    ...
}
A partir de este momento, si en la petición anterior sustituimos el valor del encabezado Accept por "application/xml" obtendríamos lo siguiente:
// Petición: 
GET http://localhost:2805/api/contacts/1 HTTP/1.1 
Accept: application/xml
 
// Respuesta:
HTTP/1.1 200 OK 

Content-Type: application/xml; charset=utf-8  
Content-Length: 178
 
<Contact xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance 
         xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <Id>1</Id>
  <Name>John Smith</Name>
  <Phone>998-12-32-12</Phone>
</Contact>
Aunque este comportamiento será válido en la mayoría de escenarios, el framework proporciona otras fórmulas para controlar el formato de salida sin necesidad de atender a dicho encabezado.

1. El filtro [Produces]

Si queremos forzar un formato determinado, lo más sencillo es, sin duda, utilizar el filtro [Produces]. Este filtro, aplicable a controladores o acciones específicas, permite establecer manualmente el tipo de contenido a retornar, ignorando el encabezado Accept indicado por el cliente.

Por ejemplo, la siguiente acción retornará los datos formateados como XML sea cual sea la petición:
[Produces("application/xml")]
public IActionResult Get(int id)
{
    ...
}
Bueno, obviamente lo anterior es cierto siempre está instalado que el formateador de salida encargado de gestionar las respuestas de tipo "application/xml" (hemos visto antes cómo hacerlo).

En caso contrario, el cliente obtendrá un error HTTP 406 (Not acceptable) porque no existirá ningún formateador capaz de generar correctamente ese tipo de respuesta.

2. Especificar el formateador mediante un parámetro de query string

Otra posibilidad interesante, es poder utilizar otra información contenida en la query string para determinar el formateador a utilizar. Para ello, el framework nos proporciona el filtro [FormatFilter], utilizable tanto en acciones como en controladores, que establece el formato de salida en función del valor indicado en el parámetro de ruta "format" asociado a la petición actual. Observad su uso sobre la siguiente acción:
[FormatFilter]
[HttpGet("{id}")]
public IActionResult Get(int id)
{
    var contact = _repository.Get(id);
    if (contact == null)
        return NotFound();
    return Ok(contact);
}
Tras incluir este filtro, el solicitante podría especificar el formato en el que desea obtener los datos directamente indicándolo en la query string. Una petición hacia /api/contacts/1?format=xml retornaría los datos en XML, mientras que serían retornados en JSON si la petición se dirige a /api/contacts/1?format=json.

Es importante tener en cuenta que MVC necesita conocer los mapeos entre los posibles valores del parámetro "format" y los tipos de contenido a los que representan. Es decir, necesita saber que cuando encuentre en la query string el valor "xml", éste se corresponde con el formato de salida "application/xml".

Estos mapeos se encuentran en la colección FormatterMappings, y deben ser configurados en la clase Startup. Por ejemplo, el siguiente código establece mapeos de los valores "js" y "xml" a sus respectivos media types:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(opt =>
    {
        opt.FormatterMappings.SetMediaTypeMappingForFormat("xml", "application/xml");
        opt.FormatterMappings.SetMediaTypeMappingForFormat("js", "application/json");
    });
    ...
}
De esta manera, cuando el parámetro de ruta "format" tenga un valor "xml", la respuesta se generará usando el formateador configurado para los resultados de tipo "application/xml", mientras que "js" se mapeará a "application/json". Por defecto, el framework introduce automáticamente en FormatterMappings el mapeo entre "json" y "application/json", por lo que no sería necesario realizarlo.

3. Especificar el formateador en la ruta de la petición

Antes hemos visto que "format" podemos añadirlo como parámetro a la query string, pero ciertamente no queda todo lo elegante que debería, por lo que siempre podríamos jugar con el sistema de routing y hacer que dicho parámetro sea parte de la ruta. Veamos el siguiente ejemplo, que nos permitiría hacer peticiones como /api/xml/contacts/1 para obtener los datos del contacto en formato XML:
[FormatFilter]

[Route("api/{format}/contacts")]

public class ContactsController : Controller
{
   [FormatFilter]    
   [HttpGet("{id}")]    
   public IActionResult Get(int id)
   {
    ...    
   }

}

4. Especificar el formateador mediante la extensión del recurso solicitado

Otra posibilidad sería usar el formato como si fuera la extensión del recurso solicitado, de forma que una petición como /api/contacts/1.xml retornaría el contacto en XML, mientras que /api/contacts/1.js o /api/contacts/1.json lo retornaría en JSON. Esto podemos conseguirlo de forma igualmente sencilla, sólo retocando un poco las rutas:
[FormatFilter]
[HttpGet("{id}.{format?}")]
public IActionResult Get(int id)
{
    ...
}

5. ¿Y si quiero seleccionar el formateador usando criterios personalizados?

Pues también podemos hacerlo, pues existe la posibilidad de implementar nuestra propia lógica de selección de formato. Aunque no veo a priori muchos escenarios en los que podría resultar interesante, siempre es bueno saber que estamos hablando de un mecanismo fácilmente extensible.

Por ejemplo, observad el siguiente código, donde heredamos de la implementación por defecto de la clase FormatFilter y hacemos que el formato retornado sea "json" o "xml" aleatoriamente (poco útil, pero seguro que da para echar unas risas con el desarrollador encargado de consumir los servicios ;DD)
public class RandomFormatFilter : FormatFilter
{
    private readonly Random _rnd;

    public RandomFormatFilter(IOptions<MvcOptions> options) : base(options)
    {
        _rnd = new Random();
    }
    public override string GetFormat(ActionContext context)
    {
        return _rnd.Next(0, 2) == 1 ? "json" : "xml";
    }
}
Para activar este selector de formato bastaría con añadir la siguiente línea a la configuración de servicios de ASP.NET Core:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(opt =>
    {
        opt.FormatterMappings.SetMediaTypeMappingForFormat("xml", "application/xml");
        opt.FormatterMappings.SetMediaTypeMappingForFormat("js", "application/json");

    }).AddXmlSerializerFormatters();

    services.AddSingleton<FormatFilter, RandomFormatFilter>();    
}
Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!