Primary Constructors in C#

Primary Constructors in C#

Primary constructors provide a compact way to declare the parameters a type needs when it is created. They were introduced for classes and structs in C# 12, building on the positional-constructor syntax that records have supported for longer.

The basic idea

Without a primary constructor, the same type requires an explicit constructor:

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

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

With a primary constructor, the parameters appear after the type name:

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

The parameters name and age are in scope throughout the type body. Assigning them to properties, as above, gives the object stored state. A primary-constructor parameter is not automatically a property or field.

Using parameters in methods

Without a primary constructor, a value used by a property must usually be stored in a field or property:

public class Greeter
{
    private readonly string _name;

    public Greeter(string name)
    {
        _name = name;
    }

    public string Message => $"Hello, {_name}!";
}

With a primary constructor, the parameter can be used directly by members:

public class Greeter(string name)
{
    public string Message => $"Hello, {name}!";
}

If a method or member uses a parameter after construction, the compiler creates storage for that captured value when necessary. It is usually clearer to expose long-lived data through an explicitly named property or field, especially when the value represents part of the object’s public state.

Base classes and dependency injection

Without a primary constructor, passing a dependency to a base class takes an explicit constructor:

public class ReportService : Service
{
    private readonly IClock _clock;

    public ReportService(IClock clock) : base(clock)
    {
        _clock = clock;
    }

    public DateTime CreatedAt => _clock.UtcNow;
}

With a primary constructor, the dependency and base-class call are visible in the type declaration:

public class ReportService(IClock clock) : Service(clock)
{
    public DateTime CreatedAt => clock.UtcNow;
}

They also work well with dependency-injection scenarios because the required dependencies are visible in the type declaration.

Validation and additional constructors

Without a primary constructor, validation can be performed in the traditional constructor:

public class Account
{
    private readonly string _id;

    public Account(string id)
    {
        _id = string.IsNullOrWhiteSpace(id)
            ? throw new ArgumentException("An account ID is required.", nameof(id))
            : id;
    }

    public string Id => _id;
}

With a primary constructor, the validation can move into a field initializer:

public class Account(string id)
{
    private readonly string _id =
        string.IsNullOrWhiteSpace(id)
            ? throw new ArgumentException("An account ID is required.", nameof(id))
            : id;

    public string Id => _id;
}

A type with a primary constructor can also declare additional constructors. Those constructors must eventually delegate to the primary constructor, directly or indirectly.

When should you use one?

Primary constructors are a good fit when a type has a small set of required inputs and straightforward initialization. Use a traditional constructor when construction involves many steps, overloads, complex validation, or behavior that would be harder to understand in a compact declaration.

The main benefit is not merely fewer lines of code: a primary constructor makes a type’s required inputs visible at the point where the type is declared.