C# Object Initializers Run After Code in the Default Constructor

Object initializers are one of those C# features that look entirely intuitive on the surface. Declare a new object, set some properties in curly braces, and move on. But beneath that clean syntax lies a specific execution order that, if misunderstood, can produce subtle and frustrating bugs. The rule is simple: the default constructor runs to completion before any property assignments from the object initializer take effect.

I've seen this catch experienced developers off guard more than once. This post covers exactly what that means, why it matters, and how to design a type so the initialization order works for it rather than against it.

The Execution Order

When an object is instantiated with an object initializer, C# follows a strict two-phase sequence:

  1. The default (or specified) constructor executes in full
  2. The properties listed in the object initializer are assigned, one by one, in the order they appear

There's no interleaving. The constructor has no awareness that an object initializer will follow, and the object initializer has no ability to influence what happens inside the constructor.

What the Ordering Affects

Consider a class that logs its own state during construction:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person()
    {
        Console.WriteLine("Constructor running...");
        Console.WriteLine($"Name: {Name}, Age: {Age}");
    }
}

// Usage
var person = new Person
{
    Name = "John",
    Age = 30
};

The output is:

Constructor running...
Name: null, Age: 0

Even though Name and Age are assigned at the call site, the constructor sees only the default values: null for the string and 0 for the integer. The property assignments haven't happened yet. If the constructor contained validation logic, branching decisions, or event subscriptions that depended on those property values, it would operate on incomplete data.

Collection Initializers

Collection initializers follow the same two-phase pattern. The collection's constructor runs first, and then the compiler generates a series of Add calls:

var list = new List<int> { 1, 2, 3 };

This is semantically equivalent to:

var list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);

This distinction becomes important when working with custom collections. If the Add method performs validation, triggers side effects, or involves synchronization (as in a concurrent collection), those operations execute after construction, not during it. Designing the Add method with that ordering in mind prevents a class of initialization bugs.

Common Pitfalls

This execution order creates several recurring traps:

  1. Constructor-time validation: A constructor that validates property values will always see default values when paired with an object initializer. The validation runs before the caller has had any opportunity to set the properties.

  2. Dependent properties: Computed or derived properties that depend on other properties being populated will produce incorrect results if evaluated in the constructor.

  3. Event wiring: If the constructor subscribes to events or registers callbacks that reference property values, those handlers will capture the default values rather than the values the caller intended.

Safer Patterns

Each of the following patterns avoids the two-phase initialization problem by ensuring that all required data is available at construction time.

1. Immutable Objects with Parameterized Constructors

The most straightforward approach is to require all necessary values as constructor parameters:

public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

// Usage
var person = new Person("John", 30);

This guarantees three things: every property is set before any constructor logic that depends on it, properties can't be mutated after creation, and the object is never in a partially initialized state.

2. Record Types (C# 9+)

Record types express the same intent with far less ceremony:

public record Person(string Name, int Age);

// Usage
var person = new Person("John", 30);

// Records support deconstruction
var (name, age) = person;

Records provide immutable properties, value-based equality, built-in deconstruction, and a concise positional syntax. For data-carrying types where identity is defined by the values rather than by reference, records are the idiomatic choice in modern C#.

3. Factory Methods with Private Constructors

When construction involves validation, transformation, or conditional logic that shouldn't be the caller's concern, a factory method paired with a private constructor provides a clean separation:

public class Person
{
    public string Name { get; }
    public int Age { get; }

    private Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public static Person Create(string name, int age)
    {
        return new Person(name, age);
    }
}

// Usage
var person = Person.Create("John", 30);

Because the constructor is private, callers can't use object initializers or bypass the factory method. All creation logic is centralized in one place, and the object is always fully initialized before it becomes accessible.

4. Builder Pattern

When an object has many optional properties or when the construction process involves multiple steps, the Builder pattern provides a fluent API that accumulates configuration before producing the final object:

public class PersonBuilder
{
    private string name;
    private int age;

    public PersonBuilder WithName(string name)
    {
        this.name = name;
        return this;
    }

    public PersonBuilder WithAge(int age)
    {
        this.age = age;
        return this;
    }

    public Person Build()
    {
        return new Person(name, age);
    }
}

// Usage
var person = new PersonBuilder()
    .WithName("John")
    .WithAge(30)
    .Build();

The builder accumulates state incrementally, but the target object is constructed atomically. Validation can run inside Build(), ensuring the final object meets all invariants.

Best Practices

  1. Favor immutability. Objects that are fully initialized at construction time and can't be modified afterward eliminate an entire category of state-related bugs. This is doubly true when object initializers are involved, because mutable properties invite the two-phase initialization trap.

  2. Use constructor parameters for required data. If a property must be set for the object to function correctly, make it a constructor parameter. This turns a runtime surprise into a compile-time error.

  3. Validate early, validate in the constructor. When using parameterized constructors, the constructor is the natural place for input validation. The object either constructs successfully in a valid state or throws before it ever exists.

  4. Reserve object initializers for optional, non-critical properties. Object initializers work well for configuration-style properties that have sensible defaults. They're a poor fit for properties that drive core behavior.

  5. Remember that collection initializers follow the same rules. The Add calls happen after construction, so any constructor logic in a custom collection won't see the initial items.

Under the Hood

The C# compiler transforms object initializer syntax into a constructor call followed by a sequence of property setter calls. Examining the generated IL makes the ordering unambiguous:

// Create new instance
IL_0000: newobj instance void Person::.ctor()
IL_0005: dup

// Set Name property
IL_0006: ldstr "John"
IL_000B: callvirt instance void Person::set_Name(string)
IL_0010: dup

// Set Age property
IL_0011: ldc.i4.s 30
IL_0013: callvirt instance void Person::set_Age(int32)

The newobj instruction runs the constructor. Only after it returns do the callvirt instructions execute the property setters. There's no mechanism for the constructor to defer to or interleave with the initializer. They're separate operations at the IL level.

These transformations are easy to explore directly with tools like SharpLab or .NET Fiddle.

Conclusion

The execution order of object initializers in C# is deterministic and well-defined: the constructor runs first, and property assignments follow. This isn't a quirk or an edge case. It's a fundamental aspect of how the language compiles initializer syntax. Understanding this ordering is essential for writing constructors that behave predictably and for choosing initialization patterns that keep an object in a consistent state from the moment it is created.

For the authoritative specification, see the Microsoft documentation on Object and Collection Initializers.

References