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

534 lines
11 KiB
Markdown

# 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
```bash
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
```xml
<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`:
```protobuf
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:
```xml
<ItemGroup>
<Protobuf Include="Protos\cqrs_services.proto" GrpcServices="Server" />
</ItemGroup>
```
## Step 3: Implement Handlers
### Command Handler (With Result)
```csharp
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)
```csharp
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
```csharp
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
```csharp
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
```bash
dotnet build
dotnet run
```
**Server starts on:**
```
Now listening on: https://localhost:5001
```
## Step 6: Test with grpcurl
### Install grpcurl
**macOS:**
```bash
brew install grpcurl
```
**Windows:**
```powershell
choco install grpcurl
```
**Linux:**
```bash
# Download from GitHub releases
```
### List Services
```bash
grpcurl -plaintext localhost:5001 list
```
**Output:**
```
grpc.reflection.v1alpha.ServerReflection
myapp.CommandService
myapp.QueryService
```
### Describe Service
```bash
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
```bash
grpcurl -plaintext \
-d '{"name": "John Doe", "email": "john@example.com"}' \
localhost:5001 \
myapp.CommandService/CreateUser
```
**Response:**
```json
{
"userId": 42
}
```
### Call GetUser
```bash
grpcurl -plaintext \
-d '{"userId": 42}' \
localhost:5001 \
myapp.QueryService/GetUser
```
**Response:**
```json
{
"id": 42,
"name": "John Doe",
"email": "john@example.com"
}
```
### Call DeleteUser
```bash
grpcurl -plaintext \
-d '{"userId": 42}' \
localhost:5001 \
myapp.CommandService/DeleteUser
```
**Response:**
```json
{}
```
## Step 7: Add Validation
### Install FluentValidation
```bash
dotnet add package FluentValidation
```
### Create Validator
```csharp
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
```csharp
builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();
```
### Test Validation
```bash
grpcurl -plaintext \
-d '{"name": "", "email": "invalid"}' \
localhost:5001 \
myapp.CommandService/CreateUser
```
**Error Response:**
```json
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:**
```csharp
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](proto-file-setup.md)** - Learn .proto file conventions
2. **[Source Generators](source-generators.md)** - Understand how code generation works
3. **[Service Implementation](service-implementation.md)** - Explore generated code
4. **[gRPC Clients](grpc-clients.md)** - 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:
```csharp
app.MapGrpcReflectionService();
```
### Validation Not Working
**Issue:** Validation doesn't run
**Solution:** Ensure validator is registered:
```csharp
builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();
```
## See Also
- [gRPC Integration Overview](README.md)
- [Proto File Setup](proto-file-setup.md)
- [Source Generators](source-generators.md)
- [gRPC Clients](grpc-clients.md)
- [gRPC Troubleshooting](grpc-troubleshooting.md)