Envío de datos de formulario HTML en ASP.NET API web: carga de archivos y MIME de varias partes

Parte 2: Carga de archivos y MIME de varias partes

En este tutorial se muestra cómo cargar archivos en una API web. También se describe cómo procesar datos MIME de varias partes.

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>

Captura de pantalla de un formulario HTML que muestra un campo Image Caption con el texto Summer Vacation y un selector de archivos de imagen.

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 varias partes.

El formato de un mensaje MIME de varias partes 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 partes, una para cada control de formulario. Los límites de las partes se indican mediante las líneas que comienzan con guiones.

Nota:

El límite del elemento incluye un componente aleatorio ("41184676334") para asegurarse de que la cadena de límite no aparece accidentalmente dentro de una parte del mensaje.

Cada parte 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 de la parte. Si se omite este encabezado, el valor predeterminado es text/plain.

En el ejemplo anterior, el usuario cargó un archivo denominado GrandCanyon.jpg, con el tipo de contenido image/jpeg; y el valor de la entrada de texto era "Summer Vacation".

Carga de archivos

Ahora echemos un vistazo a un controlador de API web que lee los archivos de un mensaje MIME de varias partes. 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 asincrónicas 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);
        }
    }

}

Tenga en cuenta que la acción del controlador no acepta 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 multimedia.

El método IsMultipartContent comprueba si la solicitud contiene un mensaje MIME de varias partes. Si no es así, el controlador devuelve el código de estado HTTP 415 (tipo de medio no admitido).

La clase MultipartFormDataStreamProvider es un objeto auxiliar que asigna secuencias de archivos para los archivos cargados. Para leer el mensaje MIME de varias partes, llame al método ReadAsMultipartAsync . Este método extrae todas las partes del mensaje y las escribe en las secuencias proporcionadas por 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 formularios

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 de 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 un 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>

Captura de pantalla del formulario HTML con el círculo de Ida y vuelta rellenado y las casillas Sólo mostrar vuelos sin parada y Mis fechas de viaje son flexibles están marcadas.

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:

  • trip: ida y vuelta
  • opciones: sin escalas
  • opciones: fechas
  • asiento de ventana