BlazorGrpcWebCodeFirst
                                
                                 BlazorGrpcWebCodeFirst copied to clipboard
                                
                                    BlazorGrpcWebCodeFirst copied to clipboard
                            
                            
                            
                        Sample implementation of gRPC-Web code-first approach in Blazor WebAssembly ASP.NET Core hosted project.
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 (commit):
1. Blazor.Server - Prepare the ASP.NET Core host
Add NuGet packages:
- Grpc.AspNetCore.Web (prerelease)
- 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<MyService>();
	// ...
}
4. Blazor.Client (Blazor Web Assembly) - consume the service
Add NuGet packages:
- Grpc.Net.Client
- Grpc.Net.Client.Web (prerelease)
- protobuf-net.Grpc
4A. Direct consumption of the service
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<IMyService>();
	this.result = await testFacade.DoSomething(request);
}
4B. Consumption via dependency injection
Register a GrpcChannel in your Program.cs (or Startup.cs:ConfigureServices())
builder.Services.AddSingleton(services =>
{
	// Get the service address from appsettings.json
	var config = services.GetRequiredService<IConfiguration>();
	var backendUrl = config["BackendUrl"];
	// If no address is set then fallback to the current webpage URL
	if (string.IsNullOrEmpty(backendUrl))
	{
		var navigationManager = services.GetRequiredService<NavigationManager>();
		backendUrl = navigationManager.BaseUri;
	}
	// Create a channel with a GrpcWebHandler that is addressed to the backend server.
	//
	// GrpcWebText is used because server streaming requires it. If server streaming is not used in your app
	// then GrpcWeb is recommended because it produces smaller messages.
	var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
	return GrpcChannel.ForAddress(
		backendUrl,
		new GrpcChannelOptions
		{
			HttpHandler = httpHandler,
			//CompressionProviders = ...,
			//Credentials = ...,
			//DisposeHttpClient = ...,
			//HttpClient = ...,
			//LoggerFactory = ...,
			//MaxReceiveMessageSize = ...,
			//MaxSendMessageSize = ...,
			//ThrowOperationCanceledOnCancellation = ...,
		});
});
Register the individual services (you might want to extract the "logic" to an extension method for better readability).
builder.Services.AddTransient<IMyService>(services =>
{
	var grpcChannel = services.GetRequiredService<GrpcChannel>();
	return grpcChannel.CreateGrpcService<IMyService>();
});
And now you can consume the services whereever needed (e.g. from .razor file):
@inject	IMyService MyService
@code
{
	async Task Submit()
	{
		this.result = await MyService.DoSomething(request);
	}
}
Advanced scenarios
For more advanced usage with error-handling, authentication + authorization and more, see our Havit.Blazor project template:
- https://github.com/havit/NewProjectTemplate-Blazor
References, Credits
- Steve Sanderson: Using gRPC-Web with Blazor WebAssembly
- Use gRPC in browser apps | Microsoft Docs
- protobuf-net.Grpc - Getting Started
- https://github.com/grpc/grpc-dotnet/blob/master/examples/Blazor/Client/Program.cs
- credits to @dani-herrera-udg for upgrading to .NET 6
Known Issues
- https://github.com/dotnet/runtime/issues/62054 - may be hitting GC bug in .NET6