Mi (pequeño) homenaje al gran, genial e irrepetible Terry Pratchett

Terry Pratchett ha sido uno de los grandes escritores de novelas de fantasía. Su serie más conocida Mundodisco, cuenta con 41 libros escritos en un estilo desenfadado y humorístico que parodian el género fantástico pero que a la vez encierran durísimas y mordaces críticas contra muchos aspectos de nuestra sociedad. El humor de Pratchett es reconocido como uno de los más ácidos e inteligentes a la vez que absurdos y esta mezcla es explosiva: sus libros te hacen estallar en carcajadas a la vez que reflexionar. Para mi ha sido uno de los escritores que más me ha impactado.

Son muy conocidas sus frases, sacadas tanto de sus libros como de sus charlas y hay webs que se dedican a recopilarlas. Algunas hacen gracia por si mismas, otras hacen mucha más gracia dentro del contexto del libro. En inglés, la página “L-Space Web” contiene probablemente la mayor recopilación de frases de Pratchett. En castellano es “la concha de gran A’Tuin” quien contiene otra buena colección.

Ahora que Terry Pratchett ha muerto he pensado que es el momento de hacerle un pequeño (pequeñísimo) homenaje, similar a otros que se están haciendo al estilo de “GNU Terry Pratchett”.

En mi caso he dedicido crear un middlewarwe OWIN que añade una cabecera HTTP con una cita de Terry Pratchett al azar. Las citas están extraídas de la lista contenida en L-Space Web, aunque el middleware es configurable para que puedas usar tus propias citas si lo prefieres.

El código principal del middleware es muy sencillo:

  1. public class PratchettOwinMiddleware
  2. {
  3.     private readonly AppFunc _next;
  4.     private readonly PratchettQuotesFactory _quotesFactory;
  5.     private readonly Random _random;
  6.     public PratchettOwinMiddleware(AppFunc next, PratchettQuotesFactory quotesFactory)
  7.     {
  8.         _next = next;
  9.         _quotesFactory = quotesFactory;
  10.         _random = new Random();
  11.     }
  12.  
  13.     public async Task Invoke(IDictionary<string, object> environment)
  14.     {
  15.         await _next.Invoke(environment);
  16.         var headers = environment["owin.ResponseHeaders"] as IDictionary<string, string[]>;
  17.         var quotes = _quotesFactory.GetQuotes();
  18.         var quote = quotes[_random.Next(0, quotes.Length)];
  19.         headers.Add("X-Pratchett-Quote", new string[] { quote});
  20.     }
  21. }

Básicamente se dedica a añadir la cabecera X-Pratchett-Quote a cualquier petición que sea procesada. Para registrarlo en el pipeline de OWIN se usa un método de extensión sobre IAppBuilder definido en la clase PratchettAppBuilderExtensions:

  1.   public static class PratchettAppBuilderExtensions
  2.   {
  3.       public static void UseTerryPratchett(this IAppBuilder app)
  4.       {
  5.           app.Use(typeof(PratchettOwinMiddleware),
  6.               new PratchettQuotesFactory(
  7.                   new InternalFileQuoteParser(),
  8.                     () =>Assembly.GetExecutingAssembly().GetManifestResourceStream("PratchettQuotes.terry_quotes.txt")));
  9.       }
  10.  
  11.       public static void UseTerryPratchett(this IAppBuilder app, IQuoteParser quoteParser, string filename)
  12.       {
  13.           app.Use(typeof(PratchettOwinMiddleware),
  14.               new PratchettQuotesFactory(quoteParser, () => new FileStream(filename, FileMode.Open, FileAccess.Read)));
  15.       }
  16.  
  17.       public static void UseTerryPratchett(this IAppBuilder app, IQuoteParser quoteParser, Func<Stream> quotesProvider)
  18.       {
  19.           app.Use(typeof(PratchettOwinMiddleware), new PratchettQuotesFactory(quoteParser, quotesProvider));
  20.       }
  21.   }

El método está sobrecargado para admitir tus propias citas. En este caso debes pasar también una clase que implemente la interfaz IQuoteParser que indique como leer los datos del stream que contiene las citas. No se incluye ninguna implementación de dicha interfaz (bueno, se incluye una pero es interna ya que se usa para parsear los datos del fichero de citas, que está sacado literalmente de L-Space Web).

Si llamas simplemente a UseTerryPratchett() se usará el fichero de citas que está incrustado dentro del ensamblado.

El resultado lo puedes ver en esta captura de la pestaña red de Chrome:

image

El código fuente del proyecto lo teneis en github: https://github.com/eiximenis/PratchettQuotes

Saludos!:D

WebApi: Subir un fichero y datos adicionales

El modelo de binding que tiene WebApi es bastante más simple que el de MVC y por ello algunas tareas que en MVC eran más simples en WebApi pasan a ser un poco más complejas. Una de esas tareas es la subida de ficheros y de datos adicionales.

A diferencia de MVC donde el contenido del cuerpo de la petición puede ser inspeccionado numerosas veces (según los value providers que tengamos configurados) en WebApi el cuerpo de la petición es un stream que puede ser leído una sola vez. A nivel práctico eso implica que de todos los parámetros que pueda recibir el controlador tan solo uno puede ser puede ser enlazado con los datos del cuerpo de la petición. El resto deben ser enlazados desde otros sitios (usualmente la URL).

Así imagina que tienes el siguiente formulario que quieres enviar a un controlador:

  1. <form method=«post» action=«@Url.Action(«Index», «Upload»)« enctype=«multipart/form-data»>
  2.     <p>
  3.         Name: <input type=«text» name=«Beer.Name» /> <br />
  4.         Rating: <input type=«number» name=«Beer.Rating» /><br />
  5.         Image: <input type=«file» name=«image» /><br />
  6.         <input type=«submit» value=«send!» />
  7.     </p>
  8. </form>

En MVC te bastará con declarar un parámetro de tipo HttpPostedFileBase para el fichero y otro para el resto de valores (name y rating). El framework se encargará del resto:

image

En WebApi eso no es posible, ya que aquí tenemos dos parámetros (image y beer) enlazados con datos provenientes del cuerpo de la petición. Por lo tanto, en WebApi tanto los datos del fichero subido como el resto de valores deberán venir juntos (asumiendo que están todos en el cuerpo de la petición como es este ejemplo).

Además la subida de ficheros en WebApi no se gestiona enlanzando ningún parámetro del controlador, si no instanciando y usando un objeto del tipo MultipartFileStreamProvider. Esta clase lee el cuerpo de la petición y guarda el fichero en disco. Pero claro, si este objeto lee el cuerpo de la petición ya nadie más puede hacerlo en WebApi, lo que implica que los datos asociados también debe leerlos. Por suerte existe la clase derivada MultiPartFormDataStreamProvider que almacena los datos adicionales en la propiedad FormData. Así podemos tener el siguiente código en un ApiController:

  1. [HttpPost]
  2. [Route(«api/upload»)]
  3. public async Task<IHttpActionResult> Upload()
  4. {
  5.     var folder = HostingEnvironment.MapPath(«~/Uploads»);
  6.     var provider = new MultipartFormDataStreamProvider(folder);
  7.     var data = await Request.Content.ReadAsMultipartAsync(provider);
  8.     return Ok();
  9. }

Y podemos ver como en data tenemos tanto la ubicación del fichero en disco (del servidor) puesto que ya ha sido guardado, así como una propiedad llamada FormData que es un NameValueCollection con los datos adicionales:

image

A partir de aquí, convertir esta NameValueCollection en un objeto tipado, ya depende de tí. P. ej. puedes usar este método de extensión:

  1. static class NameValueCollectionExtensions
  2. {
  3.     public static T AsObject<T>(this NameValueCollection source, string prefix)
  4.         where T : class, new()
  5.     {
  6.         var result = new T();
  7.         string fullPrefix = string.IsNullOrEmpty(prefix) ? prefix : prefix + «.»;
  8.         foreach (var key in source.AllKeys.Where(k => k.StartsWith(fullPrefix)).
  9.             Select(kwp => kwp.Substring(fullPrefix.Length)))
  10.         {
  11.             var prop = typeof(T).GetProperty(key);
  12.             if (prop != null && prop.CanWrite)
  13.             {
  14.                 prop.SetValue(result, Convert.ChangeType(source[fullPrefix + key], prop.PropertyType));
  15.             }
  16.         }
  17.  
  18.  
  19.         return result;
  20.     }

Este método es muy limitado (no admite propiedades complejas) pero si los datos que envías junto a los ficheros son simples, te puede servir. En este caso se usaría de la siguiente manera:

  1. var beer = data.FormData.AsObject<Beer>(«Beer»);

Y por supuesto, si necesitas soluciones más complejas que esas evalúa soluciones como Slapper.AutoMapper o similares que ya te lo dan todo hecho… no vayas por ahí reinventando la rueda (o si, vamos… eso ya es cosa tuya :P)!

Saludos!