---
title: "Primary constructors are more than syntactic sugar"
description: "Did you know that switching from a \"regular\" constructor to a primary constructor can change *when* your fields get initialized? Let's see why that matters."
updated: 2026-10-26
tags: ["C#", ".NET"]
canonical: "https://steven-giesel.com/blogPost/a3c741d3-f261-4bda-8da6-bffbbbd7aa5a/primary-constructors-are-more-than-syntactic-sugar"
---

# Primary constructors are more than syntactic sugar

Did you know that switching from a "regular" constructor to a primary constructor can change *when* your fields get initialized? Let's see why that matters.

In this short blog post, I will show you the difference and what the compiler does under the hood.

## The problem

Imagine you have a base class that does some work in its constructor, and that work calls a virtual member:

```csharp
abstract class Base
{
    protected Base(Func<int?> check)
    {
        Console.WriteLine($"{GetType().Name}: check() = {check()}, Message = {Message}");
    }

    protected abstract string Message { get; }
}
```

Yes, yes - calling virtual members inside a constructor is a code smell. But in the real world, things are not always that obvious. In bUnit, the base constructor of a `WaitForHelper` starts a timer, and once that timer fires, it reads the virtual `TimeoutErrorMessage`. Same problem, just hidden behind a `Timer` and another thread.

Now let's derive from it the "classic" way:

```csharp
class Regular : Base
{
    private readonly int? count;

    public Regular(int? count) : base(() => count)
    {
        this.count = count;
    }

    protected override string Message => $"count = {count?.ToString() ?? "null"}";
}
```

And the same thing with a primary constructor:

```csharp
class Primary(int? count) : Base(() => count)
{
    protected override string Message => $"count = {count?.ToString() ?? "null"}";
}
```

Looks equivalent, right? Let's run both:

```csharp
new Regular(3);
new Primary(3);
```

```no-class
Regular: check() = 3, Message = count = null
Primary: check() = 3, Message = count = 3
```

Well - not so equivalent after all.

## What the compiler does

If we look at the lowered code (I used ILSpy, but [sharplab.io](https://sharplab.io) does the job as well), the difference becomes obvious. Here is the `Regular` class:

```csharp
public Regular(int? count)
{
    var obj = new <>c__DisplayClass1_0();
    obj.count = count;
    base..ctor(new Func<int?>(obj.<.ctor>b__0));
    this.count = obj.count;
}
```

The lambda captures the parameter in a closure (the display class), so `check()` sees the correct value. But the field `this.count` is only assigned *after* the base constructor returns. Everything the base constructor does in the meantime sees `null`.

And here is the `Primary` class:

```csharp
public Primary(int? count)
{
    <count>P = count;
    base..ctor(new Func<int?>(<.ctor>b__0_0));
}
```

Because `count` is used inside a member (`Message`), the compiler captures it into a hidden field `<count>P`. And that field is assigned *before* the base constructor gets called. That is the same order as field initializers, which also run before `base(...)` in C#.

So in the bUnit case: with the "classic" constructor, if the timer fired before the derived constructor body ran, the error message said "did not result in any matching element(s)" instead of "did not result in exactly {n} matching element(s)". With a primary constructor, the value is already there.

## Small caveat

The generated `<count>P` field is not `readonly`. You can reassign `count` inside your class, and the compiler won't stop you. If you need the `readonly` guarantee, you are back to declaring a field yourself - and a field initializer (`private readonly int? count = count;`) also runs before the base constructor. Just don't use the parameter in a member *and* in a field initializer at the same time. Then you have two copies of the same value, and the compiler warns you with CS9124.