Tracking upload or download progress in Blazor WASM with Refit

Edit: See Christian J.s comment below for an explanation of why this happens and a more elegant solution than what I propose.

Refit is a popular library for building REST API clients in .NET. It simplifies the process of defining API endpoints and generating strongly-typed client code from them. However, Refit does not include a way to track the upload or download progress. I will in the following code example show you one way of adding this functionality to Refit.

To help Refit track the upload progress we need the help of this nuget package.

dotnet add package System.Net.Http.Handlers

It provides the ProgressMessageHandler class that intercepts HTTP requests and responses and raises events that provide information about the progress of the upload/download operation.

First off lets add some code to our Program.cs file.

builder.Services.AddSingleton<UploadProgessEvents>();

builder.Services
    .AddRefitClient<IApiClient>()
    .ConfigureHttpClient(c =>
        {
            c.BaseAddress = new Uri(builder.Configuration["Api"]!);
        })
    .AddHttpMessageHandler(x =>
    {
        var uploadProgressEvents = x.GetService<UploadProgessEvents>();

        var ph = new ProgressMessageHandler();
        ph.HttpSendProgress += (obj, args) => uploadProgressEvents!.InitiateSendProgressEvent(obj, args);       // Fire custom events registered from components
        ph.HttpReceiveProgress += (obj, args) => uploadProgressEvents!.InitiateReceiveProgressEvent(obj, args); // Fire custom events registered from components
        return ph;
    });

Program.cs

There are two things worth explaining here.

1. Before we add Refit we add a UploadProgessEvents singleton. This is our custom class that holds the events our Blazor components can subscribe to. These will be raised every time there is a progress update. It is important that this is a singleton so that the same instance is used both when the message handler is created and in the components where it is used.

public class UploadProgessEvents
{
    public event EventHandler<HttpProgressEventArgs> HttpSendProgressHandlers;
    public event EventHandler<HttpProgressEventArgs> HttpReceiveProgressHandlers;

    public void InitiateSendProgressEvent(object? obj, HttpProgressEventArgs args)
        => HttpSendProgressHandlers?.Invoke(obj, args);

    public void InitiateReceiveProgressEvent(object? obj, HttpProgressEventArgs args)
        => HttpReceiveProgressHandlers?.Invoke(obj, args);
}

UploadProgressEvents.cs

2. Inside the AddHttpMessageHandler we get our UploadProgessEvents singleton and create a new ProgressMessageHandler. Finally we add our custom events to the events of the ProgressMessageHandler so they are raised every time there is a progress update.

Next up is our Blazor component. Here I only demonstrate uploading, but we already included the necessary code to track download progress as well if needed.

@implements IAsyncDisposable

<InputFile OnChange="@LoadFiles" multiple />

@code {
	[Inject]
    IApiClient _apiClient { get; set; }

    [Inject]
    UploadProgessEvents _progessEvents { get; set; }
    
	private long TotalBytesToTransfer;
    
	protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
            return;

        // Register event handler for updating gui on upload progress
        _progessEvents.HttpSendProgressHandlers += UploadProgress;
    }
    
    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
     	var filesToUpload = e.GetMultipleFiles();
        TotalBytesToTransfer = filesToUpload.Sum(f => f.Size);
        
        var streamParts = e.GetMultipleFiles().Select(x => new StreamPart(x.OpenReadStream(ServerConfiguration.MaxUploadSize), x.Name, x.ContentType));
        await _apiClient.UploadMedia(streamParts);
        foreach (var streamPart in streamParts)
            streamPart.Value.Close();
    }
    
    private async void UploadProgress(object? obj, HttpProgressEventArgs args)
    {
        Console.WriteLine($"Uploading {currentUploadingSegment.File.Name}. {(double)args.BytesTransferred / TotalBytesToTransfer * 100}% complete");
    }
    
    public async ValueTask DisposeAsync()
    {
    	// Unsubscribe to topic
        _progessEvents.HttpSendProgressHandlers -= UploadProgress;
    }
}

FileUpload.razor

In OnAfterRenderAsync we register our UploadProgress event handler that will be triggered every time there is a progress update.

When the user has selected the files they want to upload the LoadFiles method is triggered. Here we first calculate the total amount of bytes that is to be uploaded to the server. Then we create an array of StreamPart objects that contain the files to upload. Finally we then call the UploadMedia method on the Refit client to start uploading the files.

In the UploadProgress method we first get the bytes transferred and write the completion percentage to console.

Notice we also implement the IAsyncDisposable interface and remove our event handler when the component is disposed.

One issue I encountered while implementing this solution was that I only got a value for the BytesTransferred property of the HttpProgressEventArgs object, and not for the TotalBytes or ProgressPercentage properties. I am are not sure why this was the case, but the BytesTransferred property is sufficient for tracking the progress of the upload.

In conclusion, while Refit is a powerful library for building type-safe HTTP clients in .NET, it lacks built-in support for tracking file upload progress. Fortunately, by leveraging the ProgressMessageHandler class in the System.Net.Http.Handlers nuget, we implement upload progress updates on our own. By following the code examples provided in this post, you should be able to successfully implement file upload progress tracking in your Blazor applications using Refit.