7 Ways To Create Copies Of Immutable Objects In C#

NET category image

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:

C#
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!

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Flutter โค๏ธ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!

Become A Testing Expert!

Become a proficient Flutter app tester with my detailed guide. This ebook covers everything from unit tests over widget tests up to dependency mocking.