dotnet-cqrs/docs/grpc-integration/proto-file-setup.md

609 lines
12 KiB
Markdown

# Proto File Setup
.proto file creation and conventions for CQRS services.
## Overview
Protocol Buffer (.proto) files define the contract between gRPC clients and servers. They specify:
- Service definitions (RPCs)
- Message structures (commands, queries, DTOs)
- Data types and field numbers
- Import dependencies
## File Structure
### Basic Template
```protobuf
syntax = "proto3";
package myapp;
// Imports
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
// Options (optional)
option csharp_namespace = "MyApp.Grpc";
// Service definitions
service CommandService {
// RPCs here
}
service QueryService {
// RPCs here
}
// Message definitions
message CreateUserCommand {
// Fields here
}
```
### File Organization
**Recommended structure:**
```
Protos/
├── cqrs_services.proto # Main CQRS services
├── common.proto # Shared messages
└── google/ # Google common types (auto-imported)
└── protobuf/
├── empty.proto
├── timestamp.proto
└── wrappers.proto
```
##Syntax
### Syntax Declaration
**Always use proto3:**
```protobuf
syntax = "proto3";
```
### Package Declaration
Groups related messages and services:
```protobuf
package myapp;
```
**In C#, this becomes:**
```csharp
namespace MyApp.Grpc
{
// Generated classes
}
```
### Namespace Override
```protobuf
option csharp_namespace = "MyCompany.MyApp.Grpc";
```
## Service Definitions
### Command Service
```protobuf
service CommandService {
// Command with result
rpc CreateUser (CreateUserCommand) returns (CreateUserResponse);
// Command without result
rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
// Command with complex result
rpc UpdateOrder (UpdateOrderCommand) returns (OrderDto);
}
```
### Query Service
```protobuf
service QueryService {
// Single entity query
rpc GetUser (GetUserQuery) returns (UserDto);
// List query
rpc ListUsers (ListUsersQuery) returns (UserListResponse);
// Search query
rpc SearchProducts (SearchProductsQuery) returns (ProductSearchResponse);
}
```
## Message Definitions
### Commands
```protobuf
message CreateUserCommand {
string name = 1;
string email = 2;
int32 age = 3;
bool is_active = 4;
}
message CreateUserResponse {
int32 user_id = 1;
}
message DeleteUserCommand {
int32 user_id = 1;
}
```
### Queries
```protobuf
message GetUserQuery {
int32 user_id = 1;
}
message ListUsersQuery {
int32 page = 1;
int32 page_size = 2;
string sort_by = 3;
bool descending = 4;
}
```
### DTOs
```protobuf
message UserDto {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_active = 5;
google.protobuf.Timestamp created_at = 6;
}
message UserListResponse {
repeated UserDto users = 1;
int32 total_count = 2;
int32 page = 3;
int32 page_size = 4;
}
```
## Data Types
### Scalar Types
| .proto Type | C# Type | Notes |
|-------------|---------|-------|
| `double` | `double` | |
| `float` | `float` | |
| `int32` | `int` | Variable-length encoding |
| `int64` | `long` | Variable-length encoding |
| `uint32` | `uint` | Variable-length encoding |
| `uint64` | `ulong` | Variable-length encoding |
| `sint32` | `int` | Signed, better for negative |
| `sint64` | `long` | Signed, better for negative |
| `fixed32` | `uint` | Fixed 4 bytes |
| `fixed64` | `ulong` | Fixed 8 bytes |
| `sfixed32` | `int` | Fixed 4 bytes, signed |
| `sfixed64` | `long` | Fixed 8 bytes, signed |
| `bool` | `bool` | |
| `string` | `string` | UTF-8 or ASCII |
| `bytes` | `ByteString` | Binary data |
### Choosing Numeric Types
**Use int32/int64 for:**
- Most integer fields
- IDs, counts, quantities
**Use sint32/sint64 for:**
- Frequently negative values
- Temperature, coordinates, deltas
**Use fixed32/fixed64 for:**
- Values > 2^28 (usually positive)
- Better performance when value is consistently large
### Complex Types
```protobuf
// Nested message
message Address {
string street = 1;
string city = 2;
string postal_code = 3;
string country = 4;
}
message User {
int32 id = 1;
string name = 2;
Address address = 3; // Nested message
}
// Repeated field (list)
message UserListResponse {
repeated UserDto users = 1;
}
// Map
message UserPreferences {
map<string, string> settings = 1;
}
// Enum
enum UserRole {
USER_ROLE_UNSPECIFIED = 0; // Required default
USER_ROLE_ADMIN = 1;
USER_ROLE_MODERATOR = 2;
USER_ROLE_USER = 3;
}
message User {
int32 id = 1;
string name = 2;
UserRole role = 3;
}
```
## Field Numbers
### Rules
1. **Uniqueness**: Each field must have a unique number within a message
2. **Range**: 1 to 536,870,911 (excluding 19000-19999)
3. **Reserved**: 1-15 use 1 byte encoding (use for frequently set fields)
4. **Reserved**: 16-2047 use 2 bytes encoding
### Best Practices
```protobuf
message UserDto {
// Use 1-15 for frequently set fields
int32 id = 1;
string name = 2;
string email = 3;
// Use 16+ for less common fields
google.protobuf.Timestamp created_at = 16;
google.protobuf.Timestamp updated_at = 17;
google.protobuf.Timestamp deleted_at = 18;
}
```
### Reserved Fields
Prevent reusing deleted field numbers:
```protobuf
message UserDto {
reserved 4, 5; // Reserved field numbers
reserved "old_field", "deprecated_field"; // Reserved names
int32 id = 1;
string name = 2;
string email = 3;
// Fields 4 and 5 cannot be used
int32 age = 6;
}
```
## Default Values
In proto3, all fields have default values:
| Type | Default |
|------|---------|
| Numeric | 0 |
| Bool | false |
| String | "" (empty string) |
| Bytes | Empty bytes |
| Enum | First value (must be 0) |
| Message | null |
| Repeated | Empty list |
**Important:** You cannot distinguish between "not set" and "default value" in proto3.
## Optional Fields
### Explicit Optional
```protobuf
message UpdateUserCommand {
int32 user_id = 1;
optional string name = 2; // Can be null
optional string email = 3; // Can be null
optional int32 age = 4; // Can be null
}
```
**In C#:**
```csharp
command.Name = "John"; // Set
command.Email = null; // Not set
command.Age = 0; // Set to 0 or not set? Ambiguous!
```
### Wrapper Types
For nullable primitives, use wrapper types:
```protobuf
import "google/protobuf/wrappers.proto";
message UpdateUserCommand {
int32 user_id = 1;
google.protobuf.StringValue name = 2; // Nullable string
google.protobuf.Int32Value age = 3; // Nullable int
google.protobuf.BoolValue is_active = 4; // Nullable bool
}
```
**Available wrappers:**
- `DoubleValue`
- `FloatValue`
- `Int64Value`
- `UInt64Value`
- `Int32Value`
- `UInt32Value`
- `BoolValue`
- `StringValue`
- `BytesValue`
## Common Imports
### Google Protobuf Types
```protobuf
import "google/protobuf/empty.proto"; // Empty (void)
import "google/protobuf/timestamp.proto"; // DateTime
import "google/protobuf/duration.proto"; // TimeSpan
import "google/protobuf/wrappers.proto"; // Nullable primitives
```
### Empty Response
```protobuf
import "google/protobuf/empty.proto";
service CommandService {
rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
}
```
### Timestamps
```protobuf
import "google/protobuf/timestamp.proto";
message UserDto {
int32 id = 1;
string name = 2;
google.protobuf.Timestamp created_at = 3;
google.protobuf.Timestamp updated_at = 4;
}
```
## Naming Conventions
### Services
```protobuf
// ✅ Good - Singular, describes domain
service UserService { }
service OrderService { }
service ProductService { }
// ❌ Bad
service UsersService { } // Plural
service userService { } // Lowercase
service User { } // Ambiguous
```
### RPCs
```protobuf
// ✅ Good - Verb + Noun
rpc CreateUser (...) returns (...);
rpc UpdateOrder (...) returns (...);
rpc DeleteProduct (...) returns (...);
rpc GetUserById (...) returns (...);
rpc ListOrders (...) returns (...);
// ❌ Bad
rpc User (...) returns (...); // No verb
rpc create_user (...) returns (...); // Snake case
rpc CREATEUSER (...) returns (...); // All caps
```
### Messages
```protobuf
// ✅ Good - PascalCase
message CreateUserCommand { }
message UserDto { }
message OrderListResponse { }
// ❌ Bad
message create_user_command { } // Snake case
message userDto { } // Camel case
message User { } // Ambiguous
```
### Fields
```protobuf
// ✅ Good - snake_case
message UserDto {
int32 user_id = 1;
string first_name = 2;
string last_name = 3;
google.protobuf.Timestamp created_at = 4;
}
// ❌ Bad
message UserDto {
int32 UserId = 1; // PascalCase
string firstName = 2; // CamelCase
string LastName = 3; // PascalCase
}
```
## Complete Example
```protobuf
syntax = "proto3";
package ecommerce;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option csharp_namespace = "ECommerce.Grpc";
// ==================
// Services
// ==================
service CommandService {
rpc CreateProduct (CreateProductCommand) returns (CreateProductResponse);
rpc UpdateProduct (UpdateProductCommand) returns (google.protobuf.Empty);
rpc DeleteProduct (DeleteProductCommand) returns (google.protobuf.Empty);
rpc PlaceOrder (PlaceOrderCommand) returns (PlaceOrderResponse);
}
service QueryService {
rpc GetProduct (GetProductQuery) returns (ProductDto);
rpc ListProducts (ListProductsQuery) returns (ProductListResponse);
rpc SearchProducts (SearchProductsQuery) returns (ProductSearchResponse);
}
// ==================
// Commands
// ==================
message CreateProductCommand {
string name = 1;
string description = 2;
double price = 3;
int32 stock = 4;
string category = 5;
}
message CreateProductResponse {
int32 product_id = 1;
}
message UpdateProductCommand {
int32 product_id = 1;
google.protobuf.StringValue name = 2;
google.protobuf.StringValue description = 3;
google.protobuf.DoubleValue price = 4;
google.protobuf.Int32Value stock = 5;
}
message DeleteProductCommand {
int32 product_id = 1;
}
message PlaceOrderCommand {
int32 customer_id = 1;
repeated OrderItem items = 2;
}
message OrderItem {
int32 product_id = 1;
int32 quantity = 2;
}
message PlaceOrderResponse {
int32 order_id = 1;
double total_amount = 2;
}
// ==================
// Queries
// ==================
message GetProductQuery {
int32 product_id = 1;
}
message ListProductsQuery {
int32 page = 1;
int32 page_size = 2;
}
message SearchProductsQuery {
string keyword = 1;
string category = 2;
google.protobuf.DoubleValue min_price = 3;
google.protobuf.DoubleValue max_price = 4;
}
// ==================
// DTOs
// ==================
message ProductDto {
int32 id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock = 5;
string category = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message ProductListResponse {
repeated ProductDto products = 1;
int32 total_count = 2;
int32 page = 3;
int32 page_size = 4;
}
message ProductSearchResponse {
repeated ProductDto products = 1;
int32 total_count = 2;
}
```
## Best Practices
### ✅ DO
- Use proto3 syntax
- Use snake_case for field names
- Use PascalCase for message/service names
- Reserve deleted field numbers
- Use 1-15 for frequently set fields
- Import google common types
- Document complex messages
- Version your .proto files
### ❌ DON'T
- Don't change field numbers
- Don't reuse reserved numbers
- Don't use negative field numbers
- Don't mix naming conventions
- Don't skip field numbers unnecessarily
- Don't forget to import dependencies
## See Also
- [gRPC Integration Overview](README.md)
- [Getting Started](getting-started-grpc.md)
- [Source Generators](source-generators.md)
- [Service Implementation](service-implementation.md)
- [Protocol Buffers Language Guide](https://protobuf.dev/programming-guides/proto3/)