How to Implement Object Cloning in .NET

Understanding Object Cloning in .NET

If you've ever modified a copied object only to watch the original change too, you've experienced the challenge of object references in .NET. When you assign one object to another variable, you're just copying the reference, not the actual object. Both variables point to the same data in memory.

Object cloning solves this by creating independent copies of your objects. You need cloning when working with undo/redo functionality, creating object snapshots for comparison, or passing objects to methods that might modify them. The technique you choose depends on whether you need a shallow copy that shares nested references or a deep copy that duplicates everything.

You'll learn five practical approaches to cloning in .NET. We'll start with simple shallow copies using MemberwiseClone, then move to deep copying with copy constructors, serialization techniques, and modern record types. By the end, you'll know exactly which approach fits your situation.

Shallow Copy vs Deep Copy

Before implementing cloning, you need to understand the fundamental difference between shallow and deep copies. A shallow copy duplicates the object itself but keeps the same references to any nested objects. If your object contains a list or another custom type, both the original and the copy will point to that same nested object.

A deep copy, on the other hand, recursively clones all nested objects. Every level of your object graph gets duplicated, creating completely independent copies. This means changes to nested objects in the clone won't affect the original.

Person.cs - Demonstrating the difference
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }

    // Shallow copy using MemberwiseClone
    public Person ShallowCopy()
    {
        return (Person)this.MemberwiseClone();
    }
}

// Usage example
var original = new Person
{
    Name = "John",
    Age = 30,
    Address = new Address { Street = "123 Main St", City = "Boston" }
};

var shallowCopy = original.ShallowCopy();
shallowCopy.Name = "Jane";
shallowCopy.Address.City = "New York";

Console.WriteLine($"Original: {original.Name}, {original.Address.City}");
Console.WriteLine($"Copy: {shallowCopy.Name}, {shallowCopy.Address.City}");

// Output:
// Original: John, New York
// Copy: Jane, New York

Notice how changing the Name property only affects the copy, but modifying the nested Address object changes both. The MemberwiseClone method creates a shallow copy, so both objects share the same Address reference. This behavior catches many developers off guard when they expect complete independence.

Implementing Deep Copy with Copy Constructors

A copy constructor provides explicit control over how your objects get cloned. You define a constructor that takes an instance of the same type and manually copies each property. For nested objects, you call their copy constructors recursively.

This approach gives you complete transparency. Anyone reading your code can see exactly what gets copied and how. It's more verbose than other methods, but the explicitness prevents surprises.

Person.cs - Copy constructor approach
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }

    // Copy constructor for Address
    public Address(Address other)
    {
        Street = other.Street;
        City = other.City;
        ZipCode = other.ZipCode;
    }

    public Address() { }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }
    public List PhoneNumbers { get; set; }

    // Copy constructor for Person
    public Person(Person other)
    {
        Name = other.Name;
        Age = other.Age;

        // Deep copy the Address
        Address = other.Address != null
            ? new Address(other.Address)
            : null;

        // Deep copy the list
        PhoneNumbers = other.PhoneNumbers != null
            ? new List(other.PhoneNumbers)
            : null;
    }

    public Person() { }
}

// Usage
var original = new Person
{
    Name = "Alice",
    Age = 28,
    Address = new Address { Street = "456 Oak Ave", City = "Seattle" },
    PhoneNumbers = new List { "555-0100", "555-0101" }
};

var deepCopy = new Person(original);
deepCopy.Name = "Bob";
deepCopy.Address.City = "Portland";
deepCopy.PhoneNumbers.Add("555-0102");

Console.WriteLine($"Original: {original.Name}, {original.Address.City}");
Console.WriteLine($"Original phones: {original.PhoneNumbers.Count}");
Console.WriteLine($"Copy: {deepCopy.Name}, {deepCopy.Address.City}");
Console.WriteLine($"Copy phones: {deepCopy.PhoneNumbers.Count}");

// Output:
// Original: Alice, Seattle
// Original phones: 2
// Copy: Bob, Portland
// Copy phones: 3

The copy constructor creates truly independent objects. Changes to the nested Address or the PhoneNumbers list in the copy don't affect the original. This pattern works well for domain models where you want explicit control and the object graph isn't too complex.

Cloning Through Serialization

Serialization offers an automated way to deep clone objects without writing manual copying code for every property. You serialize the object to a format like JSON, then deserialize it back to create a new instance. This works for complex object graphs where implementing copy constructors would be tedious.

The downside is performance. Serialization and deserialization add overhead compared to direct property copying. You also need to ensure all types in your object graph are serializable. For occasional cloning operations where convenience matters more than speed, this approach works well.

Program.cs - JSON serialization cloning
using System.Text.Json;

public static class ObjectExtensions
{
    public static T DeepCopy(this T source) where T : class
    {
        if (source == null)
            return null;

        var json = JsonSerializer.Serialize(source);
        return JsonSerializer.Deserialize(json);
    }
}

public class Department
{
    public string Name { get; set; }
    public string Code { get; set; }
}

public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
    public Department Department { get; set; }
    public List Skills { get; set; }
}

// Usage
var original = new Employee
{
    Name = "Sarah",
    Salary = 75000,
    Department = new Department { Name = "Engineering", Code = "ENG" },
    Skills = new List { "C#", "SQL", "Azure" }
};

var clone = original.DeepCopy();
clone.Name = "Mike";
clone.Department.Name = "Sales";
clone.Skills.Add("Python");

Console.WriteLine($"Original: {original.Name}, {original.Department.Name}");
Console.WriteLine($"Original skills: {string.Join(", ", original.Skills)}");
Console.WriteLine($"Clone: {clone.Name}, {clone.Department.Name}");
Console.WriteLine($"Clone skills: {string.Join(", ", clone.Skills)}");

// Output:
// Original: Sarah, Engineering
// Original skills: C#, SQL, Azure
// Clone: Mike, Sales
// Clone skills: C#, SQL, Azure, Python

The serialization approach provides true deep copying with minimal code. The extension method works for any type without needing custom logic for each class. Just be aware that non-serializable properties like delegates or unmanaged resources won't be copied.

Modern Cloning with C# Records

C# records introduced a with expression that creates copies with specific properties modified. The compiler generates a copy constructor automatically for record types. This gives you concise syntax for creating modified copies, which is perfect for immutable data patterns.

Records perform shallow copying by default. If your record contains reference types, you'll need custom logic for deep cloning. However, for simple data transfer objects or value-like types, records provide the cleanest cloning syntax available.

Program.cs - Using records with expressions
public record Address(string Street, string City, string ZipCode);

public record Person(string Name, int Age, Address Address)
{
    // Custom deep copy method for records
    public Person DeepCopy()
    {
        return this with
        {
            Address = this.Address with { }
        };
    }
}

// Usage with built-in shallow copy
var original = new Person(
    "Emma",
    32,
    new Address("789 Pine St", "Denver", "80201")
);

// Shallow copy using with expression
var shallowCopy = original with { Name = "Olivia" };

Console.WriteLine($"Original: {original.Name}, {original.Address.City}");
Console.WriteLine($"Shallow copy: {shallowCopy.Name}");
Console.WriteLine($"Same address reference: {ReferenceEquals(original.Address, shallowCopy.Address)}");

// Deep copy using custom method
var deepCopy = original.DeepCopy() with { Name = "Sophia" };
deepCopy = deepCopy with
{
    Address = deepCopy.Address with { City = "Austin" }
};

Console.WriteLine($"Original city: {original.Address.City}");
Console.WriteLine($"Deep copy city: {deepCopy.Address.City}");

// Output:
// Original: Emma, Denver
// Shallow copy: Olivia
// Same address reference: True
// Original city: Denver
// Deep copy city: Austin

The with expression provides elegant syntax for creating modified copies. For shallow copies, it's perfect as-is. For deep cloning nested records, you need to apply with expressions recursively or implement a custom DeepCopy method that handles each level.

The ICloneable Interface

The ICloneable interface has been part of .NET since the beginning, but it's fallen out of favor in modern C# development. The interface requires implementing a Clone method that returns an object type, forcing callers to cast the result. More importantly, the interface doesn't specify whether Clone performs a shallow or deep copy.

Despite these limitations, you'll still encounter ICloneable in older codebases. Understanding its implementation helps when maintaining legacy code or working with libraries that expect it.

Product.cs - ICloneable implementation
public class Product : ICloneable
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public List Tags { get; set; }

    public object Clone()
    {
        // Shallow clone first
        var clone = (Product)this.MemberwiseClone();

        // Deep clone collections
        clone.Tags = this.Tags != null
            ? new List(this.Tags)
            : null;

        return clone;
    }

    // Better alternative: strongly-typed method
    public Product DeepCopy()
    {
        return new Product
        {
            Name = this.Name,
            Price = this.Price,
            Tags = this.Tags != null ? new List(this.Tags) : null
        };
    }
}

// Usage
var original = new Product
{
    Name = "Laptop",
    Price = 999.99m,
    Tags = new List { "electronics", "computers" }
};

// Using ICloneable (requires cast)
var clone1 = (Product)original.Clone();

// Using strongly-typed method (no cast needed)
var clone2 = original.DeepCopy();

clone1.Tags.Add("sale");
clone2.Tags.Add("featured");

Console.WriteLine($"Original tags: {string.Join(", ", original.Tags)}");
Console.WriteLine($"Clone1 tags: {string.Join(", ", clone1.Tags)}");
Console.WriteLine($"Clone2 tags: {string.Join(", ", clone2.Tags)}");

// Output:
// Original tags: electronics, computers
// Clone1 tags: electronics, computers, sale
// Clone2 tags: electronics, computers, featured

The strongly-typed DeepCopy method provides better usability than ICloneable. It returns the correct type without casting and clearly communicates its intent. If you're writing new code, prefer explicit cloning methods over implementing ICloneable.

Try It Yourself

Here's a complete example combining the techniques we've covered. This project file includes all necessary dependencies for the cloning approaches.

CloningDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Program.cs - Complete demo
using System.Text.Json;

// Extension method for serialization-based cloning
public static class CloneExtensions
{
    public static T DeepClone(this T source) where T : class
    {
        if (source == null) return null;
        var json = JsonSerializer.Serialize(source);
        return JsonSerializer.Deserialize(json)!;
    }
}

public class ContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }

    public ContactInfo() { }

    public ContactInfo(ContactInfo other)
    {
        Email = other.Email;
        Phone = other.Phone;
    }
}

public record PersonRecord(
    string Name,
    int Age,
    ContactInfo Contact);

var person = new PersonRecord(
    "David",
    35,
    new ContactInfo { Email = "david@example.com", Phone = "555-1234" }
);

// Method 1: Shallow copy with records
var shallowCopy = person with { Name = "Daniel" };

// Method 2: Deep copy via serialization
var deepCopy = person.DeepClone();
deepCopy = deepCopy with { Name = "Dylan" };

Console.WriteLine("Cloning demonstration:");
Console.WriteLine($"Original: {person.Name}, {person.Contact.Email}");
Console.WriteLine($"Shallow: {shallowCopy.Name}, {shallowCopy.Contact.Email}");
Console.WriteLine($"Deep: {deepCopy.Name}, {deepCopy.Contact.Email}");

// Output:
// Cloning demonstration:
// Original: David, david@example.com
// Shallow: Daniel, david@example.com
// Deep: Dylan, david@example.com

Run this code with dotnet run to see different cloning approaches in action. Experiment by modifying nested properties in each copy and observing which techniques maintain independence between the original and cloned objects.

Choosing the Right Cloning Strategy

Your choice of cloning approach depends on several factors. Consider the complexity of your object graph, performance requirements, and how often you'll need to clone objects. Each technique has trade-offs that make it suitable for different scenarios.

Use MemberwiseClone when you need shallow copies and understand the implications of shared references. It's the fastest option but only appropriate when you don't have nested reference types or when sharing those references is acceptable. Value types within the object get copied properly, but reference types remain shared.

Choose copy constructors when you want explicit control and your object graph is relatively simple. This approach provides the best performance for deep copying while making your intent crystal clear. The downside is maintenance overhead. Every time you add a property, you must update the copy constructor.

Opt for serialization-based cloning when dealing with complex object graphs or when you rarely need to clone objects. The JSON approach works for most scenarios and requires minimal code. However, avoid it in performance-critical paths or when cloning happens frequently. The serialization overhead adds up quickly.

Leverage records with the with expression when working with immutable data structures or value-like objects. Records shine in functional programming patterns where you create modified copies frequently. For shallow copying of simple types, records provide unbeatable syntax. For deep copying, combine the with expression with serialization or implement custom cloning logic.

Avoid ICloneable in new code unless you're implementing an interface required by a library or framework. The lack of type safety and unclear semantics make it a poor choice for modern development. Instead, create strongly-typed Clone or Copy methods that clearly communicate their behavior.

Frequently Asked Questions (FAQ)

What's the difference between shallow copy and deep copy?

A shallow copy duplicates the top-level object but shares references to nested objects. When you modify a nested object in the copy, the original sees those changes. A deep copy recursively clones all nested objects, creating completely independent copies with no shared references.

Should I use ICloneable in modern C# code?

ICloneable is generally discouraged in modern C# because its Clone method returns object type requiring casts, and it doesn't specify whether it performs shallow or deep copy. Copy constructors or factory methods provide clearer semantics and better type safety.

How do C# records handle cloning?

Records provide a built-in with expression that creates shallow copies with specific properties modified. The compiler generates a copy constructor automatically. For deep cloning of records with nested objects, you still need to implement custom logic.

When should I use serialization for cloning?

Serialization-based cloning works well for complex object graphs where manually implementing deep copy would be tedious. However, it's slower than manual copying and requires all types to be serializable. Use it when convenience outweighs performance concerns.

Back to Articles