在 ASP.NET Web API 中发送 HTML 表单数据:文件上传和多部分 MIME
第 2 部分:文件上传和多部分 MIME
本教程介绍如何将文件上传到 Web API。 它还介绍如何处理多部分 MIME 数据。
注意
下面是用于上传文件的 HTML 表单示例:
<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>
此窗体包含文本输入控件和文件输入控件。 当窗体包含文件输入控件时, enctype 属性应始终为“multipart/form-data”,这指定表单将作为多部分 MIME 消息发送。
通过查看示例请求,最容易理解多部分 MIME 消息的格式:
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--
此消息分为两 个部分,每个窗体控件各有一部分。 部件边界由以短划线开头的线条指示。
注意
部件边界包括一个随机组件 (“41184676334”) ,以确保边界字符串不会意外出现在消息部件内。
每个消息部分包含一个或多个标头,后跟部件内容。
- Content-Disposition 标头包含控件的名称。 对于文件,它还包含文件名。
- Content-Type 标头描述部件中的数据。 如果省略此标头,则默认值为 text/plain。
在前面的示例中,用户上传了一个名为 GrandCanyon.jpg 的文件,其内容类型为 image/jpeg;文本输入的值为“Summer Vacation”。
文件上传
现在,让我们看一个 Web API 控制器,该控制器从多部分 MIME 消息读取文件。 控制器将以异步方式读取文件。 Web API 支持使用 基于任务的编程模型进行异步操作。 首先,如果面向.NET Framework 4.5,则为代码,该版本支持 async 和 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);
}
}
}
请注意,控制器操作不采用任何参数。 这是因为我们在操作中处理请求正文,而不调用媒体类型的格式化程序。
IsMultipartContent 方法检查请求是否包含多部分 MIME 消息。 否则,控制器返回 HTTP 状态代码 415 (不支持的媒体类型) 。
MultipartFormDataStreamProvider 类是一个帮助程序对象,用于为上传的文件分配文件流。 若要读取多部分 MIME 消息,请调用 ReadAsMultipartAsync 方法。 此方法提取所有消息部分,并将其写入 MultipartFormDataStreamProvider 提供的流中。
方法完成后,可以从 FileData 属性( MultipartFileData 对象的集合)获取有关文件的信息。
- MultipartFileData.FileName 是保存文件的服务器上的本地文件名。
- MultipartFileData.Headers 包含部件标头 (不包含 请求标头) 。 可以使用此标头访问 Content_Disposition 和 Content-Type 标头。
顾名思义, ReadAsMultipartAsync 是一种异步方法。 若要在方法完成后执行工作,请使用延续任务 (.NET 4.0) 或 await 关键字 (keyword) (.NET 4.5) 。
下面是前面代码的 .NET Framework 4.0 版本:
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;
}
读取窗体控件数据
前面显示的 HTML 窗体具有文本输入控件。
<div>
<label for="caption">Image Caption</label>
<input name="caption" type="text" />
</div>
可以从 MultipartFormDataStreamProvider 的 FormData 属性获取控件的值。
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 是一个 NameValueCollection ,其中包含窗体控件的名称/值对。 集合可以包含重复键。 请考虑以下形式:
<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>
请求正文可能如下所示:
-----------------------------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 集合将包含以下键/值对:
- 行程:往返
- 选项:不间断
- 选项:日期
- seat: window