Skip to main content
Summary: AdminWeb uses a specialized Protobuffer structure to optimize binary size and build caching. This document defines the naming conventions, project structure, and best practices for defining service communication contracts.

Overview

AdminWeb’s microservices communicate via gRPC using Protocol Buffers (.proto files) for serialization. To reduce binary size and improve build caching, we use a unique naming convention that separates client-side, server-side, and shared message definitions.

Core Principle

Each .proto file belongs to one of five specialized project types that control visibility and compilation scope.

Protobuffer Project Types

1. *.client - Client Communication Contracts

Purpose: Used by clients/projects to communicate with a gRPC service Visibility:
  • Includes only messages needed by clients
  • Shipped with WASM applications
  • Minimal binary footprint
  • No backend-specific types
Location:
Protos/
  Donations.client/
    Donations.Client.csproj
    Protos/
      donations.proto
      messages.proto
Example donations.client.proto:
syntax = "proto3";

package donations.client;
option csharp_namespace = "Donations.Client.Contracts";

// Messages visible to web frontend
message CreateDonationRequest {
    string member_id = 1;
    double amount = 2;
    string description = 3;
}

message DonationResponse {
    string donation_id = 1;
    double amount = 2;
    string status = 3;
    string created_at = 4;
}

service DonationService {
    rpc CreateDonation(CreateDonationRequest) returns (DonationResponse);
    rpc GetDonation(GetDonationRequest) returns (DonationResponse);
}
Referenced by:
  • Blazor Web applications (Frontend)
  • Sa.App.Client projects
  • Public APIs
  • Any WASM client

2. *.server - Server Implementation Contracts

Purpose: Used by the gRPC service that hosts the API Visibility:
  • Includes the complete service definition
  • References both client and shared messages
  • Only used by ServiceApi projects
  • Not distributed to clients
Location:
Protos/
  Donations.server/
    Donations.Server.csproj
    Protos/
      donations.proto
Example donations.server.proto:
syntax = "proto3";

package donations.server;
option csharp_namespace = "Donations.Server.Contracts";

import "donations.client.proto";      // Import client messages
import "donations.shared.proto";      // Import shared internal messages

service DonationService {
    rpc CreateDonation(donations.client.CreateDonationRequest) 
        returns (donations.client.DonationResponse);
    
    rpc GetDonation(GetDonationRequest) 
        returns (donations.client.DonationResponse);
    
    rpc ProcessDonationAsync(ProcessDonationRequest) 
        returns (google.protobuf.Empty);
}

// Server-only messages (internal)
message ProcessDonationRequest {
    string donation_id = 1;
    string internal_reference = 2;
    ProcessingContext context = 3;
}
Referenced by:
  • *.ServiceApis projects (gRPC service hosts)
  • *.Workers projects (for inter-service calls)

3. *.shared - Backend-Only Shared Messages

Purpose: Shared message definitions between backend services only Visibility:
  • Backend-to-backend communication only
  • Never exported to WASM/clients
  • Used for internal service-to-service contracts
  • Reusable across multiple services
Location:
Protos/
  Common.shared/
    Common.Shared.csproj
    Protos/
      common-models.proto
      enums.proto
Example common.shared.proto:
syntax = "proto3";

package common.shared;
option csharp_namespace = "Common.Shared.Models";

// Shared between services, not exposed to clients
message MemberContext {
    string member_id = 1;
    string club_id = 2;
    repeated string roles = 3;
    int64 created_timestamp = 4;
}

message AuditTrail {
    string action = 1;
    string actor_id = 2;
    string resource_id = 3;
    int64 timestamp = 4;
    string details = 5;
}

message PaginationRequest {
    int32 page = 1;
    int32 page_size = 2;
    string sort_by = 3;
}

message PaginatedResponse {
    int32 total_count = 1;
    int32 page = 2;
    int32 page_size = 3;
}
Referenced by:
  • Multiple *.ServiceApis projects
  • *.Workers projects
  • *.CoreLibs projects

4. *.shared_external - Client-Accessible Shared Messages

Purpose: Shared message definitions exported to WASM clients Visibility:
  • Backend-to-backend and backend-to-frontend
  • Shipped with client applications
  • Reusable data structures visible to frontend
  • Carefully managed for binary size
Location:
Protos/
  Common.shared_external/
    Common.SharedExternal.csproj
    Protos/
      common-types.proto
Example common.shared_external.proto:
syntax = "proto3";

package common.shared_external;
option csharp_namespace = "Common.SharedExternal.Models";

// Shared between services and exposed to clients
message Address {
    string street = 1;
    string city = 2;
    string postal_code = 3;
    string country = 4;
}

message Contact {
    string email = 1;
    string phone = 2;
    string phone_country_code = 3;
}

message TimePeriod {
    int64 start_timestamp = 1;
    int64 end_timestamp = 2;
}

message ErrorResponse {
    string error_code = 1;
    string message = 2;
    string trace_id = 3;
}
Referenced by:
  • Blazor Web frontend
  • *.ServiceApis projects
  • *.Workers projects
  • WASM clients

5. *.shared_enums - Enumeration Definitions

Purpose: Centralized enum definitions shared across multiple services Visibility:
  • Can be exported to clients or keep backend-only
  • Single source of truth for enums
  • Prevents duplication
  • Easy versioning and maintenance
Location:
Protos/
  Common.shared_enums/
    Common.SharedEnums.csproj
    Protos/
      member-status.proto
      payment-status.proto
      role-types.proto
Example payment-status.proto:
syntax = "proto3";

package common.shared_enums;
option csharp_namespace = "Common.SharedEnums";

enum PaymentStatus {
    PAYMENT_STATUS_UNSPECIFIED = 0;
    PAYMENT_STATUS_PENDING = 1;
    PAYMENT_STATUS_PROCESSING = 2;
    PAYMENT_STATUS_COMPLETED = 3;
    PAYMENT_STATUS_FAILED = 4;
    PAYMENT_STATUS_REFUNDED = 5;
    PAYMENT_STATUS_CANCELLED = 6;
}

enum PaymentMethod {
    PAYMENT_METHOD_UNSPECIFIED = 0;
    PAYMENT_METHOD_CARD = 1;
    PAYMENT_METHOD_BANK_TRANSFER = 2;
    PAYMENT_METHOD_INVOICE = 3;
    PAYMENT_METHOD_SWISH = 4;
}

enum TransactionType {
    TRANSACTION_TYPE_UNSPECIFIED = 0;
    TRANSACTION_TYPE_PAYMENT = 1;
    TRANSACTION_TYPE_REFUND = 2;
    TRANSACTION_TYPE_ADJUSTMENT = 3;
}
Referenced by:
  • Any project that needs these enums
  • Can be imported into client or server proto files

Project Structure & Organization

Protos/
├── README.md                              # Proto documentation
├── Protobuffers/                          # All .proto files
│   ├── common/
│   │   ├── enums.proto
│   │   ├── messages.proto
│   │   └── models.proto
│   ├── donations/
│   │   ├── donations.proto
│   │   └── payment.proto
│   └── exercises/
│       ├── exercises.proto
│       └── workouts.proto

├── Common.SharedEnums/                    # Enum definitions
│   ├── Common.SharedEnums.csproj
│   └── Protos/
│       └── (links to Protobuffers/common/enums.proto)

├── Common.Shared/                         # Internal backend messages
│   ├── Common.Shared.csproj
│   └── Protos/
│       └── (links to relevant .proto files)

├── Common.SharedExternal/                 # Frontend-accessible messages
│   ├── Common.SharedExternal.csproj
│   └── Protos/
│       └── (links to relevant .proto files)

├── Donations.Client/                      # Donations client contracts
│   ├── Donations.Client.csproj
│   └── Protos/
│       └── donations.proto

├── Donations.Server/                      # Donations server implementation
│   ├── Donations.Server.csproj
│   └── Protos/
│       └── donations.proto

└── ...more services

Proto File Organization

Naming Conventions:
✅ Good:
  - common-models.proto       (lowercase with hyphens)
  - payment-status.proto
  - address-book.proto

❌ Avoid:
  - CommonModels.proto        (PascalCase)
  - paymentStatus.proto       (camelCase)
  - addressBook_v2.proto      (mixed naming)
Logical Grouping:
// Group related messages together
message PaymentRequest { ... }
message PaymentResponse { ... }
message PaymentHistory { ... }

// Service at the end
service PaymentService { ... }

Versioning Strategy

File-Level Versioning

Use package versioning rather than filename versioning:
// ❌ Avoid multiple versions
// - donations_v1.proto
// - donations_v2.proto

// ✅ Use package versions instead
package donations.v1;  // or v2, v3
option csharp_namespace = "Donations.V1.Contracts";

message CreateDonationRequest {
    string member_id = 1;
    double amount = 2;
    string description = 3;
}

Backward Compatibility

When evolving messages:
// Version 1
message CreateDonationRequest {
    string member_id = 1;
    double amount = 2;
    string description = 3;
}

// Evolution - add new optional field
// Reserved numbers prevent reuse
message CreateDonationRequest {
    string member_id = 1;
    double amount = 2;
    string description = 3;
    string receipt_email = 4;      // New field - backwards compatible
    
    reserved 10, 11, 12;           // Prevent future field reuse
    reserved "old_field", "legacy"; // Reserve removed field names
}

Best Practices for Message Definitions

1. Use Meaningful Field Numbers

// ❌ Poor - arbitrary numbering
message User {
    string name = 1;
    string email = 5;        // Gap
    string phone = 9;        // Larger gap
}

// ✅ Good - sequential for related fields
message User {
    string name = 1;
    string email = 2;
    string phone = 3;
    
    // Separate group - different concern
    string external_id = 10;
    int64 created_timestamp = 11;
}

2. Use repeated for Collections

// ❌ Avoid nested arrays
message Order {
    string order_id = 1;
    LineItem[] items = 2;  // Not idiomatic
}

// ✅ Use repeated
message Order {
    string order_id = 1;
    repeated LineItem items = 2;
}

message LineItem {
    string product_id = 1;
    int32 quantity = 2;
    double unit_price = 3;
}

3. Use Well-Known Types

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/empty.proto";

message Payment {
    string payment_id = 1;
    google.protobuf.Timestamp created_at = 2;    // Instead of int64
    google.protobuf.Duration processing_time = 3; // Instead of int32
}

// Empty response for void operations
service OrderService {
    rpc CancelOrder(CancelOrderRequest) returns (google.protobuf.Empty);
}

4. Mark Fields as Optional/Required Clearly

// Proto3 - all fields are optional by default
// Use comments to clarify intent

message UserProfile {
    string user_id = 1;           // Required: validated on client
    string display_name = 2;      // Required: shown in UI
    string bio = 3;               // Optional: defaults to empty string
    string avatar_url = 4;        // Optional: falls back to default avatar
    bool newsletter_signup = 5;   // Optional: defaults to false
}

5. Use Enums for Constants

// ✅ Good - self-documenting
enum OrderStatus {
    ORDER_STATUS_UNSPECIFIED = 0;
    ORDER_STATUS_PENDING = 1;
    ORDER_STATUS_CONFIRMED = 2;
    ORDER_STATUS_SHIPPED = 3;
    ORDER_STATUS_DELIVERED = 4;
    ORDER_STATUS_CANCELLED = 5;
}

message Order {
    string order_id = 1;
    OrderStatus status = 2;  // Type-safe instead of string
}

// ❌ Avoid - prone to typos
message Order {
    string order_id = 1;
    string status = 2;  // "pending", "PENDING", "Pending"? Ambiguous
}

6. Nest Messages for Scope

// ✅ Good - clearly related messages
message Order {
    string order_id = 1;
    
    message LineItem {
        string product_id = 1;
        int32 quantity = 2;
    }
    
    repeated LineItem items = 2;
}

// Usage: Order.LineItem item = new Order.LineItem();

Build Caching Optimization

Why Separate Projects Matter

The separation into .client, .server, .shared, etc. enables optimal build caching:
Dependency Graph (Reduced):

Blazor.Web ──┐
             ├─→ Donations.Client ──→ Common.SharedEnums
             └─→ Common.SharedExternal ──→ Common.SharedEnums

Donations.ServiceApis ──┐
                        ├─→ Donations.Server ──→ Donations.Client
                        ├─→ Common.Shared ──→ Common.SharedEnums
                        └─→ Common.SharedExternal

Workers ────┐
            ├─→ Common.Shared
            ├─→ Common.SharedExternal
            └─→ Common.SharedEnums

Benefits

  1. Frontend only rebuilds when .client or .shared_external changes
    • No rebuild if internal backend messages change
    • Faster iteration for web developers
  2. Services rebuild independently
    • Worker changes don’t force ServiceApi rebuild
    • Parallel builds possible
  3. Reduced WASM size
    • Only necessary messages included
    • No internal-only types shipped to browser

Cache Busting

Only change .client or .shared_external when absolutely necessary:
// ❌ Don't move internal-only fields here
message UserProfile {
    string user_id = 1;
    string internal_audit_id = 2;      // ← WRONG: Only used internally
    string display_name = 3;
}

// ✅ Keep internal fields in .shared
// common.shared/user-audit.proto
message UserAudit {
    string internal_audit_id = 1;
    int64 timestamp = 2;
}

// common.shared_external/user-profile.proto
message UserProfile {
    string user_id = 1;
    string display_name = 2;
}

Project File Template

*.client.csproj Example

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net9.0</TargetFramework>
        <RootNamespace>Donations.Client.Contracts</RootNamespace>
        <GenerateDocumentationFile>false</GenerateDocumentationFile>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Grpc.Tools" Version="2.59.0" PrivateAssets="All" />
        <PackageReference Include="Google.Protobuf" Version="3.25.0" />
        <PackageReference Include="Grpc.Net.Client" Version="2.59.0" />
    </ItemGroup>

    <ItemGroup>
        <ProjectReference Include="../Common.SharedEnums/Common.SharedEnums.csproj" />
    </ItemGroup>

    <ItemGroup>
        <Protobuf Include="../Protobuffers/donations/donations.proto" Access="Public" />
    </ItemGroup>

</Project>

*.server.csproj Example

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net9.0</TargetFramework>
        <RootNamespace>Donations.Server.Contracts</RootNamespace>
        <GenerateDocumentationFile>false</GenerateDocumentationFile>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Grpc.Tools" Version="2.59.0" PrivateAssets="All" />
        <PackageReference Include="Google.Protobuf" Version="3.25.0" />
        <PackageReference Include="Grpc.AspNetCore" Version="2.59.0" />
    </ItemGroup>

    <ItemGroup>
        <ProjectReference Include="../Common.SharedEnums/Common.SharedEnums.csproj" />
        <ProjectReference Include="../Common.Shared/Common.Shared.csproj" />
        <ProjectReference Include="../Donations.Client/Donations.Client.csproj" />
    </ItemGroup>

    <ItemGroup>
        <Protobuf Include="../Protobuffers/donations/donations.proto" Access="Public" />
    </ItemGroup>

</Project>

Import Patterns

Safe Imports

// ✅ Good - clear dependency chain
// donations.server.proto
import "donations.client.proto";      // Depends on client
import "common.shared.proto";         // Depends on shared
import "common.shared_enums.proto";   // Depends on enums

// ❌ Avoid - circular dependencies
// donations.client.proto
import "donations.server.proto";  // ERROR: Creates cycle

Code Generation

C# Options

option csharp_namespace = "Donations.Client.Contracts";

// Proto3 with required fields
message CreateOrderRequest {
    string customer_id = 1;
    repeated LineItem items = 2;
}

// Generated C# code automatically creates:
// - Properties for each field
// - Proto utility methods
// - Serialization support

Common Mistakes & Solutions

ProblemSolution
Proto file in wrong project typeVerify file belongs in correct .client/.server/.shared project
Circular import dependenciesReview import chain, refactor to single direction
Including internal messages in .clientMove internal-only messages to .shared instead
Field number reuseAlways reserve old field numbers to prevent incompatibility
Inconsistent naming conventionsUse lowercase with hyphens for filenames, PascalCase in proto definitions
WASM bloatAudit .shared_external for unnecessary fields
Service rebuild on unrelated changesEnsure proto file is in correct project type for better caching