534 lines
11 KiB
Markdown
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)
|