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!

Deja un comentario

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