C# Primary Constructor Field Access: Storing Parameters
c# primary constructor field access: Learn how to access and store primary constructor parameters as fields in C# 12, including common pitfalls and best practices.
In C# 12, primary constructors let you declare constructor parameters directly in the class declaration. These parameters are in scope throughout the class body, but they are not automatically stored as fields. If you need to access them later, you must explicitly assign them to fields or properties. This article explains how to handle c# primary constructor field access correctly, what common mistakes to avoid, and how the behavior differs between classes and structs.
What Primary Constructors Actually Provide
A primary constructor is declared by placing parameters directly after the type name:
public class Person(string name, int age) { // name and age are in scope here }
The parameters name and age are available throughout the class body, including in method bodies, property initializers, and field initializers. However, they are not automatically captured as instance state. They behave like ordinary constructor parameters that are in scope for the entire type definition. This is a key difference from positional records, where parameters automatically become public properties.
Storing Primary Constructor Parameters as Fields
To access the parameters after the constructor finishes, you must store them explicitly. The most straightforward approach is to assign them to private fields:
public class Person(string name, int age) { private readonly string _name = name; private readonly int _age = age; public string Name => _name; public int Age => _age; }
Here, the primary constructor parameters are used to initialize the fields. The fields are then accessible throughout the class. This pattern gives you full control over how the parameters are stored and exposed. You can also use properties with initializers:
public class Person(string name, int age) { public string Name { get; } = name; public int Age { get; } = age; }
Both approaches work. The choice depends on whether you need mutable state or additional logic in the getter.
Accessing Primary Constructor Parameters in Methods and Properties
Because the parameters are in scope for the entire class, you can use them directly in method bodies without storing them first. This is useful when the parameter is only needed during construction or for a one-time calculation:
public class Rectangle(double width, double height) { public double Area() => width * height; }
In this example, width and height are used directly in the Area method. They are not stored as fields. This works because the method is called on an instance, and the primary constructor parameters are captured by the compiler as hidden fields when they are used outside the constructor. The compiler generates the necessary storage automatically when you reference the parameters in instance members. This is a subtle but important behavior: the parameters are not always just constructor arguments; they become captured state when used in non-constructor members.
Common Mistakes with Primary Constructor Field Access
A frequent mistake is assuming that primary constructor parameters are automatically available as fields. For example, the following code will not compile:
public class Person(string name) { public string Name => name; // Error: name is not accessible here }
Actually, this does compile in C# 12 because the compiler captures name as a hidden field when it is used in a property getter. The error occurs only if you try to assign to name outside the constructor, because it is not a writable field. But the more common mistake is forgetting to store the parameter when you need to modify it later. For instance, if you want a mutable property, you cannot write:
public class Person(string name) { public string Name { get; set; } = name; // This works, but it's a new property }
This is fine, but it creates a separate property that is initialized from the parameter. If you later change Name, the original name parameter is not affected. If you need to keep the parameter as a field and allow mutation, you must declare a field explicitly:
public class Person(string name) { private string _name = name; public string Name { get => _name; set => _name = value; } }
Another common mistake is using the primary constructor parameter in a field initializer that runs before the parameter is assigned. In C#, field initializers run before the constructor body, but with primary constructors, the parameters are available in field initializers because they are in scope. However, if you try to use the parameter in a field initializer that also references another field, the order of initialization can be tricky. For example:
public class Example(int value) { private int _doubled = value * 2; private int _tripled = _doubled + value; // This uses _doubled, which is initialized first }
This works because field initializers execute in declaration order. But if you reorder the fields, you may get a null or default value. Always be aware of the order.
Primary Constructors in Structs vs Classes
Structs have a different constraint: all fields must be definitely assigned when the constructor exits. With primary constructors, the parameters are not automatically fields, so you must assign them to fields or properties to satisfy the compiler. For example:
public struct Point(double x, double y) { public double X { get; } = x; public double Y { get; } = y; }
If you omit the assignments, the struct will not compile because the compiler cannot guarantee that X and Y are initialized. In classes, the default values are allowed, so you can have uninitialized fields, but that is usually not desirable. Structs also have a default parameterless constructor that initializes all fields to default values, so you cannot rely on primary constructor parameters being present when the default constructor is used.
Maintainability and Readability Considerations
Primary constructors can make a class declaration more concise, but they can also hide the fact that parameters are not stored. When reading code, it is not immediately obvious which parameters become fields and which are only used during construction. This can lead to confusion, especially in larger classes. A good practice is to explicitly store parameters as fields or properties, even if you only use them in one method, to make the class's state clear. Alternatively, you can use positional records when you want automatic property generation. Records are designed for immutable data, while primary constructors are more general.
Another consideration is that primary constructor parameters are captured by the compiler as hidden fields when used in instance members. This means the class has additional hidden state that may not be visible in the source. This can affect serialization, equality, and debugging. If you need to control the storage, always declare fields explicitly.
Compatibility and Language Version Requirements
Primary constructors for classes and structs are a C# 12 feature. They require the .NET 8 SDK or later, and the language version must be set to C# 12. If you are targeting an older framework, you can still use primary constructors if the compiler supports the language version, but the runtime does not need to be .NET 8 because the feature is purely a compile-time transformation. However, some tooling and analyzers may not fully support it. When working in a codebase that must support older language versions, you should stick to traditional constructors.
When to Use Primary Constructors vs Traditional Constructors
Primary constructors are best suited for small, focused classes where the constructor parameters are used directly in a few members. They reduce boilerplate and make the dependency on constructor arguments explicit. For larger classes with many parameters or complex initialization logic, a traditional constructor with a body gives you more control and readability. Also, if you need to perform validation or other logic before assigning fields, a traditional constructor is clearer. The choice depends on the complexity of the class and the team's coding standards.
A practical approach is to use primary constructors for simple data holders or dependency injection, and traditional constructors for anything that requires more than a few lines of initialization. This keeps the code maintainable without sacrificing clarity.