BlazorT3_1200x303

This post reivews how to get started with gRPC within your Blazor apps. We'll start with a basic Blazor WebAssembly app being hosted from an ASP.NET Core server, then modify it to use gRPC.

The standard communication protocol between frontend clients—like Blazor—and servers have been RESTful APIs using JSON structured data as the default for more than a few years now. The most typical implementations follow strict <verb> /resource patterns, which are great for uniformity, but it does make things a little hard to model in many scenarios. Sometimes it would be great if you could just use more human-readable text like addItem, createOrUpdateEvent, or addAttendeeToEvent, which can be a little hard with the strict requirements of REST.

This is where gRPC steps into the picture and can address some of these issues. gRPC is a re-imagining of RPC (Remote Procedure Call) with cross-language benefits. gRPC allows your frontend code to call backend servers using protocol buffers in a language-agnostic manner and allows you to pre-define objects to send and receive data. Another added benefit of using gRPC is the data can be transmitted in binary format, which can see a huge reduction in the amount of data transmitted over the wire, thereby greatly improving performance.

Recently, Microsoft has released gRPC for Web for Blazor to allow us to take full advantage of this new protocol to send data back and forth from the server. The great thing about this release is that gRPC has first-class support within Blazor as opposed to being patched on as an afterthought.

I’m going to go over how easy it is to get started with gRPC within your Blazor apps by starting with a basic Blazor WebAssembly app being hosted from an ASP.NET Core server, then modifying it to use gRPC. Let’s jump into it.

Creating Our Basic Blazor Project

First, we are going to start by setting up a basic Blazor app and Server within the same project. To get started, we just want to create our Blazor app and Server using the standard Blazor WebAssembly template and Server Template.

Creating new Blazor WebAssembly app for server project

Next up, we are going to create the server to host our Blazor WebAssembly app.

Creating new ASP.NET Core Web Applications for our server project

Now that we have a working Blazor app, we are going to start modifying the endpoints to be served over gRPC along with modifying the Blazor app.

Here's a working example of the code at this point.

Adding gRPC Endpoints to Server

We are going to start by modifying the server to support gRPC endpoints before we update the client. This will allow us to add support for gRPC without breaking our system. To start, we are going to modify the server by adding NuGet packages for gRPC:

  • Add NuGet packages to Server
    • Grpc.AspNetCore
    • Grpc.AspNetCore.Web

After the packages are added, we need to add our BeachConditions protocol definition into Protos/BeachConditions.proto. The proto file is going to define the service calls along with the inputs and outputs. One of the big benefits of defining our service using protos is that the code will be generated for us at compile-time, we will just have to fill in the implementation for each service call.

syntax = "proto3";

option csharp_namespace = "Protos";

package beachConditions;

// The beach conditions services that define the calls
service BeachConditions {
  rpc getAllBeachConditions (GetAllBeachConditionRequest) returns (GetAllBeachConditionResponse);
  rpc createBeachCondition (CreateBeachConditionRequest) returns (CreateBeachConditionResponse);
}

// request object for getAllBeachConditions
message GetAllBeachConditionRequest {
}

//response object for getAllBeachConditions
message GetAllBeachConditionResponse {
  repeated BeachCondition beachConditions = 1;
}

// request object for createBeachCondition
message CreateBeachConditionRequest {
    string name = 1;
    string condition = 2;
}

// response object for createBeachCondition
message CreateBeachConditionResponse {
    BeachCondition createdBeachCondition = 1;
}

// common object
message BeachCondition {
    int64 beachId = 1;
    string name = 2;
    string condition = 3;
}

For reference: BeachConditions.proto.

After adding our common definition, we need to add our C# service code for this protocol buffer. So, we are going to add Services/BeachConditionsService.cs. Earlier I said that the .proto file would generate the service for us, and it has by generating the BeachConditions.BeachConditionsBase. It is up to us to implement the code.

using Grpc.Core;
using Microsoft.Extensions.Logging;
using Protos;
using System;
using System.Threading.Tasks;

namespace Server.Services
{
    public class BeachConditionsService : BeachConditions.BeachConditionsBase
    {
        private readonly ILogger<BeachConditionsService> _logger;
        public BeachConditionsService(ILogger<BeachConditionsService> logger)
        {
            _logger = logger;
        }

        public override Task<GetAllBeachConditionResponse> getAllBeachConditions(GetAllBeachConditionRequest request, ServerCallContext context)
        {
            GetAllBeachConditionResponse result = new GetAllBeachConditionResponse();
            result.BeachConditions.Add(new BeachCondition { BeachId = 1, Name = "North Point", Condition = "Sandy" });
            result.BeachConditions.Add(new BeachCondition { BeachId = 2, Name = "South Point", Condition = "Murky" });

            return Task.FromResult(result);
        }

        public override Task<CreateBeachConditionResponse> createBeachCondition(CreateBeachConditionRequest request, ServerCallContext context)
        {
            BeachCondition beachCondition = new BeachCondition
            {
                BeachId = new Random().Next(),
                Name = request.Name,
                Condition = request.Condition
            };

            CreateBeachConditionResponse response = new CreateBeachConditionResponse
            {
                CreatedBeachCondition = beachCondition
            };

            return Task.FromResult(response);
        }
    }
}

For reference: BeachConditionsService.cs.

Now that we have defined our service along with hooking it up to our implementation, we need to add it to the server to allow it to be exposed. In order to do this, we need to modify our Startup.cs file in the Server project.

  • Add services.AddGrpc(); to the ConfigureServices method
  • Add app.UseGrpcWeb(); to the Configure method
  • Add endpoints.MapGrpcService<BeachConditionsService>().EnableGrpcWeb(); to the Configure method’s endpoint configuration
    • This line injects the BeachConditionsService into the server along with telling our server that it should be hosted over gRPC web

For reference: Startup.cs.

At this point, our server can run and host gRPC and non-gRPC endpoints, along with hosting our Blazor app. This is a pretty important milestone because you will notice that we are hosting gRPC and REST API calls from the same server along with hosting the Blazor app. You can definitely use this to your advantage as you migrate over toward using gRPC where it makes sense.

Modifying our App to Support gRPC

Now it is time for us to update our Blazor client. To start, we are going to need to add some NuGet packages along with adding references within _imports.razor:

  • Add NuGet packages to Client
    • Grpc.Net.Client.Web
    • Grpc.Net.Client
    • Grpc.Tools
    • Google.Protobuf
  • Add @using Grpc.Net.Client to the _imports.razor

Now that our Blazor app has all the necessary references, we need to start injecting some services to allow us to access gRPC channels at the page level. First, we are going to modify Program.cs by adding the code to define the gRPC channel.

            //Add gRPC service
            builder.Services.AddSingleton(services =>
            {
                // Get the service address from appsettings.json
                var config = services.GetRequiredService<IConfiguration>();
                var backendUrl = “https://localhost:5001”;

                // 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.GrpcWebText, new HttpClientHandler());

                return GrpcChannel.ForAddress(backendUrl, new GrpcChannelOptions { HttpHandler = httpHandler });
            });

The only thing left to do is define the protocol buffer in the Blazor client and then create a page to display the results. The protocol buffer definition is going to be an exact copy of what we defined for our server. There are many different ways to share this data between clients, I’m just copy/pasting the file to make the example a little more straightforward. You can just as easily create a separate library that is shared by both projects. You can see where we have placed the BeachConditions.proto by checking out the project here.

Now, let us add our new page that calls out to the server over gRPC.

@page "/beachconditions"
@inject GrpcChannel Channel
@using Google.Protobuf.WellKnownTypes

<h1>Beach Conditions</h1>

<p>Conditions at different points on the coast line.</p>

@if (beachConditions == null)
{
    <p><em>Loading Beach Conditions...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>BeachId</th>
                <th>Name</th>
                <th>Condition</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var beachCondition in beachConditions)
            {
                <tr>
                    <td>@beachCondition.BeachId</td>
                    <td>@beachCondition.Name</td>
                    <td>@beachCondition.Condition</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {

    private IList<Protos.BeachCondition> beachConditions;

    protected override async Task OnInitializedAsync()
    {
        var client = new Protos.BeachConditions.BeachConditionsClient(Channel);

        var getAllBeachConditionsRequest = new Protos.GetAllBeachConditionRequest();

        Protos.GetAllBeachConditionResponse response = await client.getAllBeachConditionsAsync(getAllBeachConditionsRequest);

        beachConditions = response.BeachConditions;
    }
}

For reference: BeachConditions.razor.

If you open the network tab and open the Beach Conditions tab, you’ll notice that the beach condition data was sent over in binary data and displayed out. While the payload is pretty small, you’ll see bigger improvements in performance as the number of fields increases due to the compression gains you receive by using the protocol buffers.

Beach Conditions page with Network tab showing Binary Data

Discuss Improvements of gRPC for Blazor and Other Generic Endpoints

At this point, you have seen how easy it is to make the change and you may be wondering, "Why should I make this change now?" Two of the biggest benefits of gRPC over REST APIs is the strict definition of services/objects and performance.

The contract-based services allow you to model your data with certainty that whatever you send/receive the data will be exactly what is sent since there is no serialization layer. In many JSON payloads, there is a layer of uncertainty when parsing data and validating that certain data is parsed into the correct type. With defined payloads, you’ll know the data is modeled in the correct way.

The other added benefit is the performance (data load times) you’ll get from reducing payload sizes. With protocol buffers, the data is serialized into a binary format that removes unnecessary data like field names and types. Since protobuf fields are defined by their position, it is unnecessary to send over duplicate data (field names) since the other side knows about the payload it’s receiving. The payload reduction will vary depending on your object structure, but the smaller the payload, the fewer packets that need to be sent to complete the request.

Hopefully, you are encouraged to jump in and start adopting gRPC in your project. It has been around for a while and has been used extensively in server-to-server communications and as support grows for browser-to-server communication, it opens up another avenue for it to shine. Happy Coding!

If you want to check out the complete project, check it out on GitHub at this link.


Richard Reedy
About the Author

Richard Reedy

Richard Reedy has been working in the software field for over 12 years. He has worked on everything from operations to backend server development to really awesome frontend UI. He enjoys building great products and the teams around them. His latest venture is enabling technology to better serve food trucks around the United States.

Related Posts

Comments

Comments are disabled in preview mode.