Being able to upload files and use them afterward is the required feature of many applications. Sometimes this is not a trivial task to accomplish.
So, this is going to be the topic for this blog post.
We are going to upload files to the server (.NET Core Web API part) and then use those files in our Angular client app. Also, in our next article, we are going to show how to download files using ASP.NET Core WebAPI and Angular and with it, we are going to complete this story.
In this post, we will stick to the images, but the logic is reusable for other file types as well.
VIDEO: Uploading Files with ASP.NET Core WEB API and Angular video.
We have created the starter project to work with through this blog post and it can be downloaded from Upload Files .NET Core Angular Starter Project. We strongly recommend downloading this project because it would be much easier for you to follow along. In this project, we’ll create a new user and display all the created users as an additional feature. We are going to modify the create-logic part by adding an upload functionality having a new user created together with an image path related to them.
Let’s start.
Controller and Action Logic – .NET Core Part
After we’ve downloaded our starter project, we can start by opening the UploadFilesServer
project.
This project is created on top of the SQL database, so to create that database, we need to run the update-database
command in a Package Manager Console window. By doing this, we will execute our migrations and create the database with the required table.
The next step is to create a newResources
folder and inside it a new folder Images
:
To continue, let’s create a simple API Controller file in the Controllers
folder and name it UploadController
.
Let’s modify that file by adding a new action that will be responsible for the upload logic:
[HttpPost, DisableRequestSizeLimit] public IActionResult Upload() { try { var file = Request.Form.Files[0]; var folderName = Path.Combine("Resources", "Images"); var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName); if (file.Length > 0) { var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); var fullPath = Path.Combine(pathToSave, fileName); var dbPath = Path.Combine(folderName, fileName); using (var stream = new FileStream(fullPath, FileMode.Create)) { file.CopyTo(stream); } return Ok(new { dbPath }); } else { return BadRequest(); } } catch (Exception ex) { return StatusCode(500, $"Internal server error: {ex}"); } }
We use a POST action for the upload-related logic and disable the request size limit as well.
The logic inside this action is pretty straightforward. We extract the file from the request and provide the path to store the file. Moreover, if the file has a length greater than zero, we just take its name and provide a full path on the server to store our file and a path to the database. This database path is going to be returned as a result of this action after we place our stream into the defined folder. We could also check if a file with the same name already exists, but didn’t want to make the code more complicated at this moment.
To avoid the MultiPartBodyLength
error, we are going to modify our configuration in the Program.cs
class:
builder.Services.Configure<FormOptions>(o => { o.ValueLengthLimit = int.MaxValue; o.MultipartBodyLengthLimit = int.MaxValue; o.MemoryBufferThreshold = int.MaxValue; });
Improve Reading from a Form Body
In our previous example, we use the Request.Form
to read a form body and for smaller applications, this is just fine. But here, we are using a synchronous way to read the content from the form body, which in larger applications with so many users can lead to thread pool starvation. To prevent that, we can use asynchronous reading with the Request.ReadFormAsync()
expression.
All we have to do is modify the action signature and modify our code inside just a bit:
[HttpPost, DisableRequestSizeLimit] public async Task<IActionResult> Upload() { try { var formCollection = await Request.ReadFormAsync(); var file = formCollection.Files.First(); //everything else is the same
This way, we read the form body in an asynchronous way and prevent the thread pool starvation.
Serving Static Files
Usually, all the files in the wwwroot
folder are servable for the client applications. We provide that by adding app.UseStaticFiles()
in the Startup
class in the Configure
method. Of course, our uploaded images will be stored in the Resources
folder, and due to that, we need to make it servable as well. To do that, let’s modify the Program.cs
class:
app.UseHttpsRedirection(); app.UseCors("CorsPolicy"); app.UseStaticFiles(); app.UseStaticFiles(new StaticFileOptions() { FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"Resources")), RequestPath = new PathString("/Resources") });
And that’s all it takes. We have prepared our server-side app and it is time to jump right to the client-side code.
If you want to learn in great detail about .NET Core project development, you can visit the .NET Core Tutorial.
Upload Files – Angular Part
Let’s open the UploadFilesClient
project and take a look at the app component files. For the sake of simplicity, we have implemented all of our logic inside the app component.
To learn in great detail about Angular project development, you can read the Angular Tutorial.
So, the first thing we are going to do is to create a new Upload component in which we will handle all the upload-related logic:
ng g component upload --skip-tests
This will create three files in the upload
folder, and we are going to modify the upload.component.ts
file first:
import { HttpClient, HttpEventType, HttpErrorResponse } from '@angular/common/http'; import { Component, EventEmitter, OnInit, Output } from '@angular/core'; @Component({ selector: 'app-upload', templateUrl: './upload.component.html', styleUrls: ['./upload.component.css'] }) export class UploadComponent implements OnInit { progress: number; message: string; @Output() public onUploadFinished = new EventEmitter(); constructor(private http: HttpClient) { } ngOnInit() { } uploadFile = (files) => { if (files.length === 0) { return; } let fileToUpload = <File>files[0]; const formData = new FormData(); formData.append('file', fileToUpload, fileToUpload.name); this.http.post('https://localhost:5001/api/upload', formData, {reportProgress: true, observe: 'events'}) .subscribe({ next: (event) => { if (event.type === HttpEventType.UploadProgress) this.progress = Math.round(100 * event.loaded / event.total); else if (event.type === HttpEventType.Response) { this.message = 'Upload success.'; this.onUploadFinished.emit(event.body); } }, error: (err: HttpErrorResponse) => console.log(err) }); } }
So, what’s going on here?
We create two public properties. The first one is to hold the message when the upload action is finished and the second one is to show the upload progress. In the uploadFile
function, we create a formData
object and append the file that we want to upload.
The next action is to send a post request and pay attention to it. Besides the URL and body properties, we have another JSON object which states that we want to track changes of our HTTP request progress. As long as the upload is in progress, we will update the progress variable and show that percentage on the screen, but as soon as the upload is finished, we are going to write a message on the screen and emit a new event.
This event contains the body of our response, which is simply the database path of our uploaded file. We need that path to display the uploaded image with other user details.
The files with the small size will be instantly uploaded so, we will see 100% progress as soon as we select our file. But for the larger files, the progress bar will update its values for sure.
Template File Modification
To display all of the mentioned functionalities on the screen, we need to modify the upload.component.html
file now:
<div class="row" style="margin-bottom:15px;"> <div class="col-md-3"> <input type="file" #file placeholder="Choose file" (change)="uploadFile(file.files)" style="display:none;"> <button type="button" class="btn btn-success" (click)="file.click()">Upload File</button> </div> <div class="col-md-4"> <span class="upload" *ngIf="progress > 0"> {{progress}}% </span> <span class="upload" *ngIf="message"> {{message}} </span> </div> </div>
This logic is pretty straightforward except for the part where we hide the actual upload control and use its reference (#file
) to invoke its click event with the button, which looks much better. We could have styled the upload control as well, but this is the better way, at least from our point of view.
Finally, let’s modify the upload.component.css
file:
.upload{ font-weight:bold; color:#28a745; margin-left: 15px; line-height: 36px; }
And add a selector from the upload component to the app.component.html
file:
<app-upload></app-upload> <div class="row"> <div class="offset-md-5 col-md-2"> <button type="button" class="btn btn-primary" (click)="onCreate()">Create </button> </div> </div>
Excellent. We can now inspect our result:
We can check our Resources/Images
folder as well, to be sure that the files are really uploaded:
Using Uploaded Files in Our Application
As soon as we press the Create
button on our form, we are going to see our newly created user. But its profile picture won’t be rendered. So, let’s fix that.
First, we need to react to the onUploadFinished
event from the update component, and to do that let’s modify the app.component.html
file:
<app-upload (onUploadFinished)="uploadFinished($event)"></app-upload>
This change forces us to modify the app.component.ts
file as well.
First, let’s add an additional property to that file:
response: {dbPath: ''};
Then let’s add the uploadFinished
function to populate this property:
uploadFinished = (event) => { this.response = event; }
With this modification, we have the response object in which we can find a path to be saved in the database.
Lastly, we have to modify the user object in the onCreate
function in the same file:
onCreate = () => { this.user = { name: this.name, address: this.address, imgPath: this.response.dbPath }
Now we know the image file path related to the created user, so let’s use that knowledge to render that picture next to other user details.
To do that, let’s change the table inside the app.component.html
file:
<table class="table table-striped"> <thead> <tr> <th scope="col">Image</th> <th scope="col">Name</th> <th scope="col">Address</th> </tr> </thead> <tbody> <tr *ngFor="let user of users"> <td><img [src]="createImgPath(user.imgPath)" alt="profile picture" style="width:60px; height:60px;"></td> <td>{{user.name}}</td> <td>{{user.address}}</td> </tr> </tbody> </table>
And let’s modify the app.component.ts
file by adding the createImgPath
function:
public createImgPath = (serverPath: string) => { return `https://localhost:5001/${serverPath}`; }
Now, once we upload the file and create a user, we can see all the info in the table:
Uploading Multiple Files
If we want to upload multiple files in any of our projects, we need to modify both the server and client-side code.
So let’s start with the server-side:
public IActionResult Upload() { try { var files = Request.Form.Files; var folderName = Path.Combine("StaticFiles", "Images"); var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName); if (files.Any(f => f.Length == 0)) { return BadRequest(); } foreach (var file in files) { var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); var fullPath = Path.Combine(pathToSave, fileName); var dbPath = Path.Combine(folderName, fileName); //you can add this path to a list and then return all dbPaths to the client if require using (var stream = new FileStream(fullPath, FileMode.Create)) { file.CopyTo(stream); } } return Ok("All the files are successfully uploaded."); } catch (Exception ex) { return StatusCode(500, "Internal server error"); } }
After this modification, let’s change the client-side. First, we need to modify the input type file control by adding the multiple
attribute:
<input type="file" #file placeholder="Choose file" (change)="uploadFile(file.files)" style="display:none;" multiple>
After that, we are going to modify the uploadFile
function:
uploadFile = (files) => { if (files.length === 0) { return; } let filesToUpload : File[] = files; const formData = new FormData(); Array.from(filesToUpload).map((file, index) => { return formData.append('file'+index, file, file.name); }); this.http.post('https://localhost:5001/api/upload', formData, {reportProgress: true, observe: 'events'}) .subscribe( {next: (event) => { if (event.type === HttpEventType.UploadProgress) this.progress = Math.round(100 * event.loaded / event.total); else if (event.type === HttpEventType.Response) { this.message = 'Upload success.'; this.onUploadFinished.emit(event.body); } }, error: (err: HttpErrorResponse) => console.log(err) }); }
One interesting thing to pay attention to is the use of the Array.from()
function. Even though the files
variable contains all the selected files, it is not an array. So, in order to use the map
function, we are using the Array.from()
syntax, which will convert the array-like object into the new array copy. The rest of the logic is pretty straight forward.
And that is all that takes. Now you can test your code and check that your files are uploaded.
Conclusion
In this article, we have learned:
- How to code our server-side action to handle file uploading
- The way to create an upload component in our Angular application
- How to use uploaded files in the Angular application
In the next article, you can read how to Download Files with ASP.NET Core Web API and Angular.