49

I used to use MultipartFormDataStreamProvider to process multipart requests.

Since I want the uploaded file to be stored in memory, instead of a disk file, I've changed my code to use MultipartMemoryStreamProvider. The file loading seems to be working fine but I am no longer able to access other form values which were available through provider.FormData under MultipartFormDataStreamProvider. Could someone show me how to do this?

The raw request captured by Fiddler:

POST http://myserver.com/QCCSvcHost/MIME/RealtimeTrans/ HTTP/1.1
Content-Type: multipart/form-data; boundary="XbCY"
Host: na-w-lxu3
Content-Length: 1470
Expect: 100-continue
Connection: Keep-Alive

--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=PayloadType

X12_270_Request_005010X279A1
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=ProcessingMode

RealTime
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=PayloadID

e51d4fae-7dec-11d0-a765-00a0c91e6fa6
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=TimeStamp

2007-08-30T10:20:34Z
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=SenderID

HospitalA
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=ReceiverID

PayerB
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=CORERuleVersion

2.2.0
--XbCY
Content-Disposition: form-data; name=Payload; filename=276_5010.edi

ISA*00*~SE*16*0001~GE*1*1~IEA*1*191543498~
--XbCY--

My controller code:

string payload = null;
NameValueCollection nvc = null;
string fname = null;
StringBuilder sb = new StringBuilder();
sb.AppendLine();
foreach (StreamContent item in provider.Contents)
{
    fname = item.Headers.ContentDisposition.FileName;
    if (!String.IsNullOrWhiteSpace(fname))
    {
        payload = item.ReadAsStringAsync().Result;
    }
    else
    {
        nvc = item.ReadAsFormDataAsync().Result;
    }
}
Liam
  • 27,717
  • 28
  • 128
  • 190
user2434400
  • 619
  • 2
  • 7
  • 13

2 Answers2

123

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();
}
lozzajp
  • 916
  • 7
  • 15
Kiran
  • 56,921
  • 15
  • 176
  • 161
  • Thanks Kiran for the input. When I try what you suggested, it doesn't seem to like the line NameValueCollection nvc = await content.ReadAsFormDataAsync(); for some reason. I am getting error:."ExceptionMessage:No MediaTypeFormatter is available to read an object of type 'FormDataCollection' from content with media type 'multipart/form-data'". Any ideas? – user2434400 Jun 12 '13 at 21:53
  • 1
    Is this 'content' just like how i have mentioned above, that is it the content of the array of content? i ask this because looks like you are try to read the content of the whole request rather than the inner content – Kiran Jun 12 '13 at 21:56
  • I tried both: nvc = Request.Content.ReadAsFormDataAsync().Result; and nvc = provider.Contents[0].ReadAsFormDataAsync().Result; But I am getting similar errors. – user2434400 Jun 12 '13 at 22:11
  • Hmm...could you share how your raw request looks like? and also the code which you have... – Kiran Jun 12 '13 at 22:16
  • Kiran, I couldn't figure out how to add code and request to the comment so I edited my origional post. My guess is that the request is OK since it worked with MultipartFormDataStreamProvider. Thanks again! – user2434400 Jun 12 '13 at 22:33
  • Thanks for more info. Looks like I was wrong about ReadAsFormDataAsync() as it looks for mediatype `application/x-www-form-urlencoded`. I have now modified the my code in the post. Let me know if this works. – Kiran Jun 12 '13 at 22:50
  • Thanks again! I will give it a try and let you know. Really appreciate your help! – user2434400 Jun 12 '13 at 22:54
  • 1
    It works beautifully Kiran! Thank you so much for your help! I am just a little surprised that Web API didn't make this more convenient and save us the trouble of writing parsing code. Have a great day! – user2434400 Jun 12 '13 at 23:27
  • 2
    Glad that helped!. Yes, I totally agree. I will bring this up to the team. Thanks!. BTW, sorry for the multiple updates to the above post. I have now included a complete sample. – Kiran Jun 12 '13 at 23:38
  • 1
    Too bad that I don't have enough "repatation" point to vote this anwser up. I strongly recommend someone else do this. Many thinks to Kiran! – user2434400 Jun 13 '13 at 13:24
  • went through a bunch of tutorials and sites and this post was the only thing to get me up and working. nice work and thank you :) – Tony Sep 03 '14 at 03:52
  • Great stuff. Thanks bro – Hugo Nava Kopp Mar 24 '17 at 12:20
  • This answer helped me alot!! – SeegeDev Mar 05 '18 at 19:48
  • @KiranChalla That line of code: return token.Substring(1, token.Length - 2); does not work. my filename has double quotes around the filename and they literally do not get removed. This changes the file extension .txt to .txt" which causes a problem to the enduser opening the file... " – Pascal Mar 08 '18 at 07:41
  • Fantastic work! This must be included in the .Net framework. – jean Oct 09 '19 at 17:39
  • @KiranChalla Did the team ever discuss this "obstacle course" with web API? It's been almost seven years now :) – Fredrik Norlin May 05 '20 at 11:00
1

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());
   }
}
Greg Veres
  • 1,770
  • 19
  • 28