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:
- A student who has many hobbies (a list of strings)
- An exam with multiple scores (a list of numbers)
- A configuration file with key-value settings (a dictionary)
- 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 Keyword | What It Creates | C# Equivalent | Use Case |
|---|---|---|---|
repeated | An ordered list of items | RepeatedField<T> (like List<T>) | Lists, arrays, sequences |
map | A set of key-value pairs | MapField<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:
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 wordrepeatedis the only keyword you need to turn any single-value field into a list. There is nolist,array, or[]syntax — justrepeated.
Here's how it looks conceptually:
| Field Declaration | Can Hold | Example 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.
Let's break down what each repeated field stores:
| Field | Type | Example Data |
|---|---|---|
hobbies | repeated string | ["Reading", "Swimming", "Music"] |
exam_scores | repeated int32 | [95, 88, 92, 78] |
gpa_history | repeated double | [3.8, 3.9, 3.7, 4.0] |
semester_passed | repeated 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.
Here, each Semester contains a list of Course objects. In JSON representation, it would look like this:
Real-world analogy: ASemestercontainingrepeated Courseis 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:
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.
A developer can have multiple skills — that's a repeated enum field:
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 Declaration | Generated 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 |
|---|
| ✔ CORRECT — Add to existing collection |
|---|
Here are all the common operations you can do with RepeatedField<T>:
| Operation | C# Code | Description |
|---|---|---|
| Add one item | student.Hobbies.Add("Reading") | Adds a single item to the end |
| Add many items | student.Hobbies.AddRange(new[] { "A", "B" }) | Adds multiple items at once |
| Access by index | var first = student.Hobbies[0] | Get item at specific position |
| Count items | int count = student.Hobbies.Count | Number of items in the list |
| Check if contains | student.Hobbies.Contains("Reading") | Returns true if item exists |
| Remove item | student.Hobbies.Remove("Reading") | Removes the first matching item |
| Clear all | student.Hobbies.Clear() | Removes all items |
| Iterate | foreach (var h in student.Hobbies) | Loop through all items |
| LINQ | student.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:
| Rule | Explanation |
|---|---|
| Order is preserved | Items stay in the order you add them. The first item added is at index 0. |
| Duplicates are allowed | You can add the same value multiple times. ["A", "A", "B"] is valid. |
| Empty is the default | If no items are added, the field is an empty list, not null. |
No repeated repeated | You cannot have a list-of-lists directly. Use a wrapper message instead. |
No repeated map | You cannot have a list of maps directly. Use a wrapper message instead. |
Cannot be optional | In 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 |
|---|
| ✔ USE A WRAPPER MESSAGE |
|---|
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:
For example:
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:
9. Map with Scalar Values
The simplest maps have scalar types as both keys and values:
In C#, you use them like any dictionary:
Best practice: Always useTryGetValuewhen you're not sure if a key exists. Accessing a key that doesn't exist with the[]indexer will throw aKeyNotFoundException.
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:
Each entry in the map stores a full ContactInfo object as its value. In C#:
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:
Here, the key is a year (integer) and the value is a full score record:
The allowed key types for maps are:
| Allowed Key Types | NOT Allowed as Keys |
|---|---|
string | float |
int32, int64 | double |
uint32, uint64 | bytes |
sint32, sint64 | enum |
fixed32, fixed64 | message types |
sfixed32, sfixed64 | |
bool |
Why can't float/double be keys? Floating-point numbers have precision issues —0.1 + 0.2doesn't exactly equal0.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 Declaration | Generated 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 |
|---|
| ✔ CORRECT — Add to existing map |
|---|
Common operations for MapField<TKey, TValue>:
| Operation | C# Code | Description |
|---|---|---|
| Add/Set a value | config.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 value | var val = config.Settings["key"] | Gets value; throws if key not found |
| Safe get | config.Settings.TryGetValue("key", out var v) | Returns false if key not found |
| Check key exists | config.Settings.ContainsKey("key") | Returns true if key is in the map |
| Remove a key | config.Settings.Remove("key") | Removes the key-value pair |
| Count entries | int count = config.Settings.Count | Number of key-value pairs |
| Clear all | config.Settings.Clear() | Removes all entries |
| Iterate | foreach (var kvp in config.Settings) | Loop through all key-value pairs |
| Get all keys | var keys = config.Settings.Keys | Collection of all keys |
| Get all values | var vals = config.Settings.Values | Collection 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
| Rule | Explanation |
|---|---|
| Keys must be unique | If the same key appears twice in wire data, only the last value is kept. |
| Order is not guaranteed | Unlike repeated fields, map entries have no guaranteed order on the wire. |
No repeated map | You cannot put a map inside a repeated field. Use a wrapper message. |
No map of map | You cannot nest maps directly. Use a wrapper message for the inner map. |
Keys cannot be float, double, bytes, enums, or messages | Only integral types, bool, and string are allowed as keys. |
| Values can be any type | Scalars, enums, or messages are all valid as map values. |
Cannot be optional | Map fields cannot be marked as optional in proto3. |
What about map-of-maps or repeated maps? Use wrapper messages:
| ✘ NOT ALLOWED |
|---|
| ✔ USE WRAPPER MESSAGES |
|---|
14. Repeated vs Map — When to Use Which?
This is one of the most common questions beginners have. Here is a clear guide:
| Use Case | Use repeated | Use map |
| You need an ordered list | Yes — order is preserved | No — order is not guaranteed |
| You need to look up by key | No — you'd have to scan the whole list | Yes — fast lookup by key |
| Items have a natural unique ID | Can work, but less efficient | Yes — use the ID as the key |
| Duplicates are possible | Yes — duplicates allowed | No — keys must be unique |
| Simple list of values | Yes — straightforward | Overkill if you don't need keys |
| Configuration/settings | Not ideal | Yes — natural key-value structure |
| Tags, categories, skills | Yes — simple list of values | No — no need for keys |
Rule of thumb: If you need to find items by a key, use amap. If you just need a list of things, userepeated. 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:
This Classroom message uses:
repeated StudentProfile students— an ordered list of all students in the classmap<string, int32> subject_hours— a dictionary mapping each subject to its weekly hoursmap<string, StudentProfile> student_lookup— a dictionary for quickly finding a student by IDrepeated string rules— a simple list of classroom rules
In C#, you can populate this message like so:
Design pattern: Having both arepeatedlist and amaplookup for the same data (likestudents+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
The C# Service Implementation — ClassroomServiceImpl.cs
Registering in Program.cs
17. Common Mistakes to Avoid
| Mistake | What Goes Wrong | How to Fix It |
|---|---|---|
| Assigning a new list/dictionary to a repeated/map field |
| Compile error — repeated and map fields only have getters | Use .Add(), .AddRange(), or the [] indexer instead: |
| Checking for null on a repeated/map field |
|---|
| Works but is unnecessary — these fields are never null | Check the count instead: |
Using repeated repeated |
| Compile error — nested repeated is not allowed | Wrap the inner list in a message: |
Using float or double as map key |
| Compile error — floating-point types are not allowed as map keys | Use string or an integer type as the key: |
| Using an enum as a map key |
|---|
| Compile error — enums are not allowed as map keys | Use int32 and cast from the enum value: |
Accessing a map key that doesn't exist with [] |
Throws KeyNotFoundException at runtime | Use TryGetValue for safe access: |
| Assuming map order is preserved |
|---|
| Map iteration order is undefined and can vary between languages | If you need order, use a repeated field instead, or sort after receiving. |
18. Summary
| Feature | repeated (List) | map (Dictionary) |
| Syntax | repeated Type name = N; | map<K, V> name = N; |
| C# Type | RepeatedField<T> | MapField<TKey, TValue> |
| Order | Preserved | Not guaranteed |
| Duplicates | Allowed | Keys must be unique |
| Default value | Empty list (never null) | Empty map (never null) |
| Can hold scalars | Yes | Yes (as values) |
| Can hold messages | Yes | Yes (as values) |
| Can hold enums | Yes | Yes (as values only, not keys) |
| Can be nested | No (repeated repeated is invalid) | No (map of map is invalid) — use wrappers |
Can be assigned (=) | No — use .Add() | No — use ["key"] = value |
Key takeaways:
repeatedcreates an ordered list — use it for arrays, sequences, and collections where order matters.mapcreates a key-value dictionary — use it for lookups, settings, and any association between unique keys and values.- Both are never null in C# — they're always initialized as empty collections.
- You cannot assign new collections to these fields — always add to the existing
RepeatedFieldorMapField. - For nested collections (list-of-lists, map-of-maps), use wrapper messages to work around the single-level restriction.
- Use
TryGetValuefor safe map lookups, and checkCountinstead of null for emptiness.