loading...
Cover image for Blazor and Dragons! How to Consume gRPC-web From Blazor

Blazor and Dragons! How to Consume gRPC-web From Blazor

mizrael profile image David Guida Originally published at davideguida.com ・4 min read

Hi All! Today we’re going to talk about how to consume a gRPC service from a Blazor client. And we’re going to do it with Dungeons & Dragons!

People who know me, know that deep down, I’m a big nerd. I started playing D&D when I was a freshman in college and kept going for years. Eventually, I began writing my own campaigns, holding sessions as Dungeon Master, and even participating in competitions. And winning.

I play Baldur’s Gate 2 at least once a year. Each time with a different class/race. Oh boy, that game is massive!

The last game I bought on Steam? Icewind Dale. Of course, I played it in the past but never had the pleasure of “owning” it.

So what does all of this with Blazor and gRPC? Well, a few days ago I was looking for a fun way to study them both. And I thought: is there anything out in the interwebz that I can leverage?

And the answer, of course, was yes: the D&D 5e REST API! It’s free and doesn’t require any auth so it’s a perfect way to feed some data in our app.

My goal? Well as I said, I just wanted to play with Blazor and gRPC. And have some fun in the meantime.

So I wrote a simple Blazor webassembly gRPC client to display an archive of D&D classes. You can click on a row and get redirected to a detail page. Easy peasy.

The data is coming from a separate application, exposing a gRPC service. This server is basically just a simple proxy over the D&D REST API. I deliberately decided to not add any caching or other fancy things, just to focus on the transport.

Now, due to browser limitations, we can’t really use gRPC here, but we can rely on gRPC-web instead. Let me quote James Newton-King:

It is impossible to implement the gRPC HTTP/2 spec in the browser because there is no browser API with enough fine-grained control over HTTP requests. gRPC-Web solves this problem by being compatible with HTTP/1.1 and HTTP/2.

For those interested, the full article is here.

Yes, before you say it, I’m not good at naming things.

As usual, the code is available on GitHub, help yourself.

Let’s explore the client today. The first step is to add our .proto file, defining the layout of our messages:

syntax = "proto3";
import "google/protobuf/empty.proto";
option csharp_namespace = "BlazorAndDragons.Client";

package classes;

service Classes {
    rpc GetAll (google.protobuf.Empty) returns (GetAllResponse);
    rpc GetDetails(GetDetailsRequest) returns (GetDetailsResponse);
}

message GetAllResponse {
    message ClassArchiveItem{
        string id = 1;
        string name = 2;
    } 

    repeated ClassArchiveItem data = 1;
}

message GetDetailsRequest{
    string id = 1;
}

message GetDetailsResponse{
    string id=1;
    string name=2;
    int32 hitDie=3;
    repeated Proficiency proficiencies=4;

    message Proficiency{
        string name=1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Not much to say here. We have two methods, one returning all the available classes (and taking no parameter: thank you google.protobuf.Empty ). The other one instead returns the class details given the id.

Notice how I’ve leveraged the repeated keyword to define arrays of complex objects.

Now, an extremely important step is to make sure our .proto definition is properly referenced in our .csproj file:

<ItemGroup>
    <Protobuf Include="Protos\classes.proto" GrpcServices="Client" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

That GrpcServices=”Client” will basically tell Visual Studio to generate the client proxy classes for us. Quite handy indeed.

Now we have to plug the client in our DI container in our Program.cs:

var builder = WebAssemblyHostBuilder.CreateDefault(args);           
builder.Services.AddSingleton(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var serverUrl = config["ServerUrl"];
    var channel = GrpcChannel.ForAddress(serverUrl, new GrpcChannelOptions
                {
                    HttpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler())
                });
    var client = new Classes.ClassesClient(channel);
    return client;
 });
Enter fullscreen mode Exit fullscreen mode

As you can see, I’m using GrpcWebMode.GrpcWeb here. I’m not doing any streaming, just unary calls so no need to use GrpcWebMode.GrpcWebText. This gives me the benefit of smaller messages:

*GrpcWebMode.GrpcWeb*

*GrpcWebMode.GrpcText*

For those interested, here’s a nice article explaining the difference between unary and streamed calls.

Now the last step: let’s call our service! That’s actually the easy part:

@page "/"
@using Google.Protobuf.WellKnownTypes
@inject Classes.ClassesClient Client

<h1>Classes</h1>

@if (_classes == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in _classes)
            {
                <tr>
                    <td><a href="/class/@item.Id">@item.Name</a></td>
                </tr>
            }
        </tbody>
    </table>
}
@code {
    private GetAllResponse.Types.ClassArchiveItem[] _classes;
    protected override async Task OnInitializedAsync()
    {
        var results = await this.Client.GetAllAsync(new Empty());
        this._classes = results?.Data?.ToArray();
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the Client is injected and invoked during page initialization. Stop. No strings attached. Told you t’was easy.

In another article we’ll take a look at our server instead. Thanks for reading!

Discussion

pic
Editor guide