Last Updated: May 22, 2026
Creating an object and then setting half a dozen properties on it line by line is the kind of code C# was supposed to outgrow. Object initializers and collection initializers are the language's answer: a compact { ... } block right after new that sets properties or adds items as part of the construction expression. The result reads like a literal value rather than a sequence of assignments, and the compiler does exactly the same work behind the scenes.
Without initializers, populating a Product object means writing a constructor for every combination of fields you might want to set, or settling for a parameterless constructor and a stack of assignment statements. Both options age badly. The constructor route leads to a class with ten different constructors that all delegate to one master constructor. The assignment route gives you something like this:
Four assignment statements to build one product. The apple variable is half-built between each line, and a reader has to scan all four lines before they know what kind of product this is. Object initializers fix both problems by letting you write the same code as one expression:
Same output, very different shape. The whole thing is one expression that you can pass to a method, return from a function, or store in a collection without ever giving the half-built object a name. That's the headline benefit: an object built with an initializer doesn't exist in a partly-configured state at any line a reader can point to.
Object initializer syntax looks magical until you see what the compiler turns it into. There's no special "initializer" feature in the runtime, the C# compiler rewrites the syntax into a constructor call followed by a sequence of property assignments. The block:
becomes, at the IL level, the moral equivalent of:
The compiler picks a parameterless constructor (or whichever constructor you call explicitly, more on that in a moment), allocates the object, and then runs each property setter in source order. The whole result is then assigned to the target variable in a single step. Crucially, the variable apple only ever observes the fully-initialized object. The temporary, half-built object exists during the assignments but never leaks out.
The blue node is the initializer expression. The orange node is the constructor call that allocates the object. The teal nodes are the property setters running in source order. The green node is the final assignment to the variable on the left. The order matters: if two properties have setter logic that interacts, the second one runs after the first.
Several details matter here:
set or init). A read-only property without a setter cannot appear in an initializer.new Product() with no braces) and is implicitly empty.The compiler doesn't force you to use the parameterless constructor. You can pass arguments to any accessible constructor and then add an initializer block to set additional properties. The shape is new Type(args) { props }, where the constructor handles the mandatory state and the initializer handles the optional or convenient extras.
Name and Price are passed positionally to the constructor and live on read-only properties (no set accessor). Stock and Category are settable, so they take values from the initializer block. The combined form is especially useful when a class has a small number of must-supply values plus a longer tail of optional configuration.
The constructor runs first, then the property setters. That means any logic inside the constructor sees the values passed as arguments, but it does not see the values that the initializer is about to assign. If you have a constructor that computes a derived field from Stock, it won't see the initializer's Stock = 50, it will see whatever the constructor itself assigns. Check the ordering when constructor logic depends on a property the caller might set in the initializer.
The constructor ran before Discount = 0.10m was applied, so it saw Discount at its default value of 0 and decided the tier was "Standard". The initializer then set Discount, but Tier was already computed. If you need cross-property logic that reflects initializer values, do it in a method the caller invokes after construction, or accept the values as constructor parameters in the first place.
When a property's type is itself a class with settable properties, you can write an initializer for the inner object as the value of the outer property. The nested block builds the inner object, the outer initializer wires it up, and you never have to mention a temporary variable for the nested piece.
Three classes, three nested initializers, one expression. The shape mirrors the data: an order has a customer, a customer has an address. Without nested initializers, the same code would need three temporary variables and three sets of assignments to wire everything up.
There is a sharper form of nested initializer that comes up occasionally. If the property already holds an object (perhaps the class assigns one as a default field initializer), you can write Customer = { Name = "Alice" } without the new keyword. That reuses the existing object and sets properties on it, rather than replacing it. This is rare and usually a sign that the API design is unusual, but it exists:
The Customer property has no setter, so you can't write Customer = new Customer { ... }. But the property already has a default value, and Customer = { Name = "Alice" } reaches into that default and sets Name on it. The compiler is essentially writing cart.Customer.Name = "Alice" for you.
Collection initializers extend the same idea from objects to collections. Instead of building a List<T> and calling Add for every element, you write the elements directly inside braces. The compiler rewrites the block into a sequence of Add calls behind the scenes.
The compiler produces something like var temp = new List<string>(); temp.Add("apple"); temp.Add("milk"); ... and then assigns the temporary to groceries. There's no magic, just Add calls.
For a collection initializer to work on a type, two conditions have to be met:
IEnumerable (the non-generic interface from System.Collections, which IEnumerable<T> extends).Add method whose parameter signature matches the elements in the braces.List<T>, HashSet<T>, Queue<T>, and Stack<T> all qualify. So do most custom collection types that follow the standard pattern. Arrays are a special case: they use a slightly different new[] { ... } syntax, which is technically an array initializer, not a collection initializer, but the visual effect is the same.
Collection initializers also compose with object initializers. You can build a list of fully-configured objects in one expression by combining the two:
Three Product objects, each built with its own object initializer, all added to a List<Product> via a collection initializer. The whole catalog is one expression that the compiler unfolds into three Add calls and twelve property assignments. Readable, compact, and easy to extend.
Collection initializers use the type's Add method, which for List<T> is amortized O(1) per call. When the final size is known up front, prefer new List<T>(capacity) { ... } to skip the doubling reallocations that happen when the list grows past its initial capacity.
Dictionaries deserve their own section because they have two different initializer syntaxes, both legal, with different ergonomics. The older form treats each entry as a call to the dictionary's Add(key, value) method, written as a tuple-shaped pair inside braces:
Each { key, value } pair becomes a call to Add("APL", ...). This works for any type that exposes a two-argument Add method, not just dictionaries.
C# 6 introduced a second form that uses indexer assignment syntax: { [key] = value }. It looks more like normal indexer code and makes the key-value relationship clearer at a glance:
The two forms produce nearly identical results but have one important behavioral difference. The Add form throws an ArgumentException at runtime if the same key appears twice, because Dictionary.Add rejects duplicate keys. The indexer form silently overwrites, because the indexer assigns rather than adds:
Pick the form that matches what you want. The Add form is louder about bugs, a duplicate key crashes the construction and you find out immediately. The indexer form is more permissive, useful when you genuinely want "the last value wins" or when the initializer is built from data that might contain duplicates by design.
| Form | Syntax | Duplicate keys | Best for |
|---|---|---|---|
Add | { key, value } | Throws ArgumentException | Static dictionaries where duplicate keys would be a bug |
| Indexer | [key] = value | Overwrites silently | Dynamic data, configuration overrides, "last wins" semantics |
C# 9 added a third accessor for properties: init. An init-only property can be set during object construction, including from an object initializer, but is read-only afterward. This is the modern way to build types that are immutable after creation without forcing every value through the constructor.
From the caller's perspective, init looks just like set when writing the initializer block. The difference shows up after construction: any attempt to assign to apple.Price later is a compile error (CS8852). The object is immutable, but you didn't have to write a constructor with three parameters to get there.
Init-only properties are how you get the "construct once, then read-only" pattern without sacrificing the readability of object initializers. They pair especially well with records and with classes where every property is conceptually part of identity (the price of apple is 1.99m, not something to be edited later). For mutable working data, stick with set.
A short sample comparing the three accessors makes the difference concrete:
| Accessor | Set in constructor? | Set in initializer? | Set after construction? |
|---|---|---|---|
get; (no setter) | Only via backing field or auto-init expression | No | No |
get; set; | Yes | Yes | Yes |
get; init; | Yes | Yes | No |
The init accessor was introduced specifically to make object initializers work with immutable types. Before C# 9, "immutable after construction" meant "every field gets a constructor parameter," which scaled badly past a handful of fields. With init, the construction expression carries the values and the resulting object is sealed.
Object and collection initializers are simple in concept, but there are a few rules the compiler enforces strictly. Knowing the rules saves you from staring at error messages later.
Read-only properties cannot appear in an initializer. The property has to expose a set or init accessor that the calling code is allowed to use. A { get; }-only property is off limits:
The fix is to expose a setter (set for mutable, init for "set once during construction"), or to accept the value as a constructor parameter.
Private setters block initializers from outside the class. A { get; private set; } property can be set from inside the class but not from an initializer block written in another file or assembly. Accessibility rules apply to the initializer the same way they apply to a normal assignment:
Collection types must implement `IEnumerable` and expose `Add`. Both are required. A class with an Add(int) method but no IEnumerable implementation cannot use the collection initializer syntax. A class that implements IEnumerable but has no Add method also cannot. The fix is to provide both:
That's the same pattern the BCL collection types follow. List<T> works with initializers because it implements IEnumerable<T> and exposes Add(T). Your own collection types can do the same.
Initializers don't help with required fields. If a property is conceptually mandatory ("every product must have a name"), an initializer can't enforce that. The caller can simply omit Name = ... and end up with a half-built product. C# 11 introduced the required keyword to plug that gap, which forces the initializer to set the property at the call site or face a compile error.
What's wrong with this code?
The missing comma after Price = 1.99m breaks the initializer. The compiler reports a syntax error, not a missing-property error, because it can't parse the next line. There's also a separate problem: Stock isn't a property on Product, so even with the comma, the line would fail with CS0117 ("does not contain a definition for 'Stock'"). Fix the comma and add the property to the class.
Object initializers don't have a runtime overhead beyond the equivalent constructor call plus setter assignments. The compiler doesn't add a level of indirection. The cost is whatever the constructor and the setters cost, no more.
C# 12 added a new syntax that overlaps with collection initializers: collection expressions, written with square brackets. The line int[] nums = [1, 2, 3]; builds an array, and the same syntax also works for List<T>, Span<T>, and any type tagged with the [CollectionBuilder] attribute. It's a single uniform form that the compiler picks the right implementation for, based on the target type.
For straightforward cases the two forms are interchangeable. Collection expressions have extra powers (spread operator .., target-typing across array, list, and span) that go beyond what initializers do. For now, when you see [ ... ] building a collection, it's the modern shorthand, and the old new List<T> { ... } form remains valid and is what older codebases use.
A worked e-commerce example uses all the pieces together: nested object initializers, a collection initializer for the items, a dictionary initializer for the catalog lookup, and init-only properties on the inner types.
Three different initializer flavors share the same expression: an object initializer for the Order, a nested object initializer for the Customer and Address, a collection initializer for the Items list, and a dictionary initializer (indexer form) for the catalog. Every property uses init so the resulting objects are immutable. The whole construction is one expression with no temporary variables and no half-built intermediate state visible to the caller.