The concept and environment
Primary constructors on ordinary classes let construction parameters remain in scope throughout the type body. They do not give an ordinary class the generated public properties or value equality associated with positional records. Counter uses its start parameter from instance members after construction, so the compiler preserves the needed state for each instance. The Value property is explicitly written, and Add deliberately changes the parameter's stored value. This C# 12 feature can reduce repetitive constructor code, but understanding what is a parameter, a declared member, and captured instance state remains essential.
Walk through the portable program
The first Counter begins with five. Assigning it to alias copies a class reference, so both variables identify the same instance. Add increases that instance's captured start to seven; both Value reads therefore print seven. The final construction calls the explicitly written parameterless constructor, which chains to the primary constructor with zero. Its Value prints zero. The complete output is value:7, alias:7, and default:0. No public property named start is generated, and the separate parameterless instance does not share the earlier counter's captured state.
A deeper design boundary
A primary-constructor parameter is not a class member that you can access through this.start. It is a parameter whose scope makes several patterns possible: initialize an explicit field or property, pass a value to a base constructor, or use it from later instance behavior. Storage is needed when behavior must retain that parameter; it need not be retained merely because it was used once during initialization. Prefer one clear representation of a concept. Accidentally keeping both an initialized property and a separately captured parameter can create two values that later diverge.
Pitfalls and limits
Do not read the concise class declaration as a promise that start is readonly. This example intentionally assigns to it, and checked arithmetic makes an overflowing addition fail rather than silently wrapping. Nor does primary-constructor syntax make reference sharing disappear: alias observes the same mutation because Counter is still a class. Other constructors must reach the primary constructor through a this initializer. When validating dependencies or arguments, ensure the chosen initialization path establishes the same invariant for every constructor instead of treating shorthand as automatic validation or dependency injection.
Practice one: isolate a contract
First create var independent = new Counter(5); beside the original construction. Add two only to counter and predict values seven, seven, and five for counter, alias, and independent. Hint: construction creates the new instance; assignment to alias does not. Then call Add with a negative amount and decide whether decreasing a counter is permitted by your intended domain. The existing implementation allows it within integer bounds. If your requirement forbids decreases, write that policy and its expected rejection before changing the method.
Practice two: challenge an assumption
For a second exercise, replace the captured parameter with an explicit private field initialized from start, and have Value and Add use only that field. Predict unchanged public output. Hint: do not retain an additional mutable copy of start in one member while another member reads the field. Then create an immutable variant with a get-only Value and a method returning a new Counter. Compare alias observations between the mutable and immutable designs. The important difference is their ownership and mutation contract, not whether the constructor declaration is compact.
Review the resulting contract
Review a primary-constructor class by listing its actual declared members and tracing every use of each parameter. Identify whether a parameter is only an initialization input or remains part of later behavior. Inspect secondary constructor chaining, validation, reference sharing, and overflow policy just as you would with a conventional constructor. The sample keeps those decisions visible through small deterministic observations. In a larger dependency-bearing class, explicit readonly fields may still make intent easier to inspect; primary constructors are a language tool, not a requirement to hide meaningful state or to imitate a record.
A working example
using System;
var counter = new Counter(5);
var alias = counter;
counter.Add(2);
Console.WriteLine($"value:{counter.Value}");
Console.WriteLine($"alias:{alias.Value}");
Console.WriteLine($"default:{new Counter().Value}");
sealed class Counter(int start)
{
public Counter() : this(0) { }
public int Value => start;
public void Add(int amount) => start = checked(start + amount);
}
Expected output
value:7 alias:7 default:0
Create a separate Counter(5), mutate only the first, and compare. The captured parameter is not a generated public property.