In the previous article, we have created a form where we had to manually type the image URL. But of course, this is not a 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.

To download the source code for this article, you can visit the File Upload with Blazor WebAssembly repository.

For the complete navigation for this series, you can visit the Blazor Series page.

We are going to divide this article into the following topics:

File Upload – ASP.NET Core Web API Implementation

Before we start with the Web API upload implementation, we want to mention that we have a great article on this topic and if you like to learn more about this topic, feel free to read it.

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 a 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.

The first thing we are going to do is to install the Tewr.Blazor.FileReader library:

File Upload with Blazor WebAssembly Library Tewr.Blazor.FileReader

After the installation, we have to register it in the Program.cs class:

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("app");

        builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
        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 it 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 stream content 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:

File Upload with Blazor WebAssembly example

Great job. We can see that the upload functionality is working as we expected alongside the creation action.

Working with .NET 5

From the .NET 5 RC1, 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 RC1 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.

We have to apply a small modification to the ImageUpload.razor file:

<h3>ImageUpload</h3>

<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 use in this article. 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 our 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.