In the previous article, we have created a form where we had to manually type the image URL. But of course, this is not the solution we are aiming for. In this article, we are going to modify that logic and learn about file upload with Blazor WebAssembly. We are going to add the server logic first and then we are going to implement the client-side upload functionality.
For the complete navigation for this series, you can visit the Blazor Series page.
Time to start.
File Upload – ASP.NET Core Web API Implementation
That said, let’s start with the Web API implementation.
The first thing we are going to do is to create a new StaticFiles
folder and inside a new Images
folder. Then, let’s create a new Upload
controller and modify it with a new Upload
action:
[Route("api/upload")] [ApiController] public class UploadController : ControllerBase { [HttpPost] public IActionResult Upload() { try { var file = Request.Form.Files[0]; var folderName = Path.Combine("StaticFiles", "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(dbPath); } else { return BadRequest(); } } catch (Exception ex) { return StatusCode(500, $"Internal server error: {ex}"); } } }
We extract the file from the Request
and create paths for the local storage and the database. After that, we just copy the file to the stream and return the path for the database. Additionally, take note that you have to include the System.Net.Http.Headers
for the ContentDispositionHeaderValue
class.
Now, we have to make our StaticFiles
folder servable for the client application:
app.UseStaticFiles(); app.UseStaticFiles(new StaticFileOptions() { FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"StaticFiles")), RequestPath = new PathString("/StaticFiles") });
And that’s all it takes.
Take note that we are not disabling the request size limit, because we are going to restrict the size of the file we want to upload.
File Upload with Blazor WebAssembly
With the server-side in place, we can continue with the File Upload with Blazor WebAssembly.
We are going to show you how to use a third-party library to upload our files (if we use the older version of Blazor WebAssembly), and then how to do the same thing using without a third-party library supported from .NET 5 and above.
The first thing we are going to do is to install the Tewr.Blazor.FileReader
library:
After the installation, we have to register it in the Program.cs
class:
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri("https://localhost:5011/api/") }); builder.Services.AddScoped<IProductHttpRepository, ProductHttpRepository>(); builder.Services.AddFileReaderService(o => o.UseWasmSharedBuffer = true); await builder.Build().RunAsync();
The AddFileReaderService
method resides in the Tewr.Blazor.FileReader
namespace. Additionally, we are using the UseWasmSharedBuffer
to speed up the upload process. This option is not available for the server-side Blazor.
Now, let’s modify the IProductHttpRepository
interface:
public interface IProductHttpRepository { Task<PagingResponse<Product>> GetProducts(ProductParameters productParameters); Task CreateProduct(Product product); Task<string> UploadProductImage(MultipartFormDataContent content); }
And the ProductHttpRepository
class:
public async Task<string> UploadProductImage(MultipartFormDataContent content) { var postResult = await _client.PostAsync("https://localhost:5011/api/upload", content); var postContent = await postResult.Content.ReadAsStringAsync(); if (!postResult.IsSuccessStatusCode) { throw new ApplicationException(postContent); } else { var imgUrl = Path.Combine("https://localhost:5011/", postContent); return imgUrl; } }
Creating Component
To continue, let’s create a new ImageUpload
component (with both razor and cs files) in the Shared
folder.
We are going to modify the razor file first:
<input type="file" @ref="_input" @onchange="HandleSelected" accept=".jpg, .jpeg, png" /> @if (ImgUrl != null) { <div> <img src="@ImgUrl" style="width:300px" /> </div> }
So, here we use the input file control with the @ref
directive, @onchange
event, and the accept
attribute to restrict the file types we want to allow. Then, we display our uploaded image if the ImgUrl
property is not null. This property will be populated as soon as the upload action is finished. Additionally, we are going to set this property as a parameter because we are going to use this component for the Edit form as well. Of course, when we edit the product, it already has the image path assigned, and all we have to do is to pass that path to the Upload component.
Now, we have to modify the class file:
public partial class ImageUpload { private ElementReference _input; [Parameter] public string ImgUrl { get; set; } [Parameter] public EventCallback<string> OnChange { get; set; } [Inject] public IFileReaderService FileReaderService { get; set; } [Inject] public IProductHttpRepository Repository { get; set; } private async Task HandleSelected() { foreach (var file in await FileReaderService.CreateReference(_input).EnumerateFilesAsync()) { if (file != null) { var fileInfo = await file.ReadFileInfoAsync(); using (var ms = await file.CreateMemoryStreamAsync(4 * 1024)) { var content = new MultipartFormDataContent(); content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); content.Add(new StreamContent(ms, Convert.ToInt32(ms.Length)), "image", fileInfo.Name); ImgUrl = await Repository.UploadProductImage(content); await OnChange.InvokeAsync(ImgUrl); } } } } }
Here, we start with the _input
variable of type ElementReference
that we use for the @ref
directive. Then, we have two parameters and two injected services. The IFileReaderService
lives inside the Tewr.Blazor.FileReader
namespace, so you have to include that as well. As soon as we select our file, the HandleSelected
method will execute. In the foreach loop, we can see how to use the FileReaderReference
service and the _input
variable to enumerate and return all the files we select from the input file control.
If a file exists, we extract the info of the file and create a memory stream with a specified buffer size. Then, we create a MultipartFormDataContent
object to use as a body for our POST request. This class lives in the System.Net.Http
namespace, so you have to include it. After adding the value for the content-disposition header, we add a new StreamContent to our content variable. We have to include the System.Net.Http.Headers
namespace for the ContentDispositionHeaderValue
class.
After we create our content, we just call a repository method and invoke a parent method with the ImgUrl
parameter.
CreateProduct Component Modification
Finally, let’s modify the CreateProduct.razor
file, by removing the text box for the image and adding our newly created component:
<div class="form-group row"> <label for="image" class="col-md-2 col-form-label">Image:</label> <div class="col-md-10"> <ImageUpload OnChange="AssignImageUrl" /> </div> </div>
And let’s add the AssignImageUrl
method to the class file:
private void AssignImageUrl(string imgUrl) => _product.ImageUrl = imgUrl;
And we are done. Now, let’s see how this works in practice:
Great job. We can see that the upload functionality is working as we expected alongside the creation action.
Working With .NET 5 and Above
From .NET 5, we have the possibility to use a built-in InputFile component to upload our files. We don’t have to use any third-party libraries.
So, the prerequisites for this are having a .NET 5 installed and also having VisualStudio 16.8.0 (or above) version installed.
The implementation is almost the same as we have in this article, but of course, you don’t need any third-party library and you don’t need its registration in the Program
class.
We have to apply a small modification to the ImageUpload.razor
file:
<InputFile OnChange="@HandleSelected" multiple /> @if (ImgUrl != null) { <div> <img src="@ImgUrl" style="width:300px" /> </div> }
As you can see, this time we are using the InputFile component instead of the input element.
Then, we have to modify the ImageUpload.razor.cs
file:
public partial class ImageUpload { [Parameter] public string ImgUrl { get; set; } [Parameter] public EventCallback<string> OnChange { get; set; } [Inject] public IProductHttpRepository Repository { get; set; } private async Task HandleSelected(InputFileChangeEventArgs e) { var imageFiles = e.GetMultipleFiles(); foreach (var imageFile in imageFiles) { if(imageFile != null) { var resizedFile = await imageFile.RequestImageFileAsync("image/png", 300, 500); using (var ms = resizedFile.OpenReadStream(resizedFile.Size)) { var content = new MultipartFormDataContent(); content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); content.Add(new StreamContent(ms, Convert.ToInt32(resizedFile.Size)), "image", imageFile.Name); ImgUrl = await Repository.UploadProductImage(content); await OnChange.InvokeAsync(ImgUrl); } } } } }
Here, we don’t have the FileReaderService
injected anymore because we don’t need it. We have the InputFileChangeEventArgs e
parameter from which we can extract our files with the e.GetMultipleFiles
method. Then, we iterate through all the files, and if the file exists, we use the RequestImageFileAsync
method to request another file with the provided type and dimensions. After that, we just open the stream, create a MultiPartFormDataContent
variable, populate it, and send it to the repository method as we did with the previous implementation.
As you can see, the implementation is pretty similar to the one we previously used. But if you are working with .NET 5 project, it is better to use this implementation instead.
Conclusion
We have covered a lot of ground in this series and this article was no exception. We have learned how to create server-side upload functionality, how to create a reusable upload component to Upload File with Blazor WebAssembly, and how to use the result from the server to attach it to the object that we want to create.
So, the next thing we have to do is to learn how to handle PUT and DELETE requests in Blazor WebAssembly, and that’s exactly what we are going to do in the next article. Additionally, we are going to show you how to use javascript functions inside the Blazor WebAssembly applications.