Back to Blog
C#

C# Record Constructor: Syntax and Customization

c# record constructor: How C# record constructors work: positional syntax, explicit constructors, validation, record structs, and the copy constructor behind `with` ex...

C# RecordsPrimary ConstructorsRecord StructsInit-only Propertieswith Expressions
Illustration of a C# record constructor showing generated code and init-only properties

How the C# record constructor behaves depends on how you declare the record. When you use positional parameters, the compiler generates a primary constructor whose parameters match the positional parameters exactly. That generated constructor is the foundation of the record's initialization behavior, but it is not the only way to construct a record instance.

What the Record Primary Constructor Generates

A positional record declaration is the shortest path to a fully functional data type.

public record Person(string FirstName, string LastName);

This single line produces more than a constructor. The compiler generates:

  • A primary constructor accepting string FirstName and string LastName
  • Init-only auto-properties for both parameters
  • A protected copy constructor used by with expressions
  • Overrides for Equals, GetHashCode, and ToString
  • A Deconstruct method

The generated constructor assigns each parameter to the corresponding init-only property. Because the properties are init-only, they can be set during construction and inside object initializers, but not after the object is fully created. This gives records their value-semantics behavior: once an instance exists, its state cannot be changed through a property setter.

Writing an Explicit Constructor for a Record

The generated primary constructor is not always sufficient. When you need to control what happens during construction, define a constructor with the same signature as the primary constructor. The compiler uses your implementation instead of generating one.

public record Person(string FirstName, string LastName) { public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } }

The init-only properties are still generated from the positional parameters, and your constructor assigns them directly. This pattern is the standard way to add logic to a record constructor without losing the positional declaration benefits.

You can also add constructors with different signatures that delegate to the primary constructor.

public record Person(string FirstName, string LastName) { public Person() : this("Unknown", "Unknown") { } }

The parameterless constructor delegates to the primary constructor, supplying default values. This is useful when a serialization framework or dependency injection container requires a parameterless constructor.

Validation Inside Record Constructors

One of the most common reasons to replace the generated constructor is validation. Keeping validation in the constructor ensures every instance passes the same checks, regardless of how it is created.

public record Product(string Name, decimal Price) { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Product name is required.", nameof(name)); if (price < 0) throw new ArgumentOutOfRangeException(nameof(price), "Price cannot be negative."); Name = name; Price = price; } }

Because the with expression uses the copy constructor rather than the primary constructor, validation in the primary constructor does not run when you create a modified copy. If validation must also apply to copies, the copy constructor must enforce it independently.

Record Struct Constructors

Record structs, introduced in C# 10, behave differently from record classes in several important ways.

public readonly record struct Point(double X, double Y);

A record struct always has a public parameterless constructor that sets every field to its default value. This differs from a record class, where the primary constructor is the only public constructor unless you add more.

For a non-readonly record struct, the generated properties are get/set rather than init-only.

public record struct Point(double X, double Y);

This means you can mutate a record struct's properties after construction, which is a deliberate design choice. If you need immutability, declare the record struct as readonly.

You can replace the primary constructor of a record struct just as you can with a record class, but the public parameterless constructor remains available unless you explicitly hide it.

The Copy Constructor and with Expressions

The with expression creates a new record instance by copying the original and applying the specified property changes. This relies on a generated copy constructor.

For a record class, the copy constructor is protected.

protected Person(Person original) { FirstName = original.FirstName; LastName = original.LastName; }

For a record struct, the copy constructor is public.

You can define your own copy constructor to customize what happens during a with expression. This is useful when you need to re-run validation or recompute derived state.

public record Product(string Name, decimal Price) { protected Product(Product original) { Name = original.Name; Price = original.Price; } }

When you define a custom copy constructor, you take responsibility for copying every property. Forgetting to copy a property silently produces a record with a default value, so keep the property list in sync as the record evolves.

Performance and Allocation Considerations

The generated record members are straightforward: property assignments in the constructor, field comparisons in Equals, and property reads in GetHashCode. There is no reflection or dynamic dispatch involved.

The main performance consideration is the copy constructor. Every with expression allocates a new record instance and copies all properties. For records with many properties, or in hot paths where with is called frequently, this allocation cost can add up. If you need to update a single field repeatedly, consider whether a mutable record struct or a plain class is a better fit.

Another subtle point: when you replace the primary constructor with an explicit one, the compiler no longer generates the property assignments for you. If your constructor does not assign a property, that property retains its default value. This is easy to miss when adding a new positional parameter to an existing record.

Common Constructor Mistakes in Records

The most frequent mistake is forgetting that replacing the primary constructor removes the generated assignments. Adding a new positional parameter to a record with an explicit constructor requires updating both the parameter list and the constructor body.

Another common issue is defining a constructor with the same signature as the primary constructor but expecting the compiler to still generate the property assignments. The compiler does not; it uses your constructor verbatim.

A third mistake is assuming that validation in the primary constructor also runs for with expressions. It does not. The copy constructor is a separate code path, and if validation must apply to copies, the copy constructor must enforce it independently.

Finally, be aware that record structs have a public parameterless constructor that record classes do not. Code that relies on the absence of a parameterless constructor will behave differently when migrated from a record class to a record struct.

c# record constructor: Practical Usage and Code Examples | RYUSLOG DEV