Tag Archives: WebAssembly

HxInputFile – Blazor InputFile extension with direct (native) upload and progress indication

Starting with .NET 5 there is a InputFile component in Blazor which allows you to access content of the corresponding file(s) in form of a Stream (IBrowserFile.OpenReadStream).

If you run Blazor on server, the InputFile implementation sends the file content from browser to server through a SignalR connection almost at native speed (approx. 30% slower). In case of Blazor WebAssembly, you have to transfer the file to server on your own.

Due to unmarshalled interop Blazor is able to copy the file to WASM memory extremely quickly (approx. 100MB/sec). You can process the file on client-side (resize image etc.), but if you just want to upload the file, the cost of WASM is relatively high (in comparison to direct HTTP upload) and not suitable for larger files:

Direct (native) upload

To get over the limits of InputFile component in WASM, we can upload the files to server directly from JavaScript using XMLHttpRequest:

var data = new FormData();
data.append('file', file, file.name);

var request = new XMLHttpRequest();
request.open('POST', uploadEndpointUrl, true);
request.send(data);

(Unlike XMLHttpRequest the new fetch() API does not support progress indication.)

Progress indicator

XMLHttpRequest gives you nice progress indication by using the onprogress event:

request.upload.onprogress = function (e) {
    // e.loaded - bytes already uploaded
    // e.total - total upload size (slightly bigger than the file size)
};

HxInputFile / HxInputFileCore

Let’s put this all together and create a new component. We will call it HxInputFileCore (+ the HxInputFile which is a ready-made Bootstrap derivative).

The most important portions follow:

public partial class HxInputFileCore : InputFile, IAsyncDisposable
{
	[Parameter] public string UploadUrl { get; set; }
	[Parameter] public EventCallback<UploadProgressEventArgs> OnProgress { get; set; }
	[Parameter] public EventCallback<FileUploadedEventArgs> OnFileUploaded { get; set; }
	[Parameter] public EventCallback<UploadCompletedEventArgs> OnUploadCompleted { get; set; }
	[Parameter] public bool Multiple { get; set; }
	[Parameter] public string Id { get; set; } = "hx" + Guid.NewGuid().ToString("N");

	[Inject] protected IJSRuntime JSRuntime { get; set; }

	private DotNetObjectReference<HxInputFileCore> dotnetObjectReference;
	private IJSObjectReference jsModule;

	public HxInputFileCore()
	{
		dotnetObjectReference = DotNetObjectReference.Create(this);
	}

	protected override void OnParametersSet()
	{
		base.OnParametersSet();

		// TODO Temporary hack as base implementation of InputFile does not expose ElementReference (vNext: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/InputFile.cs)
		AdditionalAttributes ??= new Dictionary<string, object>();
		AdditionalAttributes["id"] = this.Id;
		AdditionalAttributes["multiple"] = this.Multiple;
	}

	public async Task StartUploadAsync(string accessToken = null)
	{
		jsModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/Havit.Blazor.Components.Web/hxinputfilecore.js");
		await jsModule.InvokeVoidAsync("upload", Id, dotnetObjectReference, this.UploadUrl, accessToken);
	}

	[JSInvokable("HxInputFileCore_HandleUploadProgress")]
	public async Task HandleUploadProgress(int fileIndex, string fileName, long loaded, long total)
	{
		var uploadProgress = new UploadProgressEventArgs() { /*...*/	};
		await OnProgress.InvokeAsync(uploadProgress);
	}

	[JSInvokable("HxInputFileCore_HandleFileUploaded")]
	public async Task HandleFileUploaded(int fileIndex, string fileName, long fileSize, string fileType, long fileLastModified, int responseStatus, string responseText)
	{
		var fileUploaded = new FileUploadedEventArgs() { /* ... */ };
		await OnFileUploaded.InvokeAsync(fileUploaded);
	}

	[JSInvokable("HxInputFileCore_HandleUploadCompleted")]
	public async Task HandleUploadCompleted(int fileCount, long totalSize)
	{
		var uploadCompleted = new UploadCompletedEventArgs() { /* ... */
		};
		await OnUploadCompleted.InvokeAsync(uploadCompleted);
	}

	public async ValueTask DisposeAsync()
	{
		// ...
	}
}

…and the supportive JavaScript is:

export function upload(inputElementId, hxInputFileDotnetObjectReference, uploadEndpointUrl, accessToken) {
	var inputElement = document.getElementById(inputElementId);
	var dotnetReference = hxInputFileDotnetObjectReference;
	var files = inputElement.files;
	var totalSize = 0;
	var uploadedCounter = 0;

	for (var i = 0; i < files.length; i++) {
		(function (curr) {
			var index = curr;
			var file = files[curr];
			totalSize = totalSize + file.size;

			var data = new FormData();
			data.append('file', file, file.name);

			var request = new XMLHttpRequest();
			request.open('POST', uploadEndpointUrl, true);

			if (accessToken) {
				request.setRequestHeader('Authorization', 'Bearer ' + accessToken);
			}

			request.upload.onprogress = function (e) {
				dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadProgress', index, file.name, e.loaded, e.total);
			};
			request.onreadystatechange = function () {
				if (request.readyState === 4) {
					dotnetReference.invokeMethodAsync('HxInputFileCore_HandleFileUploaded', index, file.name, file.size, file.type, file.lastModified, request.status, request.responseText);
				};

				uploadedCounter++;
				if (uploadedCounter === files.length) {
					dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadCompleted', files.length, totalSize);
				}
			}

			request.send(data);
		}(i));
	}
}

The component is part of open-sourced library HAVIT Blazor published on GitHub.

Usage

<HxInputFile @ref="hxInputFileComponent" Label="HxInputFile" UploadUrl="/file-upload-streamed/" OnProgress="HandleProgress" OnFileUploaded="HandleFileUploaded" OnUploadCompleted="HandleUploadCompleted" Multiple="true" />

<HxButton Text="Upload" OnClick="HandleUploadClick" />

@code
{
	private HxInputFile hxInputFileComponent;

	private async Task HandleUploadClick()
	{
		files.Clear();

		string accessToken = null;
		
		var accessTokenResult = await ... // use IAccessTokenProvider
		await hxInputFileComponent.StartUploadAsync(accessToken);
	}

	private Task HandleProgress(UploadProgressEventArgs progress)
	{
		// indicate progress here
	}

	private Task HandleFileUploaded(FileUploadedEventArgs fileUploaded)
	{
		// individual file uploaded
	}

	private Task HandleUploadCompleted(UploadCompletedEventArgs uploadCompleted)
	{
		// all files uploaded
	}
}

TODOs

The presented component if not feature complete. There is some more work to do:

  • Maximum file size limit
  • Limit number of files being uploaded in parallel
  • Better error-handling
  • …?

Links

See also

Blazor WebAssembly with gRPC-Web code-first approach

Do you like WCF-like approach and need to cover communication in between ASP.NET Core service and Blazor WebAssembly client? Use code-first with gRPC-Web! You can try the it right now by following a few simple steps (GitHub repo):

1. Blazor.Server – Prepare the ASP.NET Core host

Add NuGet packages:

  1. Grpc.AspNetCore.Web (prerelease)
  2. protobuf-net.Grpc.AspNetCore

Register CodeFirstGrpc() and GrpcWeb() services in Startup.cs ConfigureServices() method:

services.AddCodeFirstGrpc(config => { config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal; });

Add GrpcWeb middleware in between UseRouting() and UseEndpoints():

app.UseGrpcWeb(new GrpcWebOptions() { DefaultEnabled = true });

2. Blazor.Shared – Define the service contract (code-first)

Add System.ServiceModel.Primitives NuGet package.

Define the interface of your service:

[ServiceContract]
public interface IMyService
{
	Task DoSomething(MyServiceRequest request);

}

[DataContract]
public class MyServiceResult
{
	[DataMember(Order = 1)]
	public string NewText { get; set; }

	[DataMember(Order = 2)]
	public int NewValue { get; set; }
}

[DataContract]
public class MyServiceRequest
{
	[DataMember(Order = 1)]
	public string Text { get; set; }

	[DataMember(Order = 2)]
	public int Value { get; set; }
}

3. Blazor.Server – Implement and publish the service

Implement your service:

public class MyService : IMyService
{
	public Task DoSomething(MyServiceRequest request)
	{
		return Task.FromResult(new MyServiceResult()
		{
			NewText = request.Text + " from server",
			NewValue = request.Value + 1
		});
	}
}

Publish the service in Startup.cs:

app.UseEndpoints(endpoints =>
{
	endpoints.MapGrpcService();
	// ...
}

4. Blazor.Client (Blazor Web Assembly) – consume the service

Add NuGet packages:

  1. Grpc.Net.Client
  2. Grpc.Net.Client.Web (prerelease)
  3. protobuf-net.Grpc

Consume the service in your razor file:

var handler = new Grpc.Net.Client.Web.GrpcWebHandler(Grpc.Net.Client.Web.GrpcWebMode.GrpcWeb, new HttpClientHandler());
using (var channel = Grpc.Net.Client.GrpcChannel.ForAddress("https://localhost:44383/", new Grpc.Net.Client.GrpcChannelOptions() { HttpClient = new HttpClient(handler) }))
{
	var testFacade = channel.CreateGrpcService();
	this.result = await testFacade.DoSomething(request);
}

(You can move the plumbing to ConfigureServices() and use pure dependency injection in your razor files.)

References

  1. Steve Sanderson: Using gRPC-Web with Blazor WebAssembly
  2. Use gRPC in browser apps | Microsoft Docs
  3. protobuf-net.Grpc – Getting Started