| .. | ||
| getting-started-grpc.md | ||
| grpc-clients.md | ||
| grpc-reflection.md | ||
| grpc-troubleshooting.md | ||
| proto-file-setup.md | ||
| README.md | ||
| service-implementation.md | ||
| source-generators.md | ||
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:
CommandServiceImplclass implementingCommandService.CommandServiceBaseQueryServiceImplclass implementingQueryService.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?
- Getting Started - Create your first gRPC service
- Proto File Setup - Learn .proto file conventions
- Source Generators - Understand code generation
- Service Implementation - Explore generated code
- gRPC Clients - Build gRPC clients