Back to Blog
C#

C# Record with Expression: Copy and Modify Records

c# record with expression: Learn how to use C# record with expression to create modified copies of immutable data, with practical examples and limitations.

C#RecordsImmutabilityWith Expression.NET
Diagram showing a C# record being copied with a with expression to create a modified instance

Why Records Need the with Expression

Records in C# are designed for immutable data. When you define a record with positional parameters or properties, the compiler generates a copy constructor and a protected clone method, but you cannot modify an existing instance. To change a property, you must create a new instance. The C# record with expression is the language feature that makes this concise and type-safe.

Consider this record:

public record Person(string FirstName, string LastName, int Age);

If you want a new Person with a different Age, you cannot do person.Age = 30; because the property is init-only. Instead, you write:

var person = new Person("Jane", "Doe", 28); var olderPerson = person with { Age = 29 };

The with expression creates a shallow copy of person, then applies the specified property changes. The result is a new Person instance with FirstName and LastName copied from the original and Age set to 29.

Syntax and Basic Usage

The with expression syntax is straightforward: original with { Property1 = value1, Property2 = value2 }. You can change any number of properties, including none (which still creates a copy). The expression works on both positional records and records with explicit properties.

public record Point(int X, int Y); var p1 = new Point(1, 2); var p2 = p1 with { Y = 3 }; // X remains 1

For records with init-only properties, the with expression is the only way to "modify" them without reflection. It respects the access modifiers: you can only set properties that are accessible from the current scope.

How the with Expression Works Under the Hood

When you define a record, the compiler generates a protected copy constructor that takes an existing record and copies all its fields. The with expression calls this copy constructor and then assigns the specified properties. This is not reflection; it is compiled into direct member access, making it fast and type-safe.

For record structs (introduced in C# 10), the with expression works similarly, but the copy is a struct copy, which is typically cheaper for small data.

The generated copy constructor can be customized. If you define your own copy constructor, the with expression will use it. This allows you to control what gets copied, for example, to perform a deep copy of certain fields.

Using with Expression with Inheritance

Records support inheritance. When you use a with expression on a derived record instance, the result is of the same runtime type as the original. The compiler generates a virtual method to handle this polymorphically.

public record Base(int Id); public record Derived(int Id, string Name) : Base(Id); var derived = new Derived(1, "Test"); var modified = derived with { Name = "Changed" }; Console.WriteLine(modified.GetType()); // Derived

The with expression preserves the actual type, so you don't lose derived properties.

Practical Example: Updating Nested Records

When records contain other records, the with expression performs a shallow copy. The top-level record is new, but nested reference types are shared between the original and the copy.

public record Address(string Street, string City); public record Employee(string Name, Address Address); var employee = new Employee("Alice", new Address("123 Main", "Springfield")); var relocated = employee with { Address = new Address("456 Oak", "Shelbyville") };

Here, relocated has a new Address instance, but if you only change a property of the nested Address, you need to create a new Address first:

var updated = employee with { Address = employee.Address with { City = "Metropolis" } };

This is because the with expression does not recursively copy nested objects.

Performance and Allocation Considerations

The with expression allocates a new instance of the record. For reference-type records, this is a heap allocation. The cost is similar to calling the copy constructor and then setting a few properties. For record structs, the copy is on the stack, which can be cheaper.

If you are creating many modified copies in a hot path, consider the allocation overhead. In most application code, this is negligible, but for large records or frequent operations, you might want to measure.

One subtle point: the with expression does not trigger property setters on the original; it directly sets the backing fields in the new instance. This means any validation in the property setter is bypassed. If you need validation, you should define a custom copy constructor or use a factory method.

When to Use with Expression vs Manual Copying

Manual copying involves creating a new instance and assigning all properties. For records with many properties, this is verbose and error-prone. The with expression is more concise and ensures you don't miss a property.

ApproachProsCons
with expressionConcise, type-safe, preserves runtime typeShallow copy only, no custom logic
Manual copyFull control, can apply validationVerbose, easy to miss properties

Use the with expression when you need a simple modified copy. Use manual copying or a factory method when you need to apply complex transformation rules or validation.

Limitations and Edge Cases

  • The with expression is only available on record types (including record structs). It cannot be used on classes or structs that are not records.
  • It performs a shallow copy. Nested mutable objects are shared.
  • If a record has a property that is not init-only (e.g., a get-only property with a private set), the with expression cannot change it because the setter is not accessible.
  • The with expression cannot be used in a static context without an instance.
  • When using inheritance, the with expression uses the runtime type, but if the derived record has a different copy constructor, you might need to be aware of that.

Understanding these limitations helps you decide when to use the with expression and when to implement a custom cloning strategy.

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