Skip to main content

gRPC

52. gRPC

Short Introduction

gRPC is a modern, open-source, high-performance Remote Procedure Call (RPC) framework that can run anywhere. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features like authentication, bidirectional streaming, flow control, blocking or nonblocking bindings, and cancellation and timeouts.

Official Definition

gRPC is a language-agnostic, high-performance Remote Procedure Call (RPC) framework. gRPC uses HTTP/2 as its transport protocol and Protocol Buffers (protobuf) as its message format, enabling efficient communication between services in distributed systems.

Setup/Usage with .NET 8+ Code

Installation:

dotnet add package Grpc.AspNetCore
dotnet add package Grpc.Tools
dotnet add package Google.Protobuf

Proto Definition:

// Protos/hotel.proto
syntax = "proto3";

option csharp_namespace = "HotelManagement.Grpc";

package hotel;

// Hotel service definition
service HotelService {
rpc GetRoom (GetRoomRequest) returns (RoomResponse);
rpc CreateBooking (CreateBookingRequest) returns (BookingResponse);
rpc GetBookings (GetBookingsRequest) returns (stream BookingResponse);
rpc BookingStream (stream CreateBookingRequest) returns (stream BookingResponse);
}

// Messages
message GetRoomRequest {
int32 room_id = 1;
}

message RoomResponse {
int32 id = 1;
string name = 2;
string type = 3;
double price_per_night = 4;
int32 max_occupancy = 5;
repeated string amenities = 6;
bool is_available = 7;
}

message CreateBookingRequest {
string customer_id = 1;
int32 room_id = 2;
string check_in = 3; // ISO 8601 date string
string check_out = 4; // ISO 8601 date string
int32 guests = 5;
}

message BookingResponse {
int32 id = 1;
string customer_id = 2;
int32 room_id = 3;
string check_in = 4;
string check_out = 5;
double total_amount = 6;
string status = 7;
string created_at = 8;
}

message GetBookingsRequest {
string customer_id = 1;
int32 page = 2;
int32 page_size = 3;
}

gRPC Server Implementation:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add gRPC services
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4MB
options.MaxSendMessageSize = 4 * 1024 * 1024; // 4MB
});

// Add application services
builder.Services.AddScoped<IRoomRepository, RoomRepository>();
builder.Services.AddScoped<IBookingRepository, BookingRepository>();

var app = builder.Build();

// Configure gRPC endpoints
app.MapGrpcService<HotelGrpcService>();

// Enable gRPC-Web for browser clients
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

// Add gRPC reflection for development
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
}

app.Run();

// Services/HotelGrpcService.cs
using Grpc.Core;
using HotelManagement.Grpc;

public class HotelGrpcService : HotelService.HotelServiceBase
{
private readonly IRoomRepository _roomRepository;
private readonly IBookingRepository _bookingRepository;
private readonly ILogger<HotelGrpcService> _logger;

public HotelGrpcService(
IRoomRepository roomRepository,
IBookingRepository bookingRepository,
ILogger<HotelGrpcService> logger)
{
_roomRepository = roomRepository;
_bookingRepository = bookingRepository;
_logger = logger;
}

public override async Task<RoomResponse> GetRoom(GetRoomRequest request, ServerCallContext context)
{
try
{
var room = await _roomRepository.GetByIdAsync(request.RoomId);
if (room == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Room {request.RoomId} not found"));
}

return new RoomResponse
{
Id = room.Id,
Name = room.Name,
Type = room.Type,
PricePerNight = (double)room.PricePerNight,
MaxOccupancy = room.MaxOccupancy,
IsAvailable = room.IsAvailable
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting room {RoomId}", request.RoomId);
throw new RpcException(new Status(StatusCode.Internal, "Internal server error"));
}
}

public override async Task<BookingResponse> CreateBooking(CreateBookingRequest request, ServerCallContext context)
{
try
{
// Validate dates
if (!DateTime.TryParse(request.CheckIn, out var checkIn) ||
!DateTime.TryParse(request.CheckOut, out var checkOut))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid date format"));
}

if (checkOut <= checkIn)
{
throw new RpcException(new Status(StatusCode.InvalidArgument, "Check-out must be after check-in"));
}

// Check room availability
var room = await _roomRepository.GetByIdAsync(request.RoomId);
if (room == null || !room.IsAvailable)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, "Room not available"));
}

// Create booking
var booking = new Booking
{
CustomerId = request.CustomerId,
RoomId = request.RoomId,
CheckIn = checkIn,
CheckOut = checkOut,
Guests = request.Guests,
TotalAmount = (decimal)room.PricePerNight * (checkOut - checkIn).Days,
Status = "Confirmed",
CreatedAt = DateTime.UtcNow
};

await _bookingRepository.AddAsync(booking);

return new BookingResponse
{
Id = booking.Id,
CustomerId = booking.CustomerId,
RoomId = booking.RoomId,
CheckIn = booking.CheckIn.ToString("O"),
CheckOut = booking.CheckOut.ToString("O"),
TotalAmount = (double)booking.TotalAmount,
Status = booking.Status,
CreatedAt = booking.CreatedAt.ToString("O")
};
}
catch (RpcException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating booking");
throw new RpcException(new Status(StatusCode.Internal, "Internal server error"));
}
}

public override async Task GetBookings(GetBookingsRequest request,
IServerStreamWriter<BookingResponse> responseStream, ServerCallContext context)
{
try
{
var bookings = await _bookingRepository.GetByCustomerIdAsync(request.CustomerId,
request.Page, request.PageSize);

foreach (var booking in bookings)
{
if (context.CancellationToken.IsCancellationRequested)
break;

var response = new BookingResponse
{
Id = booking.Id,
CustomerId = booking.CustomerId,
RoomId = booking.RoomId,
CheckIn = booking.CheckIn.ToString("O"),
CheckOut = booking.CheckOut.ToString("O"),
TotalAmount = (double)booking.TotalAmount,
Status = booking.Status,
CreatedAt = booking.CreatedAt.ToString("O")
};

await responseStream.WriteAsync(response);
await Task.Delay(100, context.CancellationToken); // Simulate processing time
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error streaming bookings for customer {CustomerId}", request.CustomerId);
throw new RpcException(new Status(StatusCode.Internal, "Internal server error"));
}
}

public override async Task<BookingResponse> BookingStream(
IAsyncStreamReader<CreateBookingRequest> requestStream,
ServerCallContext context)
{
var bookingCount = 0;
var totalAmount = 0.0;

await foreach (var request in requestStream.ReadAllAsync())
{
try
{
var booking = await CreateBooking(request, context);
bookingCount++;
totalAmount += booking.TotalAmount;
}
catch (RpcException ex)
{
_logger.LogWarning("Failed to create booking in stream: {Error}", ex.Status.Detail);
}
}

return new BookingResponse
{
Id = bookingCount,
TotalAmount = totalAmount,
Status = $"Processed {bookingCount} bookings"
};
}
}

gRPC Client:

// Client/Program.cs
using Grpc.Net.Client;
using HotelManagement.Grpc;

// Create gRPC channel
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new HotelService.HotelServiceClient(channel);

// Unary call
var roomRequest = new GetRoomRequest { RoomId = 1 };
var roomResponse = await client.GetRoomAsync(roomRequest);
Console.WriteLine($"Room: {roomResponse.Name} - ${roomResponse.PricePerNight}/night");

// Server streaming
var bookingsRequest = new GetBookingsRequest
{
CustomerId = "CUST001",
Page = 1,
PageSize = 10
};

using var streamingCall = client.GetBookings(bookingsRequest);
await foreach (var booking in streamingCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Booking {booking.Id}: {booking.Status}");
}

// Client streaming example would go here...

Use Cases

  • High-Performance APIs: Low-latency, high-throughput service communication
  • Microservices Communication: Efficient inter-service communication
  • Real-time Systems: Streaming data and bidirectional communication
  • Mobile Applications: Efficient mobile-to-server communication
  • IoT Applications: Device-to-cloud communication with binary protocols
  • Multi-language Environments: Language-agnostic service interfaces

When to Use vs When Not to Use

Use gRPC when:

  • Need high-performance, low-latency communication
  • Building microservices with type-safe contracts
  • Require streaming capabilities
  • Working in polyglot environments
  • Need efficient binary serialization
  • Building internal service APIs

Consider alternatives when:

  • Building public web APIs (REST is more accessible)
  • Working with web browsers (limited gRPC support)
  • Need human-readable messages for debugging
  • Working with legacy systems
  • Team lacks Protocol Buffers expertise

Market Alternatives & Pros/Cons

Alternatives:

  • REST APIs: HTTP-based, human-readable, widely supported
  • Apache Thrift: Cross-language RPC framework
  • Apache Avro: Data serialization system
  • MessagePack: Efficient binary serialization
  • JSON-RPC: Lightweight remote procedure call protocol
  • WebSockets: Real-time bidirectional communication

Pros:

  • High performance and efficiency
  • Strong typing with Protocol Buffers
  • Built-in streaming support
  • Language agnostic
  • HTTP/2 benefits (multiplexing, compression)
  • Excellent tooling and code generation

Cons:

  • Limited browser support
  • Learning curve for Protocol Buffers
  • Binary format makes debugging harder
  • Requires HTTP/2 infrastructure
  • Less human-readable than JSON/XML

Complete Runnable Sample

Complete gRPC Solution:

<!-- HotelManagement.Grpc.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.57.0" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.57.0" />
</ItemGroup>

<ItemGroup>
<Protobuf Include="Protos\hotel.proto" GrpcServices="Server" />
</ItemGroup>
</Project>