gRPC With. Net Proto Types Created: 23 Mar 2026 Updated: 23 Mar 2026

Protocol Buffers Nested Messages

1. What Are Nested Messages?

In Protocol Buffers, a nested message is a message type defined inside another message type. It works just like a regular message, but it is scoped to its parent.

Real-world analogy: Think of a filing cabinet. The cabinet itself is the parent message. Inside it, you have folders — those are nested messages. A folder labeled "Invoices" only makes sense inside that cabinet. You could have another cabinet with its own "Invoices" folder, and they wouldn't conflict.

Here is the simplest example:

message Customer {
string name = 1;
Address home_address = 2;

// Address is defined INSIDE Customer
message Address {
string street = 1;
string city = 2;
}
}

In this example, Address is a nested message inside Customer. It belongs to Customer and is accessed as Customer.Address from outside.

2. Why Nest Messages?

You might wonder: why not just define every message at the top level? There are several good reasons to nest:

ReasonExplanationExample
OrganizationGroups related data together, making your .proto file easier to readPurchaseOrder.OrderItem clearly belongs to PurchaseOrder
Name scopingAvoids name collisions — two parent messages can each have their own AddressCustomer.Address vs Company.Address
IntentSignals to other developers that this type is only meaningful in the context of its parentPurchaseOrder.PaymentInfo is clearly about an order's payment
EncapsulationKeeps helper types close to where they are usedA Dimensions message inside ProductListing
Think of it this way: If someone asks "What is an Address?", the answer depends on context. A customer's address and a warehouse's address might have completely different fields. Nesting makes the context explicit.

3. Your First Nested Message

Let's build a Customer message that has an address. Instead of defining Address at the top level, we'll nest it inside Customer:

syntax = "proto3";

option csharp_namespace = "IndepthProtobuf";

package nested;

message Customer {
string customer_id = 1;
string name = 2;
string email = 3;
Address billing_address = 4;
Address shipping_address = 5;

// Nested message — scoped to Customer
message Address {
string street = 1;
string city = 2;
string state = 3;
string zip_code = 4;
string country = 5;
}
}

Let's break down what's happening:

PartWhat It Does
message Customer { ... }Defines the parent message
message Address { ... } (inside Customer)Defines a nested message — only exists within Customer's scope
Address billing_address = 4;Uses the nested message as a field type (no prefix needed inside the parent)
Address shipping_address = 5;Same nested type reused for another field — a customer can have two addresses

Notice that inside Customer, you refer to the nested type simply as Address. You do NOT need to write Customer.Address when referencing it from within the same parent.

4. How Nested Messages Map to C#

When protobuf generates C# code, nested messages become inner classes under a special Types class inside the parent:

Proto definition:

message Customer {
string name = 1;
Address billing_address = 2;

message Address {
string street = 1;
string city = 2;
}
}

Generated C# structure (simplified):

public sealed class Customer : IMessage<Customer>
{
public string Name { get; set; }
public Types.Address BillingAddress { get; set; }

// All nested types live inside this "Types" class
public static class Types
{
public sealed class Address : IMessage<Address>
{
public string Street { get; set; }
public string City { get; set; }
}
}
}

How you use it in C#:

// Creating a customer with a nested address
var customer = new Customer
{
Name = "Alice Johnson",
BillingAddress = new Customer.Types.Address
{
Street = "123 Main Street",
City = "Springfield",
}
};

// Reading nested message fields
Console.WriteLine(customer.BillingAddress.Street); // "123 Main Street"
Console.WriteLine(customer.BillingAddress.City); // "Springfield"
Key C# pattern: Nested messages are always accessed via ParentMessage.Types.NestedMessage. The Types class is auto-generated by protobuf to hold all nested types.

This is one of the most important things to remember:

In ProtoIn C#
Customer.AddressCustomer.Types.Address
PurchaseOrder.OrderItemPurchaseOrder.Types.OrderItem
ProductListing.DimensionsProductListing.Types.Dimensions

5. Reusing Nested Messages from Outside

A nested message can be referenced from any other message using the Parent.NestedType syntax:

message Customer {
string name = 1;
Address billing_address = 2;

message Address {
string street = 1;
string city = 2;
string country = 3;
}
}

// Another message reusing Customer.Address
message DeliveryOrder {
string order_id = 1;
Customer.Address delivery_address = 2;
string notes = 3;
}

In C#, this looks like:

var delivery = new DeliveryOrder
{
OrderId = "DEL-001",
DeliveryAddress = new Customer.Types.Address
{
Street = "456 Oak Avenue",
City = "Portland",
Country = "US"
},
Notes = "Leave at the front door"
};
Important: Even though the nested message is defined inside Customer, it can be used anywhere. Nesting is about organization and naming, not about restricting access.

6. Deeply Nested Messages (Multiple Levels)

You can nest messages inside nested messages — as many levels deep as you need:

message University {
string name = 1;
repeated Department departments = 2;

message Department {
string department_name = 1;
repeated Course courses = 2;

message Course {
string course_code = 1;
string course_name = 2;
int32 credits = 3;
}
}
}

Here we have three levels: University > Department > Course.

In C#:

var university = new University
{
Name = "MIT"
};

var csDept = new University.Types.Department
{
DepartmentName = "Computer Science"
};

var course = new University.Types.Department.Types.Course
{
CourseCode = "CS101",
CourseName = "Introduction to Programming",
Credits = 3
};

csDept.Courses.Add(course);
university.Departments.Add(csDept);

Referencing deeply nested types from other messages:

// In .proto file
message StudentEnrollment {
string student_name = 1;
University.Department.Course course = 2;
}

// In C#
var enrollment = new StudentEnrollment
{
StudentName = "Bob",
Course = new University.Types.Department.Types.Course
{
CourseCode = "CS101",
CourseName = "Intro to Programming",
Credits = 3
}
};
Warning: While deep nesting is possible, going more than 2-3 levels deep makes your code harder to work with — especially in C# where each level adds .Types. to the path. Keep it practical.

7. Combining Nested Messages and Enums

A parent message can contain both nested messages and nested enums. This is a powerful pattern for building self-contained data structures:

message ProductListing {
string product_id = 1;
string name = 2;
double price = 3;
Category category = 4;
Dimensions dimensions = 5;

enum Category {
CATEGORY_UNSPECIFIED = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_FOOD = 3;
CATEGORY_BOOKS = 4;
}

message Dimensions {
double width_cm = 1;
double height_cm = 2;
double depth_cm = 3;
double weight_kg = 4;
}
}

In C#:

var product = new ProductListing
{
ProductId = "PROD-001",
Name = "Wireless Headphones",
Price = 79.99,
Category = ProductListing.Types.Category.Electronics,
Dimensions = new ProductListing.Types.Dimensions
{
WidthCm = 18.0,
HeightCm = 20.0,
DepthCm = 8.5,
WeightKg = 0.25
}
};

Console.WriteLine($"Product: {product.Name}");
Console.WriteLine($"Category: {product.Category}");
Console.WriteLine($"Weight: {product.Dimensions.WeightKg} kg");

Notice the pattern:

  1. Nested enum: ProductListing.Types.Category
  2. Nested message: ProductListing.Types.Dimensions

Both live under the Types class in C#.

8. Repeated Nested Messages (Lists)

One of the most common patterns is a parent message that contains a list of nested messages:

message PurchaseOrder {
string order_id = 1;
Customer customer = 2;
repeated OrderItem items = 3;
PaymentInfo payment = 4;

message OrderItem {
string product_name = 1;
int32 quantity = 2;
double unit_price = 3;
}

message PaymentInfo {
string card_last_four = 1;
double total_amount = 2;
bool is_paid = 3;
}
}

The repeated keyword means "a list of." So repeated OrderItem items means "this order has zero or more items."

In C#:

var order = new PurchaseOrder
{
OrderId = "ORD-12345",
Payment = new PurchaseOrder.Types.PaymentInfo
{
CardLastFour = "4242",
TotalAmount = 149.97,
IsPaid = true
}
};

// Add items to the repeated field
order.Items.Add(new PurchaseOrder.Types.OrderItem
{
ProductName = "Wireless Mouse",
Quantity = 2,
UnitPrice = 29.99
});

order.Items.Add(new PurchaseOrder.Types.OrderItem
{
ProductName = "USB-C Cable",
Quantity = 3,
UnitPrice = 9.99
});

// Loop through items
foreach (var item in order.Items)
{
Console.WriteLine($" {item.Quantity}x {item.ProductName} @ ${item.UnitPrice}");
}

// Count items
Console.WriteLine($"Total items: {order.Items.Count}");
Key point: In C#, repeated fields become RepeatedField<T> — a special list-like collection. You use .Add(), .Count, and foreach just like with any other list. You do not assign a new list — you add items to the existing one.

9. Complete Working Example — Order Management Service

Let's put everything together in a complete, working .proto file that uses nested messages in a gRPC service:

File: Protos/nested.proto

syntax = "proto3";

option csharp_namespace = "IndepthProtobuf";

package nested;

// Customer with nested Address
message Customer {
string customer_id = 1;
string name = 2;
string email = 3;
Address billing_address = 4;
Address shipping_address = 5;

message Address {
string street = 1;
string city = 2;
string state = 3;
string zip_code = 4;
string country = 5;
}
}

// Order with nested OrderItem and PaymentInfo
message PurchaseOrder {
string order_id = 1;
Customer customer = 2;
repeated OrderItem items = 3;
PaymentInfo payment = 4;

message OrderItem {
string product_name = 1;
int32 quantity = 2;
double unit_price = 3;
}

message PaymentInfo {
string card_last_four = 1;
double total_amount = 2;
bool is_paid = 3;
}
}

// The gRPC service
service OrderManagement {
rpc CreateOrder (CreatePurchaseOrderRequest) returns (CreatePurchaseOrderResponse);
rpc GetOrder (GetPurchaseOrderRequest) returns (GetPurchaseOrderResponse);
}

message CreatePurchaseOrderRequest {
Customer customer = 1;
repeated PurchaseOrder.OrderItem items = 2;
PurchaseOrder.PaymentInfo payment = 3;
}

message CreatePurchaseOrderResponse {
string order_id = 1;
string status = 2;
double total_amount = 3;
}

message GetPurchaseOrderRequest {
string order_id = 1;
}

message GetPurchaseOrderResponse {
PurchaseOrder order = 1;
}

This service has two RPCs:

  1. CreateOrder — Receives a customer, a list of items, and payment info. Returns the new order ID and total.
  2. GetOrder — Receives an order ID. Returns the full order with all nested data.

10. The C# Service Implementation

Here is the complete C# service that implements the OrderManagement gRPC service:

File: Services/OrderManagementService.cs

using Grpc.Core;

namespace IndepthProtobuf.Services
{
public class OrderManagementService(ILogger<OrderManagementService> logger)
: OrderManagement.OrderManagementBase
{
private static readonly Dictionary<string, PurchaseOrder> Orders = new();

public override Task<CreatePurchaseOrderResponse> CreateOrder(
CreatePurchaseOrderRequest request, ServerCallContext context)
{
var orderId = $"ORD-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";

logger.LogInformation("Creating order {OrderId} for customer {CustomerName}",
orderId, request.Customer?.Name);

// Build the PurchaseOrder using nested messages
var order = new PurchaseOrder
{
OrderId = orderId,
Customer = request.Customer,
Payment = request.Payment
};

double totalAmount = 0;
foreach (var item in request.Items)
{
order.Items.Add(item);
totalAmount += item.Quantity * item.UnitPrice;
}

if (order.Payment != null)
{
order.Payment.TotalAmount = totalAmount;
order.Payment.IsPaid = true;
}

Orders[orderId] = order;

return Task.FromResult(new CreatePurchaseOrderResponse
{
OrderId = orderId,
Status = "CREATED",
TotalAmount = totalAmount
});
}

public override Task<GetPurchaseOrderResponse> GetOrder(
GetPurchaseOrderRequest request, ServerCallContext context)
{
logger.LogInformation("Looking up order {OrderId}", request.OrderId);

if (Orders.TryGetValue(request.OrderId, out var order))
{
return Task.FromResult(new GetPurchaseOrderResponse { Order = order });
}

throw new RpcException(new Status(StatusCode.NotFound,
$"Order '{request.OrderId}' not found."));
}
}
}

Let's look at how nested messages are used in this service:

CodeWhat It Does
request.CustomerAccesses the Customer message (which itself contains nested Address messages)
request.ItemsAccesses the repeated list of PurchaseOrder.OrderItem nested messages
request.PaymentAccesses the PurchaseOrder.PaymentInfo nested message
order.Items.Add(item)Adds a nested OrderItem to the repeated field
order.Payment.TotalAmountReads/writes a field on a nested message

Don't forget to register the service in Program.cs:

app.MapGrpcService<OrderManagementService>();

11. When to Nest vs. Keep Top-Level

This is one of the most important design decisions when writing proto files. Here is a simple decision guide:

ScenarioDecisionWhy
Only one message uses this typeNest itKeeps related things together; signals intent
Multiple messages share the same typeTop-levelAvoids confusing ownership; easier to reference
The type is a general concept (Address, Money, Timestamp)Top-levelGeneral concepts belong in a shared space
The type only makes sense within its parent's contextNest itPurchaseOrder.OrderItem clearly belongs to a PurchaseOrder
The nested type name would conflict with othersNest itScoping prevents name collisions
You need the type in a different .proto fileTop-levelEasier to import and reference
Rule of thumb: If you can describe the type without mentioning its parent (e.g., "an address," "a timestamp"), it should probably be top-level. If it needs its parent for context (e.g., "an order's line item"), nest it.

12. Common Mistakes to Avoid

Mistake 1: Forgetting .Types. in C#

✘ WRONG
// This will NOT compile
var address = new Customer.Address
{
Street = "123 Main St"
};
✔ CORRECT
// Must include .Types.
var address = new Customer.Types.Address
{
Street = "123 Main St"
};

Mistake 2: Unnecessary nesting when the type is shared

✘ BAD — Address used by many types
message Customer {
message Address { ... }
}

message Warehouse {
Customer.Address location = 1;
// Confusing: Why is a warehouse
// using a Customer's address?
}
✔ BETTER — Top-level shared type
message Address { ... }

message Customer {
Address home = 1;
}

message Warehouse {
Address location = 1;
// Clear: both use a general Address
}

Mistake 3: Nesting too many levels deep

✘ TOO DEEP — Hard to use in C#
message A {
message B {
message C {
message D {
string value = 1;
}
}
}
}

// In C#: A.Types.B.Types.C.Types.D 😱
✔ PRACTICAL — 1-2 levels max
message A {
message B {
string value = 1;
}
}

// In C#: A.Types.B ✓

Mistake 4: Trying to assign a new list to a repeated field

✘ WRONG — Cannot assign to repeated field
var order = new PurchaseOrder();
order.Items = new List<PurchaseOrder.Types.OrderItem>();
// Error! Items is read-only.
✔ CORRECT
var order = new PurchaseOrder();
order.Items.Add(new PurchaseOrder.Types.OrderItem
{
ProductName = "Mouse",
Quantity = 1,
UnitPrice = 29.99
});

13. Summary

Here is everything you learned in this guide:

ConceptKey Point
Nested messageA message defined inside another message, scoped to its parent
Proto syntaxJust write message Inner { ... } inside the outer message block
C# access patternParent.Types.NestedType — always include .Types.
Referencing from outsideUse Parent.NestedType in proto; Parent.Types.NestedType in C#
Deep nestingPossible but avoid more than 2-3 levels for readability
With enumsNested enums and messages work the same way under .Types.
Repeated nestedUse repeated NestedType field_name for lists; use .Add() in C#
When to nestWhen the type only makes sense within its parent's context
When to use top-levelWhen the type is shared across multiple messages or is a general concept
Final tip: Nested messages are one of protobuf's most useful features for building clean, well-organized APIs. They let you group related data, prevent naming conflicts, and clearly communicate the structure of your gRPC service. Start with 1-2 levels of nesting and keep things simple!


Share this lesson: