ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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

Object.MemberwiseClone method

In this article we compared shallow and deep copy of objects in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.