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.15, 146 vote(s). 
Ok here is what throws exception:
xRatio=2.22&yRatio=2.2222222222222223&top=0&left=0&width=450&height=300

And here after using angular.toJson()
{"xRatio":2.22,"yRatio":2.2222222222222223,"top":52,"left":70,"width":327,"height":230}

...Wait, I just remember doing some stuff...
$httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

I was using that for logging in, or registering user. Probably that's why my code wasn't working :)
tihomir-kit
Hi Ben,

you can't do that on your own, but please send me the emails you wish to be removed to tihomir@mono.hr and we'll remove you manually.
Hi Dale,

Thanks for this awesome blog. You code works fine in Chrome, but breaks in IE9. I tried adding shims as indicated by Angular-file-upload but that didn't help either.
By Jon
Thanks for the article, it works really well. I have a couple of questions though....

1) Why are you looping through all the files and uploading them individually? Isn't that what uploadAll() is for?

2) Would you mind pointing me in the direction of how to send the progress of each file back at intervals?

Thanks again, Jon.
tihomir-kit
Hi Jon,

1) I simply had a more complex scenario from this basic example and uploading them individually gave me more control over what's going on.

2) Hm, what do you mean intervals? On each chunk being uploaded? You should have the proper percentage on client-side in "progress()" callback if that's what you're asking for? (evt.loaded and evt.total)
Thanks Tihomir,

Maybe I just don't understand what the asp.net code is doing (I'm not a .net coder, I'm a JS/Angular guy), but I couldn't see where in the code it was sending anything back to indicate the progress. We tried running the code here and uploaded a small file and it just sent "ReturnTest" back when it's finished.

If you're saying that your code already has everything that is needed to receive progress updates as large files are uploaded then I'll give it another go :)
tihomir-kit
Hi,

yes well, the "ReturnTest" is what you get in .success() callback. I put it there only to show how it can be done in case someone needs that. The percentage which you want is in the .progress() callback and it is a feature coming from the "angular-file-upload" directive so yes, the functionality is there.

Press F12 in your browser once you load your page and open the JS console. You should be able to see the percentages printed out there. Now all you need to do is to output that data to the markup instead of console (put the value into a scoped variable and bind it somewhere in your html template).
So, basically I completely misunderstood how file uploads work. I thought the server had to send messages back to the client with progress updates. Actually, the client knows how much has been uploaded so evaluating progress all happens in the Javascript.

Thanks for the help and apologies for the dumb question.
martinmcd
Hi, thanks for this really helpful example. I have a question related to the fileUploadObj. I am trying to pass a folder name with the files so they are saved in a unique location each time. I can get this data successfully but I am struggling to extract it before the file is saved. It seems that the FormData is extracted after 'result' is assigned and the file written. Is there a way to do this so I can specify the location - or am I missing something simple?
Thanks.
tihomir-kit
Hi, in my opinion, the original upload location should stay as it is. Look at it as a temporary upload location (it even has Guids as file names).. After you're done with uploading, you can simply move the files to a permanent location based on the folder name you provided through the form data. Since guids are used for these temporary files, download links would be hard to guess and they wold be unique for each new file so there is no need to worry about file name collision or someone else snooping around.

Personally, I haven't tried what you described and if it's proving to be troublesome, just move the files after the upload, that's how I used it..
1 2 3 4 5 ... >>