Enviar datos de formulario HTML en ASP.NET Web API: Carga de archivos y MIME de varios elementos
Parte 2: Carga de archivos y MIME de varios elementos
En este tutorial se muestra cómo cargar archivos en una API web. También se describe cómo procesar datos MIME de varios elementos.
Este es un ejemplo de un formulario HTML para cargar un archivo:
<form name="form1" method="post" enctype="multipart/form-data" action="api/upload">
<div>
<label for="caption">Image Caption</label>
<input name="caption" type="text" />
</div>
<div>
<label for="image1">Image File</label>
<input name="image1" type="file" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
Este formulario contiene un control de entrada de texto y un control de entrada de archivo. Cuando un formulario contiene un control de entrada de archivo, el atributo enctype siempre debe ser "multipart/form-data", que especifica que el formulario se enviará como un mensaje MIME de varios elementos.
El formato de un mensaje MIME de varios elementos es más fácil de entender examinando una solicitud de ejemplo:
POST http://localhost:50460/api/values/1 HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------41184676334
Content-Length: 29278
-----------------------------41184676334
Content-Disposition: form-data; name="caption"
Summer vacation
-----------------------------41184676334
Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg"
Content-Type: image/jpeg
(Binary data not shown)
-----------------------------41184676334--
Este mensaje se divide en dos elementos, uno para cada control de formulario. Los límites de los elementos se indican mediante las líneas que comienzan por guiones.
Nota:
El límite del elemento incluye un componente aleatorio ("41184676334") para asegurarse de que la cadena del límite no aparece accidentalmente dentro de un elemento del mensaje.
Cada elemento del mensaje contiene uno o varios encabezados, seguidos del contenido del elemento.
- El encabezado Content-Disposition incluye el nombre del control. En el caso de los archivos, también contiene el nombre de archivo.
- El encabezado Content-Type describe los datos del elemento. Si se omite este encabezado, el valor predeterminado es texto/sin formato.
En el ejemplo anterior, el usuario cargó un archivo denominado GrandCanyon.jpg, con el tipo de contenido imagen/jpeg; y el valor de la entrada de texto fue "Vacaciones de verano".
Carga de archivos
Ahora echemos un vistazo a un controlador de API web que lee los archivos de un mensaje MIME de varios elementos. El controlador leerá los archivos de forma asincrónica. La API web admite acciones asincrónicas mediante el modelo de programación basado en tareas. En primer lugar, este es el código si tiene como destino .NET Framework 4.5, que admite las palabras clave async y await.
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
public class UploadController : ApiController
{
public async Task<HttpResponseMessage> PostFormData()
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
try
{
// Read the form data.
await Request.Content.ReadAsMultipartAsync(provider);
// This illustrates how to get the file names.
foreach (MultipartFileData file in provider.FileData)
{
Trace.WriteLine(file.Headers.ContentDisposition.FileName);
Trace.WriteLine("Server file path: " + file.LocalFileName);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (System.Exception e)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
}
}
}
Observe que la acción del controlador no usa ningún parámetro. Esto se debe a que procesamos el cuerpo de la solicitud dentro de la acción, sin invocar un formateador de tipo elementos multimedia.
El método IsMultipartContent comprueba si la solicitud contiene un mensaje MIME de varios elementos. Si no es así, el controlador devuelve el código de estado HTTP 415 (tipo de elementos multimedia no admitido).
La clase MultipartFormDataStreamProvider es un objeto auxiliar que asigna secuencias de archivos para los archivos cargados. Para leer el mensaje MIME de varios elementos, llame al método ReadAsMultipartAsync. Este método extrae todos los elementos del mensaje y los escribe en las secuencias proporcionadas por el MultipartFormDataStreamProvider.
Una vez completado el método, puede obtener información sobre los archivos de la propiedad FileData, que es una colección de objetos MultipartFileData.
- MultipartFileData.FileName es el nombre de archivo local en el servidor, donde se guardó el archivo.
- MultipartFileData.Headers contiene el encabezado de elemento (no el encabezado de solicitud). Puede usarlo para acceder a los encabezados Content_Disposition y Content-Type.
Como sugiere el nombre, ReadAsMultipartAsync es un método asincrónico. Para realizar el trabajo una vez completado el método, use una tarea de continuación (.NET 4.0) o la palabra clave await (.NET 4.5).
Esta es la versión de .NET Framework 4.0 del código anterior:
public Task<HttpResponseMessage> PostFormData()
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
// Read the form data and return an async task.
var task = Request.Content.ReadAsMultipartAsync(provider).
ContinueWith<HttpResponseMessage>(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
}
// This illustrates how to get the file names.
foreach (MultipartFileData file in provider.FileData)
{
Trace.WriteLine(file.Headers.ContentDisposition.FileName);
Trace.WriteLine("Server file path: " + file.LocalFileName);
}
return Request.CreateResponse(HttpStatusCode.OK);
});
return task;
}
Leer datos de control de formulario
El formulario HTML que he mostrado anteriormente tenía un control de entrada de texto.
<div>
<label for="caption">Image Caption</label>
<input name="caption" type="text" />
</div>
Puede obtener el valor del control de la propiedad FormData del MultipartFormDataStreamProvider.
public async Task<HttpResponseMessage> PostFormData()
{
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
try
{
await Request.Content.ReadAsMultipartAsync(provider);
// Show all the key-value pairs.
foreach (var key in provider.FormData.AllKeys)
{
foreach (var val in provider.FormData.GetValues(key))
{
Trace.WriteLine(string.Format("{0}: {1}", key, val));
}
}
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (System.Exception e)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
}
}
FormData es una NameValueCollection que contiene pares nombre-valor para los controles de formulario. La colección puede contener claves duplicadas. Tenga en cuenta este formulario:
<form name="trip_search" method="post" enctype="multipart/form-data" action="api/upload">
<div>
<input type="radio" name="trip" value="round-trip"/>
Round-Trip
</div>
<div>
<input type="radio" name="trip" value="one-way"/>
One-Way
</div>
<div>
<input type="checkbox" name="options" value="nonstop" />
Only show non-stop flights
</div>
<div>
<input type="checkbox" name="options" value="airports" />
Compare nearby airports
</div>
<div>
<input type="checkbox" name="options" value="dates" />
My travel dates are flexible
</div>
<div>
<label for="seat">Seating Preference</label>
<select name="seat">
<option value="aisle">Aisle</option>
<option value="window">Window</option>
<option value="center">Center</option>
<option value="none">No Preference</option>
</select>
</div>
</form>
El cuerpo de la solicitud podría tener este aspecto:
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="trip"
round-trip
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="options"
nonstop
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="options"
dates
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="seat"
window
-----------------------------7dc1d13623304d6--
En ese caso, la colección FormData contendrá los siguientes pares clave-valor:
- viaje: ida y vuelta
- opciones: sin paradas
- opciones: fechas
- asiento: ventana