Learnixo

.NET & C# Development · Lesson 146 of 229

gRPC in .NET — Protobuf, Services, and Streaming

gRPC in .NET — Protobuf, Services, and Streaming

gRPC is a high-performance RPC framework that uses HTTP/2 and Protocol Buffers (Protobuf). It is the standard communication protocol between microservices at Google, Netflix, and most large-scale .NET shops.


Why gRPC?

REST:   human-readable JSON over HTTP/1.1 — great for public APIs and browsers
gRPC:   binary Protobuf over HTTP/2 — great for service-to-service communication

gRPC advantages over REST:
  - 5–10× smaller payload (binary vs JSON)
  - Strongly typed contract (.proto file is the single source of truth)
  - HTTP/2 multiplexing — multiple calls on one connection
  - Bidirectional streaming — neither party needs to poll
  - Auto-generated client stubs in any language (C#, Go, Python, Java)
  - Built-in deadline/cancellation propagation

gRPC disadvantages:
  - Not browser-native (use gRPC-Web or REST for browser clients)
  - Binary format is not human-readable (harder to debug)
  - Requires HTTP/2 (most load balancers support it, but check)

Step 1: Define the Contract (.proto file)

PROTOBUF
// Protos/orders.proto
syntax = "proto3";

option csharp_namespace = "OrderService";

package orders;

// The service definition
service OrderService {
  // Unary RPC — one request, one response
  rpc GetOrder (GetOrderRequest) returns (OrderResponse);
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);

  // Server streaming — one request, stream of responses
  rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);

  // Client streaming — stream of requests, one response
  rpc BulkCreateOrders (stream CreateOrderRequest) returns (BulkCreateResponse);

  // Bidirectional streaming
  rpc TrackOrders (stream TrackRequest) returns (stream TrackResponse);
}

message GetOrderRequest {
  int32 order_id = 1;
}

message CreateOrderRequest {
  int32 customer_id = 1;
  repeated OrderItemMessage items = 2;
  string delivery_address = 3;
}

message OrderItemMessage {
  int32 product_id = 1;
  int32 quantity = 2;
  double unit_price = 3;
}

message OrderResponse {
  int32 id = 1;
  int32 customer_id = 2;
  double total = 3;
  string status = 4;
  string created_at = 5;
}

message ListOrdersRequest {
  int32 customer_id = 1;
  int32 page_size = 2;
}

message BulkCreateResponse {
  int32 created_count = 1;
  repeated int32 order_ids = 2;
}

message TrackRequest {
  int32 order_id = 1;
}

message TrackResponse {
  int32 order_id = 1;
  string status = 2;
  string updated_at = 3;
}

Step 2: Server Setup

XML
<!-- OrderService.csproj -->
<ItemGroup>
  <PackageReference Include="Grpc.AspNetCore" Version="2.x.x" />
</ItemGroup>

<ItemGroup>
  <!-- Tell the build system to generate C# from the .proto file -->
  <Protobuf Include="Protos\orders.proto" GrpcServices="Server" />
</ItemGroup>
C#
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaxReceiveMessageSize = 4 * 1024 * 1024;   // 4MB
    options.MaxSendMessageSize    = 4 * 1024 * 1024;
});
builder.Services.AddGrpcReflection();   // for grpcurl and Postman

var app = builder.Build();

app.MapGrpcService<OrderGrpcService>();

if (app.Environment.IsDevelopment())
    app.MapGrpcReflectionService();

app.Run();

Step 3: Implement the Service

C#
using Grpc.Core;
using OrderService;   // generated namespace from .proto

public class OrderGrpcService(IOrderRepository repo, ILogger<OrderGrpcService> logger)
    : OrderService.OrderServiceBase
{
    // ── Unary RPC ────────────────────────────────────────────────────────────
    public override async Task<OrderResponse> GetOrder(
        GetOrderRequest request,
        ServerCallContext context)
    {
        var order = await repo.GetByIdAsync(request.OrderId, context.CancellationToken);

        if (order is null)
            throw new RpcException(new Status(StatusCode.NotFound,
                $"Order {request.OrderId} not found"));

        return MapToResponse(order);
    }

    public override async Task<OrderResponse> CreateOrder(
        CreateOrderRequest request,
        ServerCallContext context)
    {
        var order = await repo.CreateAsync(new CreateOrderDto
        {
            CustomerId = request.CustomerId,
            Items = request.Items.Select(i => new OrderItemDto(
                i.ProductId, i.Quantity, (decimal)i.UnitPrice)).ToList(),
            DeliveryAddress = request.DeliveryAddress,
        }, context.CancellationToken);

        return MapToResponse(order);
    }

    // ── Server Streaming ─────────────────────────────────────────────────────
    public override async Task ListOrders(
        ListOrdersRequest request,
        IServerStreamWriter<OrderResponse> responseStream,
        ServerCallContext context)
    {
        await foreach (var order in repo.GetByCustomerAsync(
            request.CustomerId, request.PageSize, context.CancellationToken))
        {
            if (context.CancellationToken.IsCancellationRequested) break;
            await responseStream.WriteAsync(MapToResponse(order));
        }
    }

    // ── Client Streaming ─────────────────────────────────────────────────────
    public override async Task<BulkCreateResponse> BulkCreateOrders(
        IAsyncStreamReader<CreateOrderRequest> requestStream,
        ServerCallContext context)
    {
        var orderIds = new List<int>();

        await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
        {
            var order = await repo.CreateAsync(new CreateOrderDto
            {
                CustomerId      = request.CustomerId,
                Items           = request.Items.Select(i =>
                    new OrderItemDto(i.ProductId, i.Quantity, (decimal)i.UnitPrice)).ToList(),
                DeliveryAddress = request.DeliveryAddress,
            }, context.CancellationToken);
            orderIds.Add(order.Id);
        }

        return new BulkCreateResponse
        {
            CreatedCount = orderIds.Count,
            OrderIds     = { orderIds },
        };
    }

    // ── Bidirectional Streaming ───────────────────────────────────────────────
    public override async Task TrackOrders(
        IAsyncStreamReader<TrackRequest> requestStream,
        IServerStreamWriter<TrackResponse> responseStream,
        ServerCallContext context)
    {
        await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
        {
            var status = await repo.GetStatusAsync(request.OrderId, context.CancellationToken);
            await responseStream.WriteAsync(new TrackResponse
            {
                OrderId   = request.OrderId,
                Status    = status,
                UpdatedAt = DateTime.UtcNow.ToString("O"),
            });
        }
    }

    private static OrderResponse MapToResponse(Order order) => new()
    {
        Id         = order.Id,
        CustomerId = order.CustomerId,
        Total      = (double)order.Total,
        Status     = order.Status,
        CreatedAt  = order.CreatedAt.ToString("O"),
    };
}

Step 4: gRPC Client

XML
<!-- Client project -->
<ItemGroup>
  <PackageReference Include="Grpc.Net.Client" Version="2.x.x" />
  <PackageReference Include="Google.Protobuf" Version="3.x.x" />
  <PackageReference Include="Grpc.Tools" Version="2.x.x" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
  <Protobuf Include="Protos\orders.proto" GrpcServices="Client" />
</ItemGroup>
C#
// Register typed gRPC client in DI
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
    options.Address = new Uri("https://order-service:5001");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    // In production use proper TLS certs; in dev allow self-signed
    ServerCertificateCustomValidationCallback =
        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});

// Use the client
public class OrderApiController(OrderService.OrderServiceClient grpcClient) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(int id)
    {
        try
        {
            var response = await grpcClient.GetOrderAsync(
                new GetOrderRequest { OrderId = id });
            return Ok(response);
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
        {
            return NotFound(ex.Status.Detail);
        }
    }

    [HttpGet("stream/{customerId}")]
    public async IAsyncEnumerable<OrderResponse> StreamOrders(
        int customerId,
        [EnumeratorCancellation] CancellationToken ct)
    {
        using var call = grpcClient.ListOrders(
            new ListOrdersRequest { CustomerId = customerId, PageSize = 50 });

        await foreach (var order in call.ResponseStream.ReadAllAsync(ct))
            yield return order;
    }
}

Error Handling and Status Codes

C#
// gRPC status codes map to HTTP status codes in grpc-gateway
// StatusCode.NotFound       → 404
// StatusCode.InvalidArgument → 400
// StatusCode.Unauthenticated → 401
// StatusCode.PermissionDenied → 403
// StatusCode.Internal       → 500
// StatusCode.Unavailable    → 503

// Throw on server:
throw new RpcException(new Status(StatusCode.InvalidArgument,
    "CustomerId must be positive"));

// Catch on client:
try
{
    var result = await client.CreateOrderAsync(request);
}
catch (RpcException ex)
{
    logger.LogError("gRPC error {Code}: {Detail}", ex.StatusCode, ex.Status.Detail);
    // ex.Trailers — contains additional metadata from server
}

Deadlines and Cancellation

C#
// Always set a deadline on gRPC calls — prevents hanging indefinitely
var deadline = DateTime.UtcNow.AddSeconds(5);

var response = await client.GetOrderAsync(
    new GetOrderRequest { OrderId = 42 },
    deadline: deadline);

// Or use CancellationToken
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var response = await client.GetOrderAsync(
    new GetOrderRequest { OrderId = 42 },
    cancellationToken: cts.Token);

Authentication with JWT

C#
// Client: attach JWT Bearer token to every call
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(o =>
{
    o.Address = new Uri("https://order-service:5001");
})
.AddCallCredentials(async (context, metadata, sp) =>
{
    var tokenService = sp.GetRequiredService<ITokenService>();
    var token = await tokenService.GetTokenAsync();
    metadata.Add("Authorization", $"Bearer {token}");
});

// Server: validate JWT with standard ASP.NET Core auth
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => { /* standard JWT config */ });

builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();

// Protect a gRPC method:
[Authorize]
public override async Task<OrderResponse> CreateOrder(
    CreateOrderRequest request, ServerCallContext context)
{
    var userId = context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier);
    // ...
}

Testing gRPC Services

C#
// Test using WebApplicationFactory — same as REST
public class OrderGrpcServiceTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public OrderGrpcServiceTests(WebApplicationFactory<Program> factory)
        => _factory = factory;

    [Fact]
    public async Task GetOrder_Existing_ReturnsOrder()
    {
        // Create gRPC channel pointing at the test server
        var channel = GrpcChannel.ForAddress(
            _factory.Server.BaseAddress,
            new GrpcChannelOptions { HttpClient = _factory.CreateClient() });

        var client = new OrderService.OrderServiceClient(channel);

        var response = await client.GetOrderAsync(new GetOrderRequest { OrderId = 1 });

        Assert.Equal(1, response.Id);
    }

    [Fact]
    public async Task GetOrder_NotFound_ThrowsRpcException()
    {
        var channel = GrpcChannel.ForAddress(_factory.Server.BaseAddress,
            new GrpcChannelOptions { HttpClient = _factory.CreateClient() });
        var client = new OrderService.OrderServiceClient(channel);

        var ex = await Assert.ThrowsAsync<RpcException>(
            () => client.GetOrderAsync(new GetOrderRequest { OrderId = 9999 }).ResponseAsync);

        Assert.Equal(StatusCode.NotFound, ex.StatusCode);
    }
}

Interview Answer

"gRPC uses HTTP/2 and Protocol Buffers — a binary serialisation format — giving 5–10x smaller payloads and strongly typed contracts compared to REST+JSON. The .proto file is the single source of truth; Grpc.Tools generates server base classes and client stubs automatically. Four RPC types: unary (one request → one response), server streaming (one request → stream), client streaming (stream → one response), bidirectional (stream ↔ stream). In ASP.NET Core: install Grpc.AspNetCore, add the proto file as GrpcServices=Server, inherit from the generated base class, register with MapGrpcService. On the client: Grpc.Net.Client with AddGrpcClient in DI, same proto with GrpcServices=Client. Always set deadlines on client calls — gRPC propagates cancellation through the call chain. Error handling uses RpcException with StatusCode (NotFound, InvalidArgument, Internal). Testing: GrpcChannel.ForAddress with WebApplicationFactory.CreateClient() — same pattern as REST integration tests."