application/x-www-form-urlencoded與 multipart/form-data:
Fom表單中如果沒有type=file的控件,用默認的application/x-www-form-urlencoded就可以了。但是如果有type=file的話,就要用到multipart/form-data了。瀏覽器會把整個表單以控件為單位分割,並為每個部分加上 Content-Disposition(form-data或者file),Content-Type(默認為text/plain),name(控件 name)等信息,並加上分割符(boundary)。
multipart/form-data被webapi能夠識別,需要自定義MediaTypeFormatter,關於webapi中的媒體類型格式化器(Media-type Formatter),它是一種能夠做以下工作的對象:
- Read CLR objects from an HTTP message body
從HTTP消息體讀取CLR(公共語言運行時)對象 - Write CLR objects into an HTTP message body
將CLR對象寫入HTTP消息體
Web API提供了用於JSON和XML的媒體類型格式化器。框架已默認將這些格式化器插入到消息處理管線之中。客戶端在HTTP請求的Accept報頭中可以請求JSON或XML。
以下代碼是multipart/form-data的格式化方法:
using System; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Threading.Tasks; using MultipartDataMediaFormatter.Converters; using MultipartDataMediaFormatter.Infrastructure; using MultipartDataMediaFormatter.Infrastructure.Logger; namespace MultipartDataMediaFormatter { public class FormMultipartEncodedMediaTypeFormatter : MediaTypeFormatter { private const string SupportedMediaType = "multipart/form-data"; public FormMultipartEncodedMediaTypeFormatter() { SupportedMediaTypes.Add(new MediaTypeHeaderValue(SupportedMediaType)); } public override bool CanReadType(Type type) { return true; } public override bool CanWriteType(Type type) { return true; } public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType) { base.SetDefaultContentHeaders(type, headers, mediaType); //need add boundary //(if add when fill SupportedMediaTypes collection in class constructor then receive post with another boundary will not work - Unsupported Media Type exception will thrown) if (headers.ContentType == null) headers.ContentType = new MediaTypeHeaderValue(SupportedMediaType); if (!String.Equals(headers.ContentType.MediaType, SupportedMediaType, StringComparison.OrdinalIgnoreCase)) throw new Exception("Not a Multipart Content"); if (headers.ContentType.Parameters.All(m => m.Name != "boundary")) headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", "MultipartDataMediaFormatterBoundary1q2w3e")); } public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) { var httpContentToFormDataConverter = new HttpContentToFormDataConverter(); FormData multipartFormData = await httpContentToFormDataConverter.Convert(content); IFormDataConverterLogger logger; if (formatterLogger != null) logger = new FormatterLoggerAdapter(formatterLogger); else logger = new FormDataConverterLogger(); var dataToObjectConverter = new FormDataToObjectConverter(multipartFormData, logger); object result = dataToObjectConverter.Convert(type); logger.EnsureNoErrors(); return result; } public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) { if (!content.IsMimeMultipartContent()) throw new Exception("Not a Multipart Content"); var boudaryParameter = content.Headers.ContentType.Parameters.FirstOrDefault(m => m.Name == "boundary" && !String.IsNullOrWhiteSpace(m.Value)); if (boudaryParameter == null) throw new Exception("multipart boundary not found"); var objectToMultipartDataByteArrayConverter = new ObjectToMultipartDataByteArrayConverter(); byte[] multipartData = objectToMultipartDataByteArrayConverter.Convert(value, boudaryParameter.Value); await writeStream.WriteAsync (multipartData, 0, multipartData.Length); content.Headers.ContentLength = multipartData.Length; } } }
以IIs為宿主的webapi,加入以下代碼
GlobalConfiguration.Configuration.Formatters.Add(new FormMultipartEncodedMediaTypeFormatter());
如果webapi是自我宿主,加入以下代碼
new HttpSelfHostConfiguration(<url>).Formatters.Add(new FormMultipartEncodedMediaTypeFormatter());
發送對象時使用FormMultipartEncodedMediaTypeFormatter(測試代碼如下):
private ApiResult<T> PostModel<T>(T model, string url) { var mediaTypeFormatter = new FormMultipartEncodedMediaTypeFormatter(); using (new WebApiHttpServer(BaseApiAddress, mediaTypeFormatter)) using (var client = CreateHttpClient(BaseApiAddress)) using (HttpResponseMessage response = client.PostAsync(url, model, mediaTypeFormatter).Result) { if (response.StatusCode != HttpStatusCode.OK) { var err = response.Content.ReadAsStringAsync().Result; Assert.Fail(err); } var resultModel = response.Content.ReadAsAsync<ApiResult<T>>(new[] { mediaTypeFormatter }).Result; return resultModel; } }
使用MultipartDataMediaFormatter.Infrastructure.FormData這個類訪問原始http數據
[HttpPost] public void PostFileBindRawFormData(MultipartDataMediaFormatter.Infrastructure.FormData formData) { HttpFile file; formData.TryGetValue(<key>, out file); }
綁定自定義Model
//model example public class PersonModel { public string FirstName {get; set;} public string LastName {get; set;} public DateTime? BirthDate {get; set;} public HttpFile AvatarImage {get; set;} public List<HttpFile> Attachments {get; set;} public List<PersonModel> ConnectedPersons {get; set;} } //api controller example [HttpPost] public void PostPerson(PersonModel model) { //do something with the model }