Introduction
When you work with DTOs or POCOs a lot, you probably asked yourself at least once in your programming life how you would create a copy of such an object. Over the years, .NET got new ways of doing this. I want to give you a glimpse of the options from old ones (like the copy constructor) to more recent ones (record types and special keywords). The list is not complete by any means. You can find more ways in this StackOverflow thread. Here are 7 ways to create copies of immutable objects in C#.
Copy constructor
A copy constructor receives an instance of an object of its kind as an argument and returns a new copy of that object. They are more common in the C and C++ world but can also be used with C#. The downside is that you have to manually assign the properties.
Here is an example:
using System;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy = new Person(original);
Console.WriteLine(original.ToString());
Console.WriteLine(copy.ToString());
}
}
public class Person
{
public string Name { get; }
public int Age { get; }
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
// copy constructor
public Person(Person other)
{
Name = other.Name;
Age = other.Age;
}
}
🔔 Hint
If you use a record
instead of a class
, original == copy
will be true
instead of false
for classes!
Copy method
A Copy()
method is a practical way to not only generate 1:1 copies but also copies that differ in a few properties. Every property is an argument of the Copy()
method and if it’s not overridden, then the original value is used. Great approach, but can require a lot of typing for classes with many properties!
using System;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy1 = original.Copy();
var copy2 = original.Copy(age: 22);
var copy3 = original.Copy(name: "Bob");
Console.WriteLine(original.ToString());
Console.WriteLine(copy1.ToString());
Console.WriteLine(copy2.ToString());
Console.WriteLine(copy3.ToString());
}
}
public class Person
{
public string Name { get; }
public int Age { get; }
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
public Person Copy(string? name = null, int? age = null)
{
return new Person(name ?? this.Name, age ?? this.Age);
}
}
🔔 Hint
Sometimes the method is called Copy()
, sometimes With()
. In Flutter, you usually have a copyWith()
method. But they all work the same way!
Factory method pattern
With a factory method, you can also create a copy of your object. Please be aware that although this works, the factory method is rather intended to be used with abstract class definitions.
Here is a simplified example of how to use the factory method:
using System;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy = Person.CreateCopy(original);
Console.WriteLine(original.ToString());
Console.WriteLine(copy.ToString());
}
}
public class Person
{
public string Name { get; }
public int Age { get; }
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
// static factory method
public static Person CreateCopy(Person other)
{
return new Person(other.Name, other.Age);
}
}
Extension method
If you cannot modify the immutable class but still want a handy way to create a copy, an extension method might be a good solution.
Here is how it works:
using System;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy = original.Copy(); // call the extension method
Console.WriteLine(original.ToString());
Console.WriteLine(copy.ToString());
}
}
public class Person
{
public string Name { get; }
public int Age { get; }
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
public static class PersonExtensions
{
// extension to create a copy
public static Person Copy(this Person person)
{
return new Person(person.Name, person.Age);
}
}
Records and with keyword
Since C# 9, you can use Record types and the with
keyword to copy objects. It’s a lean approach since new properties in the Record don’t require any other code changes.
Let’s have a look at how this works exactly:
using System;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy1 = original with { };
Console.WriteLine(original.ToString());
Console.WriteLine(copy1.ToString());
}
}
public record Person(string Name, int Age)
{
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
}
Reflection
There is also a way to create a copy with reflection in C#. It wouldn’t be my favorite way but you can write a generic method that creates a copy of any class and you don’t have to modify it when you add new classes or change existing ones. It’s a super flexible and powerful approach!
The code uses an extension method that uses reflection to create a copy of the Person
object:
using System;
using System.Reflection;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy1 = original.CreateCopy();
Console.WriteLine(original.ToString());
Console.WriteLine(copy1.ToString());
}
}
public class Person
{
public string Name { get; }
public int Age { get; }
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
public static class PersonExtensions
{
public static Person CreateCopy(this Person original)
{
Type type = typeof(Person);
ConstructorInfo constructor = type.GetConstructors()[0];
PropertyInfo[] properties = type.GetProperties();
object[] constructorArgs = new object[properties.Length];
for (int i = 0; i < properties.Length; i++)
{
constructorArgs[i] = properties[i].GetValue(original);
}
return (Person)constructor.Invoke(constructorArgs);
}
}
Serialization
Serialization is also a very popular way to create a copy of an object. You convert your object in a different notation and then do a reconversion. This is mostly done with JSON or XML but you could also use a symmetric encryption function. However, expensive mathematical computations make this approach much slower.
Here is an example of JSON serialization:
using System;
using System.Text.Json;
public class Program
{
static void Main(string[] args)
{
var original = new Person("Alice", 30);
var copy1 = JsonSerializer.Deserialize<Person>(JsonSerializer.Serialize(original));
Console.WriteLine(original.ToString());
Console.WriteLine(copy1.ToString());
}
}
public class Person
{
public string Name { get; }
public int Age { get; }
public override string ToString()
{
return $"Person {Name} is {Age} years old";
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
Conclusion
In this article I showed you 7 ways to create copies of immutable objects in C#. My preferred approach is to use a Record and the with
keyword. But as you have seen, there is not only one way to achieve your goal.