Invio di dati del modulo HTML in API Web ASP.NET: caricamento di file e MIME multipart

Parte 2: Caricamento di file e MIME multipart

Questa esercitazione illustra come caricare file in un'API Web. Viene inoltre descritto come elaborare dati MIME multiparte.

Di seguito è riportato un esempio di modulo HTML per il caricamento di un file:

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

Screenshot di un modulo HTML che mostra un campo Image Caption con il testo Summer Vacation e una selezione file di immagine.

Questo modulo contiene un controllo input di testo e un controllo di input del file. Quando un modulo contiene un controllo di input di file, l'attributo enctype deve essere sempre "multipart/form-data", che specifica che il modulo verrà inviato come messaggio MIME multipart.

Il formato di un messaggio MIME multiparte è più semplice da comprendere esaminando una richiesta di esempio:

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

Questo messaggio è suddiviso in due parti, una per ogni controllo modulo. I limiti delle parti sono indicati dalle linee che iniziano con trattini.

Nota

Il limite della parte include un componente casuale ("41184676334") per garantire che la stringa di limite non venga accidentalmente visualizzata all'interno di una parte del messaggio.

Ogni parte del messaggio contiene una o più intestazioni, seguite dal contenuto della parte.

  • L'intestazione Content-Disposition include il nome del controllo. Per i file, contiene anche il nome del file.
  • L'intestazione Content-Type descrive i dati nella parte. Se questa intestazione viene omessa, il valore predefinito è text/plain.

Nell'esempio precedente l'utente ha caricato un file denominato GrandCanyon.jpg, con tipo di contenuto image/jpeg; e il valore dell'input di testo era "Summer Vacation".

Caricamento file

Si esamini ora un controller API Web che legge i file da un messaggio MIME multipart. Il controller leggerà i file in modo asincrono. L'API Web supporta azioni asincrone usando il modello di programmazione basato su attività. In primo luogo, ecco il codice se si ha come destinazione .NET Framework 4.5, che supporta le parole chiave async e 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);
        }
    }

}

Si noti che l'azione del controller non accetta parametri. Ciò è dovuto al fatto che il corpo della richiesta viene elaborato all'interno dell'azione, senza richiamare un formattatore di tipo multimediale.

Il metodo IsMultipartContent controlla se la richiesta contiene un messaggio MIME multipart. In caso contrario, il controller restituisce il codice di stato HTTP 415 (tipo di supporto non supportato).

La classe MultipartFormDataStreamProvider è un oggetto helper che alloca i flussi di file per i file caricati. Per leggere il messaggio MIME multipart, chiamare il metodo ReadAsMultipartAsync . Questo metodo estrae tutte le parti del messaggio e le scrive nei flussi forniti da MultipartFormDataStreamProvider.

Al termine del metodo, è possibile ottenere informazioni sui file dalla proprietà FileData , ovvero un insieme di oggetti MultipartFileData .

  • MultipartFileData.FileName è il nome del file locale nel server, in cui è stato salvato il file.
  • MultipartFileData.Headers contiene l'intestazione della parte (non l'intestazione della richiesta). È possibile usarlo per accedere alle intestazioni Content_Disposition e Content-Type.

Come suggerisce il nome, ReadAsMultipartAsync è un metodo asincrono. Per eseguire il lavoro al termine del metodo, usare un'attività di continuazione (.NET 4.0) o la parola chiave await (.NET 4.5).

Ecco la versione di .NET Framework 4.0 del codice precedente:

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;
}

Lettura dei dati del controllo modulo

Il modulo HTML mostrato in precedenza aveva un controllo di input di testo.

<div>
        <label for="caption">Image Caption</label>
        <input name="caption" type="text" />
    </div>

È possibile ottenere il valore del controllo dalla proprietà FormData di 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 è un Oggetto NameValueCollection che contiene coppie nome/valore per i controlli modulo. La raccolta può contenere chiavi duplicate. Si consideri questo modulo:

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

Screenshot del modulo HTML con il cerchio Round-Trip compilato e le caselle Mostra solo voli non stop e Le date di viaggio sono flessibili.

Il corpo della richiesta potrebbe essere simile al seguente:

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

In tal caso, l'insieme FormData conterrà le coppie chiave/valore seguenti:

  • trip: round trip
  • opzioni: senza sosta
  • opzioni: date
  • sedile: finestra