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:
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
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?
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.
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)
Thank you for your article!
ReplyDeleteit's very nice.
hi...how to do unit testing for this ?
ReplyDeleteGreat Article
ReplyDeleteReally help me
Great article, Thank you very much
ReplyDelete