29

I need to upload file sending extra paramaters.

I have found the following post in stackoverflow: Webapi ajax formdata upload with extra parameters

It describes how to do this using MultipartFormDataStreamProvider and saving data to fileserver. I do not need to save file to server, but to DB instead. And I have already working code using MultipartMemoryStreamProvider, but it doesn't use extra parameter.

Can you give me clues how to process extra paramaters in webapi?

For example, if I add file and also test paramater:

data.append("myParameter", "test"); 

Here is my webapi that processes fileupload without extra paramater:

if (Request.Content.IsMimeMultipartContent())
{               
    var streamProvider = new MultipartMemoryStreamProvider();
    var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith<IEnumerable<FileModel>>(t =>
    {
        if (t.IsFaulted || t.IsCanceled)
        {
            throw new HttpResponseException(HttpStatusCode.InternalServerError);
        }

        _fleDataService = new FileDataBLL();
        FileData fle;

        var fleInfo = streamProvider.Contents.Select(i => {         
            fle = new FileData();
            fle.FileName = i.Headers.ContentDisposition.FileName;

            var contentTest = i.ReadAsByteArrayAsync();
            contentTest.Wait();
            if (contentTest.Result != null)
            {
                fle.FileContent = contentTest.Result;
            }                       

            // get extra parameters here ??????

            _fleDataService.Save(fle);

            return new FileModel(i.Headers.ContentDisposition.FileName, 1024); //todo
        });
        return fleInfo;
    });
    return task;
}
Community
  • 1
  • 1
renathy
  • 5,125
  • 20
  • 85
  • 149

4 Answers4

39

Expanding on gooid's answer, I encapsulated the FormData extraction into the provider because I was having issues with it being quoted. This just provided a better implementation in my opinion.

public class MultipartFormDataMemoryStreamProvider : MultipartMemoryStreamProvider
{
    private readonly Collection<bool> _isFormData = new Collection<bool>();
    private readonly NameValueCollection _formData = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, Stream> _fileStreams = new Dictionary<string, Stream>();

    public NameValueCollection FormData
    {
        get { return _formData; }
    }

    public Dictionary<string, Stream> FileStreams
    {
        get { return _fileStreams; }
    }

    public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
    {
        if (parent == null)
        {
            throw new ArgumentNullException("parent");
        }

        if (headers == null)
        {
            throw new ArgumentNullException("headers");
        }

        var contentDisposition = headers.ContentDisposition;
        if (contentDisposition == null)
        {
            throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part.");
        }

        _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));
        return base.GetStream(parent, headers);
    }

    public override async Task ExecutePostProcessingAsync()
    {
        for (var index = 0; index < Contents.Count; index++)
        {
            HttpContent formContent = Contents[index];
            if (_isFormData[index])
            {
                // Field
                string formFieldName = UnquoteToken(formContent.Headers.ContentDisposition.Name) ?? string.Empty;
                string formFieldValue = await formContent.ReadAsStringAsync();
                FormData.Add(formFieldName, formFieldValue);
            } 
            else
            {
                // File
                string fileName = UnquoteToken(formContent.Headers.ContentDisposition.FileName);
                Stream stream = await formContent.ReadAsStreamAsync();
                FileStreams.Add(fileName, stream);
            }
        }
    }

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

And here's how I'm using it. Note that I used await since we're on .NET 4.5.

    [HttpPost]
    public async Task<HttpResponseMessage> Upload()
    {
        if (!Request.Content.IsMimeMultipartContent())
        {
            return Request.CreateResponse(HttpStatusCode.UnsupportedMediaType, "Unsupported media type.");
        }

        // Read the file and form data.
        MultipartFormDataMemoryStreamProvider provider = new MultipartFormDataMemoryStreamProvider();
        await Request.Content.ReadAsMultipartAsync(provider);

        // Extract the fields from the form data.
        string description = provider.FormData["description"];
        int uploadType;
        if (!Int32.TryParse(provider.FormData["uploadType"], out uploadType))
        {
            return Request.CreateResponse(HttpStatusCode.BadRequest, "Upload Type is invalid.");
        }

        // Check if files are on the request.
        if (!provider.FileStreams.Any())
        {
            return Request.CreateResponse(HttpStatusCode.BadRequest, "No file uploaded.");
        }

        IList<string> uploadedFiles = new List<string>();
        foreach (KeyValuePair<string, Stream> file in provider.FileStreams)
        {
            string fileName = file.Key;
            Stream stream = file.Value;

            // Do something with the uploaded file
            UploadManager.Upload(stream, fileName, uploadType, description);

            // Keep track of the filename for the response
            uploadedFiles.Add(fileName);
        }

        return Request.CreateResponse(HttpStatusCode.OK, "Successfully Uploaded: " + string.Join(", ", uploadedFiles));
    }
Mark Seefeldt
  • 582
  • 4
  • 9
  • 1
    Mark Seefeldt's answer work well for me in .Net 4.5. A shame this functionality is not supported out of the box. – jivangilad Sep 03 '14 at 08:21
  • what is "Contents" in MultipartFormDataMemoryStreamProvider? – SKD Dec 20 '15 at 09:32
  • It is the Contents property of its base class. Basically the content of the HTTP request. https://msdn.microsoft.com/en-us/library/system.net.http.multipartstreamprovider.contents(v=vs.118).aspx#P:System.Net.Http.MultipartStreamProvider.Contents – Mark Seefeldt Dec 20 '15 at 13:18
  • This works great for .NET4.5 using async await. @MarkSeefeldt just curious what your UploadManager looks like? I'm not too familiar with streams and want to make sure the class i send the file too is declared properly. – LogicaLInsanity Jan 29 '16 at 17:36
  • 1
    My UpdateManager.Upload function is a static method that just takes in the uploadType and determines what file uploader implementation class to send the file to. For example, we have one for an certain type of excel formatted file. It creates an ExcelPackage(stream) and then uses the ExcelPackage to read the rows data and upload into a database. Sorry, I can't give you details as it's very implementation specific. – Mark Seefeldt Jan 30 '16 at 03:03
  • it can also be helpful to have the filename AND the form value name `FormData.Add(formFieldName, fileName);` `FileStreams.Add(formFieldName, stream);` – Nateous Mar 29 '18 at 20:55
32

You can achieve this in a not-so-very-clean manner by implementing a custom DataStreamProvider that duplicates the logic for parsing FormData from multi-part content from MultipartFormDataStreamProvider.

I'm not quite sure why the decision was made to subclass MultipartFormDataStreamProvider from MultiPartFileStreamProvider without at least extracting the code that identifies and exposes the FormData collection since it is useful for many tasks involving multi-part data outside of simply saving a file to disk.

Anyway, the following provider should help solve your issue. You will still need to ensure that when you iterate the provider content you are ignoring anything that does not have a filename (specifically the statement streamProvider.Contents.Select() else you risk trying to upload the formdata to the DB). Hence the code that asks the provider is a HttpContent IsStream(), this is a bit of a hack but was the simplest was I could think to do it.

Note that it is basically a cut and paste hatchet job from the source of MultipartFormDataStreamProvider - it has not been rigorously tested (inspired by this answer).

public class MultipartFormDataMemoryStreamProvider : MultipartMemoryStreamProvider
{
    private readonly Collection<bool> _isFormData = new Collection<bool>();
    private readonly NameValueCollection _formData = new NameValueCollection(StringComparer.OrdinalIgnoreCase);

    public NameValueCollection FormData
    {
        get { return _formData; }
    }

    public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
    {
        if (parent == null) throw new ArgumentNullException("parent");
        if (headers == null) throw new ArgumentNullException("headers");

        var contentDisposition = headers.ContentDisposition;

        if (contentDisposition != null)
        {
            _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));
            return base.GetStream(parent, headers);
        }

        throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part.");
    }

    public override async Task ExecutePostProcessingAsync()
    {
        for (var index = 0; index < Contents.Count; index++)
        {
            if (IsStream(index))
                continue;

            var formContent = Contents[index];
            var contentDisposition = formContent.Headers.ContentDisposition;
            var formFieldName = UnquoteToken(contentDisposition.Name) ?? string.Empty;
            var formFieldValue = await formContent.ReadAsStringAsync();
            FormData.Add(formFieldName, formFieldValue);
        }
    }

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

    public bool IsStream(int idx)
    {
        return !_isFormData[idx];
    }
}

It can be used as follows (using TPL syntax to match your question):

[HttpPost]
public Task<string> Post()
{
    if (!Request.Content.IsMimeMultipartContent())
        throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable, "Invalid Request!"));

    var provider = new MultipartFormDataMemoryStreamProvider();

    return Request.Content.ReadAsMultipartAsync(provider).ContinueWith(p =>
    {
        var result = p.Result;
        var myParameter = result.FormData.GetValues("myParameter").FirstOrDefault();

        foreach (var stream in result.Contents.Where((content, idx) => result.IsStream(idx)))
        {
            var file = new FileData(stream.Headers.ContentDisposition.FileName);
            var contentTest = stream.ReadAsByteArrayAsync();
            // ... and so on, as per your original code.

        }
        return myParameter;
    });
}

I tested it with the following HTML form:

<form action="/api/values" method="post" enctype="multipart/form-data">
    <input name="myParameter" type="hidden" value="i dont do anything interesting"/>
    <input type="file" name="file1" />
    <input type="file" name="file2" />
    <input type="submit" value="OK" />
</form>
Community
  • 1
  • 1
gooid
  • 841
  • 8
  • 18
  • var count = provider.FileData.Count; gives error (doesn't contain a definition of FileData)? – renathy Nov 04 '13 at 07:55
  • another question: in your example how to get file content? I am not able to get it for some reason – renathy Nov 04 '13 at 08:02
  • I've updated the provider implementation and example controller, please take a look. – gooid Nov 04 '13 at 11:20
  • It doesn't compile... I have added private Collection _fileData = new Collection(); as in the link and did some file related changes, my code compiles, but this doesn't. – renathy Nov 04 '13 at 12:28
  • Which bit doesn't compile? Note that I updated the example controller action and provder.FileData.Count has gone. I purposely left out the FileData collection code used in the linked answer as it didn't seem very relevant to you (you parse the filename by checking the ContentDisposition header). – gooid Nov 04 '13 at 12:37
  • 1
    Thanks for the generic implementation of the MultipartFormDataMemoryStreamProvider. It saved me a lot of research. – Daniel Leiszen Nov 21 '16 at 15:57
  • 1
    Thanks for this. Can't believe this isn't built in, as it seems to be a very common workflow. – Joe Oct 04 '18 at 19:45
5

I really needed the media type and length of the files uploaded so I modified @Mark Seefeldt answer slightly to the following:

public class MultipartFormFile
{
    public string Name { get; set; }
    public long? Length { get; set; }
    public string MediaType { get; set; }
    public Stream Stream { get; set; }
}

public class MultipartFormDataMemoryStreamProvider : MultipartMemoryStreamProvider
{
    private readonly Collection<bool> _isFormData = new Collection<bool>();
    private readonly NameValueCollection _formData = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
    private readonly List<MultipartFormFile> _fileStreams = new List<MultipartFormFile>();

    public NameValueCollection FormData
    {
        get { return _formData; }
    }

    public List<MultipartFormFile> FileStreams
    {
        get { return _fileStreams; }
    }

    public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
    {
        if (parent == null)
        {
            throw new ArgumentNullException("parent");
        }

        if (headers == null)
        {
            throw new ArgumentNullException("headers");
        }

        var contentDisposition = headers.ContentDisposition;
        if (contentDisposition == null)
        {
            throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part.");
        }

        _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));
        return base.GetStream(parent, headers);
    }

    public override async Task ExecutePostProcessingAsync()
    {
        for (var index = 0; index < Contents.Count; index++)
        {
            HttpContent formContent = Contents[index];
            if (_isFormData[index])
            {
                // Field
                string formFieldName = UnquoteToken(formContent.Headers.ContentDisposition.Name) ?? string.Empty;
                string formFieldValue = await formContent.ReadAsStringAsync();
                FormData.Add(formFieldName, formFieldValue);
            }
            else
            {
                // File
                var file = new MultipartFormFile
                {
                    Name = UnquoteToken(formContent.Headers.ContentDisposition.FileName),
                    Length = formContent.Headers.ContentLength,
                    MediaType = formContent.Headers.ContentType.MediaType,
                    Stream = await formContent.ReadAsStreamAsync()
                };

                FileStreams.Add(file);
            }
        }
    }

    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;
    }
}
Mark Vickery
  • 1,927
  • 3
  • 22
  • 34
1

Ultimately, the following was what worked for me:

string root = HttpContext.Current.Server.MapPath("~/App_Data");

var provider = new MultipartFormDataStreamProvider(root);

var filesReadToProvider = await Request.Content.ReadAsMultipartAsync(provider);

foreach (var file in provider.FileData)
{
    var fileName = file.Headers.ContentDisposition.FileName.Replace("\"", string.Empty);
    byte[] documentData;

    documentData = File.ReadAllBytes(file.LocalFileName);

    DAL.Document newRecord = new DAL.Document
    {
        PathologyRequestId = PathologyRequestId,
        FileName = fileName,
        DocumentData = documentData,
        CreatedById = ApplicationSecurityDirector.CurrentUserGuid,
        CreatedDate = DateTime.Now,
        UpdatedById = ApplicationSecurityDirector.CurrentUserGuid,
        UpdatedDate = DateTime.Now
    };

    context.Documents.Add(newRecord);

    context.SaveChanges();
}
user1477388
  • 20,790
  • 32
  • 144
  • 264
  • One issue here is that you're calling SaveChanges() for each file. You probably should just call it once for the entire collection, so your save is atomic. – Michael Blackburn May 10 '16 at 17:52
  • According to http://stackoverflow.com/questions/1930982/when-should-i-call-savechanges-when-creating-1000s-of-entity-framework-object it's actually faster to save after each row. – user1477388 May 11 '16 at 13:03
  • Which is why I didn't claim it was faster. – Michael Blackburn May 11 '16 at 15:47
  • Per your link's accepted answer: "If you need to enter all rows in one transaction, call it after all of AddToClassName class. If rows can be entered independently, save changes after every row. Database consistence is important." – Michael Blackburn May 11 '16 at 15:55