Skip to main content

Proto File

Location: src/Services/Discount/Discount.Grpc/Protos/discount.proto
syntax = "proto3";

option csharp_namespace = "Discount.Grpc";

package discount;

// The discount service definition.
service DiscountProtoService {
	// Discount CRUD Operations
	rpc GetDiscount (GetDiscountRequest) returns (CouponModel);
	rpc CreateDiscount (CreateDiscountRequest) returns (CouponModel);
	rpc UpdateDiscount (UpdateDiscountRequest) returns (CouponModel);
	rpc DeleteDiscount (DeleteDiscountRequest) returns (DeleteDiscountResponse);
}

message GetDiscountRequest {
	string productName = 1;
}

message CouponModel {
	int32 id = 1;
	string productName = 2;
	string description = 3;
	int32 amount = 4;
}

message CreateDiscountRequest {
	CouponModel coupon = 1;
}

message UpdateDiscountRequest {
	CouponModel coupon = 1;
}

message DeleteDiscountRequest {
	string productName = 1;
}

message DeleteDiscountResponse {
	bool success = 1;
}

Service Definition

DiscountProtoService

The main service definition with four RPC methods:
service DiscountProtoService {
    rpc GetDiscount (GetDiscountRequest) returns (CouponModel);
    rpc CreateDiscount (CreateDiscountRequest) returns (CouponModel);
    rpc UpdateDiscount (UpdateDiscountRequest) returns (CouponModel);
    rpc DeleteDiscount (DeleteDiscountRequest) returns (DeleteDiscountResponse);
}
Service Type: Unary RPCs (request-response pattern) All methods use the unary pattern where the client sends a single request and receives a single response.

Message Definitions

GetDiscountRequest

message GetDiscountRequest {
    string productName = 1;
}
Purpose: Request a discount coupon for a specific product. Fields:
  • productName (string, field 1): The name of the product to look up
Example Usage:
var request = new GetDiscountRequest 
{ 
    ProductName = "IPhone X" 
};
var response = await client.GetDiscountAsync(request);

CouponModel

message CouponModel {
    int32 id = 1;
    string productName = 2;
    string description = 3;
    int32 amount = 4;
}
Purpose: Represents a discount coupon with all its details. Fields:
  • id (int32, field 1): Unique identifier for the coupon
  • productName (string, field 2): Name of the product this discount applies to
  • description (string, field 3): Human-readable description of the discount
  • amount (int32, field 4): Discount amount (in currency units or percentage)
Mapping to C# Entity:
public class Coupon
{
    public int Id { get; set; }
    public string ProductName { get; set; } = default!;
    public string Description { get; set; } = default!;
    public int Amount { get; set; }
}
The mapping is straightforward and handled automatically by Mapster:
var couponModel = coupon.Adapt<CouponModel>();
var coupon = couponModel.Adapt<Coupon>();

CreateDiscountRequest

message CreateDiscountRequest {
    CouponModel coupon = 1;
}
Purpose: Request to create a new discount coupon. Fields:
  • coupon (CouponModel, field 1): The coupon data to create (id is usually 0 or omitted)
Example Usage:
var request = new CreateDiscountRequest
{
    Coupon = new CouponModel
    {
        ProductName = "iPad Pro",
        Description = "iPad Discount",
        Amount = 200
    }
};
var response = await client.CreateDiscountAsync(request);

UpdateDiscountRequest

message UpdateDiscountRequest {
    CouponModel coupon = 1;
}
Purpose: Request to update an existing discount coupon. Fields:
  • coupon (CouponModel, field 1): The complete coupon data including the id
Example Usage:
var request = new UpdateDiscountRequest
{
    Coupon = new CouponModel
    {
        Id = 1,
        ProductName = "IPhone X",
        Description = "Updated iPhone Discount",
        Amount = 175
    }
};
var response = await client.UpdateDiscountAsync(request);

DeleteDiscountRequest

message DeleteDiscountRequest {
    string productName = 1;
}
Purpose: Request to delete a discount coupon. Fields:
  • productName (string, field 1): The product name to identify which discount to delete
Example Usage:
var request = new DeleteDiscountRequest 
{ 
    ProductName = "IPhone X" 
};
var response = await client.DeleteDiscountAsync(request);

DeleteDiscountResponse

message DeleteDiscountResponse {
    bool success = 1;
}
Purpose: Response indicating whether the delete operation succeeded. Fields:
  • success (bool, field 1): True if deletion was successful, false otherwise
Example Usage:
var response = await client.DeleteDiscountAsync(request);
if (response.Success)
{
    Console.WriteLine("Discount deleted successfully");
}

Proto File Configuration

Syntax Declaration

syntax = "proto3";
Specifies Protocol Buffers version 3 (proto3), which is the current recommended version.

C# Namespace

option csharp_namespace = "Discount.Grpc";
Generates C# code in the Discount.Grpc namespace, matching the project structure.

Package Name

package discount;
Defines the protobuf package name as discount, used for service resolution.

Code Generation

Server-Side (.csproj configuration)

<ItemGroup>
  <Protobuf Include="Protos\discount.proto" GrpcServices="Server" />
</ItemGroup>
Generated Classes:
  • DiscountProtoService - Base service class
  • DiscountProtoService.DiscountProtoServiceBase - Abstract base class to inherit
  • GetDiscountRequest, CouponModel, etc. - Message classes
Implementation:
public class DiscountService : DiscountProtoService.DiscountProtoServiceBase
{
    public override async Task<CouponModel> GetDiscount(
        GetDiscountRequest request, 
        ServerCallContext context)
    {
        // Implementation
    }
}

Client-Side (.csproj configuration)

<ItemGroup>
  <Protobuf Include="..\..\Discount\Discount.Grpc\Protos\discount.proto" GrpcServices="Client">
    <Link>Protos\discount.proto</Link>
  </Protobuf>
</ItemGroup>
Generated Classes:
  • DiscountProtoService.DiscountProtoServiceClient - Client class for calling the service
  • All message classes (same as server)
Usage:
public class MyService
{
    private readonly DiscountProtoService.DiscountProtoServiceClient _client;

    public MyService(DiscountProtoService.DiscountProtoServiceClient client)
    {
        _client = client;
    }

    public async Task<CouponModel> GetProductDiscount(string productName)
    {
        return await _client.GetDiscountAsync(
            new GetDiscountRequest { ProductName = productName });
    }
}

Field Numbering

In Protocol Buffers, each field has a unique number:
message CouponModel {
    int32 id = 1;           // Field number 1
    string productName = 2; // Field number 2
    string description = 3; // Field number 3
    int32 amount = 4;       // Field number 4
}
Important Rules:
  • Field numbers 1-15 take 1 byte to encode (use for frequently used fields)
  • Field numbers 16-2047 take 2 bytes
  • Field numbers must be unique within a message
  • Cannot reuse field numbers (breaks backward compatibility)
  • Numbers 19000-19999 are reserved for Protocol Buffers

Data Types

Proto3 to C# Type Mapping

Proto3 TypeC# TypeNotes
stringstringUTF-8 encoded
int32int32-bit signed integer
boolboolBoolean value
messageClassCustom message type

Default Values

In proto3, fields have default values when not set:
  • string: empty string ""
  • int32: 0
  • bool: false
  • message: null

Backward Compatibility

Protocol Buffers are designed for evolution: Safe Changes:
  • Adding new fields (old clients ignore them)
  • Adding new RPC methods
  • Renaming fields (only field numbers matter)
Breaking Changes:
  • Changing field numbers
  • Changing field types
  • Removing required fields
  • Removing or renaming RPC methods
Best Practices:
// Good - adding a new optional field
message CouponModel {
    int32 id = 1;
    string productName = 2;
    string description = 3;
    int32 amount = 4;
    int32 maxUsageCount = 5; // New field - safe to add
}

// Bad - changing field number
message CouponModel {
    int32 id = 1;
    string productName = 3; // Changed from 2 - BREAKING!
    string description = 2; // Changed from 3 - BREAKING!
    int32 amount = 4;
}

Service Contract Benefits

Type Safety

  • Compile-time type checking
  • No runtime serialization errors
  • IntelliSense support in IDEs

Performance

  • Binary serialization (smaller than JSON)
  • Fast encoding/decoding
  • Efficient network usage

Cross-Language Support

  • Same proto file works for C#, Java, Python, Go, etc.
  • Consistent API across services
  • Easy polyglot microservices

Documentation

  • Proto file serves as API contract
  • Self-documenting service definition
  • Easy to understand service capabilities

Testing Proto Messages

Unit Test Example

[Fact]
public void CouponModel_Should_Map_To_Entity()
{
    // Arrange
    var couponModel = new CouponModel
    {
        Id = 1,
        ProductName = "Test Product",
        Description = "Test Description",
        Amount = 100
    };

    // Act
    var coupon = couponModel.Adapt<Coupon>();

    // Assert
    Assert.Equal(couponModel.Id, coupon.Id);
    Assert.Equal(couponModel.ProductName, coupon.ProductName);
    Assert.Equal(couponModel.Description, coupon.Description);
    Assert.Equal(couponModel.Amount, coupon.Amount);
}

Advanced Proto Features

Repeated Fields (Arrays)

If you needed to return multiple coupons:
message GetDiscountsResponse {
    repeated CouponModel coupons = 1;
}
Maps to:
public class GetDiscountsResponse
{
    public RepeatedField<CouponModel> Coupons { get; set; }
}

Enums

enum DiscountType {
    PERCENTAGE = 0;
    FIXED_AMOUNT = 1;
    BUY_ONE_GET_ONE = 2;
}

message CouponModel {
    int32 id = 1;
    string productName = 2;
    string description = 3;
    int32 amount = 4;
    DiscountType type = 5;
}

Timestamps

import "google/protobuf/timestamp.proto";

message CouponModel {
    int32 id = 1;
    string productName = 2;
    string description = 3;
    int32 amount = 4;
    google.protobuf.Timestamp expiryDate = 5;
}

Resources

Next Steps

Build docs developers (and LLMs) love