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!!