Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does one correctly implement a MediaTypeFormatter to handle requests of type 'multipart/mixed'?

Tags:

Consider a web service written in ASP.NET Web API to accept any number files as a 'multipart/mixed' request. The helper method mat look as follows (assuming _client is an instance of System.Net.Http.HttpClient):

public T Post<T>(string requestUri, T value, params Stream[] streams) {     var requestMessage = new HttpRequestMessage();     var objectContent = requestMessage.CreateContent(         value,         MediaTypeHeaderValue.Parse("application/json"),         new MediaTypeFormatter[] {new JsonMediaTypeFormatter()},         new FormatterSelector());      var content = new MultipartContent();     content.Add(objectContent);     foreach (var stream in streams)     {         var streamContent = new StreamContent(stream);         streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");         streamContent.Headers.ContentDisposition =             new ContentDispositionHeaderValue("form-data")             {                 Name = "file",                 FileName = "mystream.doc"             };         content.Add(streamContent);     }      return _httpClient.PostAsync(requestUri, content)         .ContinueWith(t => t.Result.Content.ReadAsAsync<T>()).Unwrap().Result; } 

The method that accepts the request in the subclass of ApiController has a signature as follows:

public HttpResponseMessage Post(HttpRequestMessage request) {     /* parse request using MultipartFormDataStreamProvider */ } 

Ideally, I'd like to define it like this, where contact, source and target are extracted from the 'multipart/mixed' content based on the 'name' property of the 'Content-Disposition' header.

public HttpResponseMessage Post(Contact contact, Stream source, Stream target) {     // process contact, source and target } 

However, with my existing signature, posting the data to the server results in an InvalidOperationException with an error message of:

No 'MediaTypeFormatter' is available to read an object of type 'HttpRequestMessage' with the media type 'multipart/mixed'.

There are a number of examples on the internet how to send and receive files using the ASP.NET Web API and HttpClient. However, I have not found any that show how to deal with this problem.

I started looking at implementing a custom MediaTypeFormatter and register it with the global configuration. However, while it is easy to deal with serializing XML and JSON in a custom MediaTypeFormatter, it is unclear how to deal with 'multipart/mixed' requests which can pretty much be anything.

like image 920
bloudraak Avatar asked Apr 05 '12 21:04

bloudraak


People also ask

What is the role of MediaTypeFormatter?

The Media type formatters are the classes that are responsible for serializing the request/response data so that the Web API Framework can understand the request data format and also send data in the format which the client expects.

What is MediaTypeFormatter class in Web API?

Media type formatters are classes responsible for serializing request/response data so that Web API can understand the request data format and send data in the format which client expects. Web API includes following built-in media type formatters. Media Type Formatter Class.

How do I use media type formatter in Web API?

The media type determines how Web API serializes and deserializes the HTTP message body. Web API has built-in support for XML, JSON, BSON, and form-urlencoded data, and you can support additional media types by writing a media formatter. To create a media formatter, derive from one of these classes: MediaTypeFormatter.

Which is a formatter class for JSON?

JSON formatting is provided by the JsonMediaTypeFormatter class.


2 Answers

Have a look at this forum: http://forums.asp.net/t/1777847.aspx/1?MVC4+Beta+Web+API+and+multipart+form+data

Here is a snippet of code (posted by imran_ku07) that might help you implement a custom formatter to handle the multipart/form-data:

public class MultiFormDataMediaTypeFormatter : FormUrlEncodedMediaTypeFormatter {     public MultiFormDataMediaTypeFormatter() : base()     {         this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));     }      protected override bool CanReadType(Type type)     {         return true;     }      protected override bool CanWriteType(Type type)     {         return false;     }      protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)     {         var contents = formatterContext.Request.Content.ReadAsMultipartAsync().Result;         return Task.Factory.StartNew<object>(() =>         {             return new MultiFormKeyValueModel(contents);         });     }      class MultiFormKeyValueModel : IKeyValueModel     {         IEnumerable<HttpContent> _contents;         public MultiFormKeyValueModel(IEnumerable<HttpContent> contents)         {             _contents = contents;         }           public IEnumerable<string> Keys         {             get             {                 return _contents.Cast<string>();             }         }          public bool TryGetValue(string key, out object value)         {             value = _contents.FirstDispositionNameOrDefault(key).ReadAsStringAsync().Result;             return true;         }     } } 

You then need to add this formatter to your application. If doing self-host you can simply add it by including:

config.Formatters.Insert(0, new MultiFormDataMediaTypeFormatter()); 

before instantiating the HttpSelfHostServer class.

-- EDIT --

To parse binary streams you'll need another formatter. Here is one that I am using to parse images in one of my work projects.

class JpegFormatter : MediaTypeFormatter {     protected override bool CanReadType(Type type)     {         return (type == typeof(Binary));     }      protected override bool CanWriteType(Type type)     {         return false;     }      public JpegFormatter()     {         SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));         SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpg"));         SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));     }      protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)     {         return Task.Factory.StartNew(() =>             {                 byte[] fileBytes = new byte[stream.Length];                 stream.Read(fileBytes, 0, (int)fileBytes.Length);                 return (object)new Binary(fileBytes);             });      }      protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext)     {         throw new NotImplementedException();     } } 

In your controller/action you'll want to do something along the lines of:

public HttpResponseMessage UploadImage(Binary File) {  //do something with your file } 
like image 189
Jed Avatar answered Oct 26 '22 22:10

Jed


Take a look at this post https://stackoverflow.com/a/17073113/1944993 the answer of Kiran Challa is really nice!

The essential part :

Custom In-memory MultiaprtFormDataStreamProvider:

    public class InMemoryMultipartFormDataStreamProvider : MultipartStreamProvider {     private NameValueCollection _formData = new NameValueCollection();     private List<HttpContent> _fileContents = new List<HttpContent>();      // Set of indexes of which HttpContents we designate as form data     private Collection<bool> _isFormData = new Collection<bool>();      /// <summary>     /// Gets a <see cref="NameValueCollection"/> of form data passed as part of the multipart form data.     /// </summary>     public NameValueCollection FormData     {         get { return _formData; }     }      /// <summary>     /// Gets list of <see cref="HttpContent"/>s which contain uploaded files as in-memory representation.     /// </summary>     public List<HttpContent> Files     {         get { return _fileContents; }     }      public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)     {         // For form data, Content-Disposition header is a requirement         ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;         if (contentDisposition != null)         {             // We will post process this as form data             _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));              return new MemoryStream();         }          // If no Content-Disposition header was present.         throw new InvalidOperationException(string.Format("Did not find required '{0}' header field in MIME multipart body part..", "Content-Disposition"));     }      /// <summary>     /// Read the non-file contents as form data.     /// </summary>     /// <returns></returns>     public override async Task ExecutePostProcessingAsync()     {         // Find instances of non-file HttpContents and read them asynchronously         // to get the string content and then add that as form data         for (int index = 0; index < Contents.Count; index++)         {             if (_isFormData[index])             {                 HttpContent formContent = Contents[index];                 // Extract name from Content-Disposition header. We know from earlier that the header is present.                 ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition;                 string formFieldName = UnquoteToken(contentDisposition.Name) ?? String.Empty;                  // Read the contents as string data and add to form data                 string formFieldValue = await formContent.ReadAsStringAsync();                 FormData.Add(formFieldName, formFieldValue);             }             else             {                 _fileContents.Add(Contents[index]);             }         }     }      /// <summary>     /// Remove bounding quotes on a token if present     /// </summary>     /// <param name="token">Token to unquote.</param>     /// <returns>Unquoted token.</returns>     private static string UnquoteToken(string token)     {         if (String.IsNullOrWhiteSpace(token))         {             return token;         }          if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)         {             return token.Substring(1, token.Length - 2);         }          return token;     }} 

You can then the "MemoryMultiPartDataStreamProvider" in you webapi like this :

var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataStreamProvider>(new InMemoryMultipartFormDataStreamProvider());      //access form data     NameValueCollection formData = provider.FormData;      //access files     IList<HttpContent> files = provider.Files; 
like image 35
HoefMeistert Avatar answered Oct 26 '22 23:10

HoefMeistert