在 ASP.NET Web API 中发送 HTML 窗体数据:文件上传和多部分 MIMESending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME

作者: Mike Wassonby Mike Wasson

第2部分:文件上传和多部分 MIMEPart 2: File Upload and Multipart MIME

本教程演示如何将文件上传到 web API。This tutorial shows how to upload files to a web API. 还介绍了如何处理多部分 MIME 数据。It also describes how to process multipart MIME data.

下面是用于上载文件的 HTML 窗体的示例:Here is an example of an HTML form for uploading a 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>

此窗体包含文本输入控件和文件输入控件。This form contains a text input control and a file input control. 当窗体包含文件输入控件时, enctype特性应始终 "多部分/窗体数据",这将指定窗体将作为多部分 MIME 消息发送。When a form contains a file input control, the enctype attribute should always be "multipart/form-data", which specifies that the form will be sent as a multipart MIME message.

通过查看示例请求,可以最轻松地了解多部分 MIME 消息的格式:The format of a multipart MIME message is easiest to understand by looking at an example request:

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

此消息分为两部分,分别用于每个窗体控件。This message is divided into two parts, one for each form control. 部分边界由以破折号开头的行指示。Part boundaries are indicated by the lines that start with dashes.

Note

部分边界包含随机组件("41184676334"),以确保边界字符串不会意外出现在消息部分中。The part boundary includes a random component ("41184676334") to ensure that the boundary string does not accidentally appear inside a message part.

每个消息部分都包含一个或多个标头,后面是部分内容。Each message part contains one or more headers, followed by the part contents.

  • 内容处置标头包含控件的名称。The Content-Disposition header includes the name of the control. 对于文件,它还包含文件名。For files, it also contains the file name.
  • Content-type 标头介绍了该部分中的数据。The Content-Type header describes the data in the part. 如果省略此标头,则默认值为 text/简洁。If this header is omitted, the default is text/plain.

在上面的示例中,用户使用内容类型 image/jpeg 上传了一个名为 GrandCanyon 的文件;并且文本输入的值为 "夏天假期"。In the previous example, the user uploaded a file named GrandCanyon.jpg, with content type image/jpeg; and the value of the text input was "Summer Vacation".

文件上传File Upload

现在,让我们看看从多部分 MIME 消息读取文件的 Web API 控制器。Now let's look at a Web API controller that reads files from a multipart MIME message. 控制器将异步读取文件。The controller will read the files asynchronously. Web API 使用基于任务的编程模型支持异步操作。Web API supports asynchronous actions using the task-based programming model. 首先,如果你针对的是支持asyncawait关键字的 .NET Framework 4.5,则代码为。First, here is the code if you are targeting .NET Framework 4.5, which supports the async and await keywords.

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

}

请注意,控制器操作不采用任何参数。Notice that the controller action does not take any parameters. 这是因为,我们处理操作中的请求正文,而不调用媒体类型格式化程序。That's because we process the request body inside the action, without invoking a media-type formatter.

IsMultipartContent方法检查请求是否包含多部分 MIME 消息。The IsMultipartContent method checks whether the request contains a multipart MIME message. 否则,控制器将返回 HTTP 状态代码415(不支持的媒体类型)。If not, the controller returns HTTP status code 415 (Unsupported Media Type).

MultipartFormDataStreamProvider类是一个帮助器对象,它为上传的文件分配文件流。The MultipartFormDataStreamProvider class is a helper object that allocates file streams for uploaded files. 若要读取多部分 MIME 消息,请调用ReadAsMultipartAsync方法。To read the multipart MIME message, call the ReadAsMultipartAsync method. 此方法提取所有消息部分,并将其写入MultipartFormDataStreamProvider提供的流中。This method extracts all of the message parts and writes them into the streams provided by the MultipartFormDataStreamProvider.

当方法完成时,可以从FileData属性获取有关文件的信息,该属性是MultipartFileData对象的集合。When the method completes, you can get information about the files from the FileData property, which is a collection of MultipartFileData objects.

  • MultipartFileData是服务器上保存文件的本地文件名。MultipartFileData.FileName is the local file name on the server, where the file was saved.
  • MultipartFileData包含部件标题(而不是请求标头)。MultipartFileData.Headers contains the part header (not the request header). 你可以使用它来访问_处置和 Content-type 标头的内容。You can use this to access the Content_Disposition and Content-Type headers.

顾名思义, ReadAsMultipartAsync是一种异步方法。As the name suggests, ReadAsMultipartAsync is an asynchronous method. 若要在方法完成后执行工作,请使用延续任务(.net 4.0)或await关键字(.net 4.5)。To perform work after the method completes, use a continuation task (.NET 4.0) or the await keyword (.NET 4.5).

下面是前面代码的 .NET Framework 4.0 版本:Here is the .NET Framework 4.0 version of the previous code:

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

读取窗体控件数据Reading Form Control Data

我前面介绍的 HTML 窗体具有文本输入控件。The HTML form that I showed earlier had a text input control.

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

可以从MultipartFormDataStreamProviderFormData属性中获取该控件的值。You can get the value of the control from the FormData property of the 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是包含窗体控件的名称/值对的NameValueCollectionFormData is a NameValueCollection that contains name/value pairs for the form controls. 集合可以包含重复键。The collection can contain duplicate keys. 请考虑以下形式:Consider this form:

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

请求正文可能如下所示:The request body might look like this:

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

在这种情况下, FormData集合将包含以下键/值对:In that case, the FormData collection would contain the following key/value pairs:

  • 行程:往返行程trip: round-trip
  • 选项:不间断options: nonstop
  • 选项:日期options: dates
  • 座位:窗口seat: window