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

Protocol Buffers Collections — Repeated Fields and Map Fields

1. Why Do We Need Collections in Protobuf?

So far in our Protobuf journey, we've seen how to define simple fields — a single string, a single integer, a single boolean. But in real applications, you almost always need to store multiple items of the same type:

  1. A student who has many hobbies (a list of strings)
  2. An exam with multiple scores (a list of numbers)
  3. A configuration file with key-value settings (a dictionary)
  4. A classroom with many students (a list of complex objects)
Real-world analogy: Think of a shopping bag. A single field is like holding one apple in your hand — simple. But a repeated field is like the shopping bag itself — it can hold zero, one, or many apples. A map field is like a labeled locker system — each locker has a unique key (locker number) and holds one item (the value).

Protobuf gives you two collection types to handle these cases:

Protobuf KeywordWhat It CreatesC# EquivalentUse Case
repeatedAn ordered list of itemsRepeatedField<T> (like List<T>)Lists, arrays, sequences
mapA set of key-value pairsMapField<TKey, TValue> (like Dictionary<TKey, TValue>)Dictionaries, lookup tables, settings

Let's explore each one in detail.

2. What Is a Repeated Field?

A repeated field is a field that can hold zero or more values of the same type. Think of it as a list or an array.

The syntax is simple — just put the word repeated before the type:

message StudentProfile {
string student_id = 1;
string name = 2;
repeated string hobbies = 3; // A list of strings
}

That's it! The hobbies field can now hold zero, one, or one thousand strings. Each entry is stored in order, and you can access them by index.

Key point: The word repeated is the only keyword you need to turn any single-value field into a list. There is no list, array, or [] syntax — just repeated.

Here's how it looks conceptually:

Field DeclarationCan HoldExample Values
string name = 1;Exactly one string"Alice"
repeated string hobbies = 3;Zero or more strings["Reading", "Swimming", "Coding"]

3. Repeated Scalar Fields

You can repeat any scalar type: strings, integers, doubles, booleans — anything you can use as a regular field.

message StudentProfile {
string student_id = 1;
string name = 2;
repeated string hobbies = 3; // List of strings
repeated int32 exam_scores = 4; // List of integers
repeated double gpa_history = 5; // List of doubles
repeated bool semester_passed = 6; // List of booleans
}

Let's break down what each repeated field stores:

FieldTypeExample Data
hobbiesrepeated string["Reading", "Swimming", "Music"]
exam_scoresrepeated int32[95, 88, 92, 78]
gpa_historyrepeated double[3.8, 3.9, 3.7, 4.0]
semester_passedrepeated bool[true, true, true, false]
Performance note: Repeated fields of numeric types (int32, int64, double, float, bool, and all other scalar numeric types) are automatically packed on the wire in proto3. This means they are stored more efficiently — all values are packed into a single length-delimited chunk instead of being sent as separate fields. You don't need to do anything special for this — proto3 does it automatically.

4. Repeated Message Fields

Repeated fields aren't limited to simple types. You can also create a list of complex message objects. This is one of the most powerful features in Protobuf.

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

message Semester {
string semester_name = 1;
repeated Course courses = 2; // List of Course messages
repeated string announcements = 3; // Mix with scalar repeated too
}

Here, each Semester contains a list of Course objects. In JSON representation, it would look like this:

{
"semester_name": "Fall 2025",
"courses": [
{ "course_code": "CS101", "title": "Intro to Programming", "credits": 3 },
{ "course_code": "CS201", "title": "Data Structures", "credits": 4 },
{ "course_code": "MATH301", "title": "Linear Algebra", "credits": 3 }
],
"announcements": [
"Welcome to Fall 2025!",
"Registration closes Friday"
]
}
Real-world analogy: A Semester containing repeated Course is like a school timetable. The timetable (Semester) has many classes (Courses) listed on it. Each class is a complete object with its own code, title, and credit hours.

In C#, you work with this exactly like a list:

var semester = new Semester
{
SemesterName = "Fall 2025"
};

// Add courses one by one
semester.Courses.Add(new Course
{
CourseCode = "CS101",
Title = "Intro to Programming",
Credits = 3
});

// Or access by index
Course firstCourse = semester.Courses[0];
Console.WriteLine(firstCourse.Title); // "Intro to Programming"

// Iterate over all courses
foreach (var course in semester.Courses)
{
Console.WriteLine($"{course.CourseCode}: {course.Title}");
}

5. Repeated Enum Fields

You can also have a list of enum values. This is useful when something can have multiple categories, tags, or skills.

enum Skill {
SKILL_UNSPECIFIED = 0;
SKILL_CSHARP = 1;
SKILL_JAVA = 2;
SKILL_PYTHON = 3;
SKILL_JAVASCRIPT = 4;
SKILL_GO = 5;
SKILL_RUST = 6;
}

message Developer {
string developer_id = 1;
string name = 2;
repeated Skill skills = 3; // List of enum values
}

A developer can have multiple skills — that's a repeated enum field:

var dev = new Developer
{
DeveloperId = "D001",
Name = "Charlie"
};

dev.Skills.Add(Skill.Csharp);
dev.Skills.Add(Skill.Java);
dev.Skills.Add(Skill.Python);

// Check if developer knows C#
bool knowsCsharp = dev.Skills.Contains(Skill.Csharp); // true

// Count skills
int skillCount = dev.Skills.Count; // 3
Tip: Repeated enum fields work identically to repeated scalar fields. The enum values are stored as integers on the wire, so they benefit from the same packed encoding.

6. How Repeated Fields Map to C#

When the Protobuf compiler generates C# code from your .proto file, repeated fields become RepeatedField<T> properties. This is a special collection type from the Google.Protobuf library that behaves very similarly to List<T>.

Proto DeclarationGenerated C# Property Type
repeated string hobbies = 3;RepeatedField<string> Hobbies
repeated int32 exam_scores = 4;RepeatedField<int> ExamScores
repeated double gpa_history = 5;RepeatedField<double> GpaHistory
repeated Course courses = 2;RepeatedField<Course> Courses
repeated Skill skills = 3;RepeatedField<Skill> Skills

Critical rule: You cannot assign a new list to a repeated field. The property has only a getter, no setter. You must add items to the existing collection:

✘ WRONG — Will not compile
var student = new StudentProfile();

// You CANNOT assign a new list!
student.Hobbies = new List<string>
{
"Reading", "Swimming"
};
✔ CORRECT — Add to existing collection
var student = new StudentProfile();

// Add items to the existing collection
student.Hobbies.Add("Reading");
student.Hobbies.Add("Swimming");

// Or use AddRange for multiple items
student.ExamScores.AddRange(new[] { 95, 88, 92 });

Here are all the common operations you can do with RepeatedField<T>:

OperationC# CodeDescription
Add one itemstudent.Hobbies.Add("Reading")Adds a single item to the end
Add many itemsstudent.Hobbies.AddRange(new[] { "A", "B" })Adds multiple items at once
Access by indexvar first = student.Hobbies[0]Get item at specific position
Count itemsint count = student.Hobbies.CountNumber of items in the list
Check if containsstudent.Hobbies.Contains("Reading")Returns true if item exists
Remove itemstudent.Hobbies.Remove("Reading")Removes the first matching item
Clear allstudent.Hobbies.Clear()Removes all items
Iterateforeach (var h in student.Hobbies)Loop through all items
LINQstudent.Hobbies.Where(h => h.StartsWith("R"))All LINQ methods work
Important: A repeated field is never null. Even if no items are added, it will be an empty collection with Count == 0. You never need to check for null — just check the count.

7. Rules for Repeated Fields

Here are the key rules to remember:

RuleExplanation
Order is preservedItems stay in the order you add them. The first item added is at index 0.
Duplicates are allowedYou can add the same value multiple times. ["A", "A", "B"] is valid.
Empty is the defaultIf no items are added, the field is an empty list, not null.
No repeated repeatedYou cannot have a list-of-lists directly. Use a wrapper message instead.
No repeated mapYou cannot have a list of maps directly. Use a wrapper message instead.
Cannot be optionalIn proto3, you cannot mark a repeated field as optional. It's always zero-or-more.

What about list-of-lists? Since repeated repeated is not allowed, you wrap the inner list in a message:

✘ NOT ALLOWED
message Matrix {
repeated repeated int32 rows = 1; // ERROR!
}
✔ USE A WRAPPER MESSAGE
message Row {
repeated int32 values = 1;
}

message Matrix {
repeated Row rows = 1; // List of Row messages
}

8. What Is a Map Field?

A map field is a set of key-value pairs — like a dictionary in C# or an object/hash map in other languages. Each key is unique, and each key maps to exactly one value.

The syntax is:

map<key_type, value_type> field_name = field_number;

For example:

message AppConfig {
string app_name = 1;
map<string, string> settings = 2; // A string-to-string dictionary
}
Real-world analogy: Think of a physical phone book. Each person's name (the key) maps to their phone number (the value). You look up a name to find a number. You can't have two people with the exact same name in the book — keys must be unique. That's exactly how a map field works.

In JSON, a map looks like this:

{
"app_name": "MyApp",
"settings": {
"theme": "dark",
"language": "en",
"timezone": "UTC"
}
}

9. Map with Scalar Values

The simplest maps have scalar types as both keys and values:

message AppConfig {
string app_name = 1;
map<string, string> settings = 2; // string → string
map<string, int32> feature_flags = 3; // string → integer
map<string, bool> permissions = 4; // string → boolean
}

In C#, you use them like any dictionary:

var config = new AppConfig
{
AppName = "MyApp"
};

// Add key-value pairs to the map
config.Settings["theme"] = "dark";
config.Settings["language"] = "en";
config.Settings["timezone"] = "UTC";

// Feature flags: 0 = off, 1 = on
config.FeatureFlags["dark_mode"] = 1;
config.FeatureFlags["beta_features"] = 0;

// Boolean permissions
config.Permissions["can_edit"] = true;
config.Permissions["can_delete"] = false;
config.Permissions["is_admin"] = true;

// Reading values
string theme = config.Settings["theme"]; // "dark"
bool canEdit = config.Permissions["can_edit"]; // true

// Safe reading with TryGetValue
if (config.Settings.TryGetValue("language", out var lang))
{
Console.WriteLine($"Language: {lang}"); // "Language: en"
}
Best practice: Always use TryGetValue when you're not sure if a key exists. Accessing a key that doesn't exist with the [] indexer will throw a KeyNotFoundException.

10. Map with Message Values

Maps become really powerful when the value is a message type. This lets you create a dictionary of complex objects:

message ContactInfo {
string phone = 1;
string email = 2;
string city = 3;
}

message AddressBook {
string owner_name = 1;
map<string, ContactInfo> contacts = 2; // name → ContactInfo
}

Each entry in the map stores a full ContactInfo object as its value. In C#:

var book = new AddressBook { OwnerName = "Alice" };

// Add complex message objects as values
book.Contacts["Bob"] = new ContactInfo
{
Phone = "+1-555-0202",
Email = "bob@example.com",
City = "London"
};

book.Contacts["Charlie"] = new ContactInfo
{
Phone = "+1-555-0303",
Email = "charlie@example.com",
City = "Tokyo"
};

// Look up a contact by name
if (book.Contacts.TryGetValue("Bob", out var bob))
{
Console.WriteLine($"Bob's email: {bob.Email}"); // "bob@example.com"
Console.WriteLine($"Bob's city: {bob.City}"); // "London"
}

// Iterate over all contacts
foreach (var kvp in book.Contacts)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value.Email}");
}
Tip: Map with message values is extremely common in real-world gRPC APIs. It's perfect for lookup tables, caches, or any scenario where you need to quickly find a complex object by its identifier.

11. Map with Integer Keys

Map keys don't have to be strings. You can use integer types too:

message ScoreRecord {
string subject = 1;
int32 score = 2;
string grade = 3;
}

message Transcript {
string student_name = 1;
map<int32, ScoreRecord> scores_by_year = 2; // year → ScoreRecord
}

Here, the key is a year (integer) and the value is a full score record:

var transcript = new Transcript { StudentName = "Alice" };

transcript.ScoresByYear[2023] = new ScoreRecord
{
Subject = "Computer Science",
Score = 95,
Grade = "A+"
};

transcript.ScoresByYear[2024] = new ScoreRecord
{
Subject = "Mathematics",
Score = 88,
Grade = "A"
};

// Look up by year
if (transcript.ScoresByYear.TryGetValue(2023, out var record))
{
Console.WriteLine($"2023: {record.Subject} — {record.Grade}");
}

The allowed key types for maps are:

Allowed Key TypesNOT Allowed as Keys
stringfloat
int32, int64double
uint32, uint64bytes
sint32, sint64enum
fixed32, fixed64message types
sfixed32, sfixed64
bool
Why can't float/double be keys? Floating-point numbers have precision issues — 0.1 + 0.2 doesn't exactly equal 0.3. If you used floats as keys, you'd get unreliable lookups. That's why Protobuf forbids them as map keys.

12. How Map Fields Map to C#

Map fields become MapField<TKey, TValue> in C# — a special dictionary-like collection from the Google.Protobuf library.

Proto DeclarationGenerated C# Property Type
map<string, string> settings = 2;MapField<string, string> Settings
map<string, int32> feature_flags = 3;MapField<string, int> FeatureFlags
map<string, ContactInfo> contacts = 2;MapField<string, ContactInfo> Contacts
map<int32, ScoreRecord> scores_by_year = 2;MapField<int, ScoreRecord> ScoresByYear

Just like repeated fields, you cannot assign a new dictionary to a map field — the property only has a getter:

✘ WRONG — Will not compile
var config = new AppConfig();

// You CANNOT assign a new dictionary!
config.Settings = new Dictionary<string, string>
{
["theme"] = "dark"
};
✔ CORRECT — Add to existing map
var config = new AppConfig();

// Add items to the existing MapField
config.Settings["theme"] = "dark";
config.Settings["language"] = "en";

// Or use Add method
config.Settings.Add("timezone", "UTC");

Common operations for MapField<TKey, TValue>:

OperationC# CodeDescription
Add/Set a valueconfig.Settings["key"] = "value"Adds or overwrites the value for a key
Add (throws if exists)config.Settings.Add("key", "value")Adds value; throws if key already exists
Get a valuevar val = config.Settings["key"]Gets value; throws if key not found
Safe getconfig.Settings.TryGetValue("key", out var v)Returns false if key not found
Check key existsconfig.Settings.ContainsKey("key")Returns true if key is in the map
Remove a keyconfig.Settings.Remove("key")Removes the key-value pair
Count entriesint count = config.Settings.CountNumber of key-value pairs
Clear allconfig.Settings.Clear()Removes all entries
Iterateforeach (var kvp in config.Settings)Loop through all key-value pairs
Get all keysvar keys = config.Settings.KeysCollection of all keys
Get all valuesvar vals = config.Settings.ValuesCollection of all values
Important: Just like repeated fields, a map field is never null. It is always initialized as an empty collection. You never need to check for null — just check the count or use ContainsKey.

13. Rules and Restrictions for Map Fields

RuleExplanation
Keys must be uniqueIf the same key appears twice in wire data, only the last value is kept.
Order is not guaranteedUnlike repeated fields, map entries have no guaranteed order on the wire.
No repeated mapYou cannot put a map inside a repeated field. Use a wrapper message.
No map of mapYou cannot nest maps directly. Use a wrapper message for the inner map.
Keys cannot be float, double, bytes, enums, or messagesOnly integral types, bool, and string are allowed as keys.
Values can be any typeScalars, enums, or messages are all valid as map values.
Cannot be optionalMap fields cannot be marked as optional in proto3.

What about map-of-maps or repeated maps? Use wrapper messages:

✘ NOT ALLOWED
message Broken {
// ERROR: Cannot repeat a map
repeated map<string, string> items = 1;

// ERROR: Cannot nest maps
map<string, map<string, int32>> nested = 2;
}
✔ USE WRAPPER MESSAGES
message StringMap {
map<string, string> entries = 1;
}

message Fixed {
// A list of dictionaries
repeated StringMap items = 1;

// A dictionary of dictionaries
map<string, StringMap> nested = 2;
}

14. Repeated vs Map — When to Use Which?

This is one of the most common questions beginners have. Here is a clear guide:

Use CaseUse repeatedUse map
You need an ordered listYes — order is preservedNo — order is not guaranteed
You need to look up by keyNo — you'd have to scan the whole listYes — fast lookup by key
Items have a natural unique IDCan work, but less efficientYes — use the ID as the key
Duplicates are possibleYes — duplicates allowedNo — keys must be unique
Simple list of valuesYes — straightforwardOverkill if you don't need keys
Configuration/settingsNot idealYes — natural key-value structure
Tags, categories, skillsYes — simple list of valuesNo — no need for keys
Rule of thumb: If you need to find items by a key, use a map. If you just need a list of things, use repeated. Many real-world messages use both — a repeated field for ordered data plus a map for quick lookups.

15. Combining Repeated and Map in One Message

In real applications, you often use both repeated and map fields in the same message. Here's an example that combines everything we've learned:

message Classroom {
string classroom_id = 1;
string teacher_name = 2;
repeated StudentProfile students = 3; // Ordered list of students
map<string, int32> subject_hours = 4; // subject name → weekly hours
map<string, StudentProfile> student_lookup = 5; // student_id → quick lookup
repeated string rules = 6; // Simple list of rules
}

This Classroom message uses:

  1. repeated StudentProfile students — an ordered list of all students in the class
  2. map<string, int32> subject_hours — a dictionary mapping each subject to its weekly hours
  3. map<string, StudentProfile> student_lookup — a dictionary for quickly finding a student by ID
  4. repeated string rules — a simple list of classroom rules

In C#, you can populate this message like so:

var classroom = new Classroom
{
ClassroomId = "CS-101",
TeacherName = "Dr. Smith"
};

// Build a student
var alice = new StudentProfile
{
StudentId = "S001",
Name = "Alice Johnson"
};
alice.Hobbies.Add("Reading");
alice.Hobbies.Add("Swimming");
alice.ExamScores.AddRange(new[] { 95, 88, 92 });

// Add to the repeated field (ordered list)
classroom.Students.Add(alice);

// Also add to the map field (fast lookup by ID)
classroom.StudentLookup[alice.StudentId] = alice;

// Add subject hours (map)
classroom.SubjectHours["Math"] = 5;
classroom.SubjectHours["Science"] = 4;
classroom.SubjectHours["English"] = 3;

// Add rules (repeated string)
classroom.Rules.Add("Be on time");
classroom.Rules.Add("Respect others");

// Now you can use both:
// Ordered iteration via repeated field:
foreach (var student in classroom.Students)
{
Console.WriteLine($"Student: {student.Name}");
}

// Fast lookup via map field:
if (classroom.StudentLookup.TryGetValue("S001", out var found))
{
Console.WriteLine($"Found: {found.Name}"); // "Alice Johnson"
}

// Check subject hours:
Console.WriteLine($"Math hours: {classroom.SubjectHours["Math"]}"); // 5
Design pattern: Having both a repeated list and a map lookup for the same data (like students + student_lookup) is a common pattern when you need both ordered iteration AND fast key-based access.

16. Complete Working Example

Here is the complete .proto file with all the concepts we covered, plus a gRPC service definition:

The Proto File — collections.proto

syntax = "proto3";

option csharp_namespace = "IndepthProtobuf";

package collections;

// Repeated scalar fields
message StudentProfile {
string student_id = 1;
string name = 2;
repeated string hobbies = 3;
repeated int32 exam_scores = 4;
repeated double gpa_history = 5;
repeated bool semester_passed = 6;
}

// Repeated message fields
message Course {
string course_code = 1;
string title = 2;
int32 credits = 3;
}

message Semester {
string semester_name = 1;
repeated Course courses = 2;
repeated string announcements = 3;
}

// Repeated enum fields
enum Skill {
SKILL_UNSPECIFIED = 0;
SKILL_CSHARP = 1;
SKILL_JAVA = 2;
SKILL_PYTHON = 3;
SKILL_JAVASCRIPT = 4;
SKILL_GO = 5;
SKILL_RUST = 6;
}

message Developer {
string developer_id = 1;
string name = 2;
repeated Skill skills = 3;
}

// Map with scalar values
message AppConfig {
string app_name = 1;
map<string, string> settings = 2;
map<string, int32> feature_flags = 3;
map<string, bool> permissions = 4;
}

// Map with message values
message ContactInfo {
string phone = 1;
string email = 2;
string city = 3;
}

message AddressBook {
string owner_name = 1;
map<string, ContactInfo> contacts = 2;
}

// Map with integer keys
message ScoreRecord {
string subject = 1;
int32 score = 2;
string grade = 3;
}

message Transcript {
string student_name = 1;
map<int32, ScoreRecord> scores_by_year = 2;
}

// Combining repeated and map
message Classroom {
string classroom_id = 1;
string teacher_name = 2;
repeated StudentProfile students = 3;
map<string, int32> subject_hours = 4;
map<string, StudentProfile> student_lookup = 5;
repeated string rules = 6;
}

// gRPC Service
service ClassroomService {
rpc AddStudent (AddStudentRequest) returns (AddStudentResponse);
rpc GetClassroom (GetClassroomRequest) returns (GetClassroomResponse);
rpc SearchDevelopers (SearchDevelopersRequest) returns (SearchDevelopersResponse);
rpc GetContacts (GetContactsRequest) returns (GetContactsResponse);
}

message AddStudentRequest {
string classroom_id = 1;
StudentProfile student = 2;
}

message AddStudentResponse {
string classroom_id = 1;
int32 total_students = 2;
}

message GetClassroomRequest {
string classroom_id = 1;
}

message GetClassroomResponse {
Classroom classroom = 1;
}

message SearchDevelopersRequest {
Skill skill = 1;
}

message SearchDevelopersResponse {
repeated Developer developers = 1;
}

message GetContactsRequest {
string owner_name = 1;
}

message GetContactsResponse {
AddressBook address_book = 1;
}

The C# Service Implementation — ClassroomServiceImpl.cs

using Grpc.Core;

namespace IndepthProtobuf.Services
{
public class ClassroomServiceImpl(ILogger<ClassroomServiceImpl> logger)
: ClassroomService.ClassroomServiceBase
{
private static readonly Dictionary<string, Classroom> Classrooms = new();
private static readonly List<Developer> Developers = new();
private static readonly Dictionary<string, AddressBook> AddressBooks = new();

public override Task<AddStudentResponse> AddStudent(
AddStudentRequest request, ServerCallContext context)
{
logger.LogInformation("Adding student {Name} to classroom {Id}",
request.Student.Name, request.ClassroomId);

if (!Classrooms.TryGetValue(request.ClassroomId, out var classroom))
{
classroom = new Classroom
{
ClassroomId = request.ClassroomId,
TeacherName = "Default Teacher"
};
Classrooms[request.ClassroomId] = classroom;
}

// Add to the repeated field (list)
classroom.Students.Add(request.Student);

// Add to the map field (dictionary) for quick lookup
classroom.StudentLookup[request.Student.StudentId] = request.Student;

return Task.FromResult(new AddStudentResponse
{
ClassroomId = request.ClassroomId,
TotalStudents = classroom.Students.Count
});
}

public override Task<GetClassroomResponse> GetClassroom(
GetClassroomRequest request, ServerCallContext context)
{
logger.LogInformation("Getting classroom {Id}", request.ClassroomId);

if (!Classrooms.TryGetValue(request.ClassroomId, out var classroom))
{
classroom = CreateSampleClassroom(request.ClassroomId);
Classrooms[request.ClassroomId] = classroom;
}

return Task.FromResult(new GetClassroomResponse { Classroom = classroom });
}

public override Task<SearchDevelopersResponse> SearchDevelopers(
SearchDevelopersRequest request, ServerCallContext context)
{
logger.LogInformation("Searching for skill {Skill}", request.Skill);

if (Developers.Count == 0) SeedDevelopers();

var response = new SearchDevelopersResponse();
var matched = Developers.Where(d => d.Skills.Contains(request.Skill));
response.Developers.AddRange(matched);

return Task.FromResult(response);
}

public override Task<GetContactsResponse> GetContacts(
GetContactsRequest request, ServerCallContext context)
{
logger.LogInformation("Getting contacts for {Owner}", request.OwnerName);

if (!AddressBooks.TryGetValue(request.OwnerName, out var book))
{
book = CreateSampleAddressBook(request.OwnerName);
AddressBooks[request.OwnerName] = book;
}

return Task.FromResult(new GetContactsResponse { AddressBook = book });
}

private static Classroom CreateSampleClassroom(string classroomId)
{
var student1 = new StudentProfile { StudentId = "S001", Name = "Alice" };
student1.Hobbies.Add("Reading");
student1.Hobbies.Add("Swimming");
student1.ExamScores.AddRange(new[] { 95, 88, 92 });

var classroom = new Classroom
{
ClassroomId = classroomId,
TeacherName = "Dr. Smith"
};
classroom.Students.Add(student1);
classroom.StudentLookup[student1.StudentId] = student1;
classroom.SubjectHours["Math"] = 5;
classroom.SubjectHours["Science"] = 4;
classroom.Rules.Add("Be on time");
classroom.Rules.Add("Respect others");

return classroom;
}

private static void SeedDevelopers()
{
var dev1 = new Developer { DeveloperId = "D001", Name = "Charlie" };
dev1.Skills.Add(Skill.Csharp);
dev1.Skills.Add(Skill.Java);

var dev2 = new Developer { DeveloperId = "D002", Name = "Diana" };
dev2.Skills.Add(Skill.Csharp);
dev2.Skills.Add(Skill.Go);

Developers.AddRange(new[] { dev1, dev2 });
}

private static AddressBook CreateSampleAddressBook(string ownerName)
{
var book = new AddressBook { OwnerName = ownerName };

book.Contacts["Alice"] = new ContactInfo
{
Phone = "+1-555-0101",
Email = "alice@example.com",
City = "New York"
};

book.Contacts["Bob"] = new ContactInfo
{
Phone = "+1-555-0202",
Email = "bob@example.com",
City = "London"
};

return book;
}
}
}

Registering in Program.cs

using IndepthProtobuf.Services;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();

var app = builder.Build();

app.MapGrpcService<GreeterService>();
app.MapGrpcService<OrderManagementService>();
app.MapGrpcService<ClassroomServiceImpl>();

app.Run();

17. Common Mistakes to Avoid

MistakeWhat Goes WrongHow to Fix It
Assigning a new list/dictionary to a repeated/map field

student.Hobbies = new List<string>();
Compile error — repeated and map fields only have gettersUse .Add(), .AddRange(), or the [] indexer instead:
student.Hobbies.Add("Reading");
Checking for null on a repeated/map field
if (student.Hobbies != null)
Works but is unnecessary — these fields are never nullCheck the count instead:
if (student.Hobbies.Count > 0)
Using repeated repeated
repeated repeated int32 matrix = 1;
Compile error — nested repeated is not allowedWrap the inner list in a message:
message Row { repeated int32 values = 1; }
message Matrix { repeated Row rows = 1; }
Using float or double as map key
map<float, string> data = 1;
Compile error — floating-point types are not allowed as map keysUse string or an integer type as the key:
map<string, string> data = 1;
Using an enum as a map key
map<MyEnum, string> data = 1;
Compile error — enums are not allowed as map keysUse int32 and cast from the enum value:
map<int32, string> data = 1;
Accessing a map key that doesn't exist with []
var val = config.Settings["missing_key"];
Throws KeyNotFoundException at runtimeUse TryGetValue for safe access:
config.Settings.TryGetValue("key", out var val);
Assuming map order is preserved
// "First added will be first iterated" — WRONG
Map iteration order is undefined and can vary between languagesIf you need order, use a repeated field instead, or sort after receiving.

18. Summary

Featurerepeated (List)map (Dictionary)
Syntaxrepeated Type name = N;map<K, V> name = N;
C# TypeRepeatedField<T>MapField<TKey, TValue>
OrderPreservedNot guaranteed
DuplicatesAllowedKeys must be unique
Default valueEmpty list (never null)Empty map (never null)
Can hold scalarsYesYes (as values)
Can hold messagesYesYes (as values)
Can hold enumsYesYes (as values only, not keys)
Can be nestedNo (repeated repeated is invalid)No (map of map is invalid) — use wrappers
Can be assigned (=)No — use .Add()No — use ["key"] = value


Key takeaways:

  1. repeated creates an ordered list — use it for arrays, sequences, and collections where order matters.
  2. map creates a key-value dictionary — use it for lookups, settings, and any association between unique keys and values.
  3. Both are never null in C# — they're always initialized as empty collections.
  4. You cannot assign new collections to these fields — always add to the existing RepeatedField or MapField.
  5. For nested collections (list-of-lists, map-of-maps), use wrapper messages to work around the single-level restriction.
  6. Use TryGetValue for safe map lookups, and check Count instead of null for emptiness.


Share this lesson: