ASP.NET MVC – Formato de salida según Content-Type

El otro día escribí un post donde vimos como mostrar una vista en PDF o HTML en función de una URL del tipo /controlador/accion(formato)/parámetros. El post estaba centrado básicamente en la tabla de rutas y cómo la URL clásica de ASP.NET MVC /Controlador/Accion/Parámetros no es una obligación sinó básicamente una convención.

Hadi Hariri realizó un comentario, muy interesante a mi jucio. Venía a decir que antes que añadir en la ruta el parámetro formato es mejor usar el campo Accept de la request. Copio literalmente: “La tercera opcion, que lo hace más transparente al usuario y además está en acorde a ReST, es la de usar las el ContentType en la petición, que es lo que yo normalmente hago.

Si quieres exponer una API lo más ReST posible en ASP.NET MVC y que tenga salidas en distintos formatos, sin duda deberías tener en cuenta la sugerencia de Hadi.

1. La cabecera de la petición http

Cuando un cliente envía una petición http a un servidor, que contiene una cabecera con varios parámetros. Dicha cabecera tiene varios campos que permiten especificar determinadas opciones que el cliente desea. Uno de esos campos es el campo Accept que permite indicar que formatos de respuesta acepta el cliente y en que orden.

P.ej. si hago una petición con Firefox a http://www.google.es, el contenido del campo Accept de la cabecera que firefox envia es (lo acabo de mirar con Firebug):

text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Que podríamos interpretar (más o menos) como: Mis formatos preferidos son text/html y application/xhtml+xml, si no puedes en ninguno de esos dos, envíamelo en application/xml y si no puedes, pues me tragaré lo que me mandes.

El valor exacto de dicha cabecera depende del browser… P.ej. IE8 para la misma peticion envia el siguiente valor en Accept (lo acabo de mirar con Fiddler):

image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*

Por lo tanto vemos como en el campo Accept el cliente nos dice que formatos de respuesta entiende (y en que orden los prefiere).

2. Acceso a la cabecera desde ASP.NET MVC

Imaginad que tenemos un controlador que puede devolver datos en dos formatos: XML y JSON. Y queremos usar el campo Accept de la cabecera http que envíe el cliente para devolver los datos en uno u otro formato.

Acceder a la cabecera http desde un controlador es extremadamente sencillo, usando Request.AcceptTypes, que es un array con todos los campos de la cabecera accept.

3. Devolver datos en formato XML

ASP.NET MVC no trae ningún mecanismo incluído para devolver datos en formato xml, lo que me va de coña para enseñaros como nos podemos crear un ActionResult propio:

public class XmlActionResult : ActionResult
{
private readonly object _data;
public XmlActionResult(object data)
{
_data = data;
}
public override void ExecuteResult(ControllerContext context)
{
XmlSerializer ser = new XmlSerializer(_data.GetType());
context.HttpContext.Response.ContentType = "text/xml";
ser.Serialize(context.HttpContext.Response.OutputStream, _data);
}
}

Crear un ActionResult propio es trivial: deriváis de ActionResult y implementáis el método abstracto ExecuteResult y en él hacéis lo que sea necesario (usualmente interaccionar con la Response). En este caso simplemente serializo el objeto que se le pasa con el serializador estándard de .NET. Ah si! Y pongo el content-type a text/xml que es el content-type usado para documentos en XML.

Yo suelo acompañar los ActionResults propios con un método extensor para los controladores, para llamarlos de forma similar a los ActionResults que vienen en el framework. Mi método extensor (trivial) es:

public static class ControllerExtensions
{
public static XmlActionResult Xml(this ControllerBase @this, object data)
{
return new XmlActionResult(data);
}
}

Y ahora ya puedo realizar la acción de mi controlador:

public ActionResult List()
{
var data = new GeeksModel().GetAllGeeks();
return Request.AcceptTypes.Contains("application/json") ?
(ActionResult)Json(data, JsonRequestBehavior.AllowGet) :
(ActionResult)this.Xml(data);
}

Simplemente pregunto si está el accept application/json(*) (que parece ser el content-type para JSON). Si lo está envío los datos en json y si no pues en xml! Si abrimos un navegador y vamos a /Geeks/List veremos los datos en XML porque ningún (bueno, ni FF ni IE que son los que he probado :p) envían application/json en el accept de la request.

(*) Ok, acepto que esta pregunta no es del todo correcta: debería mirar si application/json está preferido antes que text/xml (por si me manda ambos). Igual que teoricamente, debería comprobar si no me manda ninguno de los dos, y si es el caso devolver un error 406.

4. Un detallito final…

Bueno, eso parece funcionar, pero lo que chirría un poco es tener que meter este if() para comprobar en cada acción de cada controlador si la request contiene application/json o no y serializar el resultado en JSON o en XML.

Para evitar esto he encontrado dos alternativas en la red:

  1. Usar otro action result y que sea el action result quien decida si serializar los datos en XML o en JSON. Es decir, crearnos un JsonOrXmlActionResult, devolver siempre una instancia de este action result desde los controladores y en el ExecuteResult, preguntar por el campo accept y serializar en un formato en otro. Esta aproximación la he visto en el post “Create REST API using ASP.NET MVC that speaks both Json and plain Xml” del blog de Omar Al Zabir.
  2. Otra aproximación totalmente distinta (pero muy interesante) que usa un action filter para ello. Está en el blog de Aleem Bawany, en el post “ASP.NET MVC – Create easy REST API with JSON and XML”.

Os recomiendo la lectura de estos dos posts.

Un saludo y gracias a todos, especialmente a Hadi Hariri quien con su comentario anterior, ha motivado este post! 🙂

Un comentario en “ASP.NET MVC – Formato de salida según Content-Type”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *