Drag-n-Drop file upload in Blazor using JS interop and minimal API (Part 2)

In the Part 1 of this article you learned to implement drag-n-drop in a Blazor Server application. So far we are able to drag-n-drop files on to a drop target and list those file names in the fileBasket. However, no files are actually sent to the server. That's what we will do in this part of the article. Although our focus is going to be minimal API approach, we will explorer three variations of the process.

So, let's get started.

Open the same project that you created in Part 1 of this article. We will add the file upload and save functionality using three variations:

  • API endpoints defined in Program.cs
  • API endpoints defined in Configure() method of Startup.cs
  • API endpoints defined in a separate controller

Irrespective of the variant you use for the API part of the story, the jQuery code is going to remain the same.

So, let's first add the jQuery code to the JavaScript.cs file.

Go to the UploadFiles() function we added previously and modify it as shown below:

function UploadFiles(compRef) {

    let files = window.selectedFiles;
    let data = new FormData();

    for (var i = 0; i < files.length; i++) {
        data.append(files[i].name, files[i]);
    }

    $.ajax({
        type: "POST",
        url: "/api/SaveFiles",
        contentType: false,
        processData: false,
        data: data,
        beforeSend: function () {
            $("#progress").show();
        },
        success: function (message) {
            compRef.invokeMethodAsync("SetMessage",
                     message)
                .then((result) => {
                    console.log(result);
                });
        },
        error: function () {
            compRef.invokeMethodAsync("SetMessage",
                "Error while uploading files!")
                .then((result) => {
                    console.log(result);
                });
        },
        complete: function () {
            $("#progress").hide();
        }
    });
    return true;
}

The code iterates through all the selectedFiles and adds them to a FormData object. The FormData is a programmatic representation of a form and holds key-value pairs. In this case we append() a file name and its content as the key-value respectively. Once this FormData object is ready we want to send it to the server so that files can be saved on the server.

We do this by making an Ajax POST call to a Web API endpoint. In this example the API endpoint is /api/SaveFiles. We will create this endpoint later in this article. Notice that the contentType and processData properties are set to false. The data property is set to the FormData object we created earlier.

We wire four callback functions - beforeSend, success, error, and complete - while making the Ajax call. The beforeSend callback simply shows the progress indicator image. The success callback is invoked if the API call is successful. It receives a message from the API. You can use this message for your checking or client side processing. Here, we simply pass this message to SetMessage() C# method so that the component's UI can reflect the success status. This is done using invokeMethodAsync() method. If there is any error while calling the API then the error callback shows an error message using invokeMethodAsync() method. The complete callback is called when the Ajax call completes irrespective of its success or error status. Inside, we simply hide the progress indicator.

Now it's time to write the /api/SaveFiles API endpoint.

Open Program.cs and comment out all the code that's added by default.

Import the following namespace if you haven't already.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.IO;
using System.Net.Http.Headers;

Next, build the WebApplication by adding the following code.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

builder.WebHost.UseWebRoot(Path.Combine
(Directory.GetCurrentDirectory(), "wwwroot"));

await using var app = builder.Build();

I am not going to discuss minimal APIs in detail here. Consider reading my article published some time ago that explains them in detail. Here we call AddControllers() and AddRazorPages() to register DI services related to Web API and Razor Pages. We also call AddServerSideBlazor() because this is a Blazor Server app. Since we are using static resources such as JavaScript files and images from wwwroot folder, we set the web root by calling the UseWebRoot() method. Finally, we call Build() to build the WebApplication instance.

Now comes the important piece of code - defining the /api/SaveFiles API endpoint. Add the following MapPost() call.

app.MapPost("/api/SaveFiles", (IWebHostEnvironment env, 
HttpContext context) => {

    long size = 0;
    var files = context.Request.Form.Files;
    foreach (var file in files)
    {
        var filename = ContentDispositionHeaderValue
                        .Parse(file.ContentDisposition)
                        .FileName
                        .Trim('"');
        filename = env.WebRootPath + $@"\{filename}";
        size += file.Length;
        using (FileStream fs = System.IO.File.Create(filename))
        {
            file.CopyTo(fs);
            fs.Flush();
        }
    }
    string message = $"{files.Count} file(s) / 
{ size} bytes uploaded successfully!";
    return message;
});

We map /api/SaveFiles endpoint to a function that takes two parameters - IWebHostEnvironment and HttpContext. Inside, we grab the Files sent along the Ajax request using the context.Request object. We then create a server side file path using WebRootPath and file name. Then a new file is created on the server using File.Create() method and the file content is copied to it.

Once all the files are saved on the server, a success message with file count and total bytes uploaded is formed and returned to the client.

After writing the MapPost() implementation as discussed above complete Program.cs by adding the required middleware and routing code.

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

await app.RunAsync();

This code should be familiar to you because it also exists in the Configure() of the Startup class.

At this point if you run the app you might get an error in the App.razor file stating that Program class could not be found. This is because we are using C# top level statements and we have removed the Program class. We can expect that this error might be fixed in some future release but for the time being we will fix the error by changing App.razor like this:

<Router AppAssembly="@Assembly.GetEntryAssembly()">
...
...
</Router>

As you can see, instead of using Program class to get the current assembly we use Assembly.GetEntryAssembly() method. You could have also used Startup class in place of Program class since we have it in the project.

Now run the application, drag and drop a few files on the fileBasket and see if they get uploaded on the server. The following figure shows a successful upload operation of three image files on the server.

And

In the preceding example we used minimal API approach involving new hosting APIs and new routing APIs. In some cases you might be still using a separate Startup class. If that's the case you can use MapPost() inside the Configure() method.

Let's see how.

Comment out all the code that we just wrote inside Program.cs and substitute it with the traditional code like this:

namespace BlazorDragDropDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder
(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Then go to Startup class and add the MapPost() call to Configure() as shown below:

public void Configure(IApplicationBuilder app, 
IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }
    app.UseStaticFiles();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapPost("/api/SaveFiles", 
(HttpContext context) => {

            long size = 0;
            var files = context.Request.Form.Files;
            foreach (var file in files)
            {
                var filename = ContentDispositionHeaderValue
                                .Parse(file.ContentDisposition)
                                .FileName
                                .Trim('"');
                filename = env.WebRootPath + $@"\{filename}";
                size += file.Length;
                using (FileStream fs = 
System.IO.File.Create(filename))
                {
                    file.CopyTo(fs);
                    fs.Flush();
                }
            }
            string message = $"{files.Count} file(s) / 
{ size} bytes uploaded successfully!";
            return message;
        });
        endpoints.MapControllers();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

It's essentially the same MapPost() code but there are a couple of differences. Firstly, we added it inside UseEndpoints() call. Secondly, IWebHostEnvironment need not be injected into the MapPost() method because Configure() already has the env parameter.

Now let's move the code to a separate Web API controller. So, add a new folder named Controllers and also add an empty API controller named SaveFilesController in it.

And 

Now add a constructor that injects IWebHostEnvironment.

public class SaveFilesController : Controller
{
    private readonly IWebHostEnvironment env;

    public SaveFilesController(IWebHostEnvironment env)
    {
        this.env = env;
    }
}

Then add a Post() method that saves the files to wwwroot as before.

[HttpPost]
public IActionResult Post()
{
    long size = 0;
    var files = Request.Form.Files;
    foreach (var file in files)
    {
        var filename = ContentDispositionHeaderValue
                        .Parse(file.ContentDisposition)
                        .FileName
                        .Trim('"');
        filename = env.WebRootPath + $@"\{filename}";
        size += file.Length;
        using (FileStream fs = System.IO.File.Create(filename))
        {
            file.CopyTo(fs);
            fs.Flush();
        }
    }
    string message = $"{files.Count} file(s) / 
{ size} bytes uploaded successfully!";
    return Ok(message);
}

Notice that the Post() action returns IActionResult and hence the message is wrapped inside the Ok() method.

So now you have three variants of the file upload API. Check whether all of them are working as expected.

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant, trainer, author, and meditation teacher. He has been programming, meditating, and teaching for 25+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Facebook  Twitter  LinkedIn  YouTube

Posted On : 27 August 2021


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# Visual Studio