Web API: how to access multipart form values when using MultipartMemoryStreamProvider?
Updated 4/28/2015
You could create a custom provider based on MultipartFormDataRemoteStreamProvider
.
Example:
public class CustomMultipartFormDataProvider : MultipartFormDataRemoteStreamProvider
{
public override RemoteStreamInfo GetRemoteStream(HttpContent parent, HttpContentHeaders headers)
{
return new RemoteStreamInfo(
remoteStream: new MemoryStream(),
location: string.Empty,
fileName: string.Empty);
}
}
Updated
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;
}
}
Usage:
public async Task Post()
{
if (!Request.Content.IsMimeMultipartContent("form-data"))
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataStreamProvider>(new InMemoryMultipartFormDataStreamProvider());
//access form data
NameValueCollection formData = provider.FormData;
//access files
IList<HttpContent> files = provider.Files;
//Example: reading a file's stream like below
HttpContent file1 = files[0];
Stream file1Stream = await file1.ReadAsStreamAsync();
}
Building on the excellent answer from Kiran, I have pulled together the complete answer from the April 2015 update. It appears that at least one thing has changed in WebAPI, which is what confused me at first. The provider.Files no longer exists, it is .Content. So here is what you, minimally, need to do in order to read posted files without first storing them on disk:
Step 1: create a provider class
Add a file somewhere in your project for this class:
public class InMemoryMultipartFormDataProvider : MultipartFormDataRemoteStreamProvider
{
public override RemoteStreamInfo GetRemoteStream(HttpContent parent, HttpContentHeaders headers)
{
return new RemoteStreamInfo(
remoteStream: new MemoryStream(),
location: string.Empty,
fileName: string.Empty);
}
}
I believe this converts each file into a memory stream rather than storing it on disk.
Step 2: add a controller action parse the contents and create the streams
In your controller:
[HttpPost]
public async Task<IHttpActionResult> Upload()
{
// This endpoint only supports multipart form data
if (!Request.Content.IsMimeMultipartContent("form-data"))
{
return StatusCode(HttpStatusCode.UnsupportedMediaType);
}
// read the content in a memory stream per file uploaded
var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataProvider>(new InMemoryMultipartFormDataProvider());
// iterate over each file uploaded and do something with the results
foreach (var fileContents in provider.Contents) {
processFileAsMemoryStream(await fileContents.ReadAsStreamAsync());
}
}