C# shallow & deep copy
last modified October 18, 2026
In this article we compare shallow and deep copy of objects in C#.
Copying of data is an important task in programming. An object's fields may
hold values of value types, such as int or a struct, or references
to other objects. Copying an object can therefore be performed in two ways.
A shallow copy creates a new instance and copies all fields into it. Value type fields are copied bit by bit. For reference type fields, only the reference is copied, not the object it points to; the original and the copy share the referenced objects. Any change to a shared object is visible through both of them.
A deep copy also copies the referenced objects, recursively. The original and the copy share no mutable state; changing one of them does not affect the other.
Strings are reference types, but they are immutable. Sharing a string between two objects is harmless, because neither object can change it; assigning a new string to a field only changes that object's reference.
C# shallow copy example
The following program performs a shallow copy.
var col = new Color(23, 42, 223);
var obj1 = new Item(23, "small", col);
var obj2 = (Item) obj1.Clone();
obj2.Id += 1;
obj2.Size = "big";
obj2.Col.Red = 255;
Console.WriteLine(obj1);
Console.WriteLine(obj2);
class Color(int red, int green, int blue)
{
public int Red { get; set; } = red;
public int Green { get; set; } = green;
public int Blue { get; set; } = blue;
public override string ToString() => $"({Red}, {Green}, {Blue})";
}
class Item(int id, string size, Color col) : ICloneable
{
public int Id { get; set; } = id;
public string Size { get; set; } = size;
public Color Col { get; set; } = col;
public object Clone() => new Item(Id, Size, Col);
public override string ToString() => $"id: {Id}, size: {Size}, color: {Col}";
}
We define two classes: Item and Color. The
Item object has a reference to a Color object.
class Item(int id, string size, Color col) : ICloneable
The Item class implements the ICloneable interface.
public object Clone() => new Item(Id, Size, Col);
The ICloneable interface has a single Clone method.
Our implementation returns a new object with copied values. The
Col reference is copied as is.
var obj2 = (Item) obj1.Clone();
We create a shallow copy of the obj1 object. The
Clone method returns an object, so we cast it to
Item.
obj2.Id += 1; obj2.Size = "big"; obj2.Col.Red = 255;
We modify the copied object. We increment the id, change the size, and change the red part of the colour.
$ dotnet run id: 23, size: small, color: (255, 42, 223) id: 24, size: big, color: (255, 42, 223)
The ids and the sizes differ. But the red part of the colour is 255 in both
objects. Changing the fields of the copy (Id, Size)
did not affect the original object; changing the shared Color
object did. Both objects refer to the same Color object in memory.
The ICloneable interface does not say whether Clone
performs a shallow or a deep copy, and it returns object. For
these reasons, Microsoft recommends not implementing it in public APIs. In the
next examples, we use strongly typed copy methods instead.
C# MemberwiseClone
The protected Object.MemberwiseClone method creates a shallow copy
of the current object. It copies all fields, including private ones, without
calling a constructor.
var obj1 = new Item(23, "small", new Color(23, 42, 223));
var obj2 = obj1.ShallowCopy();
obj2.Id += 1;
obj2.Size = "big";
obj2.Col.Red = 255;
Console.WriteLine(obj1);
Console.WriteLine(obj2);
Console.WriteLine(ReferenceEquals(obj1.Col, obj2.Col));
class Color(int red, int green, int blue)
{
public int Red { get; set; } = red;
public int Green { get; set; } = green;
public int Blue { get; set; } = blue;
public override string ToString() => $"({Red}, {Green}, {Blue})";
}
class Item(int id, string size, Color col)
{
public int Id { get; set; } = id;
public string Size { get; set; } = size;
public Color Col { get; set; } = col;
public Item ShallowCopy() => (Item) MemberwiseClone();
public override string ToString() => $"id: {Id}, size: {Size}, color: {Col}";
}
Because MemberwiseClone is protected, we call it from a public
ShallowCopy method of the class.
Console.WriteLine(ReferenceEquals(obj1.Col, obj2.Col));
The ReferenceEquals method confirms that both objects refer to the
same Color instance.
$ dotnet run id: 23, size: small, color: (255, 42, 223) id: 24, size: big, color: (255, 42, 223) True
C# deep copy example
To change this behaviour, we do a deep copy next.
var obj1 = new Item(23, "small", new Color(23, 42, 223));
var obj2 = obj1.DeepCopy();
obj2.Id += 1;
obj2.Size = "big";
obj2.Col.Red = 255;
Console.WriteLine(obj1);
Console.WriteLine(obj2);
Console.WriteLine(ReferenceEquals(obj1.Col, obj2.Col));
class Color(int red, int green, int blue)
{
public int Red { get; set; } = red;
public int Green { get; set; } = green;
public int Blue { get; set; } = blue;
public Color DeepCopy() => new(Red, Green, Blue);
public override string ToString() => $"({Red}, {Green}, {Blue})";
}
class Item(int id, string size, Color col)
{
public int Id { get; set; } = id;
public string Size { get; set; } = size;
public Color Col { get; set; } = col;
public Item DeepCopy()
{
var copy = (Item) MemberwiseClone();
copy.Col = Col.DeepCopy();
return copy;
}
public override string ToString() => $"id: {Id}, size: {Size}, color: {Col}";
}
In this program, we perform a deep copy of an object.
public Color DeepCopy() => new(Red, Green, Blue);
The Color class gets its own copy method. It contains only value
type fields, so creating a new instance with the same values is enough.
public Item DeepCopy()
{
var copy = (Item) MemberwiseClone();
copy.Col = Col.DeepCopy();
return copy;
}
We first create a shallow copy with MemberwiseClone and then
replace the reference to the Color object with a copy of it. Each
class is responsible for copying its own reference type fields.
$ dotnet run id: 23, size: small, color: (23, 42, 223) id: 24, size: big, color: (255, 42, 223) False
Now the red part of the colours differs. The original object has retained its previous value (23).
C# copy constructor
A copy constructor is a constructor that takes an instance of the same class and initializes the new object from it. It is a common alternative to copy methods.
var p1 = new Person("John Doe", ["chess", "hiking"]);
var p2 = new Person(p1);
p2.Name = "Roger Roe";
p2.Hobbies.Add("cooking");
Console.WriteLine(p1);
Console.WriteLine(p2);
class Person
{
public string Name { get; set; }
public List<string> Hobbies { get; }
public Person(string name, List<string> hobbies)
{
Name = name;
Hobbies = hobbies;
}
public Person(Person other)
{
Name = other.Name;
Hobbies = new List<string>(other.Hobbies);
}
public override string ToString() =>
$"{Name}: {string.Join(", ", Hobbies)}";
}
The copy constructor creates a new list of hobbies. The strings in the list are shared, but because they are immutable, the copy is effectively deep.
$ dotnet run John Doe: chess, hiking Roger Roe: chess, hiking, cooking
C# records and the with expression
The with expression creates a copy of a record with some
properties changed. The copy is shallow.
var u1 = new User("John Doe", ["admin", "editor"]);
var u2 = u1 with { Name = "Roger Roe" };
u2.Roles.Add("viewer");
Console.WriteLine($"{u1.Name}: {string.Join(", ", u1.Roles)}");
Console.WriteLine($"{u2.Name}: {string.Join(", ", u2.Roles)}");
var u3 = u1 with { Roles = [.. u1.Roles] };
u3.Roles.Remove("admin");
Console.WriteLine($"{u1.Name}: {string.Join(", ", u1.Roles)}");
Console.WriteLine($"{u3.Name}: {string.Join(", ", u3.Roles)}");
record User(string Name, List<string> Roles);
Records are often thought of as immutable, but a record with a mutable property, such as a list, is only as immutable as that property.
var u2 = u1 with { Name = "Roger Roe" };
u2.Roles.Add("viewer");
The u2 record shares the list of roles with u1.
Adding a role to u2 also adds it to u1.
var u3 = u1 with { Roles = [.. u1.Roles] };
To avoid sharing, we pass a new list to the with expression. The
spread element .. copies the elements into a new collection.
$ dotnet run John Doe: admin, editor, viewer Roger Roe: admin, editor, viewer John Doe: admin, editor, viewer John Doe: editor, viewer
The best way to avoid such surprises is to use immutable types in records, for
instance ImmutableList<T> or
IReadOnlyList<T>.
C# copying lists
Copying a collection with new List<T>(list),
ToList, or a collection expression creates a new list, but the
elements are shared. It is a shallow copy.
List<Point> points = [new(1, 2), new(3, 4)];
List<Point> shallow = [.. points];
List<Point> deep = points.Select(p => new Point(p.X, p.Y)).ToList();
shallow.Add(new Point(5, 6));
points[0].X = 100;
Console.WriteLine($"original: {string.Join(" ", points)}");
Console.WriteLine($"shallow: {string.Join(" ", shallow)}");
Console.WriteLine($"deep: {string.Join(" ", deep)}");
class Point(int x, int y)
{
public int X { get; set; } = x;
public int Y { get; set; } = y;
public override string ToString() => $"({X}, {Y})";
}
We create a shallow and a deep copy of a list of points.
shallow.Add(new Point(5, 6)); points[0].X = 100;
The lists are independent; adding a point to the shallow copy does not change
the original list. But the first point is shared, so changing it is visible in
the shallow copy. The deep copy contains new Point objects created
with Select.
$ dotnet run original: (100, 2) (3, 4) shallow: (100, 2) (3, 4) (5, 6) deep: (1, 2) (3, 4)
C# deep copy with JSON serialization
For complex object graphs, writing copy methods for every class is tedious. A simple way to create a deep copy is to serialize the object and deserialize it back.
using System.Text.Json;
var order1 = new Order
{
Id = 1,
Customer = new Customer { Name = "John Doe", City = "Bratislava" },
Lines = [new Line { Product = "pen", Quantity = 3 }, new Line { Product = "book", Quantity = 1 }]
};
var order2 = DeepCopy(order1);
order2.Customer.City = "Košice";
order2.Lines[0].Quantity = 10;
Console.WriteLine($"{order1.Customer.City}, {order1.Lines[0].Quantity}");
Console.WriteLine($"{order2.Customer.City}, {order2.Lines[0].Quantity}");
static T DeepCopy<T>(T obj)
{
string json = JsonSerializer.Serialize(obj);
return JsonSerializer.Deserialize<T>(json)!;
}
class Order
{
public int Id { get; set; }
public Customer Customer { get; set; } = new();
public List<Line> Lines { get; set; } = [];
}
class Customer
{
public string Name { get; set; } = "";
public string City { get; set; } = "";
}
class Line
{
public string Product { get; set; } = "";
public int Quantity { get; set; }
}
The generic DeepCopy method serializes an object to JSON and
creates a new object graph from the JSON string.
$ dotnet run Bratislava, 3 Košice, 10
This approach has limitations. It is much slower than hand-written copy
methods. Only public properties that the serializer can read and write are
copied; private fields are lost. Cyclic references cause an exception unless we
set ReferenceHandler.Preserve in the serializer options. The old
BinaryFormatter, which was formerly used for this purpose, is no
longer an option: because of security problems, its implementation was removed
in .NET 9 and it always throws an exception.
Source
In this article we compared shallow and deep copy of objects in C#.
Author
List all C# tutorials.