12 KiB
12 KiB
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
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:
syntax = "proto3";
Package Declaration
Groups related messages and services:
package myapp;
In C#, this becomes:
namespace MyApp.Grpc
{
// Generated classes
}
Namespace Override
option csharp_namespace = "MyCompany.MyApp.Grpc";
Service Definitions
Command Service
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
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
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
message GetUserQuery {
int32 user_id = 1;
}
message ListUsersQuery {
int32 page = 1;
int32 page_size = 2;
string sort_by = 3;
bool descending = 4;
}
DTOs
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
// 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
- Uniqueness: Each field must have a unique number within a message
- Range: 1 to 536,870,911 (excluding 19000-19999)
- Reserved: 1-15 use 1 byte encoding (use for frequently set fields)
- Reserved: 16-2047 use 2 bytes encoding
Best Practices
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:
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
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#:
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:
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:
DoubleValueFloatValueInt64ValueUInt64ValueInt32ValueUInt32ValueBoolValueStringValueBytesValue
Common Imports
Google Protobuf Types
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
import "google/protobuf/empty.proto";
service CommandService {
rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
}
Timestamps
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
// ✅ Good - Singular, describes domain
service UserService { }
service OrderService { }
service ProductService { }
// ❌ Bad
service UsersService { } // Plural
service userService { } // Lowercase
service User { } // Ambiguous
RPCs
// ✅ 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
// ✅ Good - PascalCase
message CreateUserCommand { }
message UserDto { }
message OrderListResponse { }
// ❌ Bad
message create_user_command { } // Snake case
message userDto { } // Camel case
message User { } // Ambiguous
Fields
// ✅ 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
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