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

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

  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

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:

  • DoubleValue
  • FloatValue
  • Int64Value
  • UInt64Value
  • Int32Value
  • UInt32Value
  • BoolValue
  • StringValue
  • BytesValue

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

See Also