Wysyłanie danych formularza HTML w internetowym interfejsie API ASP.NET: przekazywanie plików i wieloczęściowy protokół MIME

Część 2. Przekazywanie plików i wieloczęściowa funkcja MIME

W tym samouczku pokazano, jak przekazywać pliki do internetowego interfejsu API. Opisano w nim również sposób przetwarzania wieloczęściowych danych MIME.

Oto przykład formularza HTML do przekazywania pliku:

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

Zrzut ekranu przedstawiający formularz HTML przedstawiający pole Podpis obrazu z tekstem Summer Vacation i selektorem pliku obrazu.

Ten formularz zawiera kontrolkę wprowadzania tekstu i kontrolkę wprowadzania pliku. Gdy formularz zawiera kontrolkę wprowadzania pliku, atrybut typu entype powinien zawsze mieć wartość "multipart/form-data", co określa, że formularz zostanie wysłany jako komunikat MIME z wieloma częściami.

Format komunikatu MIME z wieloma częściami jest najłatwiej zrozumieć, patrząc na przykładowe żądanie:

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

Ten komunikat jest podzielony na dwie części: jedną dla każdej kontrolki formularza. Granice części są wskazywane przez linie rozpoczynające się od kreski.

Uwaga

Granica części zawiera składnik losowy ("41184676334"), aby upewnić się, że ciąg graniczny nie pojawia się przypadkowo wewnątrz części komunikatu.

Każda część komunikatu zawiera co najmniej jeden nagłówek, po którym następuje część zawartości.

  • Nagłówek Content-Disposition zawiera nazwę kontrolki. W przypadku plików zawiera również nazwę pliku.
  • Nagłówek Content-Type opisuje dane w części . Jeśli ten nagłówek zostanie pominięty, wartość domyślna to tekst/zwykły.

W poprzednim przykładzie użytkownik przesłał plik o nazwie GrandCanyon.jpg z typem zawartości image/jpeg; a wartość wprowadzania tekstu to "Summer Vacation".

Przekazywanie plików

Teraz przyjrzyjmy się kontrolerowi internetowego interfejsu API, który odczytuje pliki z wieloczęściowego komunikatu MIME. Kontroler odczytuje pliki asynchronicznie. Internetowy interfejs API obsługuje akcje asynchroniczne przy użyciu modelu programowania opartego na zadaniach. Najpierw poniżej znajduje się kod docelowy .NET Framework 4.5, który obsługuje słowa kluczowe async i 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);
        }
    }

}

Zwróć uwagę, że akcja kontrolera nie wykonuje żadnych parametrów. Dzieje się tak, ponieważ przetwarzamy treść żądania wewnątrz akcji bez wywoływania formatera typu nośnika.

Metoda IsMultipartContent sprawdza, czy żądanie zawiera wieloczęściowy komunikat MIME. Jeśli nie, kontroler zwraca kod stanu HTTP 415 (nieobsługiwany typ nośnika).

Klasa MultipartFormDataStreamProvider jest obiektem pomocnika, który przydziela strumienie plików do przekazanych plików. Aby odczytać komunikat MIME z wieloma częściami, wywołaj metodę ReadAsMultipartAsync . Ta metoda wyodrębnia wszystkie części komunikatów i zapisuje je w strumieniach dostarczonych przez MultipartFormDataStreamProvider.

Po zakończeniu metody można uzyskać informacje o plikach z właściwości FileData , która jest kolekcją obiektów MultipartFileData .

  • MultipartFileData.FileName to lokalna nazwa pliku na serwerze, na którym plik został zapisany.
  • MultipartFileData.Headers zawiera nagłówek części (a nie nagłówek żądania). Służy to do uzyskiwania dostępu do nagłówków Content_Disposition i Content-Type.

Jak sugeruje nazwa, ReadAsMultipartAsync jest metodą asynchroniczną. Aby wykonać pracę po zakończeniu metody, użyj zadania kontynuacji (.NET 4.0) lub słowa kluczowego await (.NET 4.5).

Oto .NET Framework 4.0 poprzedniego kodu:

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

Odczytywanie danych kontrolki formularza

Wyświetlony wcześniej formularz HTML miał kontrolkę wprowadzania tekstu.

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

Wartość kontrolki można uzyskać z właściwości FormData obiektu 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 to element NameValueCollection , który zawiera pary nazwa/wartość dla kontrolek formularza. Kolekcja może zawierać zduplikowane klucze. Rozważmy ten formularz:

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

Zrzut ekranu przedstawiający formularz HTML z wypełnionym okręgiem Round-Trip, a pole wyboru Pokaż tylko loty nieobsługiwne i Moje daty podróży są zaznaczone elastycznymi polami.

Treść żądania może wyglądać następująco:

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

W takim przypadku kolekcja FormData będzie zawierać następujące pary klucz/wartość:

  • podróż: podróż dwukierunkowa
  • opcje: nonstop
  • opcje: daty
  • siedzenie: okno