Enviando dados de formulário HTML em ASP.NET Web API: Upload de arquivo e MIME de várias partes

Parte 2: Upload de arquivo e MIME de várias partes

Este tutorial mostra como carregar arquivos em uma API Web. Ele também descreve como processar dados MIME de várias partes.

Aqui está um exemplo de um formulário HTML para carregar um arquivo:

<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 tela de um formulário HTML mostrando um campo Legenda da Imagem com o texto Férias de Verão e um seletor de arquivo de arquivo de imagem.

Este formulário contém um controle de entrada de texto e um controle de entrada de arquivo. Quando um formulário contém um controle de entrada de arquivo, o atributo enctype sempre deve ser "multipart/form-data", que especifica que o formulário será enviado como uma mensagem MIME de várias partes.

O formato de uma mensagem MIME de várias partes é mais fácil de entender examinando uma solicitação de exemplo:

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

Essa mensagem é dividida em duas partes, uma para cada controle de formulário. Os limites de parte são indicados pelas linhas que começam com traços.

Observação

O limite da parte inclui um componente aleatório ("41184676334") para garantir que a cadeia de caracteres de limite não apareça acidentalmente dentro de uma parte da mensagem.

Cada parte da mensagem contém um ou mais cabeçalhos, seguidos pelo conteúdo da parte.

  • O cabeçalho Content-Disposition inclui o nome do controle. Para arquivos, ele também contém o nome do arquivo.
  • O cabeçalho Content-Type descreve os dados na parte. Se esse cabeçalho for omitido, o padrão será texto/sem formatação.

No exemplo anterior, o usuário carregou um arquivo chamado GrandCanyon.jpg, com tipo de conteúdo image/jpeg; e o valor da entrada de texto era "Férias de Verão".

Carregar arquivos

Agora vamos examinar um controlador de API Web que lê arquivos de uma mensagem MIME de várias partes. O controlador lerá os arquivos de forma assíncrona. A API Web dá suporte a ações assíncronas usando o modelo de programação baseado em tarefas. Primeiro, aqui está o código se você estiver direcionando .NET Framework 4.5, que dá suporte às palavras-chave 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);
        }
    }

}

Observe que a ação do controlador não usa parâmetros. Isso ocorre porque processamos o corpo da solicitação dentro da ação, sem invocar um formatador de tipo de mídia.

O método IsMultipartContent verifica se a solicitação contém uma mensagem MIME de várias partes. Caso contrário, o controlador retornará HTTP status código 415 (Tipo de Mídia Sem Suporte).

A classe MultipartFormDataStreamProvider é um objeto auxiliar que aloca fluxos de arquivos para arquivos carregados. Para ler a mensagem MIME de várias partes, chame o método ReadAsMultipartAsync . Esse método extrai todas as partes da mensagem e as grava nos fluxos fornecidos pelo MultipartFormDataStreamProvider.

Quando o método for concluído, você poderá obter informações sobre os arquivos da propriedade FileData , que é uma coleção de objetos MultipartFileData .

  • MultipartFileData.FileName é o nome do arquivo local no servidor, em que o arquivo foi salvo.
  • MultipartFileData.Headers contém o cabeçalho da parte (não o cabeçalho da solicitação). Você pode usar isso para acessar os cabeçalhos Content_Disposition e Content-Type.

Como o nome sugere, ReadAsMultipartAsync é um método assíncrono. Para executar o trabalho após a conclusão do método, use uma tarefa de continuação (.NET 4.0) ou a palavra-chave await (.NET 4.5).

Aqui está a versão .NET Framework 4.0 do 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;
}

Lendo dados de controle de formulário

O formulário HTML que mostrei anteriormente tinha um controle de entrada de texto.

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

Você pode obter o valor do controle da propriedade FormData do 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 é um NameValueCollection que contém pares nome/valor para os controles de formulário. A coleção pode conter chaves duplicadas. Considere este formulário:

<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 tela do formulário HTML com o círculo de Round-Trip preenchido e as caixas Mostrar somente voos sem interrupção e Minhas datas de viagem são flexíveis marcadas.

O corpo da solicitação pode ter esta aparência:

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

Nesse caso, a coleção FormData conteria os seguintes pares chave/valor:

  • viagem: ida e volta
  • opções: sem escala
  • opções: datas
  • seat: window