Working With gRPC in .Net
gRPC
gRPC is a high-performance Remote Procedure Call (RPC) framework. Similar like SOAP it uses a Contract-first approach to API development. It uses Protocol buffers (protobuf) to define services and messages sent between Client and servers. Protobuf messages are defined in .proto files.
The main benefits of gRPC are:
- It’s modern lightweight Contract first API development using Protocol Buffers which uses binary serialization to transfer data. It mainly helps to reduce network usage.
- You can use multiple languages to develop contracts and clients to call the services (protobuf supports wide range of programming languages) and has strongly-typed servers and clients.
- gRPC supports Clinet, server calls like REST and also supports bi-directional streaming calls where real-time services that need to handle streaming requests or responses.
REST VS gRPC
REST:
- REST is a popular resource based technique using HTTP Verbs HTTP GET,HTTP POST,HTTP PUT,HTTP PACTH,HTTP DELETE to perform CURD operations. REST’s communication often includes sending a JSON and can run over HTTP/1.1 or HTTP/2.
- It is easy to develop a RESTful web service and it has browser support to perform action over Web Clients from Browser.
- Major Communication happens using a JSON or XML, which is human readable. This makes it easier for developers to determine if the client input was sent correctly to the server and back, but it is too large to parse data.
- REST is also widely used. A lot of people have experience with it and a lot of other web services (and clients) use REST. Having a REST web service makes it easier for other people to interact with your web service.
- We don’t need any client setup to make calls to the server.
gRPC
- gRPC is an open-source RPC framework that is created and used by Google. It is built upon HTTP/2 which makes bi-directional communication possible. gRPC communicates using binary data using protocol buffers by default for serializing structured data. gRPC servers allow cross-language unary calls or stream calls.
- gRPC can use protocol buffer for data serialization. This makes payloads faster, smaller and simpler.
- Just like REST, gRPC can be used cross-language which means that if you have written a web service in Golang, a Java written application can still use that web service, which makes gRPC web services very scalable.
- gRPC uses HTTP/2 to support highly performant and scalable API’s and makes use of binary data rather than just text which makes the communication more compact and more efficient. gRPC makes better use of HTTP/2 then REST. gRPC for example makes it possible to turn-off message compression. This might be useful if you want to send an image that is already compressed. Compressing it again just takes up more time.
- It is also type-safe. This basically means that you can’t give an apple while a banana is expected. When the server expects an integer, gRPC won’t allow you to send a string because these are two different types.
- gRPC has Support to Java Script from web. It is easy to make calls form Browser using gRPC web library.
Protos
gRPC uses Protobuf as its Interface Definition Language (IDL). Protobuf IDL is a language neutral format for specifying the messages sent and received by gRPC services and Contract Defined in a Service for Communication.
Protobuf has Special syntax to define contracts and messages to Generate Services and .Net Models to communicate. When we define Contracts and Messages in protos .Net will generate a code into Managed C# language using Grpc.Tools Library.
Protobuf supports a range of native scalar value types that are equivalent C# type, but with having wide range types in C#, there is a limitation to have them converted into specific types. example:(The native scalar types don’t provide for date and time values, equivalent to .NET’s DateTimeOffset, DateTime, and TimeSpan. These types can be specified by using some of Protobuf’s Well-Known Types extensions). We have some missed types like Decimal in native support.
Proto Example:
namespace Practice
{
public class Product
{
public int ProductId { get; set;}
public string ProdcutName { get; set;}
public string Description { get; set;}
public int SKU { get; set; }
}
}
To Define C# model in Proto message we have equalent syntax below.
syntax = "proto3";option csharp_namespace = "Practice";import "google/protobuf/empty.proto";service Products{
rpc GetProducts(google.protobuf.Empty) returns (Response);
}message Response{
repeated Product products = 1;
Status status = 2;
}enum Status {
Success = 0;
Failure = 1;
Unknown = 2;
}message Product{ int32 product_id = 1;
string prodcut_name = 2;
string sescription = 3;
int32 SKU = 4;}
In the above protobuf file, every Key has their own field number, those are very important in Proto files. They’re used to identify fields in the binary encoded data, which means they can’t change from version to version of your service. The advantage is that backward compatibility and forward compatibility are possible. Clients and services will simply ignore field numbers that they don’t know about, as long as the possibility of missing values is handled.
In binary format, the field number is combined with a type identifier. Field numbers from 1 to 15 can be encoded with their type as a single byte. Numbers from 16 to 2,047 take 2 bytes. You can go higher if you need more than 2,047 fields on a message for any reason. Single byte identifiers for field numbers 1 to 15 offer better performance, so you should use them for the most basic and frequently used fields.
Error handing
While developing services, it is best practice to provide better information when something is wrong with processing a request by throwing an error to caller giving valid information about the request.
gRPC has own Exception type to provide information about exception happened while processing request. By providing a valid Status code and Error message to the client we can have better information about What’s Wrong with the request or Server.
Similar RESTful services are sending Status code to client about the request processing errors like Model State is not valid or Unauthorized request. gRPC also have different status code to give the information. An ASP.NET Core gRPC service can send an error response by throwing an RpcException, which can be caught by the client as if it were in the same process. The RpcException must include a status code and description, and can optionally include metadata and a longer exception message.
Raise errors in ASP.NET Core gRPC
public async Task<Empty> CreateProduct(Product request, ServerCallContext context)
{
if (CheckForProductExists(request.ProductId))
{
var metadata = new Metadata
{
{ "Product", request.ProductId }
};
throw new RpcException(new Status(StatusCode.AlreadyExists, "Product Already Exists"), metadata);
}
}
Catch errors in gRPC clients
try
{
await client.CreateProduct(new Product{ Id = id });
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
{
var productId = ex.Trailers.FirstOrDefault(e => e.Key == "Product");
Console.WriteLine($"Product '{productId }' already exists.");
}
catch (RpcException)
{
// Handle any other error type ...
}
Testing
Manual Testing
Test Tooling is available for gRPC that allows developers to test services without building client apps. similar to tools such as Postman and Swagger UI, gRPC has gRPCurl, gRPCui which provides command-line tool and interactive web UI to interaction with gRPC services.
To provide support to gRPCurl or gRPCui have to enable gRPC reflection to access information about services and message from protobuf’s.
gRPC ASP.NET Core has built-in support for gRPC reflection with the Grpc.AspNetCore.Server.Reflection package. To configure reflection in an app:
- Add a Grpc.AspNetCore.Server.Reflection package reference.
- Register reflection in Startup.cs:
- AddGrpcReflection to register services that enable reflection.
- MapGrpcReflectionService to add a reflection service endpoint.
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
services.AddGrpcReflection();
}public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>(); if (env.IsDevelopment())
{
endpoints.MapGrpcReflectionService();
}
});
}
Example:
Functional and Unit Testing
gRPC has great support for Writing Testing cases, we can host test gRPC server using Microsoft.AspNetCore.TestHost
and perform the testcases to validate code in different scenarios.
Interceptor
gRPC supports to add interceptor to clinet side or server side request, we can write any middleware interceptor on gRPC server before reaching the actual RPC method. It can be used for multiple purposes such as logging, tracing, rate-limiting, authentication and authorization and also modify request with authentication meta data before sending the request.
Logging Interceptor for gRPC Service
namespace ProductGrpc
{
public class LoggerInterceptor : Interceptor
{
private readonly ILogger<LoggerInterceptor> _logger; public LoggerInterceptor(ILogger<LoggerInterceptor> logger)
{
_logger = logger;
} public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation($"gRPC call {context.Method}."); try
{
return await continuation(request, context);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error thrown by {context.Method}.");
throw;
}
}
}
}
Adding Interceptor on startup.cs
namespace ProductGrpc
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.Interceptors.Add<LoggerInterceptor>();
});
} public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseRouting(); app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
});
}
}
}
gRPC Client
gRPC integration with HttpClientFactory offers a centralized way to create gRPC clients. It can be used as an alternative to configuring stand-alone gRPC client instances. Factory integration is available in the Grpc.Net.ClientFactory
NuGet package.
The factory offers the following benefits:
Provides a central location for configuring logical gRPC client instances
Manages the lifetime of the underlying HttpClientMessageHandler
Automatic propagation of deadline and cancellation in an ASP.NET Core gRPC service
Register gRPC clients
services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
});
Health Check
In microservice environment Health Check will play bigger role in knowing the Status of Service.
Health checks are used to probe whether the server is able to handle rpcs. The client-to-server health checking can happen from point to point or via some control system. A server may choose to reply “unhealthy” because it is not ready to take requests, it is shutting down or some other reason. The client can act accordingly if the response is not received within some time window or the response says unhealthy in it.
A GRPC service is used as the health checking mechanism for both simple client-to-server scenario and other control systems such as load-balancing. Being a high level service provides some benefits. Firstly, since it is a GRPC service itself, doing a health check is in the same format as a normal rpc. Secondly, it has rich semantics such as per-service health status. Thirdly, as a GRPC service, it is able reuse all the existing billing, quota infrastructure, etc, and thus the server has full control over the access of the health checking service.
in Asp.Net gRPC has great support in knowing the Heath of the service with minimal setup. To know the Health check .Net provides a couple of nuget packages to verify the Health of the service Grpc.HealthCheck
and Grpc.AspNetCore.HealthChecks
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
services.AddGrpcHealthChecks()
.AddAsyncCheck("HealthCheck", () =>
{ var result = VerifyDbConnection()
? HealthCheckResult.Unhealthy()
: HealthCheckResult.Healthy(); return Task.FromResult(result);
}, Array.Empty<string>());
} public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseRouting(); app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcHealthChecksService();
});
}
}namespace HealthCheck
{
public class Program
{
static async Task Main(string[] args)
{
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Health.HealthClient(channel); var cts = new CancellationTokenSource();
using var call = client.Watch(new HealthCheckRequest { Service = "HealthCheck" }, cancellationToken: cts.Token);
var watchTask = Task.Run(async () =>
{
try
{
await foreach (var message in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"{DateTime.Now}: Service is {message.Status}");
}
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
{
// Handle cancellation exception.
}
}); Console.ReadKey();
Console.WriteLine("Finished"); cts.Cancel();
await watchTask;
}
}
}
Benchmarks
gRPC has lot improved in latest .Net 5.0 when run the benchmarks across the Web API with gRPC on .Net Core 3.1 and .Net 5.0 there are some interesting results.
In .Net 5.0 Microsoft Team did lot of work to increase the Performance and reduction of Code generation for Proto buf file. gRPC works Great with .Net 5.0
Comparison between .Net core 3.1 with .Net 5.0 for Load balancing (From Microsoft)
Github Link:
Please Fork it and Play with it.
Thank you.