Create A To Do App With Blazor Web Assembly And gRPC

Introduction

 
In my last article, we saw how do we integrate the Blazor server-side with the gRPC service. In this article, let’s try to explore the integration between web assembly and gRPC service. Also, we will try to build a To-Do Application where the front end is into the Blazor Web assembly and back end designed into the gRPC. If you want to know and explore gRPC, refer to my previous articles.

Project Setup

 
To get started with this, let's set up the project first before going forward. We need to ensure we have the following things in place:
  • .NET Core SDK 3.0 and above (try to get latest stable version)
  • Visual Studio 2019 – Latest edition
We have two projects, the first one is the web Assembly project and another is the gRPC service project. Let's add them one by one.
 

Blazor web Assembly

 
Open Visual Studio and click on Add New Project and Select the following project type:

To Do App With Blazor Web Assembly And gRPC
 
Once we click on the above step, go to the next step, then we get the following options:
 
To Do App With Blazor Web Assembly And gRPC
 
After doing the needed steps, we will arrive at this step which will be like below:
 
To Do App With Blazor Web Assembly And gRPC
 
Select Blazor Web Assembly App here and our project will be created with this just to clarify one thing we are creating standalone application here without any server or back end support as we will have our own back end service for integration.
 

Creating a gRPC Service Project

 
Now let us add the gRPC project. To add that, again go to visual studio and create New Project and select the following option
 
To Do App With Blazor Web Assembly And gRPC
 

Implementing gRPC service

 
We are going to use the same service which we have used in the last article which is here. In this article, we will focus on only the implementation details which are new and needed for understanding the integration between the Blazor web assembly and the gRPC service.
 
Startup.cs Changes
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5. using Microsoft.AspNetCore.Builder;  
  6. using Microsoft.AspNetCore.Hosting;  
  7. using Microsoft.AspNetCore.Http;  
  8. using Microsoft.Extensions.DependencyInjection;  
  9. using Microsoft.Extensions.Hosting;  
  10. using ToDoGrpcService;  
  11. using ToDoGrpcService.Services;  
  12. using Microsoft.EntityFrameworkCore;  
  13. namespace ToDo  
  14. {  
  15.     public class Startup  
  16.     {  
  17.         // This method gets called by the runtime. Use this method to add services to the container.  
  18.         // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940  
  19.         public void ConfigureServices(IServiceCollection services)  
  20.         {  
  21.             services.AddCors(o => o.AddPolicy("AllowAll", builder =>  
  22.             {  
  23.                 builder.AllowAnyOrigin()  
  24.                        .AllowAnyMethod()  
  25.                        .AllowAnyHeader();  
  26.   
  27.             }));  
  28.             services.AddGrpc();  
  29.             services.AddDbContext<ToDoDataContext>(options => options.UseInMemoryDatabase("ToDoDatabase"));  
  30.         }  
  31.   
  32.         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.  
  33.         public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ToDoDataContext ctx)  
  34.         {  
  35.             new ToDoGenerator(ctx).ToDoDataSeed();  
  36.   
  37.             if (env.IsDevelopment())  
  38.             {  
  39.                 app.UseDeveloperExceptionPage();  
  40.             }  
  41.   
  42.             app.UseRouting();  
  43.             app.UseCors();  
  44.             app.UseGrpcWeb();  
  45.             app.UseEndpoints(endpoints =>  
  46.             {  
  47.                 endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb()  
  48.                                                   .RequireCors("AllowAll");  
  49.                 endpoints.MapGrpcService<ToDoDataService>().EnableGrpcWeb()  
  50.                                                   .RequireCors("AllowAll");  
  51.   
  52.                 endpoints.MapGet("/", async context =>  
  53.                 {  
  54.                     await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");  
  55.                 });  
  56.             });  
  57.         }  
  58.     }  
  59. }  
The most notable change that we can see here is adding the CORS policy and applying to the services next notable change is to add the middleware GrpcWeb in the pipeline and enabling the endpoints for calling them from browser.
 

What is gRPC-Web?

 
As we all know, it is not possible to call grpc service directly from the browser and it needs to be consumed from the grpc enabled client. gRPC – Web is a protocol that allows browser and JavaScript and blazor wasm clients to call the service easily.
 
Configure gRPC- Web
 
To see that let's check the following below steps.
 
Add nuget package Grpc.AspNetCore.Web
 
Configure App by Adding UseGrpcWeb and EnableGrpcWeb in Startup.cs like below: 
  1. app.UseGrpcWeb();  
  2.             app.UseEndpoints(endpoints =>  
  3.             {  
  4.                 endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb()  
  5.                                                   .RequireCors("AllowAll");  
  6.                 endpoints.MapGrpcService<ToDoDataService>().EnableGrpcWeb()  
  7.                                                   .RequireCors("AllowAll");  
  8.          }  
Code Details
  1. The above code adds grp-Web Middleware using UseGrpcWeb() after routing and before endpoints
  2. It specifies that Services are enabled for gRPC web
gRPC-Web and CORS
 
We all know that the browser generally prevents calling the services from a domain other than the domain where your app is hosted. the same restriction is applied to the gRPC-Web also in order to apply for the browser to make CORS calls with the following snippet.
  1. services.AddCors(o => o.AddPolicy("AllowAll", builder =>  
  2.             {  
  3.                 builder.AllowAnyOrigin()  
  4.                        .AllowAnyMethod()  
  5.                        .AllowAnyHeader()  
  6.                       .WithExposedHeaders("Grpc-Status""Grpc-Message""Grpc-Encoding""Grpc-Accept-Encoding");  
  7.             }));  

Implement Blazor Application

 
Now we are ready with the server application its time we Blazor application which will use these services.
 
Install Packages
 
In order to get the grpc services up and running, let's install the following packages in the application:
  1. Google.Protobuf
  2. Grpc.Net.Client
  3. Grpc.Net.Client.Web
  4. Grpc.Tools
Once we are done with these package installations, lets copy our proto files from the server application and paste in the folder named proto in the blazor application
 
Once we are done with the copy and paste, then change the properties of the file to compile these files as a client like below:
 
To Do App With Blazor Web Assembly And gRPC
 
Once we are done with this build, our project once and we will have our stubs automatically generated to use in our application
 
Configuring Client DI service in Program.cs
 
The most significant change that we need to do here in the application is to inject the services into the client application for this we have to make changes in the Program.cs, like below:
  1. using System;  
  2. using System.Net.Http;  
  3. using System.Threading.Tasks;  
  4. using Microsoft.AspNetCore.Components.WebAssembly.Hosting;  
  5. using Microsoft.Extensions.DependencyInjection;  
  6. using Grpc.Net.Client.Web;  
  7. using Microsoft.AspNetCore.Components;  
  8. using Grpc.Net.Client;  
  9. using ToDo;  
  10.   
  11. namespace BlazorWebAseemblyWithGrpc  
  12. {  
  13.     public class Program  
  14.     {  
  15.         public static async Task Main(string[] args)  
  16.         {  
  17.             var builder = WebAssemblyHostBuilder.CreateDefault(args);  
  18.             builder.RootComponents.Add<App>("app");  
  19.   
  20.             builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });  
  21.   
  22.             builder.Services.AddSingleton<BlazorClient.Services.ToDoDataService>();  
  23.   
  24.             builder.Services.AddSingleton(services =>  
  25.             {  
  26.                 var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));  
  27.                 var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;  
  28.                 var channel = GrpcChannel.ForAddress("https://localhost:5001"new GrpcChannelOptions { HttpClient = httpClient });  
  29.   
  30.                 // Now we can instantiate gRPC clients for this channel  
  31.                 return new Greeter.GreeterClient(channel);  
  32.   
  33.             });  
  34.             builder.Services.AddSingleton(services =>  
  35.             {  
  36.                 var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));  
  37.                 var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;  
  38.                 var channel = GrpcChannel.ForAddress("https://localhost:5001"new GrpcChannelOptions { HttpClient = httpClient });  
  39.   
  40.                 // Now we can instantiate gRPC clients for this channel  
  41.                 return new ToDoGrpcService.ToDoService.ToDoServiceClient(channel);  
  42.   
  43.             });  
  44.             await builder.Build().RunAsync();  
  45.         }  
  46.     }  
  47. }  
If you see here, we are injecting our ToDo service as a singleton which can be used in the application by injecting the service into the data service next step will be to add the Data service. Add the Code file and the component.
 
Designing the data service
  1. using Grpc.Net.Client;  
  2. using System;  
  3. using System.Threading.Tasks;  
  4. using ToDoGrpcService;  
  5. namespace BlazorClient.Services  
  6. {  
  7.     public class ToDoDataService  
  8.     {  
  9.         ToDoService.ToDoServiceClient _toDoServiceClient;  
  10.         public ToDoDataService(ToDoService.ToDoServiceClient toDoServiceClient)  
  11.         {  
  12.             _toDoServiceClient = toDoServiceClient;  
  13.         }  
  14.   
  15.         public async Task<bool> AddToDoData(Data.ToDoDataItem toDoDataItem)  
  16.         {  
  17.             var todoData = new ToDoGrpcService.ToDoData()  
  18.             {  
  19.                 Status = toDoDataItem.Status,  
  20.                 Title = toDoDataItem.Title,  
  21.                 Description = toDoDataItem.Description  
  22.             };  
  23.   
  24.             var response = await _toDoServiceClient.PostToDoItemAsync(todoData, null);  
  25.             return response.Status;  
  26.   
  27.         }  
  28.         public async Task<bool> UpdateToDoData(Data.ToDoDataItem toDoDataItem)  
  29.         {  
  30.            
  31.             var updateData = new ToDoGrpcService.ToDoPutQuery  
  32.             {  
  33.                 Id = toDoDataItem.Id,  
  34.                 ToDoDataItem = new ToDoGrpcService.ToDoData()  
  35.                 {  
  36.                     Id = toDoDataItem.Id,  
  37.                     Status = toDoDataItem.Status,  
  38.                     Title = toDoDataItem.Title,  
  39.                     Description = toDoDataItem.Description  
  40.                 }  
  41.             };  
  42.             var response = await _toDoServiceClient.PutToDoItemAsync(updateData, null);  
  43.             return response.Status;  
  44.         }  
  45.         public async Task<bool> DeleteDataAsync(string ToDoId)  
  46.         {  
  47.              
  48.             var response = await _toDoServiceClient.DeleteItemAsync(new ToDoGrpcService.ToDoQuery() { Id = Convert.ToInt32(ToDoId) }, null);  
  49.             return response.Status;  
  50.         }  
  51.         public async Task<ToDoGrpcService.ToDoItems> GetToDoList()  
  52.         {  
  53.               
  54.             return await _toDoServiceClient.GetToDoAsync(new Google.Protobuf.WellKnownTypes.Empty(), null);  
  55.   
  56.         }  
  57.   
  58.         public async Task<Data.ToDoDataItem> GetToDoItemAsync(int id)  
  59.         {  
  60.              
  61.             var todoItem = await _toDoServiceClient.GetToDoItemAsync(new ToDoGrpcService.ToDoQuery() { Id = Convert.ToInt32(id) }, null);  
  62.   
  63.               
  64.   
  65.             return new Data.ToDoDataItem() { Title = todoItem.Title, Description = todoItem.Description, Status = todoItem.Status, Id = todoItem.Id };  
  66.   
  67.         }  
  68.     }  
  69. }  
Add Code file for Component
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5. using Grpc.Net.Client;  
  6. using Microsoft.AspNetCore.Components;  
  7.   
  8. namespace BlazorClient.CodeFiles  
  9. {  
  10.     public partial class ToDoOperation : ComponentBase  
  11.     {  
  12.         public bool ShowModel = false;  
  13.         public bool ShowAlert = false;  
  14.         public bool ShowModeletePopup = false;  
  15.         public string OperationStatusText = "";  
  16.         public string PopupTitle = "";  
  17.         public int noOfRows = 0;  
  18.         public int noOfCoumns = 4;  
  19.          
  20.   
  21.         public BlazorClient.Data.ToDoDataItem ToDoDataItem = new BlazorClient.Data.ToDoDataItem();  
  22.         public string ActionText = "";  
  23.   
  24.         public ToDoGrpcService.ToDoItems toDoItems = new ToDoGrpcService.ToDoItems();  
  25.   
  26.         public string DeleteItemId { getset; }  
  27.   
  28.         [Inject]  
  29.         protected BlazorClient.Services.ToDoDataService ToDoService { getset; }  
  30.   
  31.         protected async override Task OnInitializedAsync()  
  32.         {  
  33.             try  
  34.             {  
  35.                 await GetToDoListAsync();  
  36.             }  
  37.             catch (Exception ex)  
  38.             {  
  39.   
  40.                 Console.WriteLine(ex);  
  41.             }  
  42.         }  
  43.   
  44.         protected async Task GetToDoListAsync()  
  45.         {  
  46.   
  47.             toDoItems = await ToDoService.GetToDoList();  
  48.             var temoRowCount = toDoItems.ToDoItemList.Count() / noOfCoumns;  
  49.             noOfRows = toDoItems.ToDoItemList.Count() % noOfCoumns == 0 ? temoRowCount : temoRowCount + 1;  
  50.              
  51.         }  
  52.   
  53.         protected async Task ShowEditForm(int Id)  
  54.         {  
  55.             Console.Write(Id);  
  56.             PopupTitle = "To Do Edit";  
  57.             ActionText = "Update";  
  58.             ToDoDataItem = await ToDoService.GetToDoItemAsync(Id);  
  59.             ShowModel = true;  
  60.         }  
  61.   
  62.         protected void ShowAddpopup()  
  63.         {  
  64.             ToDoDataItem = new Data.ToDoDataItem() { Title = "", Description = "", Status = false, Id = 0 };  
  65.             PopupTitle = "To Do Add";  
  66.             ActionText = "Add";  
  67.             ShowModel = true;  
  68.   
  69.         }  
  70.         protected void ShowDeletePopup(string Id)  
  71.         {  
  72.             Console.Write(Id);  
  73.             DeleteItemId = Id;  
  74.             ShowModeletePopup = true;  
  75.         }  
  76.   
  77.         protected async Task PostDataAsync()  
  78.         {  
  79.             try  
  80.             {  
  81.                 bool status = false;  
  82.                 if (ToDoDataItem.Id > 0)  
  83.                 {  
  84.                     status = await ToDoService.UpdateToDoData(this.ToDoDataItem);  
  85.   
  86.                 }  
  87.                 else  
  88.                 {  
  89.                     status = await ToDoService.AddToDoData(this.ToDoDataItem);  
  90.                 }  
  91.                 await Reload(status);  
  92.             }  
  93.             catch (Exception ex)  
  94.             {  
  95.   
  96.                 Console.WriteLine(ex);  
  97.             }  
  98.   
  99.         }  
  100.   
  101.         public async Task DeleteDataAsync()  
  102.         {  
  103.   
  104.             var operationStatus = await ToDoService.DeleteDataAsync(DeleteItemId);  
  105.             await Reload(operationStatus);  
  106.         }  
  107.   
  108.         protected async Task Reload(bool status)  
  109.         {  
  110.             ShowModeletePopup = false;  
  111.             ShowModel = false;  
  112.             await GetToDoListAsync();  
  113.             ShowAlert = true;  
  114.             if (status)  
  115.             {  
  116.                 OperationStatusText = "1";  
  117.             }  
  118.             else  
  119.             {  
  120.                 OperationStatusText = "0";  
  121.             }  
  122.   
  123.         }  
  124.   
  125.         protected  void DismissPopup()  
  126.         {  
  127.             ShowModel = false;  
  128.             ShowAlert = false;  
  129.             ShowModeletePopup = false;  
  130.             
  131.         }  
  132.   
  133.     }  
  134. }  
Component Template
  1. @page "/Todo"  
  2.   
  3. @inherits BlazorClient.CodeFiles.ToDoOperation  
  4.   
  5.   
  6.   
  7. <div class="row" style="margin-bottom:15px">  
  8.     <button id="btnAdd" @onclick="ShowAddpopup" class="btn btn-primary" style="background-color: #053870; color: white"><i class="oi oi-plus">Add New</i></button>  
  9. </div>  
  10.   
  11. @if (toDoItems != null && toDoItems.ToDoItemList != null)  
  12. {  
  13.     int elementStartPosition = 0;  
  14.   
  15.     @for (int i = 0; i < noOfRows; i++)  
  16.     {  
  17.          
  18. <div class="row">  
  19.     @for (int elementPosition = elementStartPosition; elementPosition < elementStartPosition + noOfCoumns; elementPosition++)  
  20.     {  
  21.   
  22.   
  23.         if (elementPosition > toDoItems.ToDoItemList.Count() - 1)  
  24.         {  
  25.             break;  
  26.         }  
  27.         var idToEdit = @toDoItems.ToDoItemList[elementPosition].Id;  
  28.         <div class="card" style="width: 18rem;margin:3px;border-radius:10px">  
  29.             <div class="card-body">  
  30.                 <h5 class="card-title" style="background-color: #053870; color: white;text-align:center;padding:5px;font-weight:500">@toDoItems.ToDoItemList[elementPosition].Title</h5>  
  31.                 <p class="card-text">@toDoItems.ToDoItemList[elementPosition].Description</p>  
  32.                 <p class="card-text">@(toDoItems.ToDoItemList[elementPosition].Status == false ? "Closed" : "Active")</p>  
  33.             </div>  
  34.             <div class="card-footer text-md-center">  
  35.                 <button class="btn btn-secondary" @onclick="@(async () => await ShowEditForm(idToEdit))">Edit</button>  
  36.   
  37.                 <button class="btn btn-danger" @onclick="@(async () => ShowDeletePopup(idToEdit.ToString()))">Trash</button>  
  38.             </div>  
  39.         </div>  
  40.   
  41.     }  
  42.     <div style="display:none">  @(elementStartPosition = elementStartPosition + noOfCoumns);</div>  
  43. </div>  
  44.   
  45.     }  
  46. }  
  47.   
  48.   
  49. @if (ShowModel == true)  
  50. {  
  51.   
  52.     <div class="modal" tabindex="-1" style="display:block;" role="dialog">  
  53.         <div class="modal-dialog">  
  54.             <div class="modal-content">  
  55.                 <div class="modal-header" style="background-color:#5c116f;color:white;height:50px">  
  56.                     <span class="modal-title">@PopupTitle</span>  
  57.                     <button type="button" class="close" @onclick="DismissPopup">  
  58.                         <span aria-hidden="true" style="color:white;">X</span>  
  59.                     </button>  
  60.                 </div>  
  61.                 <div class="modal-body">  
  62.   
  63.                     <table border="0" cellspacing="1">  
  64.                         <tr>  
  65.                             <td><strong>Title</strong></td>  
  66.                             <td><input type="text" @bind="ToDoDataItem.Title" maxlength="20" /></td>  
  67.                         </tr>  
  68.                         <tr>  
  69.                             <td><strong>Description</strong></td>  
  70.                             <td><input type="text" @bind="ToDoDataItem.Description" maxlength="20" /></td>  
  71.                         </tr>  
  72.                         <tr>  
  73.                             <td><strong>Status</strong></td>  
  74.                             <td><input type="checkbox" @bind="ToDoDataItem.Status" /></td>  
  75.                         </tr>  
  76.                         <tr>  
  77.                             <td colspan="2" align="center"><button class="btn btn-primary" id="btnPostData" @onclick="PostDataAsync">@ActionText</button></td>  
  78.   
  79.                         </tr>  
  80.                     </table>  
  81.                 </div>  
  82.             </div>  
  83.         </div>  
  84.     </div>  
  85. }  
  86.   
  87.   
  88. @if (ShowAlert == true)  
  89. {  
  90.   
  91.     <div class="modal" tabindex="-2" style="display:block;padding-top:-200px;padding-right:0px" role="dialog">  
  92.         <div class="modal-dialog">  
  93.             <div class="modal-content">  
  94.                 <div class="modal-header" style="background-color:#5c116f;color:white;height:50px">  
  95.                     <span class="modal-title">Notification</span>  
  96.                     <button type="button" class="close" @onclick="DismissPopup">  
  97.                         <span aria-hidden="true" style="color:white;">X</span>  
  98.                     </button>  
  99.                 </div>  
  100.                 <div class="modal-body">  
  101.                     @if (OperationStatusText == "1")  
  102.                     {  
  103.                         <span> All Good πŸ˜„</span>  
  104.                     }  
  105.                     else  
  106.                     {  
  107.                         <span>Nothing is good i am angry now πŸ˜ </span>  
  108.                     }  
  109.                      
  110.                 </div>  
  111.             </div>  
  112.         </div>  
  113.     </div>  
  114. }  
  115.   
  116. @if (ShowModeletePopup == true)  
  117. {  
  118.   
  119.     <div class="modal" tabindex="-3" style="display:block;padding-top:300px" role="dialog">  
  120.         <div class="modal-dialog">  
  121.             <div class="modal-content">  
  122.                 <div class="modal-header" style="background-color:#5c116f;color:white;height:50px">  
  123.                     <span class="modal-title">Status</span>  
  124.                     <button type="button" class="close" @onclick="DismissPopup">  
  125.                         <span aria-hidden="true" style="color:white;">X</span>  
  126.                     </button>  
  127.                 </div>  
  128.                 <div class="modal-body">  
  129.                     <table>  
  130.                         <tr>  
  131.                             <td colspan="2">  
  132.                                 Are you sure you want to delete this ToDo Item with Id @DeleteItemId ?  
  133.                             </td>  
  134.                         </tr>  
  135.                         <tr>  
  136.                             <td align="right"><button class="btn btn-primary" @onclick="DeleteDataAsync">Ok</button></td>  
  137.                             <td align="left"><button class="btn btn-danger">Cancel</button></td>  
  138.                         </tr>  
  139.   
  140.                     </table>  
  141.                 </div>  
  142.             </div>  
  143.         </div>  
  144.     </div>  
  145. }  
Once we are done with the above changes, we can see the output below:
 
To Do App With Blazor Web Assembly And gRPC
 
Here is how we can integrate our Blazor web assembly application to the grpc services. For the complete source code, you can follow my Git Hub Repo.
 
 
References
  • https://blog.stevensanderson.com/2020/01/15/2020-01-15-grpc-web-in-blazor-webassembly/
  • https://docs.microsoft.com/en-us/aspnet/core/grpc/browser?view=aspnetcore-3.1