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.,
0ornull). If a struct defines a primary constructor, the default parameterless constructordefault(MyStruct)ornew 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.