C# Record with Object Copy: Using `with`
c# record with object copy: Learn how to copy C# record objects using `with` expressions, understand shallow vs deep copy behavior, and know when to use them.
c# record with object copy requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Copying a C# record object is a common operation, and the with expression provides a concise syntax for creating a modified copy. But the copy is shallow by default, which affects how you handle reference-type members.
Understanding Record Copy Semantics in C#
Records in C# are designed for immutable data and value-based equality. Unlike classes, records give you a built-in with expression that produces a copy of the original object with specified properties changed. This is the primary mechanism for object copy in records.
public record Person(string FirstName, string LastName, int Age);
Given a Person instance, you can create a new one with a different age:
var original = new Person("Jane", "Doe", 30); var updated = original with { Age = 31 };
The with expression copies all properties from original into a new Person instance, then applies the property initializers inside the braces. This is a shallow copy: reference-type members are copied by reference, not cloned.
Using with to Copy a Record Object
The with expression is not limited to modifying properties. You can use it to create an exact copy without changes:
var copy = original with { };
This creates a new instance with the same property values. For value types like int or immutable references like string, this behaves like a deep copy. For mutable reference types, the new record shares the same underlying object.
The syntax is concise and avoids manual property assignment, which is especially useful when a record has many properties.
Shallow vs Deep Copy: What with Actually Does
When you use with, the compiler generates a copy constructor that copies each field. For value-type fields, the value is copied. For reference-type fields, the reference is copied, meaning both the original and the copy point to the same object.
Consider a record that contains a List<int>:
public record Order(int Id, List<int> Items);
var original = new Order(1, new List<int> { 1, 2, 3 }); var copy = original with { }; copy.Items.Add(4);
Now original.Items also contains 4 because copy.Items and original.Items reference the same list. This is a classic shallow copy behavior.
To achieve a deep copy, you must explicitly clone the reference-type members. For example:
var deepCopy = original with { Items = new List<int>(original.Items) };
This creates a new list with the same elements, so modifying deepCopy.Items does not affect original.Items.
Copying Records with Reference-Type Members
When a record contains a mutable reference type, you need to decide whether a shallow or deep copy is appropriate. The with expression gives you the flexibility to replace those members with new instances.
public record Customer(string Name, Address Address); public record Address(string Street, string City);
Since Address is a record (immutable by default), copying a Customer with with is safe because the Address reference is shared but immutable. If Address were a mutable class, you would need to clone it manually:
var copy = original with { Address = original.Address with { } };
If the address contains nested reference types, you may need a recursive deep copy. This is not automatic, so you must implement it when needed.
Performance and Allocation Considerations
The with expression creates a new instance, which allocates memory. For records with many properties, this is still efficient because the copy constructor simply assigns fields. The main cost is the allocation itself.
If you need to copy a record frequently in a hot path, consider whether you can avoid the copy by using immutable data structures or by designing your code to not require copies.
Also, be aware that with triggers the copy constructor, which for records with inheritance may call base copy constructors. This is generally fast but not zero-cost.
In scenarios where you only need to read data, sharing the reference is often sufficient. Use with only when you need a distinct instance with modified or independent state.
Common Pitfalls and Edge Cases
One pitfall is assuming with performs a deep copy. As shown, it does not. Another is using with on records with mutable properties that are not intended to be changed. Records are meant to be immutable, but you can define init-only setters or use record struct for value semantics.
Inheritance also affects copying. If you have a derived record, with preserves the runtime type:
public record Base(int X); public record Derived(int X, int Y) : Base(X); var derived = new Derived(1, 2); var copy = derived with { Y = 3 }; // copy is Derived
This is convenient, but be careful when the derived record has additional reference-type members.
When to Choose Records for Object Copy
Records are a strong choice when you need value equality and a straightforward copy mechanism. If your type is mostly data and you frequently create modified copies, records reduce boilerplate compared to classes.
Use records when:
- The object is primarily a data container.
- You want value-based equality.
- You need
withexpressions for non-destructive updates.
Avoid records when you have complex inheritance hierarchies or need fine-grained control over copy behavior, because the automatic copy may not match your requirements.