21

I want to upload a file and send along with the file some additional information, let's say a string foo and an int bar.

How would I write a ASP.NET WebAPI controller method that receives a file upload, a string, and an int?

My JavaScript:

var fileInput = document.querySelector("#filePicker");
var formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("foo", "hello world!");
formData.append("bar", 42);

var options = {
   url: "/api/foo/upload",
   data: formData,
   processData: false // Prevents JQuery from transforming the data into a query string
};
$.ajax(options);

My WebAPI controller can access the file like this:

public async Task<HttpResponseMessage> Upload()
{
    var streamProvider = new MultipartMemoryStreamProvider();
    await Request.Content.ReadAsMultipartAsync(streamProvider);
    var fileStream = await streamProvider.Contents[0].ReadAsStreamAsync();
}

But it's not clear to me how I can get at my string and my int. I figure I can probably say streamProvider.Content[1], or whatever, but that feels super nasty.

What's the Right Way© to write a WebAPI action that accepts a file upload, a string, and an int?

john chen
  • 176
  • 15
Judah Gabriel Himango
  • 58,906
  • 38
  • 158
  • 212
  • 1
    I'm probably missing something... Why can't you just make foo and bar parameters of your Upload method and let the binding magic happen? – Scrappydog May 19 '14 at 18:36
  • 2
    I was laser-focused on sending the data inside FormData (e.g. as the data inside $.post(url, formData); the model binder would never find them. Turns out, I can post the file as the form data alone, then put the other args in the URL. Then the model binder works its magic. – Judah Gabriel Himango May 20 '14 at 03:43

5 Answers5

19

You can create your own MultipartFileStreamProvider to access the additional arguments.

In ExecutePostProcessingAsync we loop through each file in multi-part form and load the custom data (if you only have one file you'll just have one object in the CustomData list).

class MyCustomData
{
    public int Foo { get; set; }
    public string Bar { get; set; }
}

class CustomMultipartFileStreamProvider : MultipartMemoryStreamProvider
{
    public List<MyCustomData> CustomData { get; set; }

    public CustomMultipartFileStreamProvider()
    {
        CustomData = new List<MyCustomData>();
    }

    public override Task ExecutePostProcessingAsync()
    {
        foreach (var file in Contents)
        {
            var parameters = file.Headers.ContentDisposition.Parameters;
            var data = new MyCustomData
            {
                Foo = int.Parse(GetNameHeaderValue(parameters, "Foo")),
                Bar = GetNameHeaderValue(parameters, "Bar"),
            };

            CustomData.Add(data);
        }

        return base.ExecutePostProcessingAsync();
    }

    private static string GetNameHeaderValue(ICollection<NameValueHeaderValue> headerValues, string name)
    {
        var nameValueHeader = headerValues.FirstOrDefault(
            x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        return nameValueHeader != null ? nameValueHeader.Value : null;
    }
}

Then in your controller:

class UploadController : ApiController
{
    public async Task<HttpResponseMessage> Upload()
    {
        var streamProvider = new CustomMultipartFileStreamProvider();
        await Request.Content.ReadAsMultipartAsync(streamProvider);

        var fileStream = await streamProvider.Contents[0].ReadAsStreamAsync();
        var customData = streamProvider.CustomData;

        return Request.CreateResponse(HttpStatusCode.Created);
    }
}
Ben Foster
  • 34,340
  • 40
  • 176
  • 285
  • 4
    Thanks. I ended up just passing the args using "simple" args, e.g. $.post("/api/foo?id=23&bar=hello", fileData). That works..provided you're not passing complex objects or arrays. I like your solution better, so I'm marking it as the answer. – Judah Gabriel Himango May 21 '14 at 17:09
  • 4
    In our attempt at this, we are getting the name of the parameter in the value portion of the key/value pair - the key field simple contains 'name'. Has something changed since May? – codeputer Jul 22 '14 at 00:39
5

I think the answers here are excellent. So others can see a somewhat simple example of how to pass data in addition to the file in summary form, included is a Javascript Function that makes the WebAPI call to the FileUpload Controller, and the snippet from the FileUpload Controller (in VB.net) that reads the additional data passed from Javascript.

Javascript:

            function uploadImage(files) {
            var data = new FormData();
            if (files.length > 0) {
                data.append("UploadedImage", files[0]);
                data.append("Source", "1")
                var ajaxRequest = $.ajax({
                    type: "POST",
                    url: "/api/fileupload/uploadfile",
                    contentType: false,
                    processData: false,
                    data: data
                });

File Upload Controller:

        <HttpPost> _
    Public Function UploadFile() As KeyValuePair(Of Boolean, String)
        Try
            If HttpContext.Current.Request.Files.AllKeys.Any() Then
                Dim httpPostedFile = HttpContext.Current.Request.Files("UploadedImage")
                Dim source = HttpContext.Current.Request.Form("Source").ToString()

So as you can see in the Javascript, the additional data passed is the "Source" key, and the value is "1". And as Chandrika has answered above, the Controller reads this passed data through "System.Web.HttpContext.Current.Request.Form("Source").ToString()".

Note that Form("Source") uses () (vs. []) as the controller code is in VB.net.

Hope this helps.

Sam
  • 51
  • 1
  • 2
3

You can extract multiple files and multiple attributes in this way:

public async Task<HttpResponseMessage> Post()
{
    Dictionary<string,string> attributes = new Dictionary<string, string>();
    Dictionary<string, byte[]> files = new Dictionary<string, byte[]>();

    var provider = new MultipartMemoryStreamProvider();
    await Request.Content.ReadAsMultipartAsync(provider);
    foreach (var file in provider.Contents)
    {
        if (file.Headers.ContentDisposition.FileName != null)
        {
            var filename = file.Headers.ContentDisposition.FileName.Trim('\"');
            var buffer = await file.ReadAsByteArrayAsync();
            files.Add(filename, buffer);
        } else
        {
            foreach(NameValueHeaderValue p in file.Headers.ContentDisposition.Parameters)
            {
                string name = p.Value;
                if (name.StartsWith("\"") && name.EndsWith("\"")) name = name.Substring(1, name.Length - 2);
                string value = await file.ReadAsStringAsync();
                attributes.Add(name, value);
            }
        }
    }
    //Your code here  
    return new HttpResponseMessage(HttpStatusCode.OK);
}
Renzo Ciot
  • 3,746
  • 2
  • 25
  • 29
0

You can do it by following way : JQuery Method:

    var data = new FormData();

    data.append("file", filesToUpload[0].rawFile);
    var doc = {};            
    doc.DocumentId = 0; 
    $.support.cors = true;
    $.ajax({
        url: '/api/document/uploaddocument',
        type: 'POST',
        contentType: 'multipart/form-data',
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        success: function (response) {
            docId = response.split('|')[0];
            doc.DocumentId = docId;
            $.post('/api/document/metadata', doc)
                .done(function (response) {
                });
          alert('Document save successfully!');
        },
        error: function (e) {
            alert(e);
        }
    });

call your 'UploadDocuement' web API

[Route("api/document/uploaddocument"), HttpPost]
[UnhandledExceptionFilter]
[ActionName("UploadDocument")]
public Task<HttpResponseMessage> UploadDocument()
{
    // Check if the request contains multipart/form-data.
    if (!Request.Content.IsMimeMultipartContent())
    {
        Task<HttpResponseMessage> mytask = new Task<HttpResponseMessage>(delegate()
        {
            return new HttpResponseMessage()
            {
                StatusCode = HttpStatusCode.BadRequest,
                Content = "In valid file & request content type!".ToStringContent()
            };
        });
        return mytask;
    }


    string root = HttpContext.Current.Server.MapPath("~/Documents");
    if (System.IO.Directory.Exists(root))
    {
        System.IO.Directory.CreateDirectory(root);
    }
    var provider = new MultipartFormDataStreamProvider(root);

    var task = Request.Content.ReadAsMultipartAsync(provider).
    ContinueWith<HttpResponseMessage>(o =>
    {
        if (o.IsFaulted || o.IsCanceled)
            throw new HttpResponseException(HttpStatusCode.InternalServerError);

        FileInfo finfo = new FileInfo(provider.FileData.First().LocalFileName);

        string guid = Guid.NewGuid().ToString();

        File.Move(finfo.FullName, Path.Combine(root, guid + "_" + provider.FileData.First().Headers.ContentDisposition.FileName.Replace("\"", "")));

        string sFileName = provider.FileData.First().Headers.ContentDisposition.FileName.Replace("\"", "");

        FileInfo FInfos = new FileInfo(Path.Combine(root, guid + "_" + provider.FileData.First().Headers.ContentDisposition.FileName.Replace("\"", "")));

        Document dbDoc = new Document()
        {
            DocumentID = 0                

        };

        context.DocumentRepository.Insert(dbDoc);
        context.Save();

        return new HttpResponseMessage()
        {
            Content = new StringContent(string.Format("{0}|File uploaded.", dbDoc.DocumentID))
        };
    }
    );
    return task;

}

Call your metadata web api by following way :

[Route("api/document/metadata"), HttpPost]
[ActionName("Metadata")]
public Task<HttpResponseMessage> Metadata(Document doc)
{
    int DocId = Convert.ToInt32(System.Web.HttpContext.Current.Request.Form["DocumentId"].ToString());
        
    Task<HttpResponseMessage> mytask = new Task<HttpResponseMessage>(delegate()
    {
        return new HttpResponseMessage()
        {
            Content = new StringContent("metadata updated")
        };
    });
    return mytask;
}
Rutix
  • 861
  • 1
  • 10
  • 22
Chandrika Prajapati
  • 867
  • 1
  • 6
  • 11
  • 1
    So, if I'm understanding that right, you're making 2 calls: one for the file, another for the metadata (e.g. the "additional arguments" part of my question). Is there a way to do this in a single call? – Judah Gabriel Himango May 19 '14 at 14:20
  • try this one in UploadDocument web api "System.Web.HttpContext.Current.Request.Form["DocumentId"].ToString()" – Chandrika Prajapati May 20 '14 at 05:20
  • in above eample ,i am making 2 calls, one for file and another for metadata, you can get it by one call like following try this one in UploadDocument web api "System.Web.HttpContext.Current.Request.Form["DocumentId"].ToString()" – Chandrika Prajapati May 21 '14 at 05:34
0

var receipents = HttpContext.Current.Request.Params["Receipents"]; var participants = HttpContext.Current.Request.Params["Participants"];

        var file = HttpContext.Current.Request.Files.Count > 0 ?  HttpContext.Current.Request.Files[0] : null;

        if (file != null && file.ContentLength > 0)
        {
            var fileName = Path.GetFileName(file.FileName);

            var path = Path.Combine(
                HttpContext.Current.Server.MapPath("~/uploads"),
                fileName
            );

            file.SaveAs(path);
        }
Iván Kollár
  • 39
  • 1
  • 3
  • The simplest solution to getting parameters and files together. – Iván Kollár Sep 23 '20 at 15:50
  • Generally, answers are much more helpful if they include an explanation of what the code is intended to do, and why that solves the problem without introducing others. – DCCoder Sep 23 '20 at 18:02