The persistence layer converts the live component graph into plain DTOs and back. This page is the internals view — the DTO fields, how property values are routed, how connection identity survives a round-trip, and what happens when a payload references a type that is not loaded. For the task-oriented walkthrough see Serialization.

DTO Shape

TypeKey fieldsNotes
DiagramDtoSchemaVersion, ThemeName, Components, LinesCurrentSchemaVersion is 2 and is written on every save.
ComponentDtoType, X, Y, Width, Height, Name, PropertyBag, TypedPropertyBag, InPointIds, OutPointIdsType is the assembly-qualified name; coordinates are sanitized (NaN/Infinity become 0).
LineDtoStartPointId, EndPointId, StartComponentIndex, EndComponentIndex, arrow/label/routing/status fieldsGUID references for saved diagrams; component indices for templates that exist before GUIDs do.

Enums are persisted as integers and colors as RGBA uint values, keeping the JSON compact and round-trip safe.

Property Routing

ToDto() walks GetProperties(includeCommon: false, includeNodeProperties: true) and routes each value by convertibility:

ValueDestination
string, bool, enum, SKColor, any IConvertiblePropertyBag via Convert.ToString (readable JSON)
Lists, dictionaries, nested objectsTypedPropertyBag via JsonSerializer.SerializeToElement
NullSkipped

On load, both bags are merged and applied through SetPropperties(values, updateNodeProperties: true, applyToPublicSetters: true), so public property setters run exactly as if the values had been typed in the property grid.

Identity and Connections

Connection points carry stable GUIDs. On save, each component records its port ids in InPointIds/OutPointIds; on load those ids are written back onto the freshly created ports before lines are resolved. That is what keeps ERD row-level and field-level connections attached across sessions.

// Save
var dto = drawingManager.ToDto();               // ports keep their ids
var json = JsonSerializer.Serialize(dto);

// Load
var restored = JsonSerializer.Deserialize<DiagramDto>(json);
drawingManager.LoadFromDto(restored);           // ids re-applied, lines resolved

Templates skip GUIDs entirely and use StartComponentIndex/EndComponentIndex instead, because the components they connect are created in the same operation.

Load Algorithm

  1. Clear — existing components are removed.
  2. Theme — ThemeName is applied through ThemeManager.ApplyTheme before anything renders.
  3. Create — for each ComponentDto, Type.GetType(comp.Type) resolves the CLR type; unknown types are skipped (a null placeholder keeps the index alignment for templates).
  4. Position — X, Y, Width, Height and Name are assigned.
  5. Properties — the two bags are merged and applied through SetPropperties. This runs before port ids so that port-count properties (for example ERD row counts) take effect first.
  6. Port identity — persisted ids are written to the matching ConnectionPoint instances.
  7. Lines — each LineDto resolves its endpoints by GUID (or by component index for templates) and reconnects them.

Invariants

  • Loading never throws for bad data. Missing bags, unknown types and unresolvable ports are skipped so a partially damaged file still opens.
  • Save is total. Every registered component type is persisted through the same generic path; families do not implement their own serialization.
  • The property grid and the loader share one path. Both call SetPropperties, so a value that round-trips through a file behaves identically to one typed in the grid.
  • Class names are API. Renaming a component class breaks older files; keep names stable or provide a migration step before LoadFromDto.

The whole-surface sweeps in the test suite assert these invariants across all 293 component types: each renders, updates, survives a serialization round-trip and accepts its own property values.