609 lines
12 KiB
Markdown
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/)
|