Twitter About Home

Using gRPC-Web with Blazor WebAssembly

gRPC-Web is a convenient, high-performance alternative to JSON-over-HTTP for single-page applications

Published Jan 15, 2020

If you already know all about gRPC and gRPC-Web, you can skip ahead to adding gRPC services to a Blazor WebAssembly application. And if you just want some sample Blazor WebAssembly + gRPC-Web apps, see this repo.

The status quo

In Blazor WebAssembly, like in all other browser-based single-page app (SPA) technologies, by far the most common way to exchange data and trigger server-side operations is JSON-over-HTTP. It’s simple: the client makes an HTTP request to some pre-agreed URL, with a pre-agreed HTTP method, sending JSON data in a pre-agreed shape, and then the server performs an operation and replies with a pre-agreed HTTP-status code and JSON data in a pre-agreed shape.

This generally works well, and people who do it can often lead fulfilling lives1. However, two weaknesses stand out:

  • JSON is a very verbose data format. It’s not optimised for bandwidth.
  • There’s no mechanism to guarantee that all these pre-agreed details of URLs, HTTP methods, status codes (etc.) are actually consistent between server and client.

What’s gRPC?

gRPC is a Remote Procedure Call (RPC) mechanism originally developed by Google. For the purposes of a SPA, you can think of it as an alternative to JSON-over-HTTP. It directly fixes the two weaknesses listed above:

  • It’s optimised for minimal network traffic, sending efficient binary-serialised messages
  • You can guarantee at compile time that the server and client agree about what endpoints exist, and what shape of data will be sent and received, without having to specify any URLs/status codes/etc.

So how does this work? To author a gRPC service, you write a .proto file which is a language-independent description of a set of RPC services and their data shapes. From this, you can code-generate strongly-typed server and client classes in whatever language you wish, so that conformity with your protocol is guaranteed at compile time. Then at runtime, gRPC deals with the (de)serialization of your data and sends/receives messages in an efficient format (protobuf by default).

Another great benefit is that it’s not REST, so you don’t have to have constant arguments with coworkers about which HTTP methods and status codes are the most blessed and auspicious ones for your scenario. It’s just simple RPC, which is all we’ve ever really wanted since we were little kids.

Why isn’t everyone already using gRPC in their SPAs?

Traditionally it’s not been possible to use gRPC from browser-based applications, because gRPC requires HTTP/2, and browsers don’t expose any APIs that let JS/WASM code control HTTP/2 requests directly.

But there is a solution! gRPC-Web is an extension to gRPC which makes it compatible with browser-based code (technically, it’s a way of doing gRPC over HTTP/1.1 requests). gRPC-Web hasn’t become prevalent yet because not many server or client frameworks have offered support for it… until now.

ASP.NET Core has offered great gRPC support since the 3.0 release. And now, building on this, we’re about to ship preview support for gRPC-Web on both server and client. If you want to dig into the details, here’s the excellent pull request from James Newton-King where it’s all implemented.

Adding gRPC services to a Blazor WebAssembly application

There’s no project template for this yet, so adding gRPC support to a Blazor WebAssembly app involves quite a lot of steps, detailed here. But the good news is that you only have to do this setup once. When you’re up and running, adding further gRPC endpoints and calling them is absolutely trivial.

First, since the gRPC-Web packages haven’t been published to NuGet.org yet, for now you’ll need to add a couple of temporary package feeds to get the nightly previews.

You can add a NuGet.config file at the root of your solution like this one. Hopefully a month or two from now this won’t be needed.

Adding gRPC services to a Blazor WebAssembly “hosted” application

If you’re hosting a Blazor WebAssembly application on ASP.NET Core server, then by default you have three projects: client, server, and shared. I’ve found that the most convenient place to define a gRPC service is in the shared project, because then its generated classes are available to both server and client.

To get started, edit your shared project’s .csproj to add references to the necessary gRPC packages:

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.11.2" />
    <PackageReference Include="Grpc.Net.Client" Version="2.27.0-dev202001100801" />
    <PackageReference Include="Grpc.Tools" Version="2.27.0-dev202001081219">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

If these packages won’t restore correctly, be sure you added the nightly feeds.

Now you can create a .proto file to define your services. For example, inside your shared project add a file called greet.proto, containing this:

syntax = "proto3";
option csharp_namespace = "GrpcGreeter";
package greet;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

To make the gRPC tooling generate server and client classes from this, go into your shared project’s .csproj and add this:

  <ItemGroup>
    <Protobuf Include="greet.proto" />
  </ItemGroup>

The solution should build without errors at this point.

Exposing a gRPC service from the server

In your server project, create a new class called GreeterService, containing this:

using Grpc.Core;
using GrpcGreeter;

public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        var reply = new HelloReply { Message = $"Hello from gRPC, {request.Name}!" };
        return Task.FromResult(reply);
    }
}

This class inherits from Greeter.GreeterBase, which is generated for you automatically from the .proto file. So when you add new endpoints to the .proto, there will be new methods for you to override here to provide an implementation.

The last bit on the server is to expose this as a gRPC-Web service using the new APIs. You’ll need to reference a package for this, so in your server project’s .csproj, add the following package references:

<PackageReference Include="Grpc.AspNetCore" Version="2.27.0-dev202001100801" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.27.0-dev202001100801" />

Now in your server’s Startup.cs file, modify ConfigureServices to add the following line:

services.AddGrpc();

Note: If you’re only going to expose gRPC services, you might not need MVC controllers any more, in which case you could remove services.AddMvc() and endpoints.MapDefaultControllerRoute() from below.

Just underneath app.AddRouting(); add the following, which deals with mapping incoming gRPC-Web requests to look like gRPC ones to the server:

app.UseGrpcWeb();

Finally, register your gRPC-Web service class inside the app.UseEndpoints block with the following line at the top of the block:

endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb();

That’s it, your gRPC-Web server is ready!

Consuming a gRPC service from the client

In your client project’s .csproj, you’ll want to add references to the following two nightly packages:

<PackageReference Include="Grpc.Net.Client.Web" Version="2.27.0-dev202001100801" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Mono" Version="3.2.0-preview1.20052.1" />

The latter one is to work around an issue in Blazor WebAssembly that will be fixed in the next preview in a couple of weeks. If these packages won’t restore correctly, be sure you added the nightly feeds.

Now set up your client app’s dependency injection system to be able to provide instances of GreeterClient. This will let you invoke the gRPC service from anywhere in your client app. In the client project’s Startup.cs, add the following inside the ConfigureServices method:

services.AddSingleton(services =>
{
    // Create a gRPC-Web channel pointing to the backend server
    var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
    var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;
    var channel = GrpcChannel.ForAddress(baseUri, new GrpcChannelOptions { HttpClient = httpClient });

    // Now we can instantiate gRPC clients for this channel
    return new Greeter.GreeterClient(channel);
});

Note that Greeter.GreeterClient is code-generated for you from the .proto file. You don’t have to implement it manually! But you will also need to add the following using statements to make the above code compile:

using GrpcGreeter;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using Microsoft.AspNetCore.Components;
using System.Net.Http;

We’re very nearly there now! There’s a working server, and hopefully a working client. We just have to actually call some gRPC services from some UI. For example, in your Index.razor file, replace the contents with this:

@page "/"
@using GrpcGreeter
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="yourName" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    string yourName = "Bert";
    string serverResponse;

    async Task GetGreeting()
    {
        var request = new HelloRequest { Name = yourName };
        var reply = await GreeterClient.SayHelloAsync(request);
        serverResponse = reply.Message;
    }
}

Try it out in a browser now. The UI will look like this:

Greeter UI

… and if you check out the requests in the dev tools’ network tab, you’ll see it’s sending and receiving binary protobuf messages:

Greeter request

Summary and sample

Now you’ve got the basics in place, you can go much further and use gRPC for all data exchange between server and client, if you wish. The gRPC tooling will code-generate all the data transfer classes for you, make the network traffic more efficient, as well as eliminate HTTP-over-JSON concerns like URLs, HTTP methods, status codes, and serialization.

For a fractionally more detailed example, here’s a complete Blazor WebAssembly hosted application that uses gRPC for fetching “weather forecast” data. If you’re interested in the exact steps needed to upgrade from the default JSON-based solution to a gRPC-Web one, see this diff that shows exactly what I changed.

Adding gRPC services to a Blazor WebAssembly “standalone” application

If you’re building a pure standalone Blazor WebAssembly application - not hosted on ASP.NET Core - then we can’t make any assumptions about what kind of server you will have. Let’s just say you have some gRPC-Web compatible service endpoint to call. Maybe it’s exposed by an ASP.NET Core server on some other host, or maybe it’s an Envoy gRPC-Web wrapper around another gRPC service. The only thing we’re concerned about here is configuring your Blazor WebAssembly application to consume it.

Most of the steps for setting up your client app are the same as in the “hosted” case above. However in some ways it’s a bit trickier because we can’t rely on some of the assumptions we can make in the “hosted” case. Here are the differences:

Getting and using the .proto file

Presumably your external gRPC service maintainer can supply you with the .proto file defining that service. It’s up to you to copy that into your client project and add a <Proto> item referencing it in your .csproj. For the tooling to work, you’ll also need to add package references like these.

Configuring the client DI service

Like in the “hosted” case, you’ll add code to your Startup.cs to add a gRPC-Web client service. The main difference this time is that you have to know what base URL is used to reach your external service, since we can no longer assume it’s the same base URL that your client app is being served from. So, your DI service entry may look more like the following:

services.AddSingleton(services =>
{
#if DEBUG
    var backendUrl = "https://localhost:5001"; // Local debug URL
#else
    var backendUrl = "https://some.external.url:12345"; // Production URL
#endif

    // Now we can instantiate gRPC clients for this channel
    var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
    var channel = GrpcChannel.ForAddress(backendUrl, new GrpcChannelOptions { HttpClient = httpClient });
    return new Greeter.GreeterClient(channel);
});

This varies the URL based on whether you built in debug mode or not.

Summary and sample

That’s it! Now your standalone Blazor WebAssembly application can consume an external gRPC-Web service. For a complete runnable example, here’s a sample standalone app that invokes a gRPC-Web service on an external URL. This sample comes with an actual gRPC-Web server for testing too, but you can consider that separate. If you want to see exactly what I changed, here’s the diff versus the default project template output.

Your feedback requested

If you want to go further with gRPC, see the ASP.NET Core gRPC docs.

Please give us feedback about your opinions and experiences with gRPC-Web, as this will help us make a choice about how and whether to make gRPC-Web a standard feature in future versions of ASP.NET Core. You can post feedback either as comments here, or perhaps better still on GitHub as an issue with “feedback” in the title.

  1. Fulfilling lives not guaranteed 

READ NEXT

Meet WebWindow, a cross-platform webview library for .NET Core

It's like Electron, but without bundling Node.js or Chromium, and without most of the the APIs.

Published Nov 18, 2019