C# has no built-in deep clone. For your own types, write a DeepClone() method that copies each field and calls DeepClone() on the objects it owns. For plain data classes, a generic JSON round trip works without touching the types: JsonSerializer.Deserialize<T>(JsonSerializer.SerializeToUtf8Bytes(obj)). MemberwiseClone(), a record's with and new List<T>(list) are all shallow: nested objects stay shared.
Classes in C# are reference types, so var b = a; gives you a second name for the same object, not a copy. A shallow copy makes a new outer object whose fields still point at the same nested objects and lists. A deep copy copies all the way down, so nothing you do to the copy can reach the original. The old Stack Overflow answer, serializing through BinaryFormatter, no longer works: .NET 9 removed it. What's left is an explicit clone method, a System.Text.Json round trip, and knowing which built-in copies are shallow. Each example runs on this page: hit Run, then edit the code and run it again.
1A DeepClone method on each classRecommended
The most reliable deep copy is the one you write. Each class gets a DeepClone() that builds a new instance, copies its value-type and string fields as they are, and calls DeepClone() on every object it owns. Lists are copied element by element. It is plain code, so it is fast, type-safe and works with private state, and you decide what "owns" means: a reference to a shared lookup object can stay shared on purpose. The classes below use primary constructors (C# 12) to stay short.
Output
Prints original: London, 2 lines, first qty 1 and copy: Paris, 3 lines, first qty 5: the city, the line quantity and the extra line all changed only in the copy. The four ReferenceEquals checks print False, so no object is shared between the two graphs. A copy constructor, public Address(Address other), is the same idea spelled differently. The cost is upkeep: add a field and forget to copy it, and the clone silently shares it, so put a test next to each DeepClone.
2Shallow copies: assignment, MemberwiseClone and with
Most "my copy changed the original" bugs come from one of three copies that look deep and aren't. Assignment copies the reference. MemberwiseClone(), the protected method every object inherits, copies the fields one by one, and a field that holds a list holds the same list in both objects. A record's with expression (C# 9) is a memberwise copy too, with the properties you name replaced.
Output
The first line prints Platform: renaming alias renamed core, because they are one object. After MemberwiseClone, the names are independent but both lists print Ada, Linus, Grace. The record behaves the same way: Focus: 2 tracks and True, since with copied the reference to the list. The fix is to replace the shared member in the same expression, focus with { Tracks = [.. focus.Tracks] }, which prints Focus: 2 tracks, copy: 3 tracks. A record whose members are all immutable (strings, numbers, other immutable records, ImmutableList<T>) never needs a deep copy: nothing in it can change, so sharing is safe.
3Generic deep clone with System.Text.Json
When you need to clone types you don't want to edit, or a one-off copy of a DTO, serialize the object and deserialize it into a new one. With System.Text.Json (built into .NET Core 3.0 and later) that is a two-line generic extension method. It rebuilds the whole graph, so nothing is shared. It only copies what the serializer sees, though, and by default that is public properties with public getters and setters.
Output
The order clones cleanly: original: London, first qty 1 against copy: Paris, first qty 5. The Settings copy shows what gets lost: Theme: dark survives, but Retries: 0 (public fields are skipped unless you set IncludeFields = true) and Token: "" (a private setter is skipped unless the property has [JsonInclude]). The last line prints Circle -> Shape: the serializer works from the declared type T, so the Circle came back as a plain Shape without its radius, unless you set up [JsonDerivedType] polymorphism. A JSON round trip is also much slower than a hand-written clone. It is fine for occasional copies of plain data, not for a hot loop. See converting an object to JSON for the serializer options.
4Clone a List<T>, an array or a Dictionary
Copying a collection has the same two levels. A new list with the same elements, [.. list] (C# 12), new List<T>(list) or list.ToList(), is a complete copy when the elements are numbers, strings or other immutable values. When the elements are mutable objects, the new list holds the same objects, so clone each one on the way in.
Output
The number lists print 3, 1, 4 | 3, 1, 4, 1, and the three older spellings each give a list of 3. The shallow copy of the cart has its own length (adding pad left cart alone) but shares the items, so the cart prints pen x99, ink x1. The per-element clones don't: after changing deep and deep2, the cart still prints pen x99, ink x1 while deep prints pen x99, ink x50. Array.Clone() is shallow too (90 0 is fine here because the elements are ints), and the dictionary line prints shallow: 2, deep: 1: the copy made with new Dictionary(tags) shares each List<string> value, while ToDictionary with a new list per value doesn't.
5Shared references and cycles
A naive deep clone assumes the objects form a tree. Real graphs often don't: two orders point at the same customer, or a parent and a child point at each other. A recursive clone then loops forever on a cycle and turns one shared object into several copies. System.Text.Json handles both with ReferenceHandler.Preserve (.NET 5+), and a hand-written clone handles them with a dictionary of objects already copied.
Output
The default serializer refuses the cycle with JsonException: A possible object cycle was detected. (the full message also suggests ReferenceHandler.Preserve). With Preserve, the copy prints Ada -> Bob -> Ada, the cycle points back at the copy (True), and the copy is not the original (False). Shared references print default: False, preserve: True: only Preserve keeps the two list slots pointing at one object. The hand-written CloneGraph prints True too. Its dictionary uses ReferenceEqualityComparer.Instance (.NET 5+) so that two different objects that happen to be equal are still copied separately, and it registers each clone before recursing, which is what stops the cycle.
6Which should you use?
| Method | Depth | Speed | Best for |
|---|---|---|---|
| obj.DeepClone() (hand-written) | Deep, you decide | Fastest | Your own types: the default |
| JsonSerializer round trip | Deep, public properties only | Slow | Plain DTOs, types you can't edit |
| ReferenceHandler.Preserve | Deep, keeps shared refs and cycles | Slow | Graphs with cycles |
| MemberwiseClone() | Shallow | Fast | Classes of only value types and strings |
| record with { … } | Shallow | Fast | Immutable records, changing a few members |
| [.. list] / new List<T>(list) | Shallow | Fast | Lists of numbers, strings, immutable items |
| BinaryFormatter | Removed in .NET 9 | Throws | Nothing: replace it |
Frequently asked questions
What is the best way to deep clone an object in C#?
For your own classes, write a DeepClone() method (or a copy constructor) that creates a new instance, copies value-type and string fields directly, and calls DeepClone() on each nested object and list element. It is the fastest option and copies private state. When you can't or don't want to edit the types, a System.Text.Json round trip, JsonSerializer.Deserialize<T>(JsonSerializer.SerializeToUtf8Bytes(obj)), deep-copies plain data classes, but it skips public fields, private setters and derived-type members by default.
Does MemberwiseClone make a deep copy?
No. MemberwiseClone() creates a new object and copies every field's value, which for a reference-type field is the reference. A List<string> field ends up shared by the original and the clone, so adding to one list shows up in both. It is a complete copy only when every field is a value type or an immutable type such as string. It is also protected, so a class has to expose it through its own method.
How do I clone a List<T> in C#?
For a shallow copy, use [.. list] (C# 12), new List<T>(list) or list.ToList(); that is a full copy when T is a number, a string or another immutable type. For a list of mutable objects, clone each element: list.Select(x => x.DeepClone()).ToList() or list.ConvertAll(x => x.DeepClone()). Otherwise both lists hold the same objects, and changing copy[0].Qty changes the original's first item too.
Does the with expression on a record make a deep copy?
No. record with { ... } is a memberwise copy with the named members replaced, so a List<T> member is shared between the original and the copy. Replace it in the same expression, p with { Tracks = [.. p.Tracks] }, or make the record's members immutable so sharing them is harmless. Records also compare by value, so p == (p with { }) is True even though the two are different objects.
Can I still use BinaryFormatter to deep clone an object?
No. From .NET 9, calling BinaryFormatter.Serialize throws PlatformNotSupportedException: BinaryFormatter serialization and deserialization have been removed. The class was also a security risk when reading untrusted data, which is why it was removed. Replace a BinaryFormatter clone with a hand-written DeepClone() or a System.Text.Json round trip; mark members with [JsonInclude] if they have private setters.
Should I implement ICloneable?
Microsoft's design guidelines advise against it in public APIs. ICloneable.Clone() returns object, so every caller has to cast, and the interface doesn't say whether the copy is deep or shallow, so a caller can't rely on either. A strongly typed public Order DeepClone() (or ShallowCopy()) says what it does and needs no cast.
Does assigning a struct copy it deeply?
Assigning a struct copies all of its fields, so changing an int field on the copy leaves the original alone. A field that holds a class, such as a List<string>, still copies only the reference: after var s2 = s1; s2.Items.Add("b");, s1.Items has the new item too. The same applies to Array.Clone() on a jagged int[][]: the inner arrays are shared.