Sometimes you need to upload files to your server from an Angular application to a Web API method. There are many ways to upload a file. In this article, I'm going to present a method that works well for small files, up to about one or two megabytes in size. You're going to build two projects; a .NET Core Web API project and an Angular project. You'll build these two projects from scratch using the Angular CLI, .NET Core, and the Visual Studio Code editor.
The result from this article is a page (as shown in Figure 1) that allows you to select one or more small files using an <input type="file">
element. You then build a custom FileToUpload
object with attributes about the file, plus the file contents. Finally, this FileToUpload
object is sent via a Web API call to a method on the server. Once the server has this file, you may choose to save it as a file on the server, into a database table, or any other location.
Build a .NET Core Web API
Let's start by building the .NET Core Web API application to which you upload files. Open an instance of Visual Studio Code. From the menu, select View > Terminal
to display a terminal window at the bottom of the editor. You should see something that looks like this:
Windows PowerShell
Copyright (C) Microsoft Corporation.
All rights reserved.
XX C:\Users\YOUR_LOGIN>
I'm sure you have a directory somewhere on your hard drive in which you create your projects. Navigate to that folder from within the terminal window. For example, I'm going to go to my D
drive and then to the \Samples
folder on that drive. Enter the following commands to create a .NET Core Web API project.
d:
cd Samples
mkdir FileUploadSample
cd FileUploadSample
mkdir FileUploadWebApi
cd FileUploadWebApi
dotnet new webapi
Open the Web API Folder
Now that you've built the Web API project, you need to add it to Visual Studio Code. Select File > Open Folder...
and open the folder where you just created the FileUploadWebApi
project (Figure 2). In my case, this is from the folder D:\Samples\FileUploadSample\FileUploadWebApi
. Once you've navigated to this folder, click the Select Folder
button.
Load Required Assets
After loading the folder, VS Code may download and install some packages. After a few more seconds, you should see a new prompt appear (Figure 3) in VS Code saying that some required assets are missing. Click on the Yes
button to add these assets to this project.
Enable Cors
The Web API project is going to run on the address localhost:5000 by default. However, when you create a new Angular application, it will run on localhost:4200 by default. This means that each Web application is running on a separate domain from each other. For your Angular application to call the Web API methods, you must tell the Web API that you're allowing Cross-Origin Resource Sharing (CORS). To use CORS, add the Microsoft.AspNetCore.Cors
package to your project. Go back to the terminal window and type the following command:
dotnet add package Microsoft.AspNetCore.Cors
After the package is installed, inform ASP.NET that you're using the services of Cors. Open the Startup.cs
file and locate the ConfigureServices()
method. Call the AddCors()
method on the services
object as shown in the following code snippet:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
Scroll down within the Startup.cs
file and locate the Configure()
method. Listing 1 shows you the code to add to allow the Web API to accept requests only from localhost:4200. Allow all HTTP
methods such as GET, PUT, etc., by calling the method named AllowAnyMethod()
. Allow any HTTP headers within the Web API call by invoking the method AllowAnyHeader()
. Call the app.UseCors()
method prior to calling the app.UseMvc()
method.
Listing 1: Call UseCors() from the Configure() method in the Startup.cs file.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseCors(options => options.WithOrigins("http://localhost:4200").AllowAnyMethod().AllowAnyHeader());
app.UseHttpsRedirection();
app.UseMvc();
}
Try It Out
It's a good idea to test out your Web API project and ensure that it accepts requests as expected. Select Debug > Start Debugging
from the Code menu to build the .NET Core Web API project and launch a browser. The browser will come up with a blank page. Type the following into the browser address bar:
http://localhost:5000/api/values
Press the Enter key to submit the request and you should see a string that looks like the following:
["value1","value2"]
Add a FileToUpload Class
A file you upload from your local hard drive has attributes that you should transmit to the Web service. These attributes are things like the file name, the file type, the size of the file, etc. You're going to also add some additional properties to hold the file contents as a Base64 encoded string and a byte array. Create a \Models
folder in the root of your Web API project. Add a new file named FileToUpload.cs
into this folder. Add the code shown in the following code snippet to this file:
using System;
public class FileToUpload
{
public string FileName { get; set; }
public string FileSize { get; set; }
public string FileType { get; set; }
public long LastModifiedTime { get; set; }
public DateTime LastModifiedDate { get; set; }
public string FileAsBase64 { get; set; }
public byte[] FileAsByteArray { get; set; }
}
Add a FileUpload Controller
Obviously, you need a controller class to which you send the file. Delete the ValuesController.cs
file from the \Controllers
folder. Add a file named FileUploadController.cs
into the \Controllers
folder. Add the code shown below to this file:
using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;
[Route("api/[controller]")]
public class FileUploadController : Controller
{
const string FILE_PATH = @"D:\Samples\";
[HttpPost]
public IActionResult Post([FromBody]FileToUpload theFile)
{
return Ok();
}
}
The above code creates the route for the FileUploadController, sets up a constant with the path to a folder on your local hard drive that your Web project has permissions to write to, and sets up the Post()
method to which you send an object of the type FileToUpload.
Instead of the hard-coded constant for the file path to write the uploaded file to, you should take advantage of the ASP.NET Core configuration system to store the path. For this article, I wanted to keep the code simple, and used a constant.
Create a Unique File Name
Start creating the code within the Post()
method now. Just above the return Ok();
line, define a new variable named filePathName
. Create the full path and file name to store the uploaded file into. Use the FILE_PATH constant, followed by the FileName
property, without the file extension, from the file object uploaded. To provide some uniqueness to the file name, add on the current date and time. Remove any characters that aren't valid for a file by using the Replace()
method. Finish the file name by adding on the file extension from the file object uploaded.
var filePathName = FILE_PATH + Path.GetFileNameWithoutExtension(theFile.FileName) + "-" +
DateTime.Now.ToString().Replace("/", "").Replace(":", "").Replace(" ", "") +
Path.GetExtension(theFile.FileName);
If you're going to have multiple users uploading files at the same time, you might also want to add a session ID, a GUID, or the user's name to the file name so you won't get any name collisions.
Remove File Type
In the Angular code you're going to write, you're going to be reading the file from the file system using the FileReader class. This class is only available on newer browsers. The FileReader class reads the file from disk as a Base64-encoded string. At the beginning of this Base64-encoded string is the type of file read from the disk followed by a comma. The rest of the file contents are after the comma. Next is a sample of what the contents of the file uploaded look like.
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAg..."
Write the following code to strip off the file type. Check to ensure that the file type is there so you don't get an error by passing a negative number to the Substring()
method.
if (theFile.FileAsBase64.Contains(","))
{
theFile.FileAsBase64 = theFile.FileAsBase64.Substring(theFile.FileAsBase64.IndexOf(",") + 1);
}
Convert to Binary
Don't store the file uploaded as a Base64-encoded string. You want the file to be useable on the server just like it was on the user's hard drive. Convert the file data into a byte array using the FromBase64String()
method on the .NET Convert class. Store the results of calling this method in to the FileAsByteArray
property on the FileToUpload
object.
theFile.FileAsByteArray = Convert.FromBase64String(theFile.FileAsBase64);
Write to a File
You're finally ready to write the file to disk on your server. Create a new FileStream
object and pass the byte array to the Write()
method of this method. Pass in a zero as the second parameter and the length of the byte array as the third parameter so the complete file is written to disk.
using (var fs = new FileStream(filePathName, FileMode.CreateNew))
{
fs.Write(theFile.FileAsByteArray, 0, theFile.FileAsByteArray.Length);
}
I've purposefully left out any error handling, but you should add some into this method. I'll leave that as an exercise for you to do. Let's move on and create an Angular project to upload files to this Web API.
Build an Angular Upload Project
Build an Angular project from within VS Code by opening the terminal window. Navigate to the folder you created at the beginning of this article. For example, I navigate to the folder D:\Samples\FileUploadSample
. Enter the following Angular CLI command in the terminal window to create a new Angular application:
ng new FileUploadAngular
Add Web API Project to Workspace
Once the project is created, add the newly created folder named FileUploadAngular to VS Code. Select the File > Add Folder to Workspace...
menu item, as shown in Figure 4.
Choose the FileUploadAngular
folder as shown in Figure 5 and click the Add button.
You should now see two projects within VS Code, as shown in Figure 6.
Save the Workspace
Click File > Save Workspace As...
and give it the name FileUploadSampleApp
. Click the Save button to store this new workspace file on disk. From now on, you may always open this application by double-clicking on the FileUploadSampleApp.code-workspace
file.
Create FileToUpload Class
Just as you created a FileToUpload
class in C# to hold the various attributes about a file, create the same class in Angular. Go back into the terminal window and navigate to the FileUploadAngular
folder, as shown in Figure 7.
Use the Angular CLI command ng g cl
to create a new class that represents a file to upload. You can create a folder named file-upload
at the same time you create the FileToUpload
class. The FileToUpload
class will be created in this new folder.
ng g cl file-upload/fileToUpload
Open the newly generated file-to-upload.ts
file and add the code shown next. Notice that this class has the same property names as the C# class you created earlier. Using the same property names allows you to post this object to the Web API and the data contained in each matching property name is automatically mapped to the properties in the C# class.
export class FileToUpload {
fileName: string = "";
fileSize: number = 0;
fileType: string = "";
lastModifiedTime: number = 0;
lastModifiedDate: Date = null;
fileAsBase64: string = "";
}
Create File Upload Service
As with any Angular application, separate the logic that communicates with a Web API into an Angular service class. You can create a new service using the Angular CLI command ng g s
. In the Integrated Terminal window, enter the following command to create a FileUploadService
class:
ng g s file-upload/fileUpload -m app.module
Open the newly generated file-upload.service.ts
file and add the import statements in the next snippet. The HttpClient
and HttpHeaders
classes that are imported are used to send an HTTP request. The Observable
class from the RxJS
library is commonly used as a return value from service calls. The FileToUpload
class is needed so you can pass the data about the file to upload to the Web API.
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { FileToUpload } from './file-to-upload';
Add two constants just below the import statements. The first constant is the path to the FileUpload
controller that you created earlier in this article. The second constant is a header used to post JSON data to the Web API.
const API_URL = "http://localhost:5000/api/FileUpload/";
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
The HttpClient
class needs to be injected into this service class to post the file information to the Web API method. Modify the constructor of the FileUploadService
class to tell the Dependency Injection system of Angular to pass in an instance of the HttpClient
class.
constructor(private http: HttpClient) { }
Add a method named uploadFile()
to which you pass an instance of the FileToUpload
class. This class is built using properties from a File
object retrieved from the user via an <input type="file">
element. Call the post()
method on the HttpClient
class passing in the URL where the controller is located, the file object to post and the HTTP header constant.
uploadFile(theFile: FileToUpload) : Observable<any> {
return this.http.post<FileToUpload>(API_URL, theFile, httpOptions);
}
File Upload Component
It's now time to build the HTML page that prompts the user for a file from their local drive that they wish to upload to the Web server. Create an Angular component using the Angular CLI command ng g c
. This command builds a .css
, .html
, and .ts
file from which you build the file upload page. In the terminal window, enter the following command:
ng g c fileUpload
Open the file-upload.component.ts
file and add two import statements for the FileUploadService
and the FileToUpload
class.
import { FileUploadService } from './file-upload.service';
import { FileToUpload } from './file-to-upload';
You should limit the maximum size of file that a user may attempt to upload. The files are going to be Base64 encoded prior to sending. When you Base64-encode something, it grows larger than the original size. So it's best to limit the size of the file to upload. Add a constant just below the import statements and set a limit of one megabyte. Feel free to play with different file sizes to see what works with your situation.
// Maximum file size allowed to be uploaded = 1MB
const MAX_SIZE: number = 1048576;
You need to add two public properties to FileUploadComponent
class. The first property, theFile
, holds an instance of a file object returned from the file upload object. There's no equivalent of the HTML file object in Angular, so you must use the any
data type. The second property, messages
, is used to display one or more messages to the user.
theFile: any = null;
messages: string[] = [];
The FileUploadComponent
class creates an instance of a FileToUpload
class from the information contained in the theFile
property. It then passes this object to the FileUploadService
to upload the file to the server. Inject the FileUploadService
into the constructor of the FileUploadComponent
.
constructor(private uploadService: FileUploadService) { }
Get File Information from Change Event
When the user selects a file from their file system, the change event is fired on the file input element. You're going to respond to this change event and call a method named onFileChange()
in your component. Write this code as shown here:
onFileChange(event) {
this.theFile = null;
if (event.target.files && event.target.files.length > 0) {
// Don't allow file sizes over 1MB
if (event.target.files[0].size < MAX_SIZE) {
// Set theFile property
this.theFile = event.target.files[0];
}
else {
// Display error message
this.messages.push("File: " + event.target.files[0].name + " is too large to upload.");
}
}
}
In the onFileChange()
method an argument, named event
, is passed in by the file input element. You check the target.files
property of this argument to see if the user selected a file. If a file is selected, check the size
property to make sure it's less than the size you placed into the MAX_SIZE
constant. If the file meets the size requirement, you retrieve the first element in the files array and assign it to the theFile
property. If the file exceeds the size requirement, push a message onto the messages array to inform the user of the file name that's in error.
Read and Upload File
Add a private method named readAndUploadFile()
to the FileUploadComponent
class as shown in Listing 2. Pass the theFile
property to this method. Although you could simply access theFile
property from this method, later in this article, you're going to need to pass a file object to this method to support multiple file uploads. Create a new instance of a FileToUpload
class and set the properties from the file object retrieved from the file input element.
Listing 2: Read a file using the FileReader class
private readAndUploadFile(theFile: any) {
let file = new FileToUpload();
// Set File Information
file.fileName = theFile.name;
file.fileSize = theFile.size;
file.fileType = theFile.type;
file.lastModifiedTime = theFile.lastModified;
file.lastModifiedDate = theFile.lastModifiedDate;
// Use FileReader() object to get file to upload
// NOTE: FileReader only works with newer browsers
let reader = new FileReader();
// Setup onload event for reader
reader.onload = () => {
// Store base64 encoded representation of file
file.fileAsBase64 = reader.result.toString();
// POST to server
this.uploadService.uploadFile(file).subscribe(resp => {
this.messages.push("Upload complete"); });
}
// Read the file
reader.readAsDataURL(theFile);
}
Use the FileReader
object from HTML 5 to read the file from disk into memory. Note that this FileReader
object only works with modern browsers. Create a new instance of a FileReader
class, and setup an onload()
event that's called after the file has been loaded by the readAsDataUrl()
method.
The readAsDataUrl()
reads the contents and returns the contents as a Base64 encoded string. Within the onload()
event, you get the file contents in the result property of the reader. Place the contents into the fileAsBase64
property of the FileToUpload
object. Call the uploadFile()
method on the FileUploadService
class, passing in this FileToUpload
object. Upon successfully uploading the file, push the message “Upload Complete” onto the messages array to have it displayed to the user. The readAndUploadFile()
method is called in response to the Upload File
button's click
event.
uploadFile(): void {
this.readAndUploadFile(this.theFile);
}
The File Upload HTML
Open the file-upload.component.html
file and delete all of the HTML within that file. Add the HTML shown in Listing 3 to create the page shown back in Figure 1. In the <input type="file" ...>
element, you see that the change event is mapped to the onFileChange()
method you wrote earlier. The Upload File button is disabled until the theFile
property is not null
. When this button is clicked on, the uploadFile()
method is called to start the upload process.
Listing 3: Use an input type of file to allow the user to select a file to upload
<h1>File Upload</h1>
<label>Select a File (< 1MB)</label><br/>
<input type="file" (change)="onFileChange($event)" /><br/>
<button (click)="uploadFile()" [disabled]="!theFile">Upload File</button><br/><br/>
<!-- ** BEGIN: INFORMATION MESSAGE AREA ** -->
<div *ngIf="messages.length > 0">
<span *ngFor="let msg of messages">
{{msg}}
<br />
</span>
</div>
<!-- ** END: INFORMATION MESSAGE AREA ** -->
After the input and button elements, you see another <div>
element that's only displayed when there are messages within the messages array. Each message is displayed within a <span>
element. These messages can display error or success messages to the user.
Modify App Module
Because you're using the HttpClient
class in your FileUploadService
class, you need to inform Angular of your intention to use this class. Open the app.module.ts
file and add a reference to the HttpClientModule
to the imports
property. After typing the comma, and HttpClientModule
, hit the Tab key, or use the light bulb in VS Code, to add the appropriate import statement to the app.module.ts
file.
imports: [BrowserModule, HttpClientModule],
Modify App Component HTML
The app.component.html
file is the first file displayed from the index.html
page. Angular generates some default HTML into this file. Delete all the code within this file and add the code shown below to display your file upload component.
<app-file-upload></app-file-upload>
Try it Out
Start the Web API project, if it's not already running, by selecting Debug > Start Debugging
from the VS Code menu. Open a terminal window in the FileUploadAngular
folder and start your Angular application using the Angular CLI command npm start
, as shown in the next snippet:
npm start
Open your browser and enter localhost:4200 into the address bar. You should see a Web page that looks like Figure 1. Click on the Choose File
button and select a file that's less than one megabyte in size. After you select a file, the name of that file appears to the right of this button, and the Upload File
button becomes enabled. Click the Upload File
button and after just a second or two, an “Upload Complete” message should be displayed. Check the folder where you specified to write the files and you should see the file name you selected followed by today's date. Open the file and ensure that all the file contents were written correctly.
Upload Multiple Files
The HTML file input type can upload multiple files by adding the multiple attribute to the input element. Each file to upload must be under one megabyte in size, but you may select as many files as you wish. Open the file-upload.component.html
file and modify the HTML shown in bold below:
<label>Select a File(s) (< 1MB)</label><br/>
<input type="file" (change)="onFileChange($event)" multiple="multiple" /><br/>
<button (click)="uploadFile()" [disabled]="!theFiles.length">Upload {{theFiles.length}} File(s)</button>
Open the file-upload.component.ts
file and locate the following line of code:
theFile: any = null;
Modify this from a single object to an array of file objects.
theFiles: any[] = [];
Modify the onFileChange()
method so it looks like Listing 4. Finally, modify the uploadFile()
method to loop through the list of file objects selected. Each time through the loop, pass the current instance of the file object retrieved from the file input type to the readAndUploadFile()
method. You now understand why it was important for you to pass a file object to the readAndUploadFile()
method.
uploadFile(): void {
for (let index = 0; index < this.theFiles.length; index++) {
this.readAndUploadFile(this.theFiles[index]);
}
}
Listing 4: Modify the onFileChange() method to support multiple files
onFileChange(event) {
this.theFiles = [];
// Any file(s) selected from the input?
if (event.target.files && event.target.files.length > 0) {
for (let index = 0; index < event.target.files.length; index++) {
let file = event.target.files[index];
// Don't allow file sizes over 1MB
if (file.size < MAX_SIZE) {
// Add file to list of files
this.theFiles.push(file);
}
else {
this.messages.push("File: " + file.name + " is too large to upload.");
}
}
}
}
Try it Out
Save all your changes. Go back to the browser and you may now select multiple files. Select a few files, click the Upload Files
button and ensure that all files are uploaded to the appropriate folder.
Summary
In this article, you learned to upload small files from your client Web application to a server using Angular and a .NET Core Web API project. On the server-side, you need to make sure that you enable CORS to pass data from one domain to another. On the client-side, you're using the FileReader
class to read data from the user's file system. The technique illustrated in this article should only be used for small files, as it would take too long to read a large file from disk, and too long to send to the server. During this time, you can't provide feedback to the user and you won't be able to display a percentage uploaded. There are several good open-source libraries for uploading large files to the server that do provide feedback as the upload process is happening.