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). 
By Ben
I've sent several different things as the "data", a single string, an object such as you've got in the code and a nested object i.e. { fileUploadObj: {...} }. I managed to get it to work by removing the parameter and just getting the values from the post body.

I didn't really like this solution because I want to store the data in a DB and thought taking the stream, saving it to a file and then reading it again was wasteful. I searched some more and and found this http://www.codeproject.com/Tips/652633/ASP-NET-WebApi-MultipartDataMediaFormatter. If you use this formatter with the comment that someone else put at the bottom you can pass the whole form post into the method as a parameter and get the byte[] without first saving the file to disk and also have the "data" passed in as a strongly typed model.

I hope this helps. Now I just need to get progress bars working and styled up.
tihomir-kit
Ben, thnx for the update.

If I remember correctly, in this line:
data: { fileUploadObj: $scope.fileUploadObj },

this part:
$scope.fileUploadObj

is what needs to be a string. So something like this:
data: { fileUploadObj: "My string" },

should work, whereas something like this shouldn't (for the particular example from the post):
data: "My string",

and the name of the variable in the param of the action should be "fileUploadObj".
By Ben
Yeah I tried that without success. I also tried using the [FromBody] attribute on the parameter but that didn't help either.

Using the formatter makes for much cleaner code. The files and data are cleanly bound up and just passed into the controller.
By Pascal
I have tried a few permutations but none of them seem to work. It works fine if I remove the data, but the moment I try and pass data into the post I get a 404 Not Found.
By Pascal
Another question, as I am not use to uploading files using async Task<HttpResponseMessage> Upload()
(obviously new to the webAPI) how do you know when the file upload is completed and how to move and rename the file. I am seeing the file in the folder but it looks something like this BodyPart_87ec6c9b-e031-41de-bd46-82b3d194b6ee
tihomir-kit
the file is done uploading after the "await" line. the application gives a unique name for each uploaded file to make sure there is no collision with other uploads. these unique names look something like "BodyPart_87ec6c9b-e031-41de-bd46-82b3d194b6ee" which you can then move and rename (to whatever you want + original extension) again.
tihomir-kit
Hi guys, I created and uploaded a fully working demo app to github: https://github.com/pootzko/angular-net-async-upload

Indeed I could not find a way to receive the data through Upload() params so I extracted and deserialized JSON data from Request FormData and then bound it to a strongly typed model.

I also updated the post to reflect the newer, improved code, hope it helps. :)

Cheers!
Hi, Tihomir - great article!

I was struggling with file upload from angular to WebAPI and then your post came :)

However, additional data didn't work for me, I got exception: Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: . Path '', line 0, position 0.

Had to pass it with angular.toJson() this way:

$upload.upload({
url: serviceBaseUrl + "/upload", // webapi url
method: "POST",
data: { fileUploadObj: angular.toJson(fileUploadObj) },
file: file,
})
By Pascal
Can you intercept and send us a copy of the json being parsed?
Every now and then I have found issues with json deserialisation.
By Ben
How do I un-subscribe from comments again? I've managed to subscribe two different emails to new comments from this discussion.
1 2 3 4 5 ... >>