In my previous post regarding an Angular file upload directive, I mentioned that I would also show how to upload to a WebAPI endpoint from that directive. Here we go..
Diving right in, the directive maps all of the files to the object that we specified. In the previous case, I passed in “vm.files,” and there was an upload button whose click event referenced “vm.uploadFiles.” This method is the one we need to implement.
The interesting bits are below. The basic premise is that we create a “FormData” object and attach the file objects that were pushed into vm.files by the directive. This will become the POST data that we send to WebAPI.
uploadFiles = function () { var start = new Date().getTime(); vm.isUploading = true; var formData = new FormData(); angular.forEach(vm.files, function (file) { formData.append(file.name, file); });
After we have the FormData object, we will use a REST service, that we have defined via $resource, to POST the form data. It has an upload method that performs a simple POST.
The try/catch/finally pattern is used here. This is a nice pattern as we can perform clean-up of the process regardless of success or failure indicating that uploading has completed. Another interesting aspect here is that we will return a JSON object from WebAPI that can indicate whatever is pertinent such as maybe the files were in a bad format. I find this a bit better than performing a simple throw from the server.
loadService.upload(formData) .then( function (result) { if (result && result.data) { result.data.forEach(function (parsedResult) { vm.isError = parsedResult.isError; if (!vm.isError) { // Do something with the results } }); if (vm.isError) { // .. there is something wrong with our files } } return result.$promise; }, function (result) { var message = "Server error occurred."; if (result.data && result.data.message) { message = result.data.message; } toastService.error(message); return $q.reject(result); }) .finally(function () { vm.isUploading = false; var end = new Date().getTime(); vm.executionTime = end - start; $log.log('Execute time: ' + vm.executionTime); }) };
The “loadService” is defined below. The salient points here are that we tell Angular not to muck with our POST data by using “transformRequest: angular.identity.”
(function () { var loadService = function ($resource) { var loadProxy = $resource('/api/fileUpload', { id: '@id' }, { upload: { url: '/api/fileUpload', method: 'POST', transformRequest: angular.identity, headers: { 'Content-Type': undefined } } }), upload = function (formData) { return loadProxy.upload(formData).$promise; }; return { upload: upload, }; }; loadService.$inject = ['$resource']; angular.module('long2know.services') .factory('loadService', loadService); })()
On the server side, the WebAPI controller will receive, and expect, multipart form content.
public async Task<IHttpActionResult> Post() { var list = new List<ParseResult>(); // Check if the request contains multipart/form-data. if (!Request.Content.IsMimeMultipartContent("form-data")) { return BadRequest("Unsupported media type"); } try { var task = Add(Request); list.Add(task.Result); return Ok(new { Data = list, FileNames = list.Select(x => x.FileName).ToList() }); } catch (Exception ex) { return BadRequest(ex.GetBaseException().Message); } }
Notice that I call an “Add” method to create a list of Tasks to process the HttpRequestMessages. This is necessary as you’ll see below because WebAPI actually has a bug that can cause the Request to never finish processing. Wrapping the processing of the request into a separate thread/Task eliminates this bug, though. I won’t go into more specifics as to why this occurs, but the Googles can provide more info.
The “Add” method is what actually parses all the files. I have a simple ParseResult which contains an “isError” property and a “FileName” property. As I mentioned above, a Task is created to parse the Request content. Using “TaskCreationOptions.LongRunning” ensures that the Task runs on its own thread to avoid the bug I mentioned above. Yes, this bug did bite me, but it was good to find a work-around. When the Task finishes, the Contents can be iterated over (the “parts,” or files, are HttpContent objects). WebAPI provides the “ReadAsStreamAsync” which allows us to read the actual binary content for each file. In the “Add” example method below, you can see that I’m only reading the content and then discarding it.
public async Task<ParseResult> Add(HttpRequestMessage request) { ParseResult result = null; var taskResult = Task.Factory .StartNew(() => Request.Content.ReadAsMultipartAsync() .ContinueWith(t => { if (t.IsFaulted || t.IsCanceled) { throw new HttpResponseException(HttpStatusCode.InternalServerError); } return t.Result; }).Result, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result; var bodyparts = taskResult.Contents; foreach (var part in bodyparts) { var fileName = part.Headers.ContentDisposition.FileName.Replace("\"", string.Empty); var mediaType = part.Headers.ContentType.MediaType; var stream = part.ReadAsStreamAsync().Result; var validationResult = true; // validate the data result = new ParseResult() { FileName = fileName, IsError = validationResult }; } return result; }
That’s really it. With these client and server-side methods, and the previous directive, you’ll have a working upload mechanism in your Angular 1.x app. I don’t have a fully working VS2015/2017 project in git to show all of the pieces working, but don’t mind putting one together if there is interest.