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