List<T> is the most frequently used collection in C# — a dynamically-sized, ordered, indexed collection of a single type. Unlike arrays, a List<T> grows automatically as you add items. It is the default choice whenever you need an ordered, mutable collection. ASP.NET Core service methods return List<Post>, Entity Framework Core materialises query results into List<T>, and controller actions build response DTOs from lists. Learning List<T> thoroughly is immediately applicable in every part of this series.
Creating and Populating Lists
// ── Creating lists ────────────────────────────────────────────────────────
var empty = new List<int>(); // empty
var withItems = new List<string> { "Alice", "Bob" }; // collection initialiser
var fromArray = new List<int>(new[] { 1, 2, 3 }); // from array
var withCap = new List<Post>(capacity: 100); // pre-allocate for 100 items
// ── Adding items ──────────────────────────────────────────────────────────
var posts = new List<Post>();
posts.Add(new Post { Id = 1, Title = "First" }); // add one
posts.AddRange(GetMorePosts()); // add collection
posts.Insert(0, new Post { Id = 0, Title = "Pinned" }); // insert at index 0
// ── Removing items ────────────────────────────────────────────────────────
posts.Remove(posts[0]); // remove specific object
posts.RemoveAt(0); // remove by index
posts.RemoveAll(p => !p.IsPublished); // remove all matching predicate
posts.Clear(); // remove everything
// ── Accessing items ───────────────────────────────────────────────────────
Post first = posts[0]; // index access — O(1)
Post last = posts[^1]; // index-from-end — C# 8+
int count = posts.Count; // total items
bool hasAny = posts.Count > 0; // check non-empty
// Prefer: posts.Any() with LINQ (covered in Chapter 9)
Note:
List<T> is backed by an array internally. When the array is full, the list allocates a new array of double the capacity and copies all items. This doubling strategy means the average cost of Add() is O(1) amortised, even though individual adds that trigger a resize are O(n). If you know approximately how many items you will add, pass a capacity to the constructor to avoid unnecessary resizes: new List<Post>(capacity: expectedCount). This is especially valuable in hot paths like response serialisation in high-traffic ASP.NET Core APIs.Tip: Prefer returning
IReadOnlyList<T> or IEnumerable<T> from service methods rather than List<T> directly. This signals to callers that they should not mutate the collection, prevents accidental modification of the service’s internal state, and allows the implementation to return any enumerable (including lazy LINQ queries) without breaking the API. The caller can always call .ToList() if they need a mutable copy.Warning: Never modify a
List<T> while iterating it with foreach — adding or removing items during iteration throws InvalidOperationException: Collection was modified. If you need to filter a list, use LINQ’s .Where() to create a new filtered list: posts = posts.Where(p => p.IsPublished).ToList(). If you must remove items by index, iterate backwards with a for loop, or use RemoveAll(predicate) which handles the iteration safely internally.Searching and Sorting
var names = new List<string> { "Charlie", "Alice", "Bob", "Dave" };
// ── Sorting ───────────────────────────────────────────────────────────────
names.Sort(); // in-place alphabetical: Alice, Bob, Charlie, Dave
names.Sort((a, b) => b.CompareTo(a)); // in-place reverse: Dave, Charlie, Bob, Alice
// LINQ OrderBy creates a NEW sorted sequence (does not modify the list)
var sorted = names.OrderBy(n => n).ToList();
var byLen = names.OrderBy(n => n.Length).ThenBy(n => n).ToList();
// ── Searching ─────────────────────────────────────────────────────────────
bool hasAlice = names.Contains("Alice"); // true
int idx = names.IndexOf("Bob"); // index of first match, -1 if not found
int lastIdx = names.LastIndexOf("Bob"); // index of last match
// Predicate-based search
string? first = names.Find(n => n.StartsWith("C")); // "Charlie" or null
List<string> all = names.FindAll(n => n.Length > 3); // ["Charlie", "Alice", "Dave"]
// ── Conversion ───────────────────────────────────────────────────────────
string[] arr = names.ToArray(); // List → array
var upper = names.Select(n => n.ToUpper()).ToList(); // project to new list
var csv = string.Join(", ", names); // "Charlie, Alice, Bob, Dave"
List vs Array
| Aspect | List<T> | T[] |
|---|---|---|
| Size | Dynamic — grows automatically | Fixed at creation |
| Add/Remove | Yes — O(1) amortised Add | No — must copy to new array |
| Index access | O(1) | O(1) |
| Memory overhead | Slightly higher (capacity buffer) | Exact fit |
| Use for | Mutable collections, most scenarios | Fixed-size data, interop, spans |
Common Mistakes
Mistake 1 — Modifying a list while iterating with foreach
❌ Wrong — InvalidOperationException:
foreach (var post in posts)
if (!post.IsPublished) posts.Remove(post); // throws!
✅ Correct:
posts.RemoveAll(p => !p.IsPublished); // ✓ safe
Mistake 2 — Returning List<T> from service methods (exposes mutability)
❌ Wrong — caller can modify the service’s internal list.
✅ Correct — return IReadOnlyList<T> or IEnumerable<T>.