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 Eric
Can you add the code for the GetFileName method?
tihomir-kit
@Eric - Sure, I fixed the code. I must have overlooked that one when I was cleaning up the code from some other stuff.
Great Post but I can't get it to work. The line "var originalFileName = GetDeserializedFileName(result.FileData.First());" throws an error "sequence contains no elements".
Also, I had to remove "string fileUploadObj" from the definition as it caused an error about not finding an action (didn't write it down).
Any ideas about what might be going wrong?
Thanks
tihomir-kit
Hi Dale,

yes, if you removed or commented out this line: "data: { fileUploadObj: $scope.fileUploadObj }," on the client side, then you also need to remove "string fileUploadObj" since no additional data is sent to the server. The error you're getting most probably means that the result.FileData is an empty collection at that time. If you put a breakpoint on that line, what do you get if you enter "result" or "result.FileData" into the immediate window? My guess is that file actually didn't upload for some reason. Did you check the "/App_Data/Tmp/FileUploads" folder, is there anything inside? Did you replace the (tilde) with the real tilde symbol in GetMultipartProvider method?
Thanks for the speedy reply. I realized my problem - I was not looping through $files on the client, just returning $files as is. So that's one problem resolved.

I still cannot get it to accept fileUploadObj though. It returns:
Message: "No HTTP resource was found that matches the reques…URI 'http://localhost:54686/breeze/files/Upload'.", MessageDetail: "No action was found on the controller 'Files' that matches the request."

If I try to pass it with a URL /breeze/files/Upload/fileUploadObj it also fails.
Removing string fileUploadObj from the Upload() definition (not on the client, on the server) works, but I would like to have access to this data.
Is the data accessible in Request.Content? Or do I need to add this to the routing file on the client?

Thanks again for the help. I'm really close to finishing this project and uploading pictures is the last hurdle.
Dale
Just a quick follow up.
In the Upload() function I can do:
var fileUploadObj = provider.FormData.GetValues("fileUploadObj");
to get the data that I need without having it in the Upload() definition.

tihomir-kit
Hi Dale, sorry for a late reply this time.. What type of object are you trying to pass to your WebAPI (backend) controller action method? How does your fileUploadObj look like when inspected in chrome devtools/firebug? If you didn't, you need to create a Model object (which has all the properties that the fileUploadObj has) on the server-side which is expected from the client call in your Upload action definition. From what you wrote in your latest reply, it seems like perhaps you didn't create that model? You will also perhaps need to use JSON.stringify() on your FormData values like this JSON.stringify(provider.FormData.GetValues("fileUploadObj")) but I'm not sure about that one, so try it out.
thank you again for your insights. I was trying to pass a string, like your example, but I will inspect it as you suggest. I have a feeling you might be right about that. I'm not a javacript or c# guy so it is very likely I have passed an object without realizing it.
Its a great article Tihomir Kit..... I read each and every line ..it gave me a lot of concepts in angularjs and offcourse the use of async-await is good to understand. I am working on a app (angularjs and MVC4), it has the same functioanlities as any mail app has like gmail,outlook. so my point to ask here is .. I have to implement the functionality like compose with attachments

My Requirement: A user will attach the multiple files to upload with compose mail data (To:abc@domain.com,Subject:DemoSubject,Body:blah...blah) and when the user click on Send Button then the files starts uploading...but here one point to discuss is.. i need all attachments with compose mail data in the apiController so that i can pass it to the third party mail api service (Exchange Server) to send the mail..

In this excellent post i saw you are uploading attachments...that is very good approach.. but i have to send all the files at once with mail data to api controller and then pass whole data to third party Server.

Here is my apicontroller Code:

[HttpPost]
public async Task<HttpResponseMessage> ComposeMail()
{

var session = HttpContext.Current.Session;
if (session != null)
{
if (session["ServiceObject"] == null)
return null;
}
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var root = HttpContext.Current.Server.MapPath("/App_Data/Temp/FileUploads");
Directory.CreateDirectory(root);
var provider = new MultipartFormDataStreamProvider(root);

var result = await Request.Content.ReadAsMultipartAsync(provider);
if (result.FormData["user"] == null)
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
//get the files
AttachmentModel fi = new AttachmentModel();
foreach (var file in result.FileData)
{
//I am stuck here how to get attachment files
//as these attachments are available as Multipart data
//but i need attachments to transfer those files to the
//third party api
}

emailService.SendtoThirdParty(emailMessage);//This method to send data with attachments
return Request.CreateResponse(HttpStatusCode.OK, "success!");
}


AngularJs Code:

$scope.SendMail = function () {
$http({
method: 'POST',
url: "/api/Emails/ComposeMail",
headers: { 'Content-Type': undefined },
transformRequest: function (data) {
var formData = new FormData();

formData.append("user", angular.toJson($scope.user));

for (var i = 0; i < data.files.length; i++) {
formData.append("file" + i, data.files[i]);
}
return formData;
},
data: { user: $scope.user, files: $scope.files }
}).
success(function (data, status, headers, config) {
alert("success!");
}).
error(function (data, status, headers, config) {
alert("failed!!!!");
});
}
By Martin
Looks nice! Is there a complete working sample available?
/M
1 2 3 4 5 ... >>