In one of the previous article, I have explained about Implementing Payment Gateway in C# (ASP.NET Core MVC Razorpay Example) but now in this article, I have mentioned how we can upload large files in ASP.NET Core with progress bar using Tus third party client.

Tus.Io is a resumable file upload client and we will be using tusdotnet, which is a .NET server implementation of the tus.io protocol that runs on both .NET 4.x and .NET Core, so, let's get started with an example

Step 1: Create a new project in your Visual Studio, I am using VS 2019, so open VS 2019, Click on "Create a project" -> then select "ASP.NET Core (Model View Controller)" as project template,

asp-net-core-file-upload-progress-min_baida7.png

select .NET core version and click on "Create"

/tus-file-upload-with-progress-bar-asp-net-core-min_lr5rwo.png

Step 2: One Visual Studio has generated template file, we will need to install Tusdotnet package in our project, so navigate to "Tools" -> Nuget package manager -> "Manage package for solution" -> Select "Browse" then search for "tus" and install tusdotnet package, as shown below

/install-tus-large-file-upload-asp-net-core-with-progress-bar-min_xu0lf6.png

Step 3: Navigate to Startup.cs and create Tus Configuration

        private DefaultTusConfiguration CreateTusConfiguration(IServiceProvider serviceProvider)
        {
            var env = (IWebHostEnvironment)serviceProvider.GetRequiredService(typeof(IWebHostEnvironment));

            //File upload path
            var tusFiles = Path.Combine(env.WebRootPath, "tusfiles");

            return new DefaultTusConfiguration
            {
                UrlPath = "/files",
                //File storage path
                Store = new TusDiskStore(tusFiles),
                //Does metadata allow null values
                MetadataParsingStrategy = MetadataParsingStrategy.AllowEmptyValues,
                //The file will not be updated after expiration
                Expiration = new AbsoluteExpiration(TimeSpan.FromMinutes(5)),
                //Event handling (various events, meet your needs)
                Events = new Events
                {
                    //Upload completion event callback
                    OnFileCompleteAsync = async ctx =>
                    {
                        //Get upload file
                        var file = await ctx.GetFileAsync();

                        //Get upload file=
                        var metadatas = await file.GetMetadataAsync(ctx.CancellationToken);

                        //Get the target file name in the above file metadata
                        var fileNameMetadata = metadatas["name"];

                        //The target file name is encoded in Base64, so it needs to be decoded here
                        var fileName = fileNameMetadata.GetString(Encoding.UTF8);

                        var extensionName = Path.GetExtension(fileName);

                        //Convert the uploaded file to the actual target file
                        File.Move(Path.Combine(tusFiles, ctx.FileId), Path.Combine(tusFiles, $"{ctx.FileId}{extensionName}"));
                    }
                }
            };
        }

In the above code, we are saving file in "tusfiles", which you can create inside wwwroot folder of your project.

and also have created endpoint to upload file as "/files".

Also, you would have to add configuration in Configure method

app.UseTus(httpContext => Task.FromResult(httpContext.RequestServices.GetService<DefaultTusConfiguration>()));

And, to use it in application, we will add it as Singleton and increase file upload size

  public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddSingleton(CreateTusConfiguration);
            services.Configure<IISServerOptions>(options =>
            {
                options.MaxRequestBodySize = int.MaxValue;
            });
        }

If you are not aware of what is AddSingleton, you can read: Difference between AddTransient, AddScoped and AddSingleton in ASP.NET Core

So your complete Startup.cs would be as below

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using tusdotnet;
using tusdotnet.Models;
using tusdotnet.Models.Configuration;
using tusdotnet.Models.Expiration;
using tusdotnet.Stores;


namespace LargeFileUploadWithProgressBar
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddSingleton(CreateTusConfiguration);
            services.Configure<IISServerOptions>(options =>
            {
                options.MaxRequestBodySize = int.MaxValue;
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseTus(httpContext => Task.FromResult(httpContext.RequestServices.GetService<DefaultTusConfiguration>()));
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }


        private DefaultTusConfiguration CreateTusConfiguration(IServiceProvider serviceProvider)
        {
            var env = (IWebHostEnvironment)serviceProvider.GetRequiredService(typeof(IWebHostEnvironment));

            //File upload path
            var tusFiles = Path.Combine(env.WebRootPath, "tusfiles");

            return new DefaultTusConfiguration
            {
                UrlPath = "/files",
                //File storage path
                Store = new TusDiskStore(tusFiles),
                //Does metadata allow null values
                MetadataParsingStrategy = MetadataParsingStrategy.AllowEmptyValues,
                //The file will not be updated after expiration
                Expiration = new AbsoluteExpiration(TimeSpan.FromMinutes(5)),
                //Event handling (various events, meet your needs)
                Events = new Events
                {
                    //Upload completion event callback
                    OnFileCompleteAsync = async ctx =>
                    {
                        //Get upload file
                        var file = await ctx.GetFileAsync();

                        //Get upload file=
                        var metadatas = await file.GetMetadataAsync(ctx.CancellationToken);

                        //Get the target file name in the above file metadata
                        var fileNameMetadata = metadatas["name"];

                        //The target file name is encoded in Base64, so it needs to be decoded here
                        var fileName = fileNameMetadata.GetString(Encoding.UTF8);

                        var extensionName = Path.GetExtension(fileName);

                        //Convert the uploaded file to the actual target file
                        File.Move(Path.Combine(tusFiles, ctx.FileId), Path.Combine(tusFiles, $"{ctx.FileId}{extensionName}"));
                    }
                }
            };
        }

    }
}

Step 4: Now, navigate to Views -> Home -> Index.cshtml and use the below code, in which we are using Tus Js client to upload file with getting progress status

@{
    ViewData["Title"] = "Home Page";
}
<link href="~/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/tus-js-client@latest/dist/tus.min.js"></script>


<div class="form-horizontal" style="margin-top:80px;">
    <div class="form-group" id="progress-group" style="display:none;">
        <div id="size"></div>
        <div class="progress">
            <div id="progress" class="progress-bar progress-bar-success progress-bar-animated progress-bar-striped" role="progressbar"
                 aria-valuemin="0" aria-valuemax="100">
                <span id="percentage"></span>
            </div>
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-10">
            <input name="file" id="file" type="file" />
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" id="submit" value="Upload" class="btn btn-success" />
            <input type="button" id="pause" value="Pause" class="btn btn-danger" />
            <input type="button" id="continue" value="Continue" class="btn btn-info" />
        </div>
    </div>
</div>



<script type="text/javascript">
    $(function () {
        var upload;

        //Upload
        $('#submit').click(function () {

            $('#progress-group').show();

            var file = $('#file')[0].files[0];

            //Create tus upload object
            upload = new tus.Upload(file, {
                //File server upload endpoint address settings
                endpoint: "files",
                //Retrying delay settings
                retryDelays: [0, 3000, 5000, 10000, 20000],
                //Metadata required by attachment server
                metadata: {
                    name: file.name,
                    contentType: file.type || 'application/octet-stream',
                    emptyMetaKey: ''
                },
                //Callback errors that cannot be resolved by retrying
                onError: function (error) {
                    console.log("Failed because: " + error)
                },
                //Upload progress callback
                onProgress: onProgress,
                //Callback after upload
                onSuccess: function () {
                    console.log("Download %s from %s", upload.file.name, upload.url);
                    $('.progress-bar-animated').removeClass('progress-bar-animated');
                }
            })

            upload.start()
        });

        //Pause
        $('#pause').click(function () {
            upload.abort()
        });

        //Continue
        $('#continue').click(function () {
            upload.start()
        });

        //Upload bar progress update function
        function onProgress(bytesUploaded, bytesTotal) {
            var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
            $('#progress').attr('aria-valuenow', percentage);
            $('#progress').css('width', percentage + '%');

            $('#percentage').html(percentage + '%');

            var uploadBytes = byteToSize(bytesUploaded);
            var totalBytes = byteToSize(bytesTotal);

            $('#size').html(uploadBytes + '/' + totalBytes);
        }

        //Convert bytes to byte, KB, MB, etc
        function byteToSize(bytes, separator = '', postFix = '') {
            if (bytes) {
                const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                const i = Math.min(parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 10), sizes.length - 1);
                return `${(bytes / (1024 ** i)).toFixed(i ? 1 : 0)}${separator}${sizes[i]}${postFix}`;
            }
            return 'n/a';
        }
    });

</script>

as you can see in above JS code, on clicking submit button, we are creatung tus js client object and calling it's endpoint "/files" to upload file.

We are also checking upload progress

                //Upload progress callback
                onProgress: onProgress,

which calls Jaavscript function

 //Upload bar progress update function
        function onProgress(bytesUploaded, bytesTotal) {
            var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
            $('#progress').attr('aria-valuenow', percentage);
            $('#progress').css('width', percentage + '%');

            $('#percentage').html(percentage + '%');

            var uploadBytes = byteToSize(bytesUploaded);
            var totalBytes = byteToSize(bytesTotal);

            $('#size').html(uploadBytes + '/' + totalBytes);
        }

which will update bootstrap progress bar width while file is uploading.

Once you will build project and check output, you will see output like below

file-upload-progress-bar-asp-net-core-tus-min_ipbo0u.gif

To download large files, you can create endpoint in HomeController like this, in which we  are passing fileId to download file.

        public static async Task HandleRoute(HttpContext context)
        {
            var config = context.RequestServices.GetRequiredService<DefaultTusConfiguration>();

            if (!(config.Store is ITusReadableStore store))
            {
                return;
            }

            var fileId = (string)context.Request.RouteValues["fileId"];
            var file = await store.GetFileAsync(fileId, context.RequestAborted);
            if (file == null)
            {
                context.Response.StatusCode = 404;
                await context.Response.WriteAsync($"File with id {fileId} was not found.", context.RequestAborted);
                return;
            }

            var fileStream = await file.GetContentAsync(context.RequestAborted);
            var metadata = await file.GetMetadataAsync(context.RequestAborted);

            context.Response.ContentType = GetContentTypeOrDefault(metadata);
            context.Response.ContentLength = fileStream.Length;

            if (metadata.TryGetValue("name", out var nameMeta))
            {
                context.Response.Headers.Add("Content-Disposition",
                    new[] { $"attachment; filename=\"{nameMeta.GetString(Encoding.UTF8)}\"" });
            }

            using (fileStream)
            {
                await fileStream.CopyToAsync(context.Response.Body, 81920, context.RequestAborted);
            }
        }

        private static string GetContentTypeOrDefault(Dictionary<string, Metadata> metadata)
        {
            if (metadata.TryGetValue("contentType", out var contentType))
            {
                return contentType.GetString(Encoding.UTF8);
            }

            return "application/octet-stream";
        }

You may also like to read:

File upload with progress bar in ASP.NET MVC

File upload on AWS S3 using C# in ASP.NET Core

File upload in asp.net core mvc (single or multiple)