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 
Step 3: In this project we will be using products.proto file which we have created in gRPC Service tutorial. In the project add a new folder and name it as Protos. In this folder add a new protobuf file and name it as products.proto. In this file, create a message structure for communication with gRPC service application as shown in listing 1


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
The component is injected with ProductsOpsService. The OnInitializedAsync() method of the component invokes the GetProductsAsync() method of the ProductsOpsService class to load all Products. These products are displayed in the HTML table. The HTML table generates Update and Delete buttons in each row. The update button is bind with navigateToUpdate() method and delete button is bind with deleteRecord() method to navigate to the UpdateProduct component and to delete the product respectively. The navigateToCreate() method of the component is bind with Create New button. This method navigate to the CreateProduct component.     

Step 8: In the Pages folder add a new Blazor component and name it as CreateProduct.razor. In this component we add HTML markup and code as shown in listing 8
@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();
	}
}

Listing 8: The Create Product Component
This component has the EditForm that contains UI for accepting Product information. The save() method of the component invokes the CreateProductAsync() method from the ProductsOpsService class to create a new product.

Similarly ,we can add the Blazor component for updating the product information. (You can refer the code form the download link that is provided at the end of the link.)

Modify the NavMenu.razor to set navigation for ProductsList component as shown in listing 9


<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>
Listing 9: The navigation 


Form the solution properties set multiple startup project and start the gRPC project first and then the Blazor application as second application to run. Now, Run the application. The gRPC service will start and Blazor application will be loaded in the browser as shown in figure 4


Figure 4: The Blazor app   
 
Click on the Products List link, the Products List component will be displayed with Products Data loaded in table as shown in Figure 5


Figure 5: Loading the Product Data  
The Products List component makes call to the gRPC service and receives the Products List from the service. Likewise, click on create button tom load Create Product component. By clicking on the Edit button, the UpdateProduct component can be loaded to update the product.

The code for this tutorial can be downloaded from this link.
  
Conclusion: The gRPC service can be directly accessed from the application running in the browser. All calls must be asynchronous calls. The most important point of accessing the gRPC service from the client application is that the GrpcChannel must be instantiated once to make as many as possible calls because the processing of creating channel is expensive.   

Comments

Popular posts from this blog

Understanding Token Based Authentication in ASP.NET Core 3.1 using JSON WEB TOKENS

Using Model Binders in ASP.NET Core

Using Session State in ASP.NET Core