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.
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.
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.
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.
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.
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.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
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.