ToDo App with Blazor wasm and Grpc

Agenda

  1. Introduction
  2. Project Setup
  3. Implementing gRPC service
  4. Implement Blazor Application
  5. Integrate Service with Client
  6. Conclusion

Introduction

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

  1. gRPC Introduction part 1
  2.  gRPC introduction – Part 2
  3. Integrating Blazor server side with gRPC

Project Setup

 To get started with this lets setup the project first before going forward we need to ensure we have 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 first one is the web Assembly project and another is the gRPC service project lets add them one by one

  • Blazor web Assembly

Open Visual Studio and click on Add new Project and Select following project type

Add Blazor Project

Once we click on above step go to next step then we get the following options

Add Blazor project

After doing needed steps we will arrive at this step which will be like below

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 the integration

  • Creating 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

One we complete all the step we will have our service ready and we are done with the project setups. Once we are done with the project setups its time we implement the gRPC service

Implementing gRPC service

We are going to use same service which we have used in the last article which is here so 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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ToDoGrpcService;
using ToDoGrpcService.Services;
using Microsoft.EntityFrameworkCore;
namespace ToDo
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(o => o.AddPolicy("AllowAll", builder =>
            {
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader();

            }));
            services.AddGrpc();
            services.AddDbContext<ToDoDataContext>(options => options.UseInMemoryDatabase("ToDoDatabase"));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ToDoDataContext ctx)
        {
            new ToDoGenerator(ctx).ToDoDataSeed();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();
            app.UseCors();
            app.UseGrpcWeb();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb()
                                                  .RequireCors("AllowAll");
                endpoints.MapGrpcService<ToDoDataService>().EnableGrpcWeb()
                                                  .RequireCors("AllowAll");

                endpoints.MapGet("/", async context =>
                {
                    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");
                });
            });
        }
    }
}

Most notable change that we can see here is the 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 which allows browser and JavaScript and blazor wasm clients to call the service easily .

Configure gRPC- Web

To see that lets check the follow below steps

  1. Add nuget package Grpc.AspNetCore.Web
  2. Configure App by Adding UseGrpcWeb and EnableGrpcWeb in Startup.cs like below

Code Details

    app.UseGrpcWeb();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb()
                                                  .RequireCors("AllowAll");
                endpoints.MapGrpcService<ToDoDataService>().EnableGrpcWeb()
                                                  .RequireCors("AllowAll");
         }
  1. Above code adds grp-Web Middleware using UseGrpcWeb() after routing and before endpoints
  2. Specifies that Services are enabled for gRPC web

gRPC-Web and CORS

we all know browser generally prevents calling the services from domain other than the domain where your app is hosted . same restriction is applied to the gRPC-Web also in order to apply for the browser to make CORS calls with following snippet

services.AddCors(o => o.AddPolicy("AllowAll", builder =>
            {
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader()
                      .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
            }));

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 lets install 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 installation lets copy our proto files from server application and paste in the folder named proto in the blazor application

Once we are done with copy and paste then change the properties of the file to compile these files as a client like below

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

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Grpc.Net.Client.Web;
using Microsoft.AspNetCore.Components;
using Grpc.Net.Client;
using ToDo;

namespace BlazorWebAseemblyWithGrpc
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.AddSingleton<BlazorClient.Services.ToDoDataService>();

            builder.Services.AddSingleton(services =>
            {
                var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
                var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;
                var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpClient = httpClient });

                // Now we can instantiate gRPC clients for this channel
                return new Greeter.GreeterClient(channel);

            });
            builder.Services.AddSingleton(services =>
            {
                var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
                var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;
                var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpClient = httpClient });

                // Now we can instantiate gRPC clients for this channel
                return new ToDoGrpcService.ToDoService.ToDoServiceClient(channel);

            });
            await builder.Build().RunAsync();
        }
    }
}

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 adding the component

Designing the data service

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;

namespace ToDoGrpcService.Services
{
    public class ToDoDataService : ToDoService.ToDoServiceBase
    {
        private readonly ToDoDataContext _dataContext;
        public ToDoDataService(ToDoDataContext dataContext)
        {
            
            _dataContext = dataContext;
           
        }

        /// <summary>
        /// Get All Data
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<ToDoItems> GetToDo(Empty request, ServerCallContext context)
        {
            ToDoItems objItems = new ToDoItems();

            foreach (var item in _dataContext.ToDoDbItems)
            {
                objItems.ToDoItemList.Add(item);
            }

            return Task.FromResult(objItems);
        }

        /// <summary>
        /// Post Data 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<ToDoPostResponse> PostToDoItem(ToDoData request, ServerCallContext context)
        {
             _dataContext.ToDoDbItems.Add(request);
            var result = _dataContext.SaveChanges();
            if (result>0)
            {
                return Task.FromResult(new ToDoPostResponse()
                {
                    Status = true,
                    StatusCode = 100,
                    StatusMessage = "Added Successfully"
                });
            }
            else
            {
                return Task.FromResult(new ToDoPostResponse()
                {
                    Status = false,
                    StatusCode = 500,
                    StatusMessage = "Issue Occured."
                });
            }
            
        }
        /// <summary>
        /// Get Item with the Id
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>

        public override Task<ToDoData> GetToDoItem(ToDoQuery request, ServerCallContext context)
        {
            var result = from data in _dataContext.ToDoDbItems
                         where data.Id == request.Id
                         select data;
            return Task.FromResult(result.First());
            
        }
        /// <summary>
        /// Deletes the Item
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<ToDoPostResponse> DeleteItem(ToDoQuery request, ServerCallContext context)
        {

            var item = (from data in _dataContext.ToDoDbItems
                                    where data.Id == request.Id
                                    select data).Single();


              _dataContext.ToDoDbItems.Remove(item);

            var result = _dataContext.SaveChanges();

            if (result > 0)
            {
                return Task.FromResult(new ToDoPostResponse()
                {
                    Status = true,
                    StatusCode = 100,
                    StatusMessage = "Deleted Successfully"
                });
            }
            else
            {
                return Task.FromResult(new ToDoPostResponse()
                {
                    Status = false,
                    StatusCode = 500,
                    StatusMessage = "Issue Occured."
                });
            }
        }
        /// <summary>
        /// Updates the item
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<ToDoPostResponse> PutToDoItem(ToDoPutQuery request, ServerCallContext context)
        {
            _dataContext.ToDoDbItems.Update(request.ToDoDataItem);
            var result = _dataContext.SaveChanges();


            if (result > 0)
            {
                return Task.FromResult(new ToDoPostResponse()
                {
                    Status = true,
                    StatusCode = 100,
                    StatusMessage = "Updated  Successfully "
                });
            }
            else
            {
                return Task.FromResult(new ToDoPostResponse()
                {
                    Status = false,
                    StatusCode = 500,
                    StatusMessage = "Issue Occured."
                });
            }
        }
    }
}

Add Code file for Component

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Components;

namespace BlazorClient.CodeFiles
{
    public partial class ToDoOperation : ComponentBase
    {
        public bool ShowModel = false;
        public bool ShowAlert = false;
        public bool ShowModeletePopup = false;
        public string OperationStatusText = "";
        public string PopupTitle = "";
        public int noOfRows = 0;
        public int noOfCoumns = 4;
       

        public BlazorClient.Data.ToDoDataItem ToDoDataItem = new BlazorClient.Data.ToDoDataItem();
        public string ActionText = "";

        public ToDoGrpcService.ToDoItems toDoItems = new ToDoGrpcService.ToDoItems();

        public string DeleteItemId { get; set; }

        [Inject]
        protected BlazorClient.Services.ToDoDataService ToDoService { get; set; }

        protected async override Task OnInitializedAsync()
        {
            try
            {
                await GetToDoListAsync();
            }
            catch (Exception ex)
            {

                Console.WriteLine(ex);
            }
        }

        protected async Task GetToDoListAsync()
        {

            toDoItems = await ToDoService.GetToDoList();
            var temoRowCount = toDoItems.ToDoItemList.Count() / noOfCoumns;
            noOfRows = toDoItems.ToDoItemList.Count() % noOfCoumns == 0 ? temoRowCount : temoRowCount + 1;
           
        }

        protected async Task ShowEditForm(int Id)
        {
            Console.Write(Id);
            PopupTitle = "To Do Edit";
            ActionText = "Update";
            ToDoDataItem = await ToDoService.GetToDoItemAsync(Id);
            ShowModel = true;
        }

        protected void ShowAddpopup()
        {
            ToDoDataItem = new Data.ToDoDataItem() { Title = "", Description = "", Status = false, Id = 0 };
            PopupTitle = "To Do Add";
            ActionText = "Add";
            ShowModel = true;

        }
        protected void ShowDeletePopup(string Id)
        {
            Console.Write(Id);
            DeleteItemId = Id;
            ShowModeletePopup = true;
        }

        protected async Task PostDataAsync()
        {
            try
            {
                bool status = false;
                if (ToDoDataItem.Id > 0)
                {
                    status = await ToDoService.UpdateToDoData(this.ToDoDataItem);

                }
                else
                {
                    status = await ToDoService.AddToDoData(this.ToDoDataItem);
                }
                await Reload(status);
            }
            catch (Exception ex)
            {

                Console.WriteLine(ex);
            }

        }

        public async Task DeleteDataAsync()
        {

            var operationStatus = await ToDoService.DeleteDataAsync(DeleteItemId);
            await Reload(operationStatus);
        }

        protected async Task Reload(bool status)
        {
            ShowModeletePopup = false;
            ShowModel = false;
            await GetToDoListAsync();
            ShowAlert = true;
            if (status)
            {
                OperationStatusText = "1";
            }
            else
            {
                OperationStatusText = "0";
            }

        }

        protected  void DismissPopup()
        {
            ShowModel = false;
            ShowAlert = false;
            ShowModeletePopup = false;
          
        }

    }
}

Component Template

@page "/Todo"

@inherits BlazorClient.CodeFiles.ToDoOperation



<div class="row" style="margin-bottom:15px">
    <button id="btnAdd" @onclick="ShowAddpopup" class="btn btn-primary" style="background-color: #053870; color: white"><i class="oi oi-plus">Add New</i></button>
</div>

@if (toDoItems != null && toDoItems.ToDoItemList != null)
{
    int elementStartPosition = 0;

    @for (int i = 0; i < noOfRows; i++)
    {
       
<div class="row">
    @for (int elementPosition = elementStartPosition; elementPosition < elementStartPosition + noOfCoumns; elementPosition++)
    {


        if (elementPosition > toDoItems.ToDoItemList.Count() - 1)
        {
            break;
        }
        var idToEdit = @toDoItems.ToDoItemList[elementPosition].Id;
        <div class="card" style="width: 18rem;margin:3px;border-radius:10px">
            <div class="card-body">
                <h5 class="card-title" style="background-color: #053870; color: white;text-align:center;padding:5px;font-weight:500">@toDoItems.ToDoItemList[elementPosition].Title</h5>
                <p class="card-text">@toDoItems.ToDoItemList[elementPosition].Description</p>
                <p class="card-text">@(toDoItems.ToDoItemList[elementPosition].Status == false ? "Closed" : "Active")</p>
            </div>
            <div class="card-footer text-md-center">
                <button class="btn btn-secondary" @onclick="@(async () => await ShowEditForm(idToEdit))">Edit</button>

                <button class="btn btn-danger" @onclick="@(async () => ShowDeletePopup(idToEdit.ToString()))">Trash</button>
            </div>
        </div>

    }
    <div style="display:none">  @(elementStartPosition = elementStartPosition + noOfCoumns);</div>
</div>

    }
}


@if (ShowModel == true)
{

    <div class="modal" tabindex="-1" style="display:block;" role="dialog">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header" style="background-color:#5c116f;color:white;height:50px">
                    <span class="modal-title">@PopupTitle</span>
                    <button type="button" class="close" @onclick="DismissPopup">
                        <span aria-hidden="true" style="color:white;">X</span>
                    </button>
                </div>
                <div class="modal-body">

                    <table border="0" cellspacing="1">
                        <tr>
                            <td><strong>Title</strong></td>
                            <td><input type="text" @bind="ToDoDataItem.Title" maxlength="20" /></td>
                        </tr>
                        <tr>
                            <td><strong>Description</strong></td>
                            <td><input type="text" @bind="ToDoDataItem.Description" maxlength="20" /></td>
                        </tr>
                        <tr>
                            <td><strong>Status</strong></td>
                            <td><input type="checkbox" @bind="ToDoDataItem.Status" /></td>
                        </tr>
                        <tr>
                            <td colspan="2" align="center"><button class="btn btn-primary" id="btnPostData" @onclick="PostDataAsync">@ActionText</button></td>

                        </tr>
                    </table>
                </div>
            </div>
        </div>
    </div>
}


@if (ShowAlert == true)
{

    <div class="modal" tabindex="-2" style="display:block;padding-top:-200px;padding-right:0px" role="dialog">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header" style="background-color:#5c116f;color:white;height:50px">
                    <span class="modal-title">Notification</span>
                    <button type="button" class="close" @onclick="DismissPopup">
                        <span aria-hidden="true" style="color:white;">X</span>
                    </button>
                </div>
                <div class="modal-body">
                    @if (OperationStatusText == "1")
                    {
                        <span> All Good 😄</span>
                    }
                    else
                    {
                        <span>Nothing is good i am angry now 😠</span>
                    }
                   
                </div>
            </div>
        </div>
    </div>
}

@if (ShowModeletePopup == true)
{

    <div class="modal" tabindex="-3" style="display:block;padding-top:300px" role="dialog">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header" style="background-color:#5c116f;color:white;height:50px">
                    <span class="modal-title">Status</span>
                    <button type="button" class="close" @onclick="DismissPopup">
                        <span aria-hidden="true" style="color:white;">X</span>
                    </button>
                </div>
                <div class="modal-body">
                    <table>
                        <tr>
                            <td colspan="2">
                                Are you sure you want to delete this ToDo Item with Id @DeleteItemId ?
                            </td>
                        </tr>
                        <tr>
                            <td align="right"><button class="btn btn-primary" @onclick="DeleteDataAsync">Ok</button></td>
                            <td align="left"><button class="btn btn-danger">Cancel</button></td>
                        </tr>

                    </table>
                </div>
            </div>
        </div>
    </div>
}

Once we are done with the above changes, we can see the output like below

Here is so far how we can integrate our Blazor web assembly application to the grpc services for complete source code you can follow it on my  Git Hub Repo

Source code link :: https://github.com/dotnetgik/BlazorWebAseemblyWithGrpc

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

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s