gRPC Services using .NET 5: Using Entity Framework Core with gRPC for Performing Database Operations

In this tutorial we will understand how to use the Entity Framework Core in gRPC service for performing database operations?

What is gRPC?

gRPC, is a modern high performance, open source Remote Procedure Call (RPC) framework. The gRPC can run on any environment. The most important features of gRPC are as follows

  1. It is language agnostic.
  2. There are tools available for many languages to generate strongly-typed server and client
  3. This supports server, client and bi-directional streaming calls
  4. Since it uses Protobuf binary serialization, it has reduced network usage
  5. This uses Contract-first API development using Protocol Buffers by default
The gRPC is ideal for developing lightweight microservices where efficiency is very critical. In real-time Point-to-Point services where streamed requests are needed to be handled then use of  gRPC can be idle.   

The .proto files

By default, gRPC uses Protocal Buffers. This is Google's mature open source mechanism for serializing the structured data. The .proto files are the service and message contract. These are uses from server to client for messaging. The .proto file provides the contract-first approach to the API development in gRPC. 

When using the gRPC in the application, the first step is, we have to create proto file where we need to define a structure for the data which we want to serialize across server to client. The code in listing 1 shows a sample protofile
message Student {
  string studentName = 1;
  int32 StudentId = 2;
  string CourseName = 3;
}



Listing 1: The Sample proto file   

The proto file in listing 1 shows how the message structure can be defined. When we create RPC methods, we can use this format in data communication across Server to client.  Once the proto file is created with the structure, we can use the protoc compiler to generate stub classes in language of our choice e.g. C#, JAVA, etc.

The figure 1 shows the use of gRPC in the Client-to-Server data communication

 

Figure 1: The use of gRPC

The figure 1 shows the use of gRPC in application development. The server application contains the gRPC service structure for messaging. In this tutorial, we will create gRPC service hosted in ASP.NET Core. The server application will define the messaging structure using proto file. The protoc compiler will generate the stub class which will be used by the server and client application for messaging. The client and server will communicate using binary serialized message over HTTP/2 protocol.      

Creating gRPC service using .NET 5 using Visual Studio 2019

Step 1: Open Visual Studio 2019. Create a new ASP.NET Core gRPC Service project as shown in the figure 2




Figure 2: Creating the gRPC Service  
 
Click on Next button and set the project name as grpc_db_ops and click on Next button. In the next window, select target framework as .NET 5.0 as shown in the figure 3



Figure3: Selecting Target Framework 

On this window, click on the Create button. The project will be created with Protos and Services folders. The Protos folder already contains greet.proto file. This file contains service definition structure for request and response messages as show in listing 2. 

syntax = "proto3";

option csharp_namespace = "grpc_db_ops";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}



 Listing 2: The greet.proto file with service structure
The Services folder contains GreeterService.cs file, this class contains the server-side logic.  The GreetrService class is derived from is derived from GreeterBase. Now the question here is, how this class is created? Well, the answer is the Grpc.AspNetCore package used in the project. Expand the Dependencies--> Packages, the project shows various dependencies of Grpc.AspNetCore package as shown in figure 4




Figure 4: The Grpc.AspNetCore package ad its dependencies

  1. Grpc.AspNetCore
    1. This is gRPC meta package for ASP.NET Core. This package allows to use gRPC service hosted in ASP.NET Core. This package provide AddGrpc() method to enable gRPC in ASP.NET Core. Using MapGrpcService() method, each gRPC service in ASP.NET Core application is added in routing pipeline.   
  2. Google.Protobuf
    1. This is C# runtime library  for protocol buffers. This is data interchange format by google.
  3. Grpc.Tools
    1. This is the most important package. This is gRPC and Protocol Buffer compiler for managed C# projects. This is used to compile the .proto files into the code. This is the reason because of which in our project we have the generated C# files based on contract structure defined in the proto file.
  4. Grpc.AspNetCore.Server.ClientFactoy
    1. This package represents HttpClientFactory integration for the gRPC .NET client when running in ASP.NET Core. The HttpClietnFactory is used to manage the communication from client to server.
The  Grpc.Tools package is responsible for generating code file from .proto file. But, the question here is that how is this happening? To understand that, right click on the project and select Edit Project File, option. This will open the project file, this file shows the Protobuf configuration as shown in listing 3
 <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>
Listing 2: The Protobuf configuration in Project file        
When we build the project, the Protobuf compiler will generate the code file from the .proto file. This file is generated in obj folder as shown in figure 5



Figure 5: The path where code files are generated
 
Step 2: Since we will be performing database operations using gRPC service, we need to add following packages in the project either using NuGet Package Manager window or using command prompt
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.Tools
If you want to install package from the command line, then open command prompt and navigate to the project folder and the following command

dotnet add package <Package-Name> 

e.g.

dotnet add package Microsoft.EntityFrameworkCore

To access the gRPC service in client application like ASP.NET Core, Blazor, etc we need to install Grpc.AspNetCore.Web package in the gRPC project.  This package provides gRPC-Web support for ASP.NET Core alongside gRPC HTTP/2 support. The Grpc.AspNetCore.Web package provide UseGrpcWeb() middleware method and EnableGrpcWeb() method. These two methods makes sure that the request from Browser clients like Blazor WebAssembly can be accepted by ASP.NETCore gRPC service.    

Creating Database and Table for Database Operations

Step 3: I have used Sql Server for creating database and table. The Script for creating database and table is shown in listing 3

Create Database App

USE [App]
 
CREATE TABLE [dbo].[Products](
[ProductRowId] [int] IDENTITY(1,1) NOT NULL,
[ProductId] [varchar](20) NOT NULL,
[ProductName] [varchar](200) NOT NULL,
[CategoryName] [varchar](200) NOT NULL,
[Manufacturer] [varchar](200) NOT NULL,
[Price] [int] NOT NULL,
PRIMARY KEY CLUSTERED 
(
[ProductRowId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
UNIQUE NONCLUSTERED 
(
[ProductId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO


Listing 3: Create database and table

Step 4: We will be using the Entity Framework Core Database First approach to generate entities in the project. Run the following command from the command prompt to scaffold entities from the database

dotnet ef dbcontext scaffold "Data Source=.;Initial Catalog=App;Integrated Security=SSPI" Microsoft.EntityFrameworkCore.SqlServer -o Models

Once this command runs successfully, the Models folder will be added in the project. This folder will contains the Product.cs and AppContext.cs files. Copy the connection string from the AppContext class and paste it in the appsettings.json as shown in the listing 4

"ConnectionStrings": {
        "AppDbContext": "Data Source=.;Initial Catalog=App;Integrated Security=SSPI"
    }
Listing 4: The Connection String  

Adding the Contract Structure in the project

Step 5: In the Protos folder add a new Protobuf file and name it as products.proto.  In this file add the structure as shown in the listing 5

syntax = "proto3";

option csharp_namespace = "grpc_db_ops";

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 5: The Products Structure

 
The products.proto file contains following message formats
  • Product, this is a message structure for exchanging product data
  • ProductRowIdFilter, this is a message structure to read the product record based on ProductRowId
  • Products, this is a message structure that will be used to exchange the products collection
  • Empty, a message structure which represents the empty response from server and empty data to be send from client to server.
The file defines the ProductsService RPC contracts for performing database operations. The csharp_namesapace represents the service namespace in which the C# code will be generated from the proto file.

Step 6: To generate the C# code from the proto file, modify the project file and add the Protobuf configuration as shown in the listing 6

<ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
	  <Protobuf Include="Protos\products.proto" GrpcServices="Server" />
  </ItemGroup>

Listing 6: The Project file defining configuration for products.proto      

Riglt-Click on the products.proto file and select properties to view properties window. In the properties window make sure that Build Action is set to Protobuf compiler and  gRPC Stub classes is set to Server Only as shown in figure 6



Figure 6: The Compilation settings for products.proto file   


Now, Build the project, the C# files will be generated in the obj folder as shown figure 7


Figure 7: The generated code files  

The ProductsGrpc.cs file contains the ProductsServiceBase class. This class contains C# code for RPC methods from the service contract which we have created in products.proto file. The Products.cs file contains Products class. This represents the message that will be exchanged from server to client. The Products.cs also contains Product class. This is the Product message that will be exchanged between client to server for creating and updating product record.

Step 7: In the Services folder add a new class file and name it as ProductsAppService.cs. In this file add code as shown in listing 7

using Grpc.Core;
using grpc_db_ops.Models;
using System.Linq;
using System.Threading.Tasks;
using static grpc_db_ops.ProductsService;

namespace grpc_db_ops
{
	public class ProductsAppService: ProductsServiceBase
	{
		private readonly AppDbContext dbContext;

		public ProductsAppService(AppDbContext dbContext)
		{
			this.dbContext = dbContext;
		}

		public override Task<Products< GetAll(Empty request, 
            ServerCallContext context)
		{
			Products response = new Products();
			var products =  
             from prd in dbContext.Products
					select new Product()
					{
					 ProductRowId = prd.ProductRowId,
					 ProductId = prd.ProductId,
					 ProductName =prd.ProductName,
					 CategoryName = prd.CategoryName,
					 Manufacturer = prd.Manufacturer,
					 Price = prd.Price
					};
			response.Items.AddRange(products.ToArray());
			return Task.FromResult(response);
		}

		public override Task<Product> GetById(ProductRowIdFilter request, 
           ServerCallContext context)
		{
		  var product = dbContext.Products.Find(request.ProductRowId);
		  var searchedProduct = new Product()
			{ 
			   ProductRowId  = product.ProductRowId,
			   ProductId = product.ProductId,
			   ProductName = product.ProductName,
			   CategoryName = product.CategoryName,
			   Manufacturer = product.Manufacturer,
			   Price = product.Price
			};
			return Task.FromResult(searchedProduct);
		}

		public override Task<Product> Post(Product request, 
           ServerCallContext context)
		{
		  var prdAdded = new Models.Product()
			{
				ProductId = request.ProductId,
				ProductName = request.ProductName,
				CategoryName = request.CategoryName,
				Manufacturer = request.Manufacturer,
				Price = request.Price
			};

		 var res = dbContext.Products.Add(prdAdded);

			dbContext.SaveChanges();
			var response = new Product()
			{
				 ProductRowId = res.Entity.ProductRowId,
				 ProductId = res.Entity.ProductId,
				 ProductName = res.Entity.ProductName,
				 CategoryName = res.Entity.CategoryName,
				 Manufacturer = res.Entity.Manufacturer,
				 Price = res.Entity.Price
			};
			return Task.FromResult<Product>(response);
		}

		public override Task<Product> Put(Product request, 
           ServerCallContext context)
	  {
		Models.Product prd = dbContext.Products.Find(request.ProductRowId);
			if (prd == null)
			{
				return Task.FromResult<Product>(null);
			}


			prd.ProductRowId = request.ProductRowId;
			prd.ProductId = request.ProductId;
			prd.ProductName = request.ProductName;
			prd.CategoryName = request.CategoryName;
			prd.Manufacturer = request.Manufacturer;
			prd.Price = request.Price;
			 
			dbContext.Products.Update(prd);
			dbContext.SaveChanges();
			return Task.FromResult<Product>(new Product() 
			{
				ProductRowId = prd.ProductRowId,
				ProductId = prd.ProductId,
				ProductName = prd.ProductName,
				CategoryName = prd.CategoryName,
				Manufacturer = prd.Manufacturer,
				Price = prd.Price
			});
		}


		public override Task<Empty> Delete(ProductRowIdFilter request, 
          ServerCallContext context)
		{
		 Models.Product prd = dbContext.Products.Find(request.ProductRowId);
			if (prd == null)
			{
				return Task.FromResult<Empty>(null);
			}

			dbContext.Products.Remove(prd);
			dbContext.SaveChanges();
			return Task.FromResult<Empty>(new Empty());
		}

	}
}


Listing 7: The ProductsAppService class for performing database operations

The ProductsAppService class is derived from ProductsServiceBase class. The ProductsAppService class overrides GetAll(), GetById(), Post(), Put(), Delete() methods from ProductsServiceBase class. These methods contains code to perform database operations using AppDbContext class. The request parameter to these methods represents the data received from the client application. This parameter represents the message formats defines in the products.proto file e.g. Empty, Product, ProductRowIdFilter. The GetAll() method responds the Products message which is represents collection of products read from Product table of the database.  

If you carefully observe the code in listing 7, the code is written to read data received from client for Product message and assign it to the Product model class that we have generated from EF Core scaffolding in Step 4. Here please understand one important point that, although the C# code is generated based on messages structure defined in the proto file, the generated Product from proto file is different than the Product model classes.

Step 8: Modify the ConfigureServices() method of the Starutp class in Startup.cs file to register the AppDbContext and CORS policies in services as shown in listing 8


public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
			
  services.AddDbContext<AppDbContext>(options=> {
		options.
               UseSqlServer(Configuration
                     		.GetConnectionString("AppDbContext"));
			});
  services.AddCors(options=> {
		  options.AddPolicy("cors", 
           policy=> {
			 policy
                          .AllowAnyMethod()
                          .AllowAnyHeader()
                          .AllowAnyOrigin()
                          .WithExposedHeaders("Grpc-Status",
                                    "Grpc-Message", "Grpc-Encoding",
                                    "Grpc-Accept-Encoding"); 
                      });
		});
}
Listing 8: Registering the services
       
In the listing 8, we have added the CORS policy so that gRPC service can be accessed from browser clients.  Especially, we have defined gRPC-specific headers to prevent the Bad gRPC response from gRPC service to the browser clients. 

Modify the Configure() method of the Startup class by adding the gRPC middleware and endpoint mapping to gRPC services. Please note that the UseGrpcWeb() middleware must be added in between Routing middleware and Endpoints middleware as shown in listing 9     

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
 if (env.IsDevelopment())
 {
	app.UseDeveloperExceptionPage();
 }
			
 app.UseRouting();
 app.UseCors("cors");
 app.UseGrpcWeb();
 app.UseEndpoints(endpoints =>
  {
	endpoints.MapGrpcService<GreeterService>()
      .EnableGrpcWeb()
	   .RequireCors("cors");
	endpoints.MapGrpcService<ProductsAppService>()
     .EnableGrpcWeb()
	  .RequireCors("cors");

	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");
	});
  });
}
Listing 9: The Middleware configuration

Build the project and make sure that there are no errors. If there are no errors then the gRPC services will be hosted successfully as shown in figure 8


Figure 8: The successful gRPC service

Consuming the gRPC Service in .NET Core Console Client Application

Step 9: In the same solution add a new .NET Core Console application. Name this application as CS_Console_Clients. In this project add a new folder and name this as Protos. Copy the products.proto file from the gRPC service project in this Protos folder. 

Step 10: In this client project add following packages
  • Google.Protobuf
  • Grpc.Net.Client
    • This package is used as a .NET Client for gRPC service
  • Grpf.Tools       
Step 11: Modify the project file of the client project to configure the products.proto file to generate the client-side code as shown in listing 10

 <ItemGroup>
    <Protobuf Include="Protos\products.proto" GrpcServices="Client" />
  </ItemGroup>
Listing 10: The proto file configuration in client-side code

Right-Click on the products.proto file and select properties and make sure that Build Action is set to you Protobuf compiler and gRPC Stub Classes is set to Client only as shown in figure 9


Figure 9: The properties for the proto file in client application

Build the client application. This will generate the client side stub classes. We can use this classes to connect to the gRPC services and send requests to the service.

Step 12: In Program.cs add the code in listing 11


using Grpc.Net.Client;
using System;
using clientnamespace;
using static clientnamespace.ProductsService;
using System.Text.Json;
namespace CS_Console_CLients
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Press any key ");
			Console.ReadLine();

			var channel = 
            	GrpcChannel.ForAddress("https://localhost:5001");

			var client = new ProductsServiceClient(channel);
			GetAll(client);
			Insert(client);
                        Console.WriteLine();
                        GetAll(client); 
			Console.ReadLine();
		}
		
               static void GetAll(ProductsServiceClient client)
		{
			var products = client.GetAll(new Empty());
			foreach (var item in products.Items)
			{
                          Console.WriteLine($"{item.ProductRowId} 
                          {item.ProductId} 
                          {item.ProductName}
                          {item.CategoryName}
                          {item.Manufacturer}
                          {item.Price}");
			}
		}
        
		static void Insert(ProductsServiceClient client)
		{
		  var product = client.Post(new Product() 
                    { 
                          ProductId="Prd-0014",
                          ProductName="Iron",
                          CategoryName="Electrical",
                          Manufacturer="MS-ECT", 
                          Price=3400 
                     });
             
                   Console.WriteLine($"Record Added 
                    {JsonSerializer.Serialize(product)}");
		}
	}
}

Listing 11: The client-side code 
  
The code uses the GrpcChannel class. This class represents the gRPC channel. This channel is an abstraction of connection to remote servers. Its is recommended that the client should use the same channel because the process of creating channel is expensive. The client should use the same channel for making as many as possible calls to the gRPC remote service. The ForAddress() method accepts the URL of the gRPC service to connect. This channel object is passed to the ProductsSreviceClient class. This is the client side stub class generated from proto file.     
 
Step 13: Right-Click on the solution and set multiple startup projects as shown in the figure 10


Figure 10: Multiple startup projects 
We will run the gRPC service first and then the Console client application. Run the application. The console client will show the List of products received from the gRPC service as shown in figure 11


Figure 11: The result

Conclusion: The gRPC service can be used as a publicly exposed endpoint for the client application for performing business operations. Using the Contract-First API, we can share contracts to the client application to stream data in binary message format from client-to-service and back.

The code this article is available on this link
   

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