Knowledge is power. We love to share it.

News related to Mono products, services and latest developments in our community.

tihomir-kit

Async upload using angular-file-upload directive and .net WebAPI service

12/08/2013Categories: ASP.NET, AngularJS
Angular-file-upload directive is an awesome lightweight AngularJS directive which handles file upload for you and lets you upload files asynchronously to the server. This post will give you basic understanding on how to upload files by using this directive together with .NET WebAPI service. For the purpose of this tutorial, I'll keep everything as simple as possible since the focus here is on connecting AngularJS and async upload with a .NET WebAPI service and not on additional functionality which can be built afterwards around angular-file-upload. Angular-file-upload installation is already explained in it's README so I won't go through that either and I'll assume you installed it properly.

I created a fully working upload demo app to go together with this post, feel free to download it
.

Firstly, the template markup needs to be created:

<div ng-controller="UploadCtrl">
    <input type="file" ng-file-select="onFileSelect($files)" multiple>
</div>
This will give us the "Choose files" button which allows the user to select multiple files at once. Through the ng-controller directive we are connecting this input field to the "UploadCtrl" angular controller which we also need to implement:

.controller('UploadCtrl', function ($scope, $http, $timeout, $upload) {
    $scope.upload = [];
    $scope.fileUploadObj = { testString1: "Test string 1", testString2: "Test string 2" };
 
    $scope.onFileSelect = function ($files) {
        //$files: an array of files selected, each file has name, size, and type.
        for (var i = 0; i < $files.length; i++) {
            var $file = $files[i];
            (function (index) {
                $scope.upload[index] = $upload.upload({
                    url: "./api/files/upload", // webapi url
                    method: "POST",
                    data: { fileUploadObj: $scope.fileUploadObj },
                    file: $file
                }).progress(function (evt) {
                    // get upload percentage
                    console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
                }).success(function (data, status, headers, config) {
                    // file is uploaded successfully
                    console.log(data);
                }).error(function (data, status, headers, config) {
                    // file failed to upload
                    console.log(data);
                });
            })(i);
        }
    }
 
    $scope.abortUpload = function (index) {
        $scope.upload[index].abort();
    }
});
What it does is - it iterates through each selected file and sends it to the .NET WebAPI service through the WebAPI call URL we specified in $scope.upload options (the "./api/files/upload" part). If you did not mess with WebAPI routes (/App_Start/WebApiConfig.cs), this route should by convention work out-of-the-box. The data option also allows us to send additional data to the WebAPI service together with the file if needed. In this case we will just send a string as a proof-of-concept. If you don't need this functionality, you can just remove or comment the "data" line out but you will also need to remove that attribute from the WebAPI method which we will create next.

public class FilesController : ApiController
{
    [HttpPost] // This is from System.Web.Http, and not from System.Web.Mvc
    public async Task<HttpResponseMessage> Upload()
    {
        if (!Request.Content.IsMimeMultipartContent())
        {
            this.Request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
        }
 
        var provider = GetMultipartProvider();
        var result = await Request.Content.ReadAsMultipartAsync(provider);
 
        // On upload, files are given a generic name like "BodyPart_26d6abe1-3ae1-416a-9429-b35f15e6e5d5"
        // so this is how you can get the original file name
        var originalFileName = GetDeserializedFileName(result.FileData.First());
 
        // uploadedFileInfo object will give you some additional stuff like file length,
        // creation time, directory name, a few filesystem methods etc..
        var uploadedFileInfo = new FileInfo(result.FileData.First().LocalFileName);
 
        // Remove this line as well as GetFormData method if you're not
        // sending any form data with your upload request
        var fileUploadObj = GetFormData<UploadDataModel>(result);
 
        // Through the request response you can return an object to the Angular controller
        // You will be able to access this in the .success callback through its data attribute
        // If you want to send something to the .error callback, use the HttpStatusCode.BadRequest instead
        var returnData = "ReturnTest";
        return this.Request.CreateResponse(HttpStatusCode.OK, new { returnData });
    }
 
    // You could extract these two private methods to a separate utility class since
    // they do not really belong to a controller class but that is up to you
    private MultipartFormDataStreamProvider GetMultipartProvider()
    {
        // IMPORTANT: replace "(tilde)" with the real tilde character
        // (our editor doesn't allow it, so I just wrote "(tilde)" instead)
        var uploadFolder = "(tilde)/App_Data/Tmp/FileUploads"; // you could put this to web.config
        var root = HttpContext.Current.Server.MapPath(uploadFolder);
        Directory.CreateDirectory(root);
        return new MultipartFormDataStreamProvider(root);
    }
 
    // Extracts Request FormatData as a strongly typed model
    private object GetFormData<T>(MultipartFormDataStreamProvider result)
    {
        if (result.FormData.HasKeys())
        {
            var unescapedFormData = Uri.UnescapeDataString(result.FormData
                .GetValues(0).FirstOrDefault() ?? String.Empty);
            if (!String.IsNullOrEmpty(unescapedFormData))
                return JsonConvert.DeserializeObject<T>(unescapedFormData);
        }
 
        return null;
    }
 
    private string GetDeserializedFileName(MultipartFileData fileData)
    {
        var fileName = GetFileName(fileData);
        return JsonConvert.DeserializeObject(fileName).ToString();
    }
 
    public string GetFileName(MultipartFileData fileData)
    {
        return fileData.Headers.ContentDisposition.FileName;
    }
}
Here is an example of how your strongly typed model should look like in case you need to send some JSON data to the WebAPI together with the file:

public class UploadDataModel
{
    public string testString1 { get; set; }
    public string testString2 { get; set; }
}
After each file is uploaded, you might want to move it to a different location, give it back the original file name, add a timestamp and a few random alphanumeric characters to it, etc... This is a very basic example which will help you implement things like upload aborting, progress and file validation. You might also want to wrap the whole angular-file-upload directive into your own directive so you could use any additional added/customized functionality across your project whenever you need it in a clean and easy way.

Happy uploading! :)
Rated 3.11, 140 vote(s). 
martinmcd
Tihomir, Thanks for your reply. I think your suggestion is fair enough and should be straightforward to get working.
Thanks
By Vadim
Thanks very much for this post. Its very usefull.
By Troy
I can't seem to get this working. It seems that the await is deadlocking threads. Basically, the await never returns. I've read a lot on SO and other sites about using await in asp.net and have tried adding .ConfigureAwait(false), but that doesn't help. Any help you could provide would be greatly appreciated. This was a great blog post and exactly what I was looking for, but I just can't get past this one issue.
tihomir-kit
Hi Troy,

I'm sorry but I don't know why you're having these problems.. Have you enabled the right MIME types for upload on the server?
By Troy
I actually (after much searching) discovered that a custom ActionInvoker we were using to capture exceptions was using an async call that was deadlocking the await in my controller. Once I removed that, it works like a charm!! Thanks again for a great code example and for the quick response!
tihomir-kit
Great news, thanks for sharing the info! :)
Greetings and thanks a bunch for your examples :) It has helped me a big deal....

The only thing that i just cannot figure out it...

HOW... can i get a normal Stream object out of this ? trying to do this but it seems i dont understand it well enough :

private Stream GetStreamFromMultiPartFile(MultipartFormDataStreamProvider result)
{
return result.GetStream(result.FileData.<something-i-dont-understand>);
}
tihomir-kit
Hi, haven't tried that out (and not sure if you can tbh), but since the file already gets stored to the server, why not just fetch it from there?

Chck these out, perhaps it will help:
http://stackoverflow.com/questions/15842496/is-it-possible-to-override-multipartformdatastreamprovider-so-that-is-doesnt-sa
http://msdn.microsoft.com/en-us/library/hh995209(v=vs.118).aspx
Thanks very much for this helpful post, i have successfully implemented this but i wonder:
- how about the list of file and status of each file and responsive progress bar (like this: http://blueimp.github.io/jQuery-File-Upload/angularjs.html)
- the uploaded files are not saved with file extension, how do you address that
Just wanna know if you have implemented these issues, otherwise i'll have to do it ;)
tihomir-kit
Hi Austin,

1) yes, I managed to implement that as well but that part of the code was overly integrated with our project and it depended on some of our other functionality too much so I unfortunately wasn't able to share it and didn't have the time to rewrite it all again. You'll have to implement that part on your own.

2) Look at the upload location as a temporary location. After you got the files there, move them to a more permanent location. You have the original file name in "originalFileName" so simply move and rename the file to what it was originally (perhaps also append a Guid or a TimeStamp in front of the original file name to avoid a situation of two files having the same name and creating a collision).
1 2 3 4 5 ... >>