Protocol Buffers ā Defining gRPC Contracts
Write .proto files for gRPC services: message types, field types and numbers, repeated fields, oneofs, enums, nested messages, and the proto3 conventions used in .NET gRPC projects.
What Protocol Buffers Are
Protocol Buffers (proto3) is a language-neutral, platform-neutral interface definition language. You define messages and services in a .proto file. The protoc compiler (or Grpc.Tools in .NET) generates strongly-typed C# classes.
REST + JSON:
Contract: OpenAPI spec (often generated after the fact)
Payload: JSON text (human-readable, verbose, large)
Versioning: URL-based or header-based (convention)
gRPC + Protobuf:
Contract: .proto file (primary artifact, checked into source)
Payload: binary (compact, ~5-10x smaller than JSON)
Versioning: field numbers (backward-compatible if rules followed)Basic Message Definition
// protos/clinical.proto
syntax = "proto3";
package systemforge.clinical.v1;
option csharp_namespace = "SystemForge.Clinical.Grpc.V1";
// Message types (like C# classes)
message Patient {
string id = 1; // field number 1 ā must never change
string name = 2;
string mrn = 3;
string department = 4;
bool is_active = 5;
string date_of_birth = 6; // ISO 8601 date string
int64 created_at_unix = 7; // Unix timestamp
}
message CreatePatientRequest {
string name = 1;
string mrn = 2;
string department = 3;
string date_of_birth = 4;
}
message CreatePatientResponse {
string patient_id = 1;
}Field Types
message FieldTypeExamples {
// Numeric
int32 count = 1; // 32-bit signed integer
int64 timestamp = 2; // 64-bit signed integer (Unix ms)
uint32 ward_number = 3; // 32-bit unsigned
float temperature = 4; // 32-bit float
double inr_value = 5; // 64-bit float (prefer for clinical values)
bool is_active = 6; // boolean
// String and bytes
string name = 7; // UTF-8 string
bytes document = 8; // raw bytes (use for binary data, NOT strings)
// Well-known types (import google/protobuf/...)
google.protobuf.Timestamp issued_at = 9; // proper timestamp
google.protobuf.Duration infusion_duration = 10; // time duration
google.protobuf.StringValue optional_note = 11; // nullable string (wrapper)
}Field Numbers ā The Critical Rule
// Field numbers identify fields in the binary format
// Rules:
// 1. Numbers 1-15 take 1 byte to encode (use for frequent fields)
// 2. Numbers 16-2047 take 2 bytes
// 3. NEVER change a field number ā binary compatibility will break
// 4. NEVER reuse a field number (even after deleting the field)
// 5. Mark deleted fields as reserved:
message Prescription {
string id = 1;
string drug_name = 2;
double dosage = 3;
string unit = 4;
// Field 5 was "prescriber_name" ā DELETED, must reserve the number
reserved 5;
reserved "prescriber_name"; // reserve name too
string prescriber_id = 6; // added later ā new number
}Collections ā Repeated Fields
message Patient {
string id = 1;
string name = 2;
repeated string allergies = 3; // repeated = array/list
repeated Prescription prescriptions = 4; // list of messages
map<string, string> metadata = 5; // key-value map
}
// In C#: generated as RepeatedField<string> and RepeatedField<Prescription>
// Not null ā always initialized as empty collectionsEnums
enum DrugOrderStatus {
DRUG_ORDER_STATUS_UNSPECIFIED = 0; // proto3 default value (must be 0)
DRUG_ORDER_STATUS_PENDING = 1;
DRUG_ORDER_STATUS_DISPENSING = 2;
DRUG_ORDER_STATUS_DISPENSED = 3;
DRUG_ORDER_STATUS_CANCELLED = 4;
}
message DrugOrder {
string id = 1;
DrugOrderStatus status = 2; // default: DRUG_ORDER_STATUS_UNSPECIFIED
}Oneofs ā Discriminated Unions
// oneof: only one of these fields can be set at a time
message NotificationPayload {
oneof notification {
PatientAlert patient_alert = 1;
DrugOrderAlert drug_order_alert = 2;
SystemMessage system_message = 3;
}
}
// C# usage:
// switch (payload.NotificationCase) {
// case NotificationPayload.NotificationOneofCase.PatientAlert:
// HandlePatientAlert(payload.PatientAlert); break;
// }Service Definition
// Clinical service definition
service ClinicalPatientService {
// Unary: single request ā single response
rpc CreatePatient (CreatePatientRequest) returns (CreatePatientResponse);
rpc GetPatient (GetPatientRequest) returns (GetPatientResponse);
rpc SearchPatients (SearchPatientsRequest) returns (SearchPatientsResponse);
// Server streaming: single request ā stream of responses
rpc StreamPatientVitals (StreamVitalsRequest)
returns (stream VitalSignResponse);
// Client streaming: stream of requests ā single response
rpc UploadObservations (stream ObservationRequest)
returns (UploadObservationsResponse);
// Bidirectional streaming: stream ā stream
rpc MonitorPatients (stream PatientMonitorRequest)
returns (stream PatientStatusResponse);
}.NET Project Integration
<!-- clinical.csproj -->
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.*" />
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<!-- Server: GrpcServices="Server" ā generates service base class -->
<Protobuf Include="Protos\clinical.proto" GrpcServices="Server" />
<!-- Client: GrpcServices="Client" ā generates client stub -->
<Protobuf Include="Protos\clinical.proto" GrpcServices="Client" />
<!-- Both (library shared between server and client) -->
<Protobuf Include="Protos\clinical.proto" GrpcServices="Both" />
</ItemGroup>Build the project ā Grpc.Tools generates C# classes in obj/ automatically.
Null Handling in proto3
proto3 does not have null ā all fields have default values:
string ā "" (empty string, not null)
int32 ā 0
bool ā false
repeated ā empty collection
Use wrapper types for nullable:
google.protobuf.StringValue ā null or string
google.protobuf.Int32Value ā null or int
Or use the Presence feature (proto3 optional):
optional string ward_id = 4; // tracks whether field was setProduction issue I've seen: A team assumed an empty string
""meant "not provided" andnullmeant "field absent". Proto3 cannot distinguish ā all unset string fields arrive as"". A drug order with no ward assignment (empty string) was treated as "ward: empty string" and routed to a non-existent ward. Usinggoogle.protobuf.StringValuefor truly optional fields makes the null/present distinction explicit.
Key Takeaway
.protofiles are the primary contract artifact for gRPC ā check them into source control and review them as carefully as API specs. Field numbers are binary compatibility anchors ā never change or reuse them. Userepeatedfor collections,oneoffor discriminated unions,enumfor status values. Proto3 defaults mean empty string, not null ā use wrapper types oroptionalfields for nullable values.
gRPC Knowledge Check
5 questions Ā· Test what you just learned Ā· Instant explanations
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.