Skip to main content

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​

MethodWhat 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:

What the compiler writes for you
// 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:

  1. The type must implement IEnumerable. Every collection on this page does.
  2. Each element must match an accessible Add method. The compiler first looks for an instance Add. If no instance Add accepts the element, it then looks for an extension Add, 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.

tip

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.

info

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.

Seeding an undo stack
Stack<string> undoHistory = new Stack<string> { "type", "bold", "delete" };

string lastAction = undoHistory.Pop(); // → "delete"
string nextAction = undoHistory.Pop(); // → "bold"
note

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.

A queue of pending orders
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.

Pre-filled job queue
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>.

Pre-filled concurrent stack
using System.Collections.Concurrent;

ConcurrentStack<int> buffers = new ConcurrentStack<int> { 1, 2 };

bool found = buffers.TryPeek(out int top); // → true
// top → 2
note

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.

An ordered playlist
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:

Mixing single items and a sequence
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.

Port map with tuple entries
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:

Braces and tuples together
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:

Adding a tuple directly
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
warning

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.

Duplicate keys throw
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.

Building a list of recipient IDs
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:

Spreading a LINQ query
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:

Spreading into a set
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:

Spreading a sequence of a derived type
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"]
note

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<object> keeps the list as one item
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:

A string spreads into a List<char>
List<char> letters = new List<char> { 'x', "abc" };
// letters → ['x', 'a', 'b', 'c']
tip

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:

CallThrowsParameter name
Add on a null Stack<T> or ConcurrentStack<T>ArgumentNullExceptionstack
Add on a null Queue<T> or ConcurrentQueue<T>ArgumentNullExceptionqueue
Add on a null LinkedList<T>ArgumentNullExceptionlist
Tuple Add on a null dictionaryArgumentNullExceptiondictionary
Tuple Add with a key that is already presentArgumentExceptionthrown by the dictionary
Spread Add on a null collectionArgumentNullExceptioncollection
Spread Add with a null sequenceArgumentNullExceptionitems

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>).

A null sequence in a spread
IEnumerable<int>? moreIds = null;

List<int> ids = new List<int> { 1, moreIds! };
// → throws ArgumentNullException (parameter "items")

See also​