Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí
Cuando salió WebApi lo hizo con la negociación de contenido incorporada de serie en el framework. Eso venía a significar, básicamente, que el framework intentaba suministrar los datos en el formato en que el cliente los había pedido. La negociación de contenido se basa (generalmente) en el uso de la cabecera accept de HTTP: el cliente manda en esa cabecera cual, o cuales, son sus formatos de respuesta preferidos. WebApi soporta de serie devolver datos en JSON y XML y el sistema es extensible para crear nuestros propios formatos.
“MVC clásico” (es decir hasta MVC5) no incluye soporte de negociación de contenido: en MVC si queremos devolver datos en formato JSON, debemos devolver explícitamente un JsonResult y si los queremos devolver en XML debemos hacerlo también explícitamente.
En ASP.NET Core tenemos a MVC6 que unifica a WebApi y MVC clásico en un solo framework. ¿Como queda el soporte para negociación de contenido en MVC6? Pues bien, existe soporte para ella, pero dependiendo de que IActionResult devolvamos en nuestros controladores. Así, si en WebApi la negociación de contenido se usaba siempre y en MVC clásico nunca, en MVC6 la negociación de contenido aplica solo si la acción del controlador devuelve un ObjectResult (o derivado). Esto nos permite como desarrolladores decidir sobre qué acciones de qué controladores queremos aplicar la negociación de contenido. Es evidente que aplicarla siempre no tiene sentido: si devolvemos una vista Razor su resultado debe ser sí o sí un HTML que se envía al cliente. No tendría sentido aplicar negociación de contenido sobre una acción que devolviese una vista. De hecho la negociación de contenido tiene sentido en APIs que devuelvan datos (no vistas) y en MVC6 para devolver datos tenemos a ObjectResult, así que es lógico que sea sobre este resultado donde se aplique la negociación de contenido.
En WebApi la negociación de contenido estaba gestionada por los formateadores (formatters). Básicamente a cada content-type se le asociaba un formateador. Si el cliente pedía datos en un determinado content-type se miraba que formateador podía devolver datos en dicho formato. Si no existía se usaba por defecto el formateador de JSON. En MVC6 se ha mantenido básicamente dicho esquema.
La principal diferencia es que en WebApi los formateadores se encargaban realmente de dos tareas totalmente separadas: por un lado procesaban (leían) los datos de entrada (es decir definían que tipos de content-types aceptaba el servidor) y también procesaban (serializaban) los datos de salida. El problema es fácil de ver: el hecho de que una API devuelva datos en un determinado formato (pongamos XML) no significa que deba aceptar datos (p. ej. un POST) en dicho formato. Pero en WebApi al implementar el formateador de XML debíamos implementar tanto el método para leer datos en XML como para escribirlos. En MVC6 se ha solucionado este detalle separando los formateadores en dos: los de entrada (leen los datos enviados por el cliente) y los de salida (envían los datos al cliente). Esto separa mejor las responsabilidades.
Vamos a ver como podemos crear un formateador que nos permita devolver datos en CSV. Dado que CSV es un formato de tipo “tabular”, solo vamos a aceptar devolver datos en este formato, siempre que esos datos sean un IEnumerable.
Creando un formateador de salida
Para crear un formateador de salida basta con implementar la interfaz IOutputFormatter, que define dos métodos:
- CanWriteResult: Que debe indicar si el formateador puede enviar el resultado al cliente
- WriteAsync: Que debe enviar los datos formateados
Una posible implementación podría ser tal y como sigue:
<div id="scid:9ce6104f-a9aa-4a17-a79f-3a39532ebf7c:61cb7d01-465c-43db-ace1-2e9160a1983f" class="wlWriterEditableSmartContent" style="float: none; padding-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; display: inline; padding-right: 0px">
<div style="border: #000080 1px solid; color: #000; font-family: 'Courier New', Courier, Monospace; font-size: 10pt">
<div style="background: #ddd; max-height: 300px; overflow: auto">
<ol start="1" style="background: #ffffff; margin: 0 0 0 2.5em; padding: 0 0 0 5px; white-space: nowrap">
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">public</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;color:#0000ff">class</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;color:#2b91af">CsvOutputFormatter</span><span style="background:#ffffff;color:#000000"> : </span><span style="background:#ffffff;color:#2b91af">IOutputFormatter</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">{</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">public</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;color:#0000ff">bool</span><span style="background:#ffffff;color:#000000"> CanWriteResult(</span><span style="background:#ffffff;color:#2b91af">OutputFormatterCanWriteContext</span><span style="background:#ffffff;color:#000000"> context)</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">{</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">if</span><span style="background:#ffffff;color:#000000"> (context.ContentType.MediaType != </span><span style="background:#ffffff;color:#a31515">"text/csv"</span><span style="background:#ffffff;color:#000000">)</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">{</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">return</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;color:#0000ff">false</span><span style="background:#ffffff;color:#000000">;</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">}</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">var</span><span style="background:#ffffff;color:#000000"> type = context.ObjectType.GetTypeInfo();</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">if</span><span style="background:#ffffff;color:#000000"> (type.ImplementedInterfaces.Any(ii => ii == </span><span style="background:#ffffff;color:#0000ff">typeof</span><span style="background:#ffffff;color:#000000">(</span><span style="background:#ffffff;color:#2b91af">IEnumerable</span><span style="background:#ffffff;color:#000000">)))</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">{</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">return</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;color:#0000ff">true</span><span style="background:#ffffff;color:#000000">;</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">}</span>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">return</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;color:#0000ff">false</span><span style="background:#ffffff;color:#000000">;</span>
</li>
<li>
<span style="background:#ffffff;color:#000000">}</span>
</li>
<li>
</li>
<li>
<span style="background:#ffffff;color:#000000"></span><span style="background:#ffffff;color:#0000ff">public</span><span style="background:#ffffff;color:#000000"> </span><span style="background:#ffffff;
color:#0000ff">async Task WriteAsync(OutputFormatterWriteContext context)
{
var response = context.HttpContext.Response;
response.ContentType = “text/csv”;
using (var writer = context.WriterFactory(response.Body, Encoding.UTF8))
{
await CsvSerializer.SerializeAsync(context.Object, writer);
await writer.FlushAsync();
}
}
}