Everything about object construction in C#: kinds of constructors, chaining, initialisation order, static and primary constructors, required members and pitfalls.
A constructor runs when an object is created and should leave it valid. If you declare none, the compiler adds a public parameterless one. Constructors can chain: : this(...) calls another constructor of the same class, : base(...) calls the base class's. For new Derived(), the order is: derived field initialisers, base field initialisers, base constructor body, derived constructor body. A static constructor runs once, before the type is first used, and is thread-safe. Primary constructors (C# 12) put parameters on the class declaration; they are captured as hidden fields, not properties, and are mutable. required members (C# 11) force callers to set them in an object initialiser. Calling a virtual method from a constructor runs the derived override before the derived constructor body has run, so it can see uninitialised state.
A constructor runs once when an object is created and must leave it in a valid state. I'd cover the kinds of constructors, chaining, the exact initialisation order in a hierarchy, static constructors, primary constructors and required members, then the virtual-call-in-constructor trap.
Order(Guid id) breaks new Order() callers and serialisers that need a parameterless constructor.this(...) and base(...)public class Order
{
public Order(Guid id, CustomerId customer)
{
ArgumentOutOfRangeException.ThrowIfEqual(id, Guid.Empty);
Id = id; Customer = customer;
}
public Order(CustomerId customer) : this(Guid.NewGuid(), customer) { } // reuse validation
}
public class ExpressOrder : Order
{
public ExpressOrder(CustomerId c, TimeSpan sla) : base(c) => Sla = sla; // base runs first
}
this(...) keeps validation in one place; base(...) chooses which base constructor runs. If omitted, base() (parameterless) is called implicitly — compile error if none exists.
For new ExpressOrder(...):
Field initialisers run derived-to-base (because the compiler emits them at the start of each constructor, before the base(...) call), but constructor bodies run base-to-derived. Static constructors run before the first use of each type (base type's static constructor isn't guaranteed to run first unless something triggers it).
static Order() { ... } runs once per type (per closed generic), automatically, thread-safely, before first access.TypeInitializationException. Keep them trivial — no I/O or configuration.public sealed class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public Task<Order?> FindAsync(int id, CancellationToken ct) => repo.FindAsync(id, ct);
}
readonly.private readonly int _size = size > 0 ? size : throw ...).init properties instead.public sealed class CreateCustomerRequest
{
public required string Email { get; init; }
public required string Name { get; init; }
public string? Phone { get; init; }
}
The compiler forces callers using object initialisers to set them; System.Text.Json (.NET 7+) enforces them during deserialisation. A constructor marked [SetsRequiredMembers] tells the compiler it sets them all.
public abstract class Report
{
protected Report() => Title = BuildTitle(); // virtual call in base ctor
protected abstract string BuildTitle();
public string Title { get; }
}
public sealed class SalesReport(string region) : Report
{
private readonly string _region = region;
protected override string BuildTitle() => $"Sales - {_region.ToUpper()}"; // _region is null here!
}
The base constructor runs before the derived constructor body, so the override executes on a partially initialised object — _region is still null → NullReferenceException. (CA2214 flags this.) Fix: avoid virtual calls in constructors; use a factory method, lazy initialisation, or pass data to the base constructor.
A plugin base class called a virtual LoadSettings() from its constructor. A new derived plugin read a configuration field initialised in its own constructor body; under load it intermittently threw NREs at start-up depending on which plugin loaded first. Moving initialisation to an explicit InitializeAsync step called by the host (and adding CA2214 as an error) fixed it — and made async loading possible, which constructors can't do.
Checks deep understanding of object creation, which affects correctness everywhere.
required members.public class Derived : Base
{
private int _x = 5;
public Derived() : base(1) { Console.WriteLine("Derived body"); }
}
becomes, in IL, roughly:
Derived::.ctor()
ldarg.0; ldc.i4.5; stfld _x // field initialisers FIRST
ldarg.0; ldc.i4.1; call Base::.ctor // then base constructor
...Derived body...
That's why initialisers run before base constructors. Constructors chaining with this(...) don't repeat initialisers — only the constructor that calls base(...) (explicitly or implicitly) contains them.
beforefieldinitbeforefieldinit, allowing the runtime to run initialisation any time before the first static field access — often eagerly, and enabling the JIT to skip initialisation checks in hot code..cctor; two types whose static constructors depend on each other across threads can deadlock during start-up.public class Counter(int start)
{
public int Next() => ++start; // 'start' captured into a hidden mutable field
}
The compiler synthesises a field like <start>P and rewrites uses. Consequences: the parameter is mutable state (no readonly), and analyzers suggest assigning to an explicit readonly field when immutability matters. For DI services this is usually fine.
init and requirednew Customer { Email = "a@b" } compiles to: call the constructor, then call set_Email/init_Email. So the object exists in an incomplete state between the constructor and the initialiser assignments — any validation must happen in the constructor or be enforced by required. required is checked by the compiler at the creation site (and by reflection-based creators that honour RequiredMemberAttribute), not at run time in arbitrary code.
ActivatorUtilities / the DI container picks the constructor with the most parameters it can satisfy (or the one marked [ActivatorUtilitiesConstructor]). Multiple public constructors can create ambiguity errors. Constructors of DI services should be cheap and side-effect free: no I/O, no async work (constructors can't be async), so resolution is fast and failures are clear. Expensive initialisation belongs in hosted services, factories or IAsyncInitializer-style patterns.
If a constructor throws, the object is never returned — but if it allocated unmanaged resources or registered event handlers before throwing, those leak. Acquire resources last, or wrap with try/catch and dispose. A finalizer may still run on the partially constructed object, so finalizers must tolerate uninitialised fields.
Structs can now declare parameterless constructors and field initialisers (C# 10), but default(T) and arrays bypass them. C# 11 auto-defaults unassigned fields. Never rely on a struct constructor having run.
Console.WriteLine to field initialisers and constructor bodies in a three-level hierarchy and observe the order.// Chaining and validation in one place
public sealed class Money
{
public Money(decimal amount) : this(amount, "AUD") { }
public Money(decimal amount, string currency)
{
ArgumentOutOfRangeException.ThrowIfNegative(amount);
ArgumentException.ThrowIfNullOrWhiteSpace(currency);
Amount = amount;
Currency = currency;
}
public decimal Amount { get; }
public string Currency { get; }
}
// Base/derived order and the virtual-call trap
public abstract class Gateway
{
protected Gateway() => Configure(); // BAD: runs override before derived ctor body
protected abstract void Configure();
}
public sealed class StripeGateway : Gateway
{
private readonly StripeOptions _options;
public StripeGateway(StripeOptions options) : base() => _options = options;
protected override void Configure() => Console.WriteLine(_options.ApiBase); // _options is null here
}
// Primary constructor for DI, captured into a readonly field
public sealed class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
private readonly IOrderRepository _repo = repo;
public Task<Order?> FindAsync(int id, CancellationToken ct)
{
logger.LogDebug("Finding order {Id}", id); // 'logger' captured (mutable capture)
return _repo.FindAsync(id, ct);
}
}
// Static constructor and required members
public sealed class CountryCodes
{
public static readonly FrozenSet<string> All;
static CountryCodes() => All = new[] { "AU", "NZ", "NP" }.ToFrozenSet();
}
public sealed class CreateUserRequest
{
public required string Email { get; init; }
public string? DisplayName { get; init; }
}
var req = new CreateUserRequest { Email = "[email protected]" }; // omit Email -> CS9035
What happens: a base constructor calls an overridable method. Why it is wrong: the override runs before the derived constructor body — fields are uninitialised, causing NREs or wrong values. Do instead: avoid virtual calls in constructors (CA2214); use factories or explicit initialisation steps.
What happens: constructors open connections, call APIs or block on .Result.
Why it is wrong: constructors can't be async, failures surface during DI resolution, and blocking causes starvation.
Do instead: keep constructors cheap; use async factory methods or hosted-service initialisation.
What happens: adding a parameterised constructor breaks serialisers, EF Core or callers. Why it is wrong: the compiler no longer generates the default constructor. Do instead: add an explicit parameterless constructor where frameworks need it (it can be private for EF Core).
What happens: static constructors read configuration or connect to services.
Why it is wrong: a failure permanently breaks the type with TypeInitializationException until restart.
Do instead: keep static initialisation trivial; use DI and options for configuration.
What happens: each overload repeats the same checks.
Why it is wrong: checks drift apart and some paths become invalid.
Do instead: chain with this(...) to one constructor that validates.
What happens: code reassigns a captured parameter, changing state unexpectedly.
Why it is wrong: captured parameters are mutable hidden fields.
Do instead: assign to explicit readonly fields when immutability matters.
requiredWhat happens: DTOs with essential properties set via initialisers, but callers forget some.
Why it is wrong: objects are created incomplete and fail later.
Do instead: use required members or constructor parameters.
What happens: a constructor subscribes to events or opens a stream, then throws. Why it is wrong: no caller gets the object to dispose it. Do instead: acquire resources last or clean up in a catch block before rethrowing.
Seniors treat the constructor as the guard of the object's invariants and pick the construction style that fits the type.
| Type | Construction style | Reason |
|---|---|---|
| Domain entity | validating constructor or static factory | never invalid |
| DI service | primary constructor | concise dependency list |
| DTO / options | required + init |
readable, serialiser-friendly |
| Value object | record or readonly struct with validation | value semantics |
| Expensive setup | factory or async CreateAsync |
constructors cannot be async |
They also know constructors cannot be async; when creation needs I/O, use a static CreateAsync factory and a private constructor, rather than starting tasks inside the constructor.
InnerException links an exception to the one that caused it, so a high-level error ("order import failed") keeps the low-level cause ("connection timeout"). GetBaseException() returns the innermost cause, and ToString() prints the whole chain. AggregateException holds several inner exceptions in InnerExceptions; you get it from Task.Wait(), .Result, Parallel.For and Task.Exception. Flatten() merges nested aggregates and Handle(predicate) marks some as handled and rethrows the rest. await unwraps it and throws only the first inner exception, so after await Task.WhenAll(...) check task.Exception.InnerExceptions to see all failures. Reflection wraps errors in TargetInvocationException, and a failing static constructor gives TypeInitializationException, which makes the type unusable for the rest of the process.
Inheritance lets a derived class reuse and specialise a base class: class SavingsAccount : Account. C# allows one base class but many interfaces. The derived class inherits all members except constructors and finalisers (private ones exist but are not accessible), can call base members with base., and can override virtual/abstract members. Declaring a member with the same name without override hides it (new), and which one runs then depends on the variable's static type, a common source of bugs. Use inheritance only for a real is-a relationship where a derived object can be used anywhere the base is expected (Liskov substitution). Deep hierarchies, inheriting just to reuse code, and fragile base classes are the main problems; composition is often better.
How to tell it (STAR)
**Situation:** At [company] a new payment provider crashed on startup with a null reference, even though its options were configured. **Task:** I had to fix it before [the provider launch] and make sure other providers were not at risk. **Action:** Stepping through the constructors showed the base class calling a virtual `Configure()` before the derived constructor had assigned its fields. That is how C# initialisation order works. I removed the virtual call from the constructor, added a factory that builds the object fully first, and turned the CA2214 analyzer back on as an error. **Result:** The provider launched on time, the analyzer later caught [one more] case in another library, and the team adopted the factory pattern for any setup hooks.
Last updated 30 Oct 2026.