Uploading Multiple Files through an ASP.NET Web API Service

I had written a post on DotNetCurry on how to upload multiple-files to an Azure Blob through an ASP.NET MVC Web Application. Today we’ll see how we can do the same but store it in a file system instead and use a Web API controller instead, so that the upload service can be hosted anywhere we want. For brevity, we’ll build our Web API sample to use ASP.NET/IIS host.

The API Controller

Step 1: We start off with a Basic Web Application Template and add an empty API Controller to it.

Step 2: Next we add an Upload method to which the Files will be posted. The complete method is as follows:

[HttpPost]
public async Task< object > UploadFile()
{
if (!Request.Content.IsMimeMultipartContent("form-data"))
{
  throw new HttpResponseException
   (Request.CreateResponse(HttpStatusCode.UnsupportedMediaType));
}
MultipartFormDataStreamProvider streamProvider = new MultipartFormDataStreamProvider(
  HttpContext.Current.Server.MapPath("~/App_Data/Temp/"));
  await Request.Content.ReadAsMultipartAsync(streamProvider);
return new
{
  FileNames = streamProvider.FileData.Select(entry => entry.LocalFileName),
};
}


This code does the following:

1. Checks if the Request.Content is a Multipart Form Data. If not, it throws an UnsupportedMediaType exception.

2. Creates an instance of MultipartFormDataStreamProvider with the Path to a ‘Temp’ folder that we have created in the App_Data folder of our web application.

3. Finally it await the call to ReadAsMultipartAsync using the stream provider we created in step 2 above. Once complete, it sends back and enumerable containing the files uploaded.

This is the basic Uploader. Let’s setup the client.

The File Upload Client

We add another empty controller, this time it is an MvcController and we’ll call it HomeController.
Now we create a Home folder under Views and add an empty View called Index.cshtml to it.

We add the following markup to the view.

@{
    ViewBag.Title = "UploadFile";
}

<h1>Upload Multiple Files to Web API</h1>
<input type="file" id="selectFile" name="selectFile" multiple="multiple" />
<input type="submit" name="fileUpload" id="fileUpload" value="Upload" />

@section Scripts{
    <script src="~/Scripts/multi-uploader.js"></script>
}


It has two inputs, one to select multiple files (selectFile) and the other an Upload (fileUpload) button. Finally we have reference to the JavaScript file multi-uploader.js. Next we’ll see what this does.

The multi-uploader.js

The JavaScript files is rather simple in our case.

1. It attaches an event handler (beginUpload) for the click event of the fileUpload button.

2. The beginUpload event handler pulls out the “selectFile” and retrieves the files in it.

3. Next it stuffs all the file content in the FormData() of the page.

4. Finally it posts all the data to the Api controller

/// <reference path="jquery-1.8.2.js" />
/// <reference path="_references.js" />
$(document).ready(function ()
{
    $(document).on("click", "#fileUpload", beginUpload);
});

function beginUpload(evt)
{
var files = $("#selectFile").get(0).files;
if (files.length > 0)
{
  if (window.FormData !== undefined)
  {
   var data = new FormData();
   for (i = 0; i < files.length; i++)
   {
    data.append("file" + i, files[i]);
   }
   $.ajax({
    type: "POST",
    url: "/api/upload",
    contentType: false,
    processData: false,
    data: data
   });
  } else
  {
   alert("This browser doesn't support HTML5 multiple file uploads!");
  }
}
}


Testing our Web API sample

Our app is almost ready to run. Just one thing to do, under the App_Data folder create a folder named Temp because this is what we specified as the destination for the MultipartFormDataStreamProvider.

1. Hit F5 to bring up the Index Page

image

2. Click browser to select a set of files and hit Upload.

3. In Solution explorer, toggle the ‘Show All Files’ button to check the new files in Temp folder. Surprised?

file-upload-guid

Yeah, the files you uploaded don’t have the name that was on the client side. While this can be a perfectly good use case, what if you wanted to preserve the file name? Well there are two solutions and we’ll try out one here:

Moving uploaded files with Client Name

In this approach, after the files have been uploaded using the ReadAsMultipartAsync call, we’ll loop through all the file data in the streamProvider and try to extract the File name from the Request Header. If it is provided good, we’ll use it else we’ll use a new GUID.

Once the filename is determined, we’ll move the File from the App_Data/Temp to App_Data using the new filename.

foreach (MultipartFileData fileData in streamProvider.FileData)
{
string fileName = "";
if (string.IsNullOrEmpty(fileData.Headers.ContentDisposition.FileName))
{
  fileName = Guid.NewGuid().ToString();
}
fileName = fileData.Headers.ContentDisposition.FileName;
if (fileName.StartsWith("\"") && fileName.EndsWith("\""))
{
  fileName = fileName.Trim('"');
}
if (fileName.Contains(@"/") || fileName.Contains(@"\"))
{
  fileName = Path.GetFileName(fileName);
}
File.Move(fileData.LocalFileName, 
  Path.Combine(HttpContext.Current.Server.MapPath("~/App_Data/"), fileName));
}

This technique is simple and works pretty well. Add the code and run the application and repeat the file uploads. This time we’ll see the files have been uploaded and moved using the correct file name.

Multi File Upload

However this two-step process is a little smelly when it comes to scaling the application. SO what’s the solution?

Creating a Custom MultipartFormDataStreamProvider

Well the solution is rather simple, we subclass MultipartFormDataStreamProvider to create NamedMultipartFormDataStreamProvider. In this, we override the GetLocalName method and put the above code in it. The class would look as follows:

public class NamedMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
{
public NamedMultipartFormDataStreamProvider(string fileName):base(fileName)
{
}

public override string GetLocalFileName(System.Net.Http.Headers.HttpContentHeaders
  headers)
{
  string fileName = base.GetLocalFileName(headers);
  if (!string.IsNullOrEmpty(headers.ContentDisposition.FileName))
  {
   fileName = headers.ContentDisposition.FileName;
  }
  if (fileName.StartsWith("\"") && fileName.EndsWith("\""))
  {
   fileName = fileName.Trim('"');
  }
  if (fileName.Contains(@"/") || fileName.Contains(@"\"))
  {
   fileName = Path.GetFileName(fileName);
  }
  return fileName;
}
}


To use the above, we need to update our Controller Code as follows

[HttpPost]
public async Task< object > UploadFile()
{
if (!Request.Content.IsMimeMultipartContent("form-data"))
{
  throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.UnsupportedMediaType));
}

NamedMultipartFormDataStreamProvider streamProvider = new
  NamedMultipartFormDataStreamProvider(
   HttpContext.Current.Server.MapPath("~/App_Data/"));

await Request.Content.ReadAsMultipartAsync(streamProvider);
return new
{
  FileNames = streamProvider.FileData.Select(entry => entry.LocalFileName),
};
}

Note that we are using NameMultipartFormDataStreamProvider and the folder this time is directly App_Data, no file move required.

Well, that covers multiple file uploads for now.

Conclusion

We saw how to upload multiple files to a Web API service. When hosted on II,S it has the 4MB limit of file request size and hence combined size of > 4Mb for all files will fail using this technique. For that we’ve to go back to Chunked Upload. We’ll leave that for another day.

Download the entire source code of this article (Github)




About The Author

Suprotim Agarwal
Suprotim Agarwal, Developer Technologies MVP (Microsoft Most Valuable Professional) is the founder and contributor for DevCurry, DotNetCurry and SQLServerCurry. He is the Chief Editor of a Developer Magazine called DNC Magazine. He has also authored two Books - 51 Recipes using jQuery with ASP.NET Controls. and The Absolutely Awesome jQuery CookBook.

Follow him on twitter @suprotimagarwal.

4 comments:

Unknown said...

Thank you for your article!
it's very nice.

Unknown said...

hi...how to do unit testing for this ?

Unknown said...

Great Article
Really help me

Unknown said...

Great article, Thank you very much