Imagine a configuration system with complex objects: dozens of properties, nested collections, references to other objects. A user wants a new configuration "like that one, but with a different name". Building it from scratch is slow and error-prone. Copying the existing one is the obvious idea.
That's the Prototype pattern. The Gang of Four put it like this:
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
Simple idea. The trouble is in one word: copy.
Shallow vs deep: the real problem
A shallow copy copies the values of the fields. If a field is a reference (a list, a nested object), the copy points to the same object as the original. Change it in the copy, and you change it in the original.
A deep copy also copies everything those references point to, so the two objects are fully independent.
The classic trap is MemberwiseClone() behind ICloneable:
public object Clone() => MemberwiseClone(); // shallow: lists and nested objects are shared! And ICloneable makes it worse: its Clone() doesn't say whether the copy is shallow or deep, and it returns object. Microsoft's own guidelines recommend not using it in public APIs. Prefer methods whose names say what they do.
Records and with
Since C# 9, records give you copying for free:
public record Configuration { public required string Name { get; init; } public required List<string> Settings { get; init; } // Explicit deep copy: the name says what you get public Configuration DeepClone() => this with { Settings = new List<string>(Settings) }; } var clone = original with { Name = "Test Config" }; clone.Settings.Add("Setting3"); // original.Settings now contains "Setting3" too: 'with' is a shallow copy! with is great, but it's shallow. With mutable collections inside, you still need an explicit DeepClone().
Immutable collections: when shallow is enough
If nothing inside can change, sharing is safe, and the shallow copy of with becomes all you need:
public record SafeConfiguration { public required string Name { get; init; } public required ImmutableList<string> Settings { get; init; } } var variant = safe with { Settings = safe.Settings.Add("Setting2") }; // 'safe' is untouched: Add returns a new list Copy constructors for entities
Not everything is a record. For domain entities a copy constructor makes the intent explicit, and lets you decide what is copied and what is not:
public class Order { public Guid Id { get; } public List<OrderLine> Lines { get; } public Order(Guid id, List<OrderLine> lines) { Id = id; Lines = lines; } // Copy constructor: explicit, and in control of what is copied public Order(Order source) { Id = Guid.NewGuid(); // a copy is a new order Lines = new List<OrderLine>(source.Lines); // OrderLine is an immutable record } } new Order(existingOrder) reads as exactly what it is.
What about serialization?
Serializing and deserializing (to JSON, for example) gives you a deep copy without writing code, but it's slow. Cloning an object with 100 properties and 5 lists of 10 elements, measured on .NET 10:
| Technique | Time |
|---|---|
Record with { } (shallow) | ~0.1 µs |
Record with + new lists (deep) | ~0.3-0.5 µs |
| Copy constructor (deep) | ~0.9 µs |
| JSON round-trip (deep) | ~25 µs |
And binary serialization is no longer an option: BinaryFormatter was removed from the runtime in .NET 9.
When to use it
- Variations of a configured object: settings, templates, test data.
- Caching: keep a fully initialized prototype and hand out copies.
- Undo/redo: save a copy of the state before every change.
A few rules
- Prefer records and
withfor data, and immutable collections so that shallow copies are safe. - Name your copies:
DeepClone(), a copy constructor, never a bareClone(). - Avoid
ICloneable. - Keep serialization for the rare cases where speed doesn't matter.
Want the full story?
This is the short version. The complete article on Level Up Coding goes through the whole history: the GoF definition and its UML, the shallow vs deep copy problem, the ICloneable controversy, serialization-based cloning, expression trees, records and the copy constructor they generate, deep copy with records, the Prototype in Domain-Driven Design, the performance numbers, and the modern best practices.
Read it here: Prototype Pattern in .NET C#: From deep cloning to modern object copying.
Thanks for reading.