Blazor WebAssembly: Debugging gRPC-Web with Custom Chrome Developer Tools

If you are working with Blazor, gRPC is a big issue for transferring data from APIs to clients. One issue of developing with gRPC-Web is debugging the transmitted data because the data is in an efficient binary message format. In this article, I will show you how to solve this problem with the help of my NuGet.

In this article:

Motivation

Since I’ve worked with Blazor WebAssemby, gRPC has also been a big topic in getting data from my APIs to my clients. But gRPC is currently not supported by browsers because it is impossible to implement the HTTP/2 gRPC spec in the browser(more here). Therefore, applications running in the browser need an alternative to use gRPC. The choice was gRPC-Web, a stripped-down version of gRPC. With gRPC-Web, sending data from the API to my client and back is possible. If you want to know more about gRPC and how gRPC-Web works in Blazor WebAssembly, check out the talk by my colleague Christian Weyer about “Blazor & gRPC – Code-first .NET SPA developer productivity.”

That means we can use gRPC-Web in the browser and everything is cool? No, unfortunately not.

One problem of developing with gRPC-Web is the debugging of the transferred data. Looking at the following image, we can see the Network tab of Chrome Browser Dev Tools.

grpc-response-protobuf

Here we see a gRPC-Web response that sent back a list of conferences to the client. But unfortunately, this is not readable because the messages are serialized with Protobuf, an efficient binary message format. But what can I do here to make the whole thing more readable?

For some time now, there has been a Chrome browser extension for the gRPC-Web Developer Tools. This extension makes it possible to show the request and response data in a pretty JSON format, as you see in the following picture in this article.
The only problem is that there is no way to use this extension in Blazor WebAssembly yet.

To solve this little problem, I have made it my task to make this handy tool also usable for Blazor WebAssembly 🙂

In this article, I will show you how to turn on the Developer Tools for gRPC-Web in your Blazor WebAssembly application using my Nuget package Thinktecture.Blazor.GrpcWeb.DevTools.

The GitHub repository for this article and also for the NuGet package can be found here.

What are the gRPC-Web Developer Tools, and what can they do?

The two main tasks of gRPC-Web Developer Tools are:

  • Collect and display all gRPC-Web network requests and responses in a list. 
  • Chrome Developer Tools extension for the official gRPC-Web library. It allows you to inspect the gRPC-Web network protocol in Chrome Developer Tools. The transferred data is displayed as a deserialized JSON object rather than in the original ProtoBuf format.

With the Developer Tools, you will get a new tab called gRPC-Web in your Chrome Developer Tools. This tool will list the configured gRPC-Web client network requests. The features are similar to the standard Network tab but only for gRPC-Web requests.

Selecting a network log entry displays the deserialized JSON for the request, response, and any error objects returned by your gRPC-Web server (See the following figure).

gRPC-Web Devloper Tools


Enable gRPC-Web Developer Tools for Blazor WebAssembly

To use the gRPC-Web Developer Tools in your project, you only have to add a few instructions to the Program.cs.
First, register the GrpcChannel class in the dependency injection. This is likely already present in your Blazor WebAssembly client code when you are using the gRPC-Web client.

				
					//Contributions.razor.cs

private GridItemsProvider<Contribution>? _contributionsProvider;
private PaginationState pagination = new PaginationState { ItemsPerPage = 100 };

protected override async Task OnInitializedAsync()
{
    _contributionsProvider = async req =>
    {
        var count = await _contributionService.GetContributionCountAsync(req.CancellationToken);
        var response = await _contributionService.GetContributionsAsync(req.StartIndex, req.Count ?? 100, req.CancellationToken);
        return GridItemsProviderResult.From(
            items: response ?? new(),
            totalItemCount: count);
    };
    pagination.TotalItemCountChanged += (sender, eventArgs) => StateHasChanged();

    await base.OnInitializedAsync();
}

private async Task GoToPageAsync(int pageIndex) =>
    await pagination.SetCurrentPageIndexAsync(pageIndex);

private string? PageButtonClass(int pageIndex)
    => pagination.CurrentPageIndex == pageIndex ? "current" : null;

private string? AriaCurrentValue(int pageIndex)
    => pagination.CurrentPageIndex == pageIndex ? "page" : null;
				
			

In the next step, the gRPC-Web service interfaces can be registered by using the AddGrpcService extension method from the blazor-grpc-web-devtooling Project.

With this extension method, the service is registered in the ServiceCollection with the help of the GrpcChannel and can then be added via DI to the component or a service (more will be explained in the next chapter).

				
					builder.Services.AddGrpcService<IConferencesService>();
builder.Services.AddGrpcService<ITimeService>();
				
			

Note: Of course, the services can also be registered and used without the Developer Tools.

To activate the developer tools, you have two options:
On the one hand, we can call the extension method EnableGrpcWebDevTools() in the Program.cs.

				
					builder.Services.EnableGrpcWebDevTools();
				
			
The second possibility is via appsetting.json. We can add the entry GrpcDevToolsEnabled to enable or disable the developer tools (see more information on this setting in the following section).
				
					{
  "GrpcDevToolsEnabled": true
}
				
			

Both options register an CallInvoker instance, which registers an additional interceptor.
This interceptor extends the base class Interceptor. The interceptor calls the JavaScript method postMessage with the help of JSInterop for each unary and server_streaming call (see more information on this interceptor in the following section).

The interceptor sends the request/response data via window.postMessage to the developer tools. The tools can receive and display the request or response, as seen in the following video snippet.

How it works under the hood

The project consists of three essential parts that work together to send the gRPC-Web requests and responses to the gRPC-Web Developer Tools.

The first part is an interceptor, which extends the base class Interceptor to get the possibility to override the gRPC-Web call methods and add its implementation or extend the existing implementation. The gRPC-Web Developer Tools currently support the two requests, Unary and ServerStreaming, which is overridden in the interceptor class GrpcMessageInterceptor. If one of the two methods is called, the base method is executed, and the request or response is also sent to the gRPC-Web Developer Tools with JSInterop.

				
					public partial class GrpcMessageInterceptor : Interceptor
{
    // ...

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var call = continuation(request, context);

        return new AsyncUnaryCall<TResponse>(
            HandleUnaryCall(context.Method.Name, request, call.ResponseAsync),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose);
    }

    public override AsyncServerStreamingCall<TResponse> 
        AsyncServerStreamingCall<TRequest, TResponse>(
            TRequest request, 
            ClientInterceptorContext<TRequest, TResponse> context, 
            AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        var streamingCall = base.AsyncServerStreamingCall(request, 
            context, continuation);

        var response = new AsyncServerStreamingCall<TResponse>(
                new AsyncStreamReaderWrapper<TResponse>(
                    streamingCall.ResponseStream, 
                    context.Method.Name, 
                    _jsRuntime),
                HandleServerStreamRequest(
                    streamingCall.ResponseHeadersAsync, 
                    request, 
                    context.Method.Name),
                streamingCall.GetStatus, 
                streamingCall.GetTrailers, 
                streamingCall.Dispose
            );

        return response;
    }
   // ....
}
				
			
And here we come to the second important point. With JSInterop, the postMessage-method is called, which sends the data to the gRPC-Web Developer Tools. Since this is a window method, no additional JavaScript file is needed here. The method can be called directly from the JSRuntime, as shown in the following code sample.
				
					internal static async Task HandleGrpcRequest<TRequest, TResponse>(
    this IJSRuntime jsRuntime, 
    string method,
    TRequest request, 
    TResponse response)
        {
            await jsRuntime.InvokeVoidAsync(
                "postMessage", 
                new GrpcDevToolsCall<TRequest, TResponse>(
                    GrpcWebDevToolsExtensionName, 
                    method, 
                    GrpcUnaryMethodName, 
                    request, 
                    response)
                );
        }
				
			

The JavaScript-Method window.postMessage() in the code above is called with the JSRuntime. This call will pass the data to the gRPC-Web Developer Tools. To let the Chrome Developer Tools know which tool to send the data to, the identifier for the gRPC-Web Developer Tools is also sent. The parameters method, GrpcUnaryMethodName, request, and response used by the gRPC-Web Developer Tools to display the requests and responses shown in the video of the previous chapter.

The last part is registering the Interceptor so that all calls will be sent to the gRPC-Web Developer Tools. To do this, I have written an extension method, which you can use to register the gRPC-Web services interfaces.

				
					public static void AddGrpcService<TService>(this IServiceCollection services)
            where TService : class
        {
            services.AddScoped(serviceProvider =>
            {
                var invoker = serviceProvider.GetService<CallInvoker>();
                if (invoker != null)
                    return GrpcClientFactory.CreateGrpcService<TService>(invoker);

                try
                {
                    var enabled = serviceProvider
                                    .GetRequiredService<IConfiguration>()?
                                    .GetValue<bool>(GrpcDevToolsSettingsKey);
                    if (enabled.HasValue && enabled.Value)
                    {
                        if (invoker == null)
                        {
                            var jsRuntime = serviceProvider.GetService<IJSRuntime>();
                            invoker = serviceProvider
                                    .GetService<GrpcChannel>()
                                    .Intercept(new GrpcMessageInterceptor(jsRuntime));
                        }
                        return GrpcClientFactory.CreateGrpcService<TService>(invoker);
                    }
                }
                catch (Exception e)
                {
                    var channel = serviceProvider.GetService<GrpcChannel>();
                    return GrpcClientFactory.CreateGrpcService<TService>(channel);
                }
            });
        }
				
			

In the code above, we see two options to enable the Developer Tools extension. On the one side, we look at the configuration, which we explained in the first part of this article. On the other side, the extension method EnableGrpcWebDevTools checks if the interceptor is already set.

And that’s it. With these three steps, we can use the gRPC-Web Developer Tools in our Blazor WebAssembly project.

Summary

With the help of a few lines of code, we have a much easier time finding errors, debugging network requests
or to better track the progress of requests in a gRPC-Web-enabled end-to-end application.
Feel free to use it. I am open to ideas and improvements and also pull requests 🙂

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
.NET
KP-round
.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.
09.10.2023