📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do constructors work in C#? Explain default and parameterised constructors, chaining with this() and base(), the order of initialisation in a class hierarchy, static constructors, primary constructors (C# 12) and required members, and why calling virtual methods from a constructor is dangerous.

Everything about object construction in C#: kinds of constructors, chaining, initialisation order, static and primary constructors, required members and pitfalls.

Mid-Level Theory Intermediate constructors initialisation oop primary-constructors required-members

🎯 Key points

  • The compiler adds a parameterless constructor only if you declare none
  • `: this(...)` chains within the class; `: base(...)` calls the base constructor
  • Order: derived field initialisers, base field initialisers, base constructor body, derived constructor body
  • Virtual calls from a constructor run the override before the derived constructor body
  • Static constructors run once before first use, are thread-safe, and break the type if they throw
  • C# 12 primary constructor parameters in classes are captured, mutable and not properties
  • `required` members (C# 11) must be set in object initialisers; `[SetsRequiredMembers]` exempts a constructor
  • Constructors cannot be async; use a static factory such as `CreateAsync`

⏱ 30-second answer

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.

🗣 Full answer (senior)

How I would answer this in an interview

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.

1. Default and parameterised constructors

  • If you declare no constructor, the compiler generates a public parameterless one (protected for abstract classes).
  • Declaring any constructor removes that default — so adding Order(Guid id) breaks new Order() callers and serialisers that need a parameterless constructor.
  • Parameterised constructors enforce invariants: required values, validation, and dependencies (DI uses the constructor to inject services).

2. Chaining with 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.

3. Order of initialisation in a hierarchy

For new ExpressOrder(...):

  1. Memory allocated and zeroed (all fields default).
  2. Derived field initialisers run.
  3. Base field initialisers run.
  4. Base constructor body runs.
  5. Derived constructor body runs.

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

4. Static constructors

  • static Order() { ... } runs once per type (per closed generic), automatically, thread-safely, before first access.
  • No parameters, no access modifiers, can't be called directly.
  • If it throws, the type is permanently broken: every access throws TypeInitializationException. Keep them trivial — no I/O or configuration.

5. Primary constructors (C# 12)

public sealed class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
    public Task<Order?> FindAsync(int id, CancellationToken ct) => repo.FindAsync(id, ct);
}
  • For classes and structs, parameters are in scope throughout the type. If used in members, the compiler captures them in hidden fields — mutable, not readonly.
  • Great for DI services; less ideal when you need validation (you can still validate via initialisers: private readonly int _size = size > 0 ? size : throw ...).
  • For records, primary constructors generate public init properties instead.
  • Pitfall: using a parameter in a member and in an initialiser creates double storage (warning CS9124).

6. Required members (C# 11)

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.

7. Virtual calls from constructors

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.

8. A production example

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.

🔍 What the interviewer is checking

Checks deep understanding of object creation, which affects correctness everywhere.

  • Green flags: explains chaining and initialisation order correctly, warns about virtual calls in constructors, understands static constructor timing and failure, knows primary constructor capture semantics and required members.
  • Red flags: thinks base constructor bodies run after derived ones, assumes primary constructor parameters are properties, or puts async work in constructors.

📚 Learn in depth

1. What the compiler emits

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.

2. Static constructor semantics and beforefieldinit

  • With an explicit static constructor, the type initialiser runs exactly at first access to a static member or first instance creation.
  • Without one (only static field initialisers), the compiler marks the type beforefieldinit, 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.
  • The runtime takes a lock while running a .cctor; two types whose static constructors depend on each other across threads can deadlock during start-up.

3. Primary constructor capture in detail

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.

4. Object initialisers, init and required

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

5. Constructors and dependency injection

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.

6. Exceptions in constructors

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.

7. Struct constructors

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.

Try it yourself

  1. Add Console.WriteLine to field initialisers and constructor bodies in a three-level hierarchy and observe the order.
  2. Reproduce the virtual-call-in-constructor NRE and enable CA2214.
  3. Write a primary-constructor class, decompile it in SharpLab, and find the captured field.
  4. Make a static constructor throw and access the type twice.

💻 Code

// 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

⚠ Common mistakes

1. Calling virtual or abstract members from constructors

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.

2. Doing I/O or async work in constructors

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.

3. Accidentally removing the parameterless constructor

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

4. Throwing or doing heavy work in static constructors

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.

5. Duplicating validation across constructors

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.

6. Assuming primary constructor parameters are readonly

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.

7. Using object initialisers without required

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

8. Leaking resources when constructors throw

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.

🏛 Senior insight: trade-offs

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.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions a static constructor throwing Why: Checks they know about TypeInitializationException and wrapped exceptions
    How to answer

    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.

  2. When: Candidate explains base() chaining Why: Moves into inheritance and what derived classes get
    How to answer

    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.

🛠 Real-world problems: how you handle them

NullReferenceException from a virtual call in a base constructor
Situation
A payments library had an abstract `Gateway` base class whose constructor called an abstract `Configure()` method. A new `StripeGateway` implementation read its options inside `Configure()`.
Signals
`NullReferenceException` in `StripeGateway.Configure` on startup, only for that provider; the options were definitely registered in DI.
Investigation
Stepped through construction and saw `Configure()` running inside the base constructor, before the derived constructor body had assigned `_options`. Analyzer CA2214 (do not call overridable methods in constructors) had been suppressed.
Resolution
Removed the virtual call from the base constructor and introduced a static `Create` factory that constructs the gateway fully and then calls `Configure()`. Re-enabled CA2214.
Prevention
Added unit tests that construct every gateway, made CA2214 a build error, and documented the construction pattern in the library's contributor guide.

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.

↩ Usually asked after

More in Core OOP

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me