ECErsel ÇAKMAK
Back to articles
Editor ArchitectureAug 25, 2026

Copy/Paste Is a Graph Problem

In a polymorphic editor, paste is not just deserialization. New identities must be allocated, internal references remapped, external references validated, and related geometry moved together.

  • .NET
  • Clipboard
  • Object Graphs
  • Serialization
  • Testing

The bug hidden behind a normal paste command

Copy and paste looks like a list operation: serialize the selected items, deserialize them, move them slightly, and add them to the target document. That model works only while every item is independent.

Editors rarely stay that simple. A label may point to a shape, a preview window may depend on a source item, and an annotation may own a separate leader point. Once items can refer to one another, the copied selection is no longer a list. It is a small object graph.

Treating it as a graph changes the central question from “How do I duplicate these objects?” to “Which nodes and edges should exist after the duplicate receives new identities?”

A selected source and dependent node becoming a new pasted subgraph with fresh identities and a remapped edge
The selected nodes receive new identities together; only then can the edge between them be rewritten correctly.

Why preserving identifiers is dangerous

Persisted editor objects commonly use stable identifiers for selection, references, history, and lookup. Reusing those identifiers during paste creates collisions: two different objects appear to be the same node to dictionaries, inspectors, undo history, or later serialization.

Every pasted object therefore needs a fresh identity. But assigning new identifiers one object at a time is not enough. A dependent item may be processed before its source, or it may still point to the source identifier from the clipboard payload.

The safer approach is a two-pass operation. First allocate every new identifier and build a complete old-to-new map. Only then mutate pasted objects and rewrite their references.

csharp

GraphPasteService.cs

Public-safe example: allocate every identity before changing any node or edge.

Dictionary<Guid, Guid> idMap = copiedItems
    .ToDictionary(item => item.Id, _ => Guid.NewGuid());

foreach (CanvasItem item in copiedItems)
{
    Guid oldId = item.Id;
    item.Id = idMap[oldId];
}

Classify every reference before rewriting it

A reference found in the clipboard payload can describe three different relationships. Each needs an explicit policy rather than one generic replacement rule.

  • Internal reference: the target is also being pasted, so the edge must point to the target's new identifier.
  • Valid external reference: the target was not copied but still exists in the destination, so preserving the edge may be correct.
  • Missing external reference: the target exists in neither set, so the edge must be cleared or rejected instead of becoming a silent dangling reference.

csharp

ReferencePolicy.cs

The remapping function makes all three edge policies visible in one place.

static Guid RemapTarget(
    Guid oldTargetId,
    IReadOnlyDictionary<Guid, Guid> pastedIds,
    IReadOnlySet<Guid> destinationIds)
{
    if (oldTargetId == Guid.Empty)
        return Guid.Empty;

    if (pastedIds.TryGetValue(oldTargetId, out Guid pastedTargetId))
        return pastedTargetId;          // internal edge

    if (destinationIds.Contains(oldTargetId))
        return oldTargetId;             // valid external edge

    return Guid.Empty;                  // missing external edge
}

Keep the paste pipeline staged

A staged pipeline makes the operation easier to reason about and test. Payload concerns are handled before document mutation, identity concerns before relationship concerns, and graph repair before placement.

Seven-stage graph-aware paste pipeline from payload validation to adding the repaired graph as one operation
Staging keeps invalid data and incomplete graphs outside the live document boundary.
  • Validate the clipboard format and payload version.
  • Reject duplicate source identifiers before building a map.
  • Deserialize into new object instances so the source selection cannot be mutated accidentally.
  • Allocate a fresh identifier for every pasted node.
  • Rewrite references using internal, external, and missing-target policies.
  • Apply the paste offset to both primary positions and dependent geometry.
  • Add the completed graph to the document as one user operation.

Placement is more than changing X and Y

Many editors offset pasted items so the duplicate is visible beside the original. Updating only the item's main position can quietly deform compound objects.

A callout may have a separate leader endpoint. A preview item may store its source point independently from its frame. A dimension may own two measurement points instead of one position. The placement transform must move every coordinate that belongs to the pasted object while leaving coordinates owned by external targets unchanged.

This is another reason to make translation a domain behavior instead of scattering coordinate changes through the paste command.

csharp

CanvasItemTranslation.cs

Compound items own their translation rules, including independent control points.

public override void Translate(Vector2 offset)
{
    Position += offset;
    LeaderEnd += offset;
}

// A simple item can keep the base implementation:
public virtual void Translate(Vector2 offset)
{
    Position += offset;
}

Polymorphism belongs at the serialization boundary

A real editor may copy text, images, dimensions, connectors, tables, and view items in one selection. The clipboard contract needs a type discriminator and a version so the correct concrete shapes can be reconstructed safely.

The version is not decorative metadata. Clipboard payloads can outlive one process through clipboard history, and older application instances may receive newer payloads. Rejecting unsupported versions produces a controlled failure instead of a partially valid graph.

The deserializer should reconstruct data, not decide document policy. Identity allocation, reference repair, target validation, and placement remain explicit steps after deserialization.

csharp

ClipboardContract.cs

The discriminator reconstructs concrete data shapes; the envelope version protects compatibility.

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(CardItem), "card")]
[JsonDerivedType(typeof(LinkItem), "link")]
public abstract class CanvasItem
{
    public Guid Id { get; set; }
}

public sealed record ClipboardEnvelope(
    int Version,
    Guid SourceDocumentId,
    List<CanvasItem> Items);

Small tests reveal the important failures

The most useful tests do not need a complete UI or a large saved project. Two or three nodes are enough to expose most graph-copying bugs.

  • Copy a source and its dependent item; verify both identities change and the dependent points to the pasted source.
  • Copy only the dependent item; preserve its reference when the original source exists in the destination.
  • Paste into a document without the external source; verify the dangling edge is cleared or reported.
  • Paste a compound item with an offset; verify its frame and independent control points move together.
  • Feed a duplicate identifier or unsupported payload version; verify the document remains unchanged.
  • Verify the original objects are unchanged after the complete copy/paste round trip.

csharp

GraphPasteTests.cs

One focused test proves both identity isolation and internal-edge repair.

[TestMethod]
public void Paste_RemapsReferenceWhenBothNodesWereCopied()
{
    ClipboardEnvelope payload = Copy(source, dependent);

    IReadOnlyList<CanvasItem> pasted = Paste(payload, destinationIds: []);

    CardItem pastedSource = pasted.OfType<CardItem>().Single();
    LinkItem pastedLink = pasted.OfType<LinkItem>().Single();

    Assert.AreNotEqual(source.Id, pastedSource.Id);
    Assert.AreNotEqual(dependent.Id, pastedLink.Id);
    Assert.AreEqual(pastedSource.Id, pastedLink.TargetItemId);
}

The reusable architecture lesson

Graph-aware paste is a compact example of a broader rule: identity and relationships are separate dimensions of cloning. Creating new objects solves identity isolation, but only an explicit edge-rewrite policy preserves meaning.

This model applies beyond visual editors. Workflow designers, node graphs, form builders, diagram tools, and scene editors all face the same boundary when a selected subgraph is moved into a new context.

The implementation can stay small when its invariants are clear: no identifier collisions, no accidental source mutation, no unexplained dangling references, and no partial geometry transforms.

Share