11 KiB
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:
- Create new gRPC request
- Enter server URL:
localhost:5001 - Import .proto files or use reflection
- Select service method
- Fill in message fields
- Click "Invoke"
Next Steps
Now that you have a basic gRPC service:
- Proto File Setup - Learn .proto file conventions
- Source Generators - Understand how code generation works
- Service Implementation - Explore generated code
- 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:
- Ensure server is running
- Check port number
- Use
-plaintextfor development (no TLS) - Use
-insecurefor 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>();