Back to Blog
C#

C# Object Copying: Shallow vs Deep Copy

c# object copying: Learn how to copy objects in C# correctly. Compare shallow and deep copy approaches, understand reference semantics, and choose the right strategy.

C#Object CopyingDeep CopyShallow CopyMemberwiseCloneRecords
Diagram showing two objects sharing references versus independent copies in C#

When you copy an object in C#, the result depends on whether you copy the reference or the underlying data. A simple assignment copies the reference, not the object. This article covers the common approaches to C# object copying, including shallow copy with MemberwiseClone, deep copy via serialization or manual construction, and the copy behavior of records.

Reference Semantics and the Assignment Trap

C# classes are reference types. When you write var second = first;, you are not creating a copy of the object; you are creating a second reference to the same instance. Any change made through second is visible through first. This is the root reason why explicit copying is needed in many applications.

public class Person { public string Name { get; set; } public Address Address { get; set; } } var first = new Person { Name = "Alice", Address = new Address { City = "Berlin" } }; var second = first; second.Name = "Bob"; second.Address.City = "Hamburg"; Console.WriteLine(first.Name); // Bob Console.WriteLine(first.Address.City); // Hamburg

Both variables point to the same object. To avoid this, you need a deliberate copying mechanism.

Shallow Copy with MemberwiseClone

The simplest built-in way to create a shallow copy is MemberwiseClone, a protected method on System.Object. It creates a new object and copies all non-static fields. For value-type fields, the values are copied. For reference-type fields, only the references are copied, so the new object and the original share the same referenced instances.

public class Person : ICloneable { public string Name { get; set; } public Address Address { get; set; } public object Clone() { return this.MemberwiseClone(); } }

Calling Clone() returns a new Person with the same Name value and the same Address reference. Modifying the Address property on either object affects both. This is acceptable when the nested objects are immutable or when sharing is intentional, but it often causes subtle bugs.

Deep Copy: Manual Construction and Copy Constructors

A deep copy creates a new object graph where every reference-type field is also copied. The most reliable way is to manually construct the copy, explicitly copying each field. This gives you full control over what is shared and what is duplicated.

public class Person { public string Name { get; set; } public Address Address { get; set; } public Person Copy() { return new Person { Name = this.Name, Address = new Address { City = this.Address.City } }; } }

Copy constructors are a common pattern. They take an instance of the same type and initialize the new object from it. This approach is explicit, maintainable, and does not rely on reflection or serialization. The downside is that you must update the copy logic whenever a new field is added.

Deep Copy via Serialization

Serialization-based deep copy converts an object to a byte stream and then deserializes it into a new instance. This works for any object graph that is serializable, but it has significant costs: it requires the type and all nested types to be marked [Serializable] (or use a serializer that handles the type), and it is typically much slower than manual copying.

using System.Text.Json; public static T DeepCopyJson<T>(T source) { var json = JsonSerializer.Serialize(source); return JsonSerializer.Deserialize<T>(json); }

JSON serialization is convenient but has limitations. It does not preserve object identity for shared references, it may not handle circular references well, and it requires public parameterless constructors or special configuration. Binary serialization is another option, but it is not cross-platform safe and is discouraged in modern .NET. Serialization should be used only when the object graph is simple and performance is not critical.

Records and with Expressions

C# 9 introduced records, which are reference types with built-in value semantics for equality. Records provide a with expression that creates a shallow copy with optional property changes. This is a convenient way to copy immutable data structures.

public record Person(string Name, Address Address); var original = new Person("Alice", new Address("Berlin")); var modified = original with { Name = "Bob" };

The modified record has the same Address reference as original. If you need a deep copy of a record, you must still copy the nested reference types manually or use another technique. Records are ideal when you want copy-and-update semantics for immutable data, but they do not solve deep copying by themselves.

Performance and Allocation Tradeoffs

The choice of copying strategy has direct performance implications. MemberwiseClone is fast because it is a low-level field-by-field copy. Manual copy constructors are also fast, but they require you to write and maintain the code. Serialization-based copying is the slowest because it involves reflection, allocation of intermediate buffers, and often string parsing.

ApproachSpeedDepthMaintenanceCircular References
MemberwiseCloneFastShallowLowPreserved
Manual constructorFastDeepHighManual handling
SerializationSlowDeepLowOften problematic
Records + withFastShallowLowPreserved

If you are copying large object graphs frequently, avoid serialization. If you need deep copies and the object graph is stable, a manual copy method is the most predictable and efficient. For simple shallow copies, MemberwiseClone or records with with are appropriate.

Choosing the Right Copy Strategy

There is no universal best way to copy objects in C#. The correct approach depends on the type's structure, whether the object graph contains cycles, whether you need immutability, and how often copying occurs.

Use MemberwiseClone when you need a quick shallow copy and you understand that nested references will be shared. Use records with with when you are working with immutable data and want a copy that differs in a few properties. Use a manual copy constructor or a dedicated Copy method when you need a deep copy and you can keep the copy logic in sync with the type's evolution. Use serialization only for prototyping, cross-boundary copying, or when the object graph is simple and performance is not a concern.

One often overlooked detail is that MemberwiseClone does not call any constructors. This means any initialization logic in constructors is skipped, which can leave the copy in an unexpected state if the class relies on constructor side effects. Manual copy methods and records avoid this problem because they go through normal construction paths.

Another consideration is inheritance. If a base class implements Clone() using MemberwiseClone, derived classes inherit that behavior, but the copy will be of the base type unless the derived class overrides it. This is a common source of bugs. A manual copy method that uses new and explicit property assignment is easier to override correctly.

For types that contain collections, a shallow copy copies the collection reference, not the elements. If you need a deep copy of a List<T> or Dictionary<K,V>, you must copy the collection and each element. This is where manual copying becomes verbose, and you may want to use a generic deep-copy helper based on reflection or expression trees. However, such helpers add complexity and can break with private fields or circular references.

Ultimately, the best strategy is the one that makes the copying behavior explicit and testable. Prefer a method named Copy or Clone that clearly documents whether the copy is shallow or deep. Avoid relying on serialization in hot paths, and always verify that nested objects are handled as expected when the type changes.

c# object copying: Practical Usage and Code Examples | RYUSLOG DEV