C# Primary Constructors for Classes and Structs

Primary constructors in C# provide a concise syntax to declare and use constructor parameters directly within the class or struct definition. Introduced for standard classes and structs in C# 12, this feature removes boilerplate code associated with private fields and constructor assignments. This article explains how primary constructors function across both classes and structs, covers syntax and parameter scoping, details their integration with base constructors and secondary constructors, and highlights the key differences between them and record types.

Syntax and Core Mechanics

A primary constructor is defined by appending a parameter list directly after the type name:

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

The parameters declared in the primary constructor (name and age) are in scope throughout the entire type declaration. They can be used to initialize properties, fields, or even accessed directly inside methods and properties.

public class Greeter(string message)
{
    public void Greet()
    {
        Console.WriteLine(message);
    }
}

When a parameter is referenced directly in a member method or property accessor rather than used purely for field initialization, the C# compiler captures the parameter by generating a hidden backing field automatically.

Differences from Record Types

In C# records, primary constructor parameters automatically generate public, init-only properties. In standard classes and structs, primary constructor parameters do not generate public properties.

If you do not explicitly assign a primary constructor parameter to a public property or field, it remains private to the type implementation and is not accessible by external callers.

Base Constructor Calls

If a class derives from a base class that requires constructor arguments, the primary constructor must pass those arguments to the base class at the type declaration level:

public class Person(string name)
{
    public string Name { get; } = name;
}

public class Employee(string name, int employeeId) : Person(name)
{
    public int EmployeeId { get; } = employeeId;
}

The base class constructor invocation Person(name) directly follows the type declaration, fulfilling the inheritance requirement without an explicit constructor block.

Secondary Constructors

If a class or struct defines additional (secondary) constructors, every secondary constructor must directly or indirectly call the primary constructor using the this(...) initializer:

public class Product(string name, decimal price)
{
    public string Name { get; } = name;
    public decimal Price { get; } = price;

    // Secondary constructor chaining to the primary constructor
    public Product(string name) : this(name, 0.0m)
    {
    }
}

This guarantees that the primary constructor logic and initialization always execute, preserving initialization invariants.

Primary Constructors in Structs

Primary constructors behave identically in structs with respect to syntax and capture semantics, but they adhere to struct-specific memory rules:

public struct Point(int x, int y)
{
    public int X { get; set; } = x;
    public int Y { get; set; } = y;
}

Key considerations for structs include:

  • Field Initialization: All fields and auto-properties must be definitively assigned before control leaves the constructor, including when using primary constructors.
  • Default Values: Structs always have an implicit parameterless constructor that initializes all fields to their default values (e.g., 0 or null). If a struct defines a primary constructor, the default parameterless constructor default(MyStruct) or new MyStruct() can still be invoked without executing the primary constructor logic.
  • Readonly Structs: In a readonly struct, all properties and captured primary constructor fields are immutable.

Mutable Parameter Capture Warning

When primary constructor parameters are captured by methods, they act as mutable state inside the instance. Modifying a captured parameter inside a method changes the value for other members referencing that parameter:

public class Counter(int start)
{
    public void Increment() => start++;
    public int CurrentCount => start;
}

While functional, this pattern can lead to confusion because constructor parameters are traditionally treated as immutable input arguments. To maintain maintainability, explicitly assign primary constructor parameters to standard fields or properties when mutation is required.