gRPC service in .NET 5: Consuming gRPC Service in Blazor WebAssembly Application
In the previous tutorial, we have seen how to perform database operations in gRPC service. In this tutorial, we will see go through the steps to consume the gRPC service in Blazor WebAssembly client application. The Blazor WebAssembly, is the Blazor application hosting model where the .NET dependencies for Blazor WebAssembly application is loaded in the browser. This application executed in the browser as interactive Web-UI as a pure browser application.
The figure 1 shows how the gRPC Service application can be consumed in the Blazor WebAssembly application.
Figure 1: The Blazor WebAssembly application
Note: Make sure that you create gRPC service by following the tutorial of create gRPC service with Entity Framework Core from this link. In this tutorial I have explained about all important packages those are mandatory in gRPC service so that it can be consumed by Browser client applications.
Step 1: Open the Visual Studio 2019 and create a new Blazor WebAssembly project. Name this project as Blazor_Client, make sure that the target framework is .NET 5. The project will be created with Pages folder with default components. The project will also have Shared folder that contains razor files for Navigation, Layout etc.
Step 2: In the project add following NuGet packages
- Google.Protobuf
- Grpc.Net.Client
- Grpc.Net.Client.Web
- Grpc.Tools
syntax = "proto3"; option csharp_namespace = "clientnamespace"; package products; service ProductsService { rpc GetAll (Empty) returns (Products); rpc GetById (ProductRowIdFilter) returns (Product); rpc Post (Product) returns (Product); rpc Put (Product) returns (Product); rpc Delete (ProductRowIdFilter) returns (Empty); } message Empty{ } message Product { int32 ProductRowId = 1; string ProductId = 2; string ProductName = 3; string CategoryName = 4; string Manufacturer = 5; int32 Price = 6; } message ProductRowIdFilter { int32 productRowId = 1; } message Products { repeated Product items = 1; }
Listing 1: The products.proto file with message structures and service contract
The Blazor client application will use Product message structure to send Product message to gRPC service to create and/or update the Product. The ProductRowIdFilter will be used to search and retrieve Product from gRPC service based on ProductRowId. The Products message represents the Product records received from the gRPC service. The Empty message represents that an empty data will be send from client application to gRPC service and empty response will be received. The ProductsService service is a contract that will be used by the client application to communicate with the gRPC service.
Step 4: We need to generate C# client stub code based on the proto file so that client application can use this code to communicate with gRPC service by calling contract methods. Right-Click on the client project and select Edit Project file. In this project file add the markup for proto file registration as shown in listing 2
<ItemGroup> <Protobuf Include="Protos\products.proto" GrpcServices="Client" /> </ItemGroup>
Listing 2: The proto file registration in project file to generate the code
Save the project file. To make sure that the client generates stub class using C# code, right-click on the products.proto file and select properties, the property window will be display. In this window make sure that Build Action is set to Protobuf compiler and gRPC Stub Classes is set to Client only as shown in figure 2
Figure 2: The proto file properties for generating the Client Stud classes
Build the project. You will see the Products.cs and ProductsGrpc.cs files in the obj-Debug-net5.0-Protos folder as shown in figure 3
Figure 3: The Client Stud classed generated
The Products.cs file contains Products and Product class. These classes are generated from Products and Product message defined in products.proto file. The ProductsGrpc.cs file contains ProductsServiceClient class. This class contains logic to invoke service method from gRPC service. The Blazor client application will use this class to communicate with gRPC service.
Step 5: In the _Imports.razor, import the references of namespaces of which classes we will be using in code as shown in listing 3
@using Grpc.Net.Client @using clientnamespace @using static clientnamespace.ProductsService; @using System.Text.Json; @using Grpc.Net.Client.Web;
Listing 3: Importing references
Step 6: To communicate with the gRPC service, the client application must create GrpcChannel. This class represents and abstraction on the channel to communicate with the gRPC service. Since the channel creation is an expensive operation, its is recommended that the same channel must be used by the client to make as many as possible calls to gRPC service. To make sure that the client uses the same channel object, we will register the GrpcChannel and ProductsServiceClient class as a singleton in Dependency Container of the Blazor application. Modify the Main() method of Program class in Program.cs by adding code as shown in listing 4
builder.Services.AddSingleton(services => { var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler())); var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpClient = httpClient }); return new ProductsServiceClient(channel); });
Listing 4: The Singleton registration GrpcChannel
The code in listing 4 creates a channel to the gRPC service using its ForAddress() method. This method accepts address of the gRPC service.
Step 6: In the project add a new folder and name this folder as Services. In this folder add a new class file and name this class file as ProductsOpsService.cs. This file contains ProductsOpService class. This class constructor injected using ProductsServiceClient class. The ProductsOpService class calls methods from ProductsServiceClient and to send requests to the gRPC service methods. The listing 5 shows the code of the ProductsOpService class
using clientnamespace; using System.Threading.Tasks; using static clientnamespace.ProductsService; namespace Blzor_Client.Services { public class ProductsOpsService { private readonly ProductsServiceClient client; public ProductsOpsService(ProductsServiceClient client) { this.client = client; } public async Task<Products> GetProductsAsync() { var products = await client.GetAllAsync(new Empty()); return products; } public async Task<Product> GetProductByIdAsync(int id) { var product = await client.GetByIdAsync(new ProductRowIdFilter() { ProductRowId= id}); return product; } public async Task<Product> CreateProductAsync(Product product) { product = await client.PostAsync(product); return product; } public async Task<Product> UpdateProductAsync(Product product) { product = await client.PutAsync(product); return product; } public async Task<Empty> DeleteProductAsync(int id) { var result = await client.DeleteAsync(new ProductRowIdFilter() { ProductRowId= id}); return result; } } }
Listing 5: The client code
Register the ProductsOpsService class as Singleton in dependency container in Main() method of the Program class in Program.cs as shown in listing 6
builder.Services.AddSingleton<ProductsOpsService>();
Listing 6: The registration of then ProductsOpsService
Step 7: In the Pages folder add a new Blazor component and name it as ProductsList.razor. In this component add the following HTML markup and C# code as shown in listing 7
@page "/productslist" @inject ProductsServiceClient client; @inject Blzor_Client.Services.ProductsOpsService service; @inject NavigationManager navigate;
<h3>List of Products</h3> <button @onclick="navigateToCreate">Create New</button> <table class="table table-bordered table-striped"> <thead> <tr> <th>Product Row Id</th> <th>Product Id</th> <th>Product Name</th> <th>Category Name</th> <th>Manufacturer</th> <th>Price</th> </tr> </thead> <tbody> @foreach (var item in products.Items.ToList()) { <tr> <td>@item.ProductRowId</td> <td>@item.ProductId</td> <td>@item.ProductName</td> <td>@item.CategoryName</td> <td>@item.Manufacturer</td> <td>@item.Price</td> <td> <button class="btn btn-warning" @onclick="((evt)=> navigateToUpdate( item.ProductRowId) )"> Edit </button> </td> <td> <button class="btn btn-danger" @onclick="((evt)=> deleteRecord( item.ProductRowId) )"> Delete </button> </td> </tr> } </tbody> </table>
@code { private Products products = new Products(); private string data = ""; protected override async Task OnInitializedAsync() { products = await service.GetProductsAsync(); data = JsonSerializer.Serialize(products.Items); } protected override bool ShouldRender() { return base.ShouldRender(); } private void navigateToCreate() { navigate.NavigateTo("/createproduct"); } void navigateToUpdate(int id) { navigate.NavigateTo("/updateproduct/" + id); } async Task deleteRecord(int id) { await service.DeleteProductAsync(id); } }Listing 7: The ProductsList.razor
@page "/createproduct" @inject NavigationManager navigate; @inject Blzor_Client.Services.ProductsOpsService service; @inject NavigationManager navigation;
<h3>Create New Product</h3> <EditForm Model="product" OnValidSubmit="save"> <div class="container"> <div class="form-group"> <label for="ProductId">Product Id</label> <InputText @bind-Value="@product.ProductId" class="form-control"> </InputText> </div> <div class="form-group"> <label for="ProductName">Product Name</label> <InputText @bind-Value="@product.ProductName" class="form-control"> </InputText> </div> <div class="form-group"> <label for="CategoryName">Category Name</label> <InputSelect @bind-Value="@product.CategoryName" class="form-control"> @foreach (var item in Categories) { <option value="@item">@item</option> } </InputSelect> </div> <div class="form-group"> <label for="Manufacturer">Manufacturer</label> <InputSelect @bind-Value="@product.Manufacturer" class="form-control"> @foreach (var item in Manufacturer) { <option value="@item">@item</option> } </InputSelect> </div> <div class="form-group"> <label for="BasePrice">Price</label> <InputNumber @bind-Value="@product.Price" class="form-control"> </InputNumber> </div> <div class="form-group"> <input type="button" class="btn btn-warning" value="Clear" /> <input type="submit" class="btn btn-success" value="Save" /> </div> </div> </EditForm>
@code { private Product product = new Product(); private List<string> Categories = new List<string>() { "Electronics", "Electrical", "IT", "Food", "Power","Cloths" }; private List<string> Manufacturer = new List<string>() { "MS-Electronics", "TS-ElctroSystems", "LS-ITSystems", "MS-Foods","LS-Kitchen", "TS-Eletro-Powers", "LMS-Fashion" }; private async Task save() { product = await service.CreateProductAsync(product); if (product.ProductRowId != 0) { navigate.NavigateTo("/productslist"); } else { navigate.NavigateTo("/error"); } } private void clear() { product = new Product(); } }
<li class="nav-item px-3"> <NavLink class="nav-link" href="productslist"> <span class="oi oi-plus" aria-hidden="true"></span> Products List </NavLink> </li>