Collection Initializers
C# collection initializers (new T { a, b, c }) only work when the type has an Add method the compiler can call. Several BCL collections have none, so new Stack<int> { 1, 2, 3 } does not compile out of the box. This page covers the Add extensions that fill those gaps, plus two more that let an initializer take tuples and whole sequences.
At a glance
| Method | What it does |
|---|---|
Add(this Stack<T>, T) | new Stack<T> { a, b, c } pushes in order, so c is on top |
Add(this Queue<T>, T) | new Queue<T> { a, b, c } enqueues in order, so a dequeues first |
Add(this LinkedList<T>, T) | new LinkedList<T> { a, b, c } appends with AddLast |
Add(this ConcurrentQueue<T>, T) | new ConcurrentQueue<T> { a, b } enqueues in order |
Add(this ConcurrentStack<T>, T) | new ConcurrentStack<T> { a, b } pushes in order, so b is on top |
Add(this IDictionary<TKey, TValue>, (TKey Key, TValue Value)) | new Dictionary<K, V> { ("a", 1), ("b", 2) } |
Add(this ICollection<T>, IEnumerable<T>) | new List<T> { 1, others, 9 } adds every item of others |
All of these are in the static class CollectionInitializerExtensions, in the System.Collections.Generic namespace. You do not need an extra using to get them.
How the compiler binds an initializer
A collection initializer is shorthand. The compiler turns each element into a call to Add on the new object:
// You write:
Stack<int> stack = new Stack<int> { 1, 2, 3 };
// The compiler emits roughly:
Stack<int> temp = new Stack<int>();
temp.Add(1);
temp.Add(2);
temp.Add(3);
Stack<int> expanded = temp;
Two rules decide whether this compiles:
- The type must implement
IEnumerable. Every collection on this page does. - Each element must match an accessible
Addmethod. The compiler first looks for an instanceAdd. If no instanceAddaccepts the element, it then looks for an extensionAdd, the same way it does for an ordinary method call.
Stack<T>, Queue<T>, ConcurrentQueue<T> and ConcurrentStack<T> have no Add method. LinkedList<T> has one, but only as an explicit ICollection<T>.Add implementation, which the compiler cannot see on a LinkedList<T> variable. The extensions on this page give the compiler something to bind to.
Instance methods are always tried first, so these extensions never change what an existing initializer does. They only make new shapes compile. See When the instance Add wins.
On the .NET 10 SDK, collection expressions bind to the same methods. Stack<int> stack = [1, 2, 3]; compiles and leaves 3 on top.
Stacks and queues
Stack
Add calls Push. Items go in in the order you write them, so the last item ends up on top. Use it to seed an undo history or a work stack in one expression.
Stack<string> undoHistory = new Stack<string> { "type", "bold", "delete" };
string lastAction = undoHistory.Pop(); // → "delete"
string nextAction = undoHistory.Pop(); // → "bold"
This matches the Stack<T>(IEnumerable<T>) constructor, which also pushes items in sequence order: new Stack<int>(new[] { 1, 2, 3 }).Peek() is 3 too.
Queue
Add calls Enqueue, so the first item you write is the first one out.
Queue<string> pendingOrders = new Queue<string> { "order-1001", "order-1002" };
string first = pendingOrders.Dequeue(); // → "order-1001"
string second = pendingOrders.Dequeue(); // → "order-1002"
ConcurrentQueue
Add calls Enqueue. Use it to build a pre-filled queue before you hand it to worker threads.
using System.Collections.Concurrent;
ConcurrentQueue<int> invoiceIds = new ConcurrentQueue<int> { 1, 2 };
int[] inOrder = invoiceIds.ToArray(); // → [1, 2]
ConcurrentStack
Add calls Push, so the last item is on top, as with Stack<T>.
using System.Collections.Concurrent;
ConcurrentStack<int> buffers = new ConcurrentStack<int> { 1, 2 };
bool found = buffers.TryPeek(out int top); // → true
// top → 2
ConcurrentQueue<T> and ConcurrentStack<T> live in System.Collections.Concurrent, but their Add extensions are in System.Collections.Generic. You need the using for the collection type, not for the extension.
Linked lists
LinkedList
Add calls AddLast, so items keep the order you write them.
LinkedList<string> playlist = new LinkedList<string> { "intro", "verse", "chorus" };
string firstTrack = playlist.First!.Value; // → "intro"
string lastTrack = playlist.Last!.Value; // → "chorus"
LinkedList<T> implements ICollection<T>, so the spread works on it too:
int[] extra = { 7, 8 };
LinkedList<int> numbers = new LinkedList<int> { 1, extra };
// numbers → [1, 7, 8]
Tuple entries for dictionaries
A dictionary initializer normally takes { key, value } pairs in braces. This extension also accepts a (key, value) tuple. It works on any IDictionary<TKey, TValue>: Dictionary, SortedDictionary, ConcurrentDictionary and others.
Dictionary<string, int> ports = new Dictionary<string, int>
{
("http", 80),
("https", 443),
};
int https = ports["https"]; // → 443
You can mix both styles in one initializer:
Dictionary<string, int> ports = new Dictionary<string, int>
{
{ "http", 80 },
("https", 443),
};
int count = ports.Count; // → 2
It is also an ordinary method, so you can call it with a tuple you already have:
IDictionary<string, decimal> prices = new Dictionary<string, decimal>();
(string Sku, decimal Price) item = ("SKU-42", 19.99m);
prices.Add(item);
decimal price = prices["SKU-42"]; // → 19.99m
A duplicate key throws ArgumentException, exactly like Add(key, value). The extension never overwrites. If you want "last one wins", use an index initializer (["http"] = 80) instead.
Dictionary<string, int> ports = new Dictionary<string, int> { ("a", 1), ("a", 2) };
// → throws ArgumentException
Spreading a sequence
Add(this ICollection<T>, IEnumerable<T>) appends every item of a sequence. Inside an initializer it works as a "spread": you can put a whole list, array or LINQ query between single items.
int[] adminIds = { 2, 3 };
List<int> recipients = new List<int> { 1, adminIds, 4 };
// recipients → [1, 2, 3, 4]
Any IEnumerable<T> works, including a lazy query. It is enumerated once, at the point where it appears:
List<int> pageNumbers = new List<int> { 0, Enumerable.Range(1, 3) };
// pageNumbers → [0, 1, 2, 3]
The target can be any ICollection<T>, and the target's own rules still apply. A HashSet<T> drops duplicates, and an empty sequence adds nothing:
HashSet<string> tags = new HashSet<string> { "sale", new[] { "sale", "new" } };
// tags → ["sale", "new"]
HashSet<string> onlyOne = new HashSet<string> { "sale", Array.Empty<string>() };
// onlyOne → ["sale"]
A sequence of a derived type spreads into a collection of the base type, because IEnumerable<T> is covariant:
public abstract record Animal(string Name);
public sealed record Dog(string Name) : Animal(Name);
public sealed record Cat(string Name) : Animal(Name);
List<Dog> dogs = new List<Dog> { new Dog("Rex") };
List<Animal> animals = new List<Animal> { new Cat("Tom"), dogs };
// animals.Select(a => a.Name) → ["Tom", "Rex"]
The spread needs a target that implements ICollection<T>. Stack<T>, Queue<T>, ConcurrentQueue<T> and ConcurrentStack<T> do not, so new Stack<int> { 1, extra } does not compile. Build those from a sequence with their constructor instead, for example new Stack<int>(extra).
The element type must line up. new List<long> { intArray } does not compile, because an IEnumerable<int> is not an IEnumerable<long>.
When the instance Add wins
The compiler only falls back to an extension when no instance Add accepts the element. So when the element is itself a T, the collection's own Add(T) runs and the sequence is added as one item. It is not spread.
List<int> inner = new List<int> { 1, 2 };
List<object> items = new List<object> { inner };
int count = items.Count; // → 1
bool sameList = ReferenceEquals(items[0], inner); // → true
The same rule protects List<string>: "abc" is a string, so new List<string> { "abc" } adds "abc". The case to watch is List<char>. A string is an IEnumerable<char> but not a char, so it spreads:
List<char> letters = new List<char> { 'x', "abc" };
// letters → ['x', 'a', 'b', 'c']
If you want a collection of collections, give the outer list the inner type, such as List<List<int>> or List<int[]>. The instance Add then accepts each inner sequence as one element and nothing spreads.
Null handling and exceptions
Every method here can also be called directly, not only from an initializer. The receiver and the spread sequence are checked:
| Call | Throws | Parameter name |
|---|---|---|
Add on a null Stack<T> or ConcurrentStack<T> | ArgumentNullException | stack |
Add on a null Queue<T> or ConcurrentQueue<T> | ArgumentNullException | queue |
Add on a null LinkedList<T> | ArgumentNullException | list |
Tuple Add on a null dictionary | ArgumentNullException | dictionary |
Tuple Add with a key that is already present | ArgumentException | thrown by the dictionary |
Spread Add on a null collection | ArgumentNullException | collection |
Spread Add with a null sequence | ArgumentNullException | items |
null items are fine wherever the collection accepts them: new Stack<string?> { null } pushes one null. A null key in a tuple entry is passed on to the dictionary, so it throws whatever the dictionary throws (ArgumentNullException for Dictionary<TKey, TValue>).
IEnumerable<int>? moreIds = null;
List<int> ids = new List<int> { 1, moreIds! };
// → throws ArgumentNullException (parameter "items")
See also
- Dictionaries:
GetOrAdd,Merge,Incrementand the other dictionary helpers. - Lists and collections: more helpers for
IList<T>andICollection<T>, including list deconstruction. - foreach and deconstruction: other features that work because the compiler binds a method by name.
- Conventions: why the extensions live in BCL namespaces.