File Upload with ASP.NET Core and Angular

Mon Mar 29, 2021

6 comments 6011 reads

Angular / ASP.NET Core / Tutorials

armin-zia

A frequently required feature in modern applications is being able to upload and download files. In this post, we're going to implement file uploads with ASP.NET Core and Angular. In future posts, I'll show you more sophisticated features such as drag & drop support, multiple file uploads, validation, and previews.

Getting Started

We're going to build a simple app that allows image file uploads, one at a time. The end result is going to look like this:

angular file upload example

Let's set up a new project and go step by step. I'm using Visual Studio 2019 Community Edition, but you could use the .NET CLI or other IDEs (e.g. Rider). Open Visual Studio and create a new project. In the second screen, search for Angular in project templates:

Create a new project in Visual Studio

Select the ASP.NET Core with Angular template and click Next. Choose a name for your project and select the location directory:

Choose a name for your project

Click next and in the last screen, choose the target framework and set the Authentication Type to None. I'm going to use .NET 5.0:

Choose the target framework and authentication type

Click Create and voila, we have a new solution to work with. Next, let's update the Angular project and get rid of the redundant files. At the time of this writing, the project template is still using an old version of Angular (v8.2.12). The following screenshot shows the generated package.json file:

old angular version

I have Angular 11.2.5 installed so I want the latest version. You could use the official Angular Update Guide but it's time-consuming and tricky to update the local version this way. I prefer a simpler approach, delete the ClientApp directory and create a new project from scratch. After you've deleted the ClientApp folder, open a terminal (PowerShell, Command-prompt) in the solution root directory and create a new Angular CLI project:

ng new ClientApp --style=scss --routing=false

This will create a new project named ClientApp with SCSS for styling, and no routing to keep things simple. Before we get to the Angular app, let's remove some of the generated files:

  • Remove WeatherForecast.cs from the project root directory
  • Remove WeatherForecastController.cs from the Controllers directory

We don't need the generated sample code, we want to have a blank solution to work with.

The Angular App

Let's set up the Angular app too, I'm using Visual Studio Code but feel free to use your favorite editor. Open the ClientApp folder in VSCode and install Bootstrap which I'm going to use for UI components:

npm install --save bootstrap

Great, now open src/styles.scss and import Bootstrap:

@import "~bootstrap/scss/bootstrap";

I prefer importing the SCSS file because overriding Bootstrap variables and styles are easier this way. Alternatively, you could import just the CSS in angular.json:

"styles": [
  "node_modules/bootstrap/dist/css/bootstrap.min.css",
  "src/styles.scss"
]

On my machine, after creating this new Angular project I'm seeing that TypeScript v4.1.5 is installed. If you're using the same version or higher, you may want to disable the strictPropertyInitialization setting in  tsconfig.json:

{
  "compilerOptions": {
    "strictPropertyInitialization": false,
  }
}

Lastly, let's add some Bootstrap styling to our app and move on to the implementation. Open src/index.html and replace the content with the following:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Angular File Upload Example</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <div class="container">
    <main role="main">
      <app-root>loading...</app-root>
    </main>
  </div>
</body>
</html>

I've just changed the document title and wrapped the root element in a container.

The Back-end Implementation

We're going to need a couple of things for the back-end. First, we need to configure a PhysicalFileProvider so that we can access the uploaded files. Then we'll need an API controller with a single endpoint that actually uploads user files and stores them on disk. How you manage file uploads depends on your project requirements, and in future posts, I'll talk about security (authentication & authorization), performance considerations, and more. But let's stick to a basic demo for now. Open Startup.cs and insert the following in the Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  // removed for brevity

  app.UseStaticFiles();    // for the wwwroot folder

  // for the wwwroot/uploads folder
  string uploadsDir = Path.Combine(env.WebRootPath, "uploads");
  if (!Directory.Exists(uploadsDir))
      Directory.CreateDirectory(uploadsDir);

  app.UseStaticFiles(new StaticFileOptions()
  {
      RequestPath = "/images",
      FileProvider = new PhysicalFileProvider(uploadsDir)
  });  

  // the rest of the method
}

The order is important here, make sure this code is inserted after the call to app.UseHttpsRedirection() and before the call to app.UseSpaStaticFiles(). So what are we doing here? We are registering two physical file providers for serving static files. The first one is for the wwwroot folder and doesn't require further configuration. Static assets like images, scripts, and stylesheets go under wwwroot by default. The second one needs some explanation.

Our sample app is going to allow image files to be uploaded, although the same code can be used for whatever file format. We're going to store uploaded files in the uploads directory, which is going to sit on the wwwroot folder.

uploads directory

We want to display uploaded images, so the ASP.NET web app needs to be able to serve the uploads directory. To do that, we register a virtual path and a PhysicalFileProvider. The RequestPath property is set to "/images" and that means all files under the uploads directory are going to be served using that prefix. Say we have uploaded a file named wallpaper.jpg. On the file system, our file is stored at wwwroot/uploads/wallpaper.jpg. But to access it via the web app we'll use the file provider we created, so the path would be /images/wallpaper.jpg.

For more information have a look at File Providers in ASP.NET Core and Static Files in ASP.NET Core.

The Upload Controller

Right-click on the Controllers folder and create a new API Controller named UploadController. Controllers are just C# classes, so you could just create a new class. Here's the complete code:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace AngularFileUploadExample.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    [Produces("application/json")]
    public class UploadController : ControllerBase
    {
        private readonly IWebHostEnvironment _hostingEnvironment;

        public UploadController(IWebHostEnvironment hostingEnvironment)
        {
            _hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        [DisableRequestSizeLimit]
        public async Task<IActionResult> UploadFile()
        {
            if (!Request.Form.Files.Any())
                return BadRequest("No files found in the request");

            if (Request.Form.Files.Count > 1)
                return BadRequest("Cannot upload more than one file at a time");

            if (Request.Form.Files[0].Length <= 0)
                return BadRequest("Invalid file length, seems to be empty");

            try
            {
                string webRootPath = _hostingEnvironment.WebRootPath;
                string uploadsDir = Path.Combine(webRootPath, "uploads");

                // wwwroot/uploads/
                if (!Directory.Exists(uploadsDir))
                    Directory.CreateDirectory(uploadsDir);

                IFormFile file = Request.Form.Files[0];
                string fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
                string fullPath = Path.Combine(uploadsDir, fileName);

                var buffer = 1024 * 1024;
                using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None, buffer, useAsync: false);
                await file.CopyToAsync(stream);
                await stream.FlushAsync();

                string location = $"images/{fileName}";

                var result = new
                {
                    message = "Upload successful",
                    url = location
                };

                return Ok(result);
            }
            catch (Exception ex)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, "Upload failed: " + ex.Message);
            }
        }
    }
}

This controller is really simple, but let's quickly go through the code. The controller inherits from the ControllerBase and is decorated with three attributes. The Route attribute adds a prefix to the controller, so this controller would be accessed at api/upload. We're also using the Produces attribute to tell ASP.NET that this controller produces JSON responses.

In the constructor, we inject an instance of IWebHostingEnvironment which we're going to use for working with the file system. This controller has a single endpoint that accepts HTTP POST requests. You also notice that we're using the DisableRequestSizeLimit attribute which disables the request body size limit, look here for more information.

Input files can be accessed on the Request object. In the method body, we check for a few things:

  • If there are no files on the request, return BadRequest (HTTP 400)
  • If there's more than one file, return BadRequest
  • If the file is empty (content length <= 0), return BadRequest

To keep things simple, we're allowing files to be uploaded one at a time. But you could make a few changes and allow multiple files. The try/catch block is where we actually upload files. First, we get the directory path for the wwwroot folder. Next, we check whether the uploads directory exists or not. If not, we create the directory. Then we get the destination path using the input file's name and store it on disk using a FileStream. Finally, we return an anonymous object with two properties:

  • Message - a success message
  • URL - the uploaded file's URL

There are many things you could do with this method. For instance, if your application only allows a particular set of file formats, you could validate file extensions before storing them. Or let's say users can't upload files larger than 10 MB, you could check the content length and reject invalid files. Perhaps you want to run a virus scanner too? You get the idea.

The Front-end Implementation

The first thing we need is to import the HttpClientModule. Open app.module.ts and replace it with the following:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule, HttpClientModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

We also want to introduce the API URL as an environment variable, so open environment.ts and replace it with the following:

export const environment = {
  production: false,
  apiUrl: 'https://localhost:5001/api',
};

Don't forget to do the same for environment.prod.ts in production.

The component template is very simple. We're going to have three rows with basic HTML elements. Open app.component.html and replace it with the following:

<div class="row mt-5">
  <div class="col">
    <div class="custom-file">
      <input type="file" accept="image/*" class="custom-file-input" id="customFile" (change)="handleFileInput($any($event.target).files)" [disabled]="working">
      <label class="custom-file-label" for="customFile">{{ uploadFileLabel }}</label>
    </div>
  </div>
  <div class="col-auto pl-sm-0">
    <button type="button" class="btn btn-outline-primary" ngbTooltip="Upload" (click)="upload()" [disabled]="working">Upload</button>
  </div>
</div>
<div class="row mt-3" *ngIf="working">
  <div class="col">
    <div class="progress">
      <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploadProgress + '%' }" [attr.aria-valuenow]="uploadProgress" aria-valuemin="0" aria-valuemax="100">{{ uploadProgress }}%</div>
    </div>
  </div>
</div>
<div class="row mt-3" *ngIf="uploadUrl?.length">
  <div class="col">
    <img [src]="uploadUrl" alt="preview" class="rounded shadow w-100">
  </div>
</div>

On the first row, we have a file input and a button. They both get disabled while the app is working (uploading). On the second row, we have a Bootstrap progress bar that shows the upload progress. And finally, we have an image element that is displayed after a successful upload, pointing to the URL we received from the API.

Lastly, let's wire up the UI to our API. Open app.component.ts and replace it with the following:

import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http';
import { Component } from '@angular/core';
import { environment } from 'src/environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(private http: HttpClient) {}

  working = false;
  uploadFile: File | null;
  uploadFileLabel: string | undefined = 'Choose an image to upload';
  uploadProgress: number;
  uploadUrl: string;

  handleFileInput(files: FileList) {
    if (files.length > 0) {
      this.uploadFile = files.item(0);
      this.uploadFileLabel = this.uploadFile?.name;
    }
  }

  upload() {
    if (!this.uploadFile) {
      alert('Choose a file to upload first');
      return;
    }

    const formData = new FormData();
    formData.append(this.uploadFile.name, this.uploadFile);

    const url = `${environment.apiUrl}/upload`;
    const uploadReq = new HttpRequest('POST', url, formData, {
      reportProgress: true,
    });

    this.uploadUrl = '';
    this.uploadProgress = 0;
    this.working = true;

    this.http.request(uploadReq).subscribe((event: any) => {
      if (event.type === HttpEventType.UploadProgress) {
        this.uploadProgress = Math.round((100 * event.loaded) / event.total);
      } else if (event.type === HttpEventType.Response) {
        this.uploadUrl = event.body.url;
      }
    }, (error: any) => {
      console.error(error);
    }).add(() => {
      this.working = false;
    });
  }
}

Let's examine the code. When you select a file, the handleFileInput method is called. This method accepts a FileList as an argument, and if the collection is not empty, we get the first item. Similar to the upload controller, this is where you can validate input files on the client-side. Say you wanted to allow certain extensions, or reject files larger than 10 MB, etc.

The upload method is straightforward too. First, we check whether we have an input file or not. If we do, we instantiate a new FormData and append the selected file to it. Next, we create an HttpRequest pointing to our API endpoint, and we set the reportProgress property to true. This is important, otherwise, you wouldn't be able to show the upload progress. Before we send the request, we reset the variables, the uploadUrl and uploadProgress properties. Finally, we send the request and subscribe to it. HttpClient.request returns an Observable. Inside the observable subscription, we check the event type. If it is UploadProgress, we update the uploadProgress variable. If the event type is Response, it means the request has been processed successfully and we have received a response (the anonymous object returned from our API).

That's it, we have a working file upload example. I'll leave it to you to develop this further and play with it. One thing to bring to your attention here is that even though you could use this implementation in many applications, it's not perfect. If you're expecting thousands of concurrent users and large file uploads, and if you need advanced security features too, you may need a more sophisticated solution like streaming the data in chunks and storing them asynchronously. Or you could leave the file system alone and go for MinIO or Azure Blob Storage.

Source Code

You can find the complete code on Github. Happy coding.

Posted in Angular , ASP.NET Core

Tagged Tutorials