dotnet-cqrs/docs/grpc-integration/getting-started-grpc.md

11 KiB

Getting Started with gRPC

Create your first gRPC service with automatic code generation.

Prerequisites

  • .NET 10 SDK
  • Basic understanding of CQRS
  • Protocol Buffers familiarity (helpful but not required)

Installation

Install Packages

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

Package References

<ItemGroup>
  <PackageReference Include="Svrnty.CQRS.Grpc" Version="1.0.0" />
  <PackageReference Include="Svrnty.CQRS.Grpc.Generators" Version="1.0.0" />
  <PackageReference Include="Grpc.AspNetCore" Version="2.68.0" />
  <PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" />
</ItemGroup>

Step 1: Create .proto File

Create Protos/cqrs_services.proto:

syntax = "proto3";

package myapp;

import "google/protobuf/empty.proto";

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

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

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

message CreateUserResponse {
  int32 user_id = 1;
}

message DeleteUserCommand {
  int32 user_id = 1;
}

// Queries
message GetUserQuery {
  int32 user_id = 1;
}

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

Step 2: Configure .csproj

Add .proto file reference to your project file:

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

Step 3: Implement Handlers

Command Handler (With Result)

using Svrnty.CQRS.Abstractions;

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

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    private readonly IUserRepository _userRepository;

    public CreateUserCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<int> HandleAsync(
        CreateUserCommand command,
        CancellationToken cancellationToken)
    {
        var user = new User
        {
            Name = command.Name,
            Email = command.Email
        };

        await _userRepository.AddAsync(user, cancellationToken);

        return user.Id;
    }
}

Command Handler (No Result)

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

public class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
{
    private readonly IUserRepository _userRepository;

    public DeleteUserCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task HandleAsync(
        DeleteUserCommand command,
        CancellationToken cancellationToken)
    {
        await _userRepository.DeleteAsync(command.UserId, cancellationToken);
    }
}

Query Handler

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>
{
    private readonly IUserRepository _userRepository;

    public GetUserQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<UserDto> HandleAsync(
        GetUserQuery query,
        CancellationToken cancellationToken)
    {
        var user = await _userRepository.GetByIdAsync(query.UserId, cancellationToken);

        if (user == null)
            throw new KeyNotFoundException($"User {query.UserId} not found");

        return new UserDto
        {
            Id = user.Id,
            Name = user.Name,
            Email = user.Email
        };
    }
}

Step 4: Configure Services

using Svrnty.CQRS;
using Svrnty.CQRS.Grpc;

var builder = WebApplication.CreateBuilder(args);

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

// Register commands
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddCommand<DeleteUserCommand, DeleteUserCommandHandler>();

// Register queries
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();

// Register repository
builder.Services.AddScoped<IUserRepository, UserRepository>();

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

var app = builder.Build();

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

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

app.Run();

That's it! The source generator automatically creates CommandServiceImpl and QueryServiceImpl.

Step 5: Build and Run

dotnet build
dotnet run

Server starts on:

Now listening on: https://localhost:5001

Step 6: Test with grpcurl

Install grpcurl

macOS:

brew install grpcurl

Windows:

choco install grpcurl

Linux:

# Download from GitHub releases

List Services

grpcurl -plaintext localhost:5001 list

Output:

grpc.reflection.v1alpha.ServerReflection
myapp.CommandService
myapp.QueryService

Describe Service

grpcurl -plaintext localhost:5001 describe myapp.CommandService

Output:

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

Call CreateUser

grpcurl -plaintext \
  -d '{"name": "John Doe", "email": "john@example.com"}' \
  localhost:5001 \
  myapp.CommandService/CreateUser

Response:

{
  "userId": 42
}

Call GetUser

grpcurl -plaintext \
  -d '{"userId": 42}' \
  localhost:5001 \
  myapp.QueryService/GetUser

Response:

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

Call DeleteUser

grpcurl -plaintext \
  -d '{"userId": 42}' \
  localhost:5001 \
  myapp.CommandService/DeleteUser

Response:

{}

Step 7: Add Validation

Install FluentValidation

dotnet add package FluentValidation

Create Validator

using FluentValidation;

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .WithMessage("Name is required")
            .MaximumLength(100)
            .WithMessage("Name must not exceed 100 characters");

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

Register Validator

builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();

Test Validation

grpcurl -plaintext \
  -d '{"name": "", "email": "invalid"}' \
  localhost:5001 \
  myapp.CommandService/CreateUser

Error Response:

ERROR:
  Code: InvalidArgument
  Message: Validation failed
  Details:
    google.rpc.BadRequest {
      field_violations: [
        { field: "name", description: "Name is required" },
        { field: "email", description: "Valid email address is required" }
      ]
    }

Complete Example

Project Structure:

MyGrpcApp/
├── MyGrpcApp.csproj
├── Program.cs
├── Protos/
│   └── cqrs_services.proto
├── Commands/
│   ├── CreateUserCommand.cs
│   ├── CreateUserCommandHandler.cs
│   ├── CreateUserCommandValidator.cs
│   ├── DeleteUserCommand.cs
│   └── DeleteUserCommandHandler.cs
├── Queries/
│   ├── GetUserQuery.cs
│   ├── GetUserQueryHandler.cs
│   └── UserDto.cs
└── Repositories/
    ├── IUserRepository.cs
    └── UserRepository.cs

Program.cs:

using FluentValidation;
using MyGrpcApp.Commands;
using MyGrpcApp.Queries;
using MyGrpcApp.Repositories;
using Svrnty.CQRS;
using Svrnty.CQRS.Grpc;

var builder = WebApplication.CreateBuilder(args);

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

// Commands
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();
builder.Services.AddCommand<DeleteUserCommand, DeleteUserCommandHandler>();

// Queries
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();

// Repositories
builder.Services.AddSingleton<IUserRepository, InMemoryUserRepository>();

// gRPC
builder.Services.AddGrpc();

var app = builder.Build();

// Map services
app.MapGrpcService<CommandServiceImpl>();
app.MapGrpcService<QueryServiceImpl>();
app.MapGrpcReflectionService();

app.Run();

Testing with Postman

Postman supports gRPC natively:

  1. Create new gRPC request
  2. Enter server URL: localhost:5001
  3. Import .proto files or use reflection
  4. Select service method
  5. Fill in message fields
  6. Click "Invoke"

Next Steps

Now that you have a basic gRPC service:

  1. Proto File Setup - Learn .proto file conventions
  2. Source Generators - Understand how code generation works
  3. Service Implementation - Explore generated code
  4. gRPC Clients - Build clients to consume your services

Troubleshooting

Build Errors

Issue: "Could not find file Protos\cqrs_services.proto"

Solution: Ensure .proto file is in Protos/ directory and referenced in .csproj.

grpcurl Connection Failed

Issue: "Failed to dial target host"

Solution:

  1. Ensure server is running
  2. Check port number
  3. Use -plaintext for development (no TLS)
  4. Use -insecure for self-signed certificates in production

Reflection Not Working

Issue: grpcurl can't list services

Solution: Add gRPC reflection:

app.MapGrpcReflectionService();

Validation Not Working

Issue: Validation doesn't run

Solution: Ensure validator is registered:

builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();

See Also