Cloning Objects Efficiently with the Prototype Pattern in C#

Creating Copies Without Reconstruction

If you've ever needed to create many objects with similar configurations, you know how tedious it is to call constructors repeatedly with the same parameters or rebuild complex object graphs from scratch. Prototypes let you clone existing objects instead of reconstructing them.

The Prototype pattern creates new instances by copying existing ones. You start with a prototype object that's already configured, then clone it when you need duplicates. This is faster than instantiating and initializing from scratch, especially for objects with expensive setup.

You'll build a document template system that clones pre-configured documents. By the end, you'll understand shallow versus deep copying and when each makes sense.

Shallow Copying with MemberwiseClone

C# provides MemberwiseClone for shallow copying. It duplicates value type fields and copies references for reference types. The clone shares references to the same nested objects as the original.

ShallowCopy.cs
namespace PrototypeDemo;

public class Address
{
    public string Street { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
}

public class Person
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
    public Address Address { get; set; } = new();

    public Person ShallowCopy()
    {
        return (Person)MemberwiseClone();
    }
}

// Usage
var original = new Person
{
    Name = "Alice",
    Age = 30,
    Address = new Address { Street = "123 Main", City = "NYC" }
};

var clone = original.ShallowCopy();
clone.Name = "Bob";
clone.Address.City = "LA";

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

The clone's Name change doesn't affect the original because string is immutable and gets a new reference. But modifying the Address affects both because they share the same Address instance. This is shallow copying's key characteristic.

Deep Copying for Independent Objects

Deep copy recursively clones all nested objects. The clone becomes fully independent with no shared references. Changes to nested objects in the clone don't affect the original.

DeepCopy.cs
namespace PrototypeDemo;

public class AddressDeep
{
    public string Street { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;

    public AddressDeep Clone() =>
        new() { Street = Street, City = City };
}

public class PersonDeep
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
    public AddressDeep Address { get; set; } = new();

    public PersonDeep DeepClone()
    {
        return new PersonDeep
        {
            Name = Name,
            Age = Age,
            Address = Address.Clone()
        };
    }
}

// Usage
var original = new PersonDeep
{
    Name = "Alice",
    Age = 30,
    Address = new AddressDeep { Street = "123 Main", City = "NYC" }
};

var clone = original.DeepClone();
clone.Address.City = "LA";

Console.WriteLine($"Original city: {original.Address.City}");
Console.WriteLine($"Clone city: {clone.Address.City}");

Now modifying the clone's address doesn't touch the original because Address.Clone creates a new instance. Each person has a completely independent address object.

Prototype Registry for Template Management

A registry stores prototype objects that clients can clone. Instead of knowing how to construct objects, clients ask the registry for a prototype by name and clone it.

DocumentRegistry.cs
namespace PrototypeDemo;

public interface IPrototype<T>
{
    T Clone();
}

public class Document : IPrototype<Document>
{
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public List<string> Tags { get; set; } = new();

    public Document Clone()
    {
        return new Document
        {
            Title = Title,
            Content = Content,
            Tags = new List<string>(Tags)
        };
    }
}

public class DocumentRegistry
{
    private readonly Dictionary<string, Document> _prototypes = new();

    public void RegisterPrototype(string key, Document prototype)
    {
        _prototypes[key] = prototype;
    }

    public Document CreateDocument(string key)
    {
        if (!_prototypes.ContainsKey(key))
            throw new ArgumentException($"Prototype {key} not found");

        return _prototypes[key].Clone();
    }
}

Clients register prototypes once, then create instances by cloning. This centralizes template management and hides construction complexity behind simple clone calls.

Using the Prototype Registry

Set up prototypes during initialization, then clone them throughout your application. Each clone starts with the same baseline configuration but can be modified independently.

Program.cs
using PrototypeDemo;

var registry = new DocumentRegistry();

registry.RegisterPrototype("report", new Document
{
    Title = "Monthly Report",
    Content = "Report template",
    Tags = new List<string> { "business", "monthly" }
});

registry.RegisterPrototype("letter", new Document
{
    Title = "Business Letter",
    Content = "Dear Sir/Madam,",
    Tags = new List<string> { "correspondence" }
});

var report1 = registry.CreateDocument("report");
report1.Content = "January sales data";

var report2 = registry.CreateDocument("report");
report2.Content = "February sales data";

Console.WriteLine($"Report 1: {report1.Content}");
Console.WriteLine($"Report 2: {report2.Content}");

Both reports start from the same prototype but have different content. Cloning saves you from repeating the title and tags setup for every report.

Try It Yourself

Build a simple prototype system that clones game characters with default equipment. This shows how prototypes speed up creation of preconfigured objects.

Steps

  1. Scaffold: dotnet new console -n PrototypeDemo
  2. Enter: cd PrototypeDemo
  3. Modify Program.cs
  4. Update .csproj
  5. Execute: dotnet run
Program.cs
var warrior = new Character("Warrior", 100, new List<string> { "Sword", "Shield" });
var mage = new Character("Mage", 60, new List<string> { "Staff", "Spellbook" });

var player1 = warrior.Clone();
player1.Name = "Conan";

var player2 = mage.Clone();
player2.Name = "Gandalf";

Console.WriteLine($"{player1.Name}: HP={player1.Health}, Items={string.Join(",", player1.Items)}");
Console.WriteLine($"{player2.Name}: HP={player2.Health}, Items={string.Join(",", player2.Items)}");

class Character
{
    public string Name { get; set; }
    public int Health { get; set; }
    public List<string> Items { get; set; }

    public Character(string name, int health, List<string> items)
    {
        Name = name;
        Health = health;
        Items = items;
    }

    public Character Clone() =>
        new(Name, Health, new List<string>(Items));
}
PrototypeDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Run result

Conan: HP=100, Items=Sword,Shield
Gandalf: HP=60, Items=Staff,Spellbook

Knowing the Limits

Skip prototypes when object creation is cheap and straightforward. If a constructor call with a few parameters is fast, cloning adds unnecessary complexity. Use prototypes when initialization is expensive or involves complex configuration that you want to reuse.

Avoid prototypes for objects with circular references or deep nesting that's hard to clone correctly. Deep copying complex graphs can be error-prone and slow. If serialization is your only option for deep cloning, the pattern might not be worth the overhead.

Watch for shared mutable state. If you forget to deep copy a nested collection or object, clones share references and modifications propagate unexpectedly. Always decide upfront which fields need deep copying and which can be shared. Document your decisions clearly.

Common Questions

When should I use prototypes vs constructors?

Use prototypes when object creation is expensive or requires complex setup that you want to reuse. If you have a pre-configured object and need many similar copies, cloning beats reconstruction. Stick with constructors for simple objects or when each instance needs unique initialization.

What's the difference between shallow and deep copy?

Shallow copy duplicates value types but shares references to reference types. Deep copy recursively clones all nested objects. Use MemberwiseClone for shallow or implement custom logic for deep. Shared references mean changes in the clone affect the original unless you deep copy.

Is ICloneable still recommended?

No, ICloneable is ambiguous because it doesn't specify shallow vs deep copy. Use custom Clone methods with clear names like ShallowCopy or DeepClone. Return concrete types instead of object to avoid casts. This makes intent explicit and prevents confusion.

How do I handle cloning with events or delegates?

Decide if subscribers should transfer to the clone. Usually you want fresh event lists, so don't copy them. Initialize new event instances in your Clone method or leave them null. Copying event handlers means clones trigger the same listeners as originals, which is rarely desired.

Back to Articles