File upload download
.NET Core ASP.NET Core C# HTTP REST SQL Top Visual Studio Web API

How to Implement a File Uploader Web API using .NET Core

Welcome to today’s blog.

In today’s blog I will be showing how to implement a file uploader using .NET Core Web API. backend architecture will be determined by how the uploaded file is stored.

These include sending the file stream to a backend Web service or Web API service that handles and validates the file stream by validating it for safety (virus scanning), file type, and size. The file contents would then either be stored somewhere on a folder somewhere on the web server, on another file server, in a cloud-based server, or on a backend data store. It all depends on your business requirements, including security of the file contents that are stored.

As with any storage solution, the backend database should be secured, as should other alternative storage means, such as a folder within a network drive. With folder storage, if the files uploaded are sensitive then ensure the folders containing them are adequately secured before use.

In this post I will focus on storing the uploaded file contents in a table within a SQL server database.

The file being uploaded will be stored in a table within a SQL server database.

The basic architecture I will use is as follows:

  1. Backend SQL server database.
  2. .NET Core Web API.
  3. Web API methods for uploading and downloading of files.

In the first section I will show how to create basic data storage for our uploaded files.

Creation of the File Upload Data Storage

First, we will create the backend. This implementation will include just one table to store uploaded files. If you have SQL server then the script to create the database and table is shown:

USE [master]
GO

CREATE DATABASE [FileDemoDB]
GO

USE [FileDemoDB]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[UploadedFiles](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[FileName] [varchar](255) NOT NULL,
	[FileSize] [bigint] NOT NULL,
	[FileContent] [varbinary](max) NOT NULL,
	[UploadDate] [datetime] NOT NULL,
	[UploadedBy] [varchar](50) NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

After this script is applied you will have an upload table ready to hold uploaded file records.

If you already have an existing database to hold the uploaded files, then you will have to do in your code is to reference that data source instead and the table name. If you have a file name, file size and file contents fields in your upload table, then the code that follows can be modified to suit your existing data store.

Implementation of a File Upload Web API Methods

In our web API, we implement the following API method:

UploadFiles(files[])

The upload file method takes a list of files and inserts them into the data base.

The upload API method returns a response structure FileUploadResponse as shown:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace File.Uploader.API.Models
{
    public class FileUploadResponse
    {
        public string ErrorMessage { get; set; }
        public List<FileUploadResponseData> Data { get; set; }
    }
}

When there is an error during the uploading of files, we set the ErrorMessage property within the response to a non-blank value.

The response structure contains a list of the metadata of the files that have been uploaded successfully. The structure FileUploadResponseData, for the metadata is shown:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace File.Uploader.API.Models
{
    public class FileUploadResponseData
    {
        public int Id { get; set; }
        public string Status { get; set; }
        public string FileName { get; set; }
        public string ErrorMessage { get; set; }
    }
}

The implementation of the upload files method is shown:

public async Task<FileUploadResponse> UploadFiles(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    List<FileUploadResponseData> uploadedFiles = new List<FileUploadResponseData>();

    try
    {
        foreach (var f in files)
        {
            string name = f.FileName.Replace(@"\\\\", @"\\");

            if (f.Length > 0)
            {
                var memoryStream = new MemoryStream();

                try
                {
                    await f.CopyToAsync(memoryStream);

                    // Upload check if less than 2mb!
                    if (memoryStream.Length < 2097152)
                    {
                        var file = new FileUploadInfo()
                        {
                            FileName = Path.GetFileName(name),
                            FileSize = memoryStream.Length,
                            UploadDate = DateTime.Now,
                            UploadedBy = "Admin",
                            FileContent = memoryStream.ToArray()
                        };

                        _db.UploadedFiles.Add(file);

                        await _db.SaveChangesAsync();

                        uploadedFiles.Add(new FileUploadResponseData()
                        {
                            Id = file.Id,
                            Status = "OK",
                            FileName = Path.GetFileName(name),
                            ErrorMessage = "",
                        });
                    }
                    else
                    {
                        uploadedFiles.Add(new FileUploadResponseData()
                        {
                            Id = 0,
                            Status = "ERROR",
                            FileName = Path.GetFileName(name),
                            ErrorMessage = "File " + f + " failed to upload"
                        });
                    }
                }
                finally
                {
                    memoryStream.Close();
                    memoryStream.Dispose();
                }
            }
        }
        return new FileUploadResponse() { Data = uploadedFiles, ErrorMessage = "" };
    }
    catch (Exception ex)
    {
        return new FileUploadResponse() { ErrorMessage = ex.Message.ToString() };
    }
}

Before the files can be passed as a parameter to the method, you will need to include the following namespace in your source so that the IFormFile interface can be recognised:

using Microsoft.AspNetCore.Http;

Please note that I have not included extensive checking for the upload. You are advised to include some validation and error checking. These can include:

  1. Limit size of individual uploaded files.
  2. Check for duplicate uploaded files for the same user.
  3. Give the option to overwrite existing files with the same file name or return an error.

The next API method we will implement is for the download:

DownloadFiles()

The download files API method when called returns a list of all the downloaded files.

For a more realistic scenario we would be advised to include additional parameters to limit the number and / or size of files downloaded to the caller.

We can limit the download in the following ways:

  1. By date time frame.
  2. By user who uploaded the file.
  3. By size of files.

With regular uploads, a database can grow rapidly, so it makes sense develop a scheduled task or job that archives or removes older files from the database. A larger upload table can take a performance hit when the number of records and file content sizes reaches a large size. 

The download method returns a response structure FileDownloadView as shown:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace File.Uploader.API.Models
{
    public class FileDownloadView
    {
        public int Id { get; set; }
        public string FileName { get; set; }
        public int FileSize { get; set; }
    }
}

Note that I have not included a file content field as sending multiple files back to a client can hang the user’s browser waiting for potentially large sized downloads.

The implementation of the download files method is shown:

public async Task<IEnumerable<FileDownloadView>> DownloadFiles()
{
    IEnumerable<FileDownloadView> downloadFiles =
    _db.UploadedFiles.ToList().Select(f =>
        new FileDownloadView
        {
            Id = f.Id,
            FileName = f.FileName,
            FileSize = f.FileContent.Length
        }
    );
    return downloadFiles;
 }

The next API method is it download a file by its ID:

DownloadFile(id)

The download file method returns a single file when presented with the id of the file.

The response will be binary.

The implementation of the download file method is shown:

public async Task<byte[]> DownloadFile(int id)
{
    try
    {
        var selectedFile = _db.UploadedFiles
            .Where(f => f.Id == id)
            .SingleOrDefault();

        if (selectedFile == null)
            return null;
        return selectedFile.FileContent;
    }
    catch (Exception ex)
    {
        return null;
    }
}

The controllers are as follows:

namespace File.Uploader.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class FileUtilityController : ControllerBase
    {
        IFileUtilityService _fileUtilityService;

        public FileUtilityController(IFileUtilityService fileUtilityService)
        {
            _fileUtilityService = fileUtilityService;
        }

        [HttpPost("UploadFiles")]
        public async Task<IActionResult> UploadFiles([FromForm] List<IFormFile> files)
        {
            var uploadResponse = await _fileUtilityService.UploadFiles(files);
            if (uploadResponse.ErrorMessage != "")
                return BadRequest(new { error = uploadResponse.ErrorMessage });
            return Ok(uploadResponse);
        }

        [Route("DownloadFile")]
        [HttpGet]
        public async Task<IActionResult> DownloadFile(int id)
        {
            var stream = await _fileUtilityService.DownloadFile(id);
            if (stream == null)
                return NotFound();
            return new FileContentResult(stream, "application/octet-stream"); 
        }

        [Route("DownloadFiles")]
        [HttpGet]
        public async Task<List<FileDownloadView>> DownloadFiles()
        {
            var files = await _fileUtilityService.DownloadFiles();
            return files.ToList();
        }
    }
}

In the final section, I will show how to test the file upload API using the Swagger UI.

Testing the File Upload Web API in Swagger

Once we run our API application, in Swagger API, it should look as follows:

We can try a quick manual testing of the above methods, first the UploadFiles() API method.

When we expand the method, we will see the parameter is an array of files with an Add item button as shown:

When clicked, the file dialog will open allowing you to select files to upload.

After the files have been selected you will see it amongst other selected files:

Now executing the method if successful will show the following results:

The request URL is of the form:

http://localhost:60129/api/FileUtility/UploadFiles

The response body is as shown:

{
  "errorMessage": "",
  "data": [
    {
      "id": 45,
      "status": "OK",
      "fileName": "test-file-4.txt",
      "errorMessage": ""
    }
  ]
}

Notice the structure is the FileUploadResponse structure.

Make a note of the id value.

Now open SSMS, connect to the database and run the query:

SELECT TOP (1000) [Id] 
      ,[FileName]
      ,[FileSize]
      ,[FileContent]
      ,[UploadDate]
      ,[UploadedBy]
FROM [FileDemoDB].[dbo].[UploadedFiles]
ORDER BY [UploadDate] DESC

The newly uploaded record will show in the first row as shown:

Next, we test the download file method.

Expand the API method. It will show one parameter, the file record id:

When this is executed you will see the result as shown:

There is also a Download file link. Clicking on the link will download your file to the Downloads folder on your local machine.

The request URL is of the form:

http://localhost:60129/api/FileUtility/DownloadFile?id=45

Finally, we will test the Download files method.

When expanded, the download files API method has no parameters:

When executed the response is as shown:

http://localhost:60129/api/FileUtility/DownloadFiles

The response body is as shown with JSON arrays for the file meta data:

[
  {
    "id": 35,
    "fileName": "test-file-7.txt",
    "fileSize": 91
  },
  {
    "id": 36,
    "fileName": "test-file-8.txt",
    "fileSize": 133
  },

Like the file download API, this is also downloadable but as a JSON file.

We have seen how to implement file uploads and downloads using a .NET Core API. With some improvements you can implement your own file upload API and use them within your own clients.

That is all for this post.

I hope you enjoyed this blog and found it useful and informative.

Social media & sharing icons powered by UltimatelySocial