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 }