dotnet-cqrs/docs/grpc-integration/README.md

11 KiB

gRPC Integration Overview

Expose commands and queries via high-performance gRPC services with automatic code generation.

What is gRPC Integration?

The Svrnty.CQRS.Grpc package with Svrnty.CQRS.Grpc.Generators source generator provides automatic gRPC service implementations for all registered commands and queries.

Key Features:

  • Automatic service generation - Source generators create implementations
  • Google Rich Error Model - Structured validation errors
  • High performance - Binary Protocol Buffers
  • Strong typing - Compile-time safety
  • gRPC reflection - Tool support (grpcurl, Postman)
  • Bidirectional streaming - Real-time communication
  • Cross-platform - Works with any gRPC client

Quick Start

Installation

dotnet add package Svrnty.CQRS.Grpc
dotnet add package Svrnty.CQRS.Grpc.Generators
dotnet add package Grpc.AspNetCore

Basic Setup

1. Define .proto file:

syntax = "proto3";

package myapp;

import "google/protobuf/empty.proto";

service CommandService {
  rpc CreateUser (CreateUserCommand) returns (CreateUserResponse);
  rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
}

service QueryService {
  rpc GetUser (GetUserQuery) returns (UserDto);
}

message CreateUserCommand {
  string name = 1;
  string email = 2;
}

message CreateUserResponse {
  int32 user_id = 1;
}

message DeleteUserCommand {
  int32 user_id = 1;
}

message GetUserQuery {
  int32 user_id = 1;
}

message UserDto {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

2. Configure services:

var builder = WebApplication.CreateBuilder(args);

// Register CQRS services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();

// Register commands and queries
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddCommand<DeleteUserCommand, DeleteUserCommandHandler>();
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();

// Add gRPC
builder.Services.AddGrpc();

var app = builder.Build();

// Map auto-generated service implementations
app.MapGrpcService<CommandServiceImpl>();
app.MapGrpcService<QueryServiceImpl>();

// Enable reflection for tools
app.MapGrpcReflectionService();

app.Run();

3. Source generator automatically creates:

  • CommandServiceImpl class implementing CommandService.CommandServiceBase
  • QueryServiceImpl class implementing QueryService.QueryServiceBase

How It Works

┌─────────────────────────────┐
│  Build Time                 │
├─────────────────────────────┤
│  1. Read .proto files       │
│  2. Discover commands/queries│
│  3. Generate service impls  │
│  4. Compile into assembly   │
└─────────────────────────────┘
            │
            ▼
┌─────────────────────────────┐
│  Runtime                    │
├─────────────────────────────┤
│  gRPC Request               │
│  → Deserialize protobuf     │
│  → Validate                 │
│  → Authorize                │
│  → Execute handler          │
│  → Serialize response       │
└─────────────────────────────┘

Commands via gRPC

Command Without Result

public record DeleteUserCommand
{
    public int UserId { get; init; }
}

public class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
{
    public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken)
    {
        // Delete user logic
    }
}

.proto definition:

service CommandService {
  rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
}

message DeleteUserCommand {
  int32 user_id = 1;
}

gRPC Client:

var client = new CommandService.CommandServiceClient(channel);

var request = new DeleteUserCommand { UserId = 123 };
await client.DeleteUserAsync(request);

Command With Result

public record CreateUserCommand
{
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;
}

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    public async Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
    {
        // Create user and return ID
        return newUserId;
    }
}

.proto definition:

service CommandService {
  rpc CreateUser (CreateUserCommand) returns (CreateUserResponse);
}

message CreateUserCommand {
  string name = 1;
  string email = 2;
}

message CreateUserResponse {
  int32 user_id = 1;
}

gRPC Client:

var client = new CommandService.CommandServiceClient(channel);

var request = new CreateUserCommand
{
    Name = "John Doe",
    Email = "john@example.com"
};

var response = await client.CreateUserAsync(request);
var userId = response.UserId;

Queries via gRPC

public record GetUserQuery
{
    public int UserId { get; init; }
}

public record UserDto
{
    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;
}

public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
    public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
    {
        // Fetch and return user
    }
}

.proto definition:

service QueryService {
  rpc GetUser (GetUserQuery) returns (UserDto);
}

message GetUserQuery {
  int32 user_id = 1;
}

message UserDto {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

gRPC Client:

var client = new QueryService.QueryServiceClient(channel);

var request = new GetUserQuery { UserId = 123 };
var user = await client.GetUserAsync(request);

Validation

Automatic Validation with Rich Error Model

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .WithMessage("Name is required");

        RuleFor(x => x.Email)
            .EmailAddress()
            .WithMessage("Valid email address is required");
    }
}

Validation failure response:

google.rpc.Status {
  code: 3  // INVALID_ARGUMENT
  message: "Validation failed"
  details: [
    google.rpc.BadRequest {
      field_violations: [
        { field: "name", description: "Name is required" },
        { field: "email", description: "Valid email address is required" }
      ]
    }
  ]
}

Client handling:

using Grpc.Core;
using Google.Rpc;

try
{
    var response = await client.CreateUserAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
    var status = ex.GetRpcStatus();
    var badRequest = status.GetDetail<BadRequest>();

    foreach (var violation in badRequest.FieldViolations)
    {
        Console.WriteLine($"{violation.Field}: {violation.Description}");
    }
}

Performance Benefits

Binary Protocol

gRPC uses Protocol Buffers (binary format) instead of JSON:

JSON (HTTP):

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com"
}

Size: ~71 bytes

Protobuf (gRPC):

Binary representation

Size: ~35 bytes

Result: ~50% smaller payload

HTTP/2 Multiplexing

  • Multiple requests over single connection
  • Header compression
  • Server push capability
  • Bidirectional streaming

gRPC vs HTTP Comparison

Feature gRPC HTTP (Minimal API)
Protocol HTTP/2 HTTP/1.1 or HTTP/2
Format Protobuf (binary) JSON (text)
Performance Very fast Fast
Payload Size Small Larger
Browser Support Limited (grpc-web) Full
Tooling grpcurl, Postman curl, Postman, Swagger
Streaming Native bidirectional Server-Sent Events
Code Generation Automatic Automatic
Type Safety Strong Strong

When to Use gRPC

Use gRPC for:

  • Microservices communication
  • High-performance APIs
  • Real-time bidirectional streaming
  • Internal APIs
  • Polyglot environments

When to Use HTTP

Use HTTP for:

  • Public APIs
  • Browser-based clients
  • Simple REST APIs
  • Legacy system integration
  • Human-readable debugging

Dual Protocol

Best of both worlds:

// Same handlers, multiple protocols
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();

// HTTP endpoints
app.MapSvrntyCommands();

// gRPC endpoints
app.MapGrpcService<CommandServiceImpl>();

Clients choose their preferred protocol!

Documentation

Getting Started

First gRPC service:

  • Installation
  • .proto file creation
  • Service registration
  • Testing with grpcurl

Proto File Setup

.proto file creation:

  • Syntax and conventions
  • Message definitions
  • Service definitions
  • Importing common types

Source Generators

How code generation works:

  • Build-time generation
  • Generated code structure
  • Customization options
  • Troubleshooting

Service Implementation

Generated service implementations:

  • CommandServiceImpl
  • QueryServiceImpl
  • Validation integration
  • Authorization integration

gRPC Reflection

gRPC reflection for tools:

  • Enabling reflection
  • Using grpcurl
  • Postman support
  • Service discovery

gRPC Clients

Consuming gRPC services:

  • C# client
  • TypeScript client
  • Go client
  • Python client

gRPC Troubleshooting

Common issues:

  • Connection errors
  • Validation errors
  • Code generation issues
  • Performance tuning

Best Practices

DO

  • Use gRPC for microservices
  • Define clear .proto contracts
  • Use gRPC reflection in development
  • Handle RpcException properly
  • Version your services
  • Use deadlines/timeouts
  • Enable compression

DON'T

  • Don't skip error handling
  • Don't expose gRPC publicly without security
  • Don't ignore validation
  • Don't use gRPC for browser apps without grpc-web
  • Don't forget cancellation tokens

What's Next?

See Also