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.25, 130 vote(s). 
tihomir-kit
Hi Martin,

no, sorry. But all the code you need to make it work is in the post. It's not difficult to implement. I might perhaps make a working example sometime in the future.
By Martin
ok, I'll give it another try! Did you deploy all files on one instance of IIS or how does your environment look like?
Right now I cannot really figure out where the files are stored...?
Martin
tihomir-kit
In the example from the post, they should be stored in "/App_Data/Tmp/FileUploads" (did you replace {tilde} with the actual tilde symbol "~"?). That is defined in the GetMultipartProvider() method from the example code.

Yes, only a single instance of IIS was used here, nothing distributed. Perhaps you need to give IIS the write permissions for that folder and that is why it doesn't work?

Put a breakpoint to the following line:
var originalFileName = GetDeserializedFileName(result.FileData.First());

and attach to the process and check what you have there.
By Raf
great post,

seems that a few things are missing. for example in the controller code, i don't see $scope.onFilesSelect = function ($files) {, so it appears to want to loop over $files on load..

also, i can't get $scope.onFilesSelect to trigger. i know my controller is connected because i can visually bind to it's properties, but selecting files via the upload button, is not doing anything (definitely not triggering the $scope.onFilesSelect
tihomir-kit
Hi RAF, yes, you are correct. I somehow missed that crucial part while I was writing the post. I fixed the controller code, thanks.

Regarding triggering the $scope.onFileSelect method - perhaps you have different names in markup and in controller? Its on "File" select, not "Files". Perhaps that fixes the problem you're having?
Great Article. Do you happen to have a working demo application based on this article?
tihomir-kit
Hi Sonny,

no, I don't have this as a demo at the moment, sorry. But it should be fairly easy to implement, give it a try. :)
By Ben
I'm having the same issue as Dale, using the javascript and controller exactly as per the blog entry ASP.NET can't find the action if it contains the string parameter. I wonder if I've changed my routes and not remembered. What's your routing configuration? I've got
config.Routes.MapHttpRoute(
name: "ApiById",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"^[0-9]+$" });

config.Routes.MapHttpRoute(
name: "ApiByAction",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { action = "Get", id = RouteParameter.Optional });
By Pascal
Hi

I'm having the same issue as ben. My routes are configured pretty much the same as ben .All my other routes on the controller work except the file upload. I have included the [AcceptsVerbs("Upload")] and [HttpPost] annotations on my method.

Any ideas?
tihomir-kit
Hi guys,

in the end I didn't have the need for the string parameter in the project where I implemented this but I remember that I had it working at one point while I was trying the upload directive out and that I was able to pass the string to my async action method without any problems. I looked at my router now, and there is only the default route there, nothing more.

I will try to recreate a fully working example for the weekend and I'll get back to you then.

PS - just to doublecheck - is your "fileUploadObj" actually a string or is it a JSON object?
1 2 3 4 5 ... >>