Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20265 min read
gRPCProtobuf.NETProtocol BuffersAPI Design
Share:š•

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

PROTOBUF
// 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

PROTOBUF
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

PROTOBUF
// 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

PROTOBUF
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 collections

Enums

PROTOBUF
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

PROTOBUF
// 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

PROTOBUF
// 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

XML
<!-- 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 set

Production issue I've seen: A team assumed an empty string "" meant "not provided" and null meant "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. Using google.protobuf.StringValue for truly optional fields makes the null/present distinction explicit.


Key Takeaway

.proto files 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. Use repeated for collections, oneof for discriminated unions, enum for status values. Proto3 defaults mean empty string, not null — use wrapper types or optional fields for nullable values.

gRPC Knowledge Check

5 questions Ā· Test what you just learned Ā· Instant explanations

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.