From 8bbe56f555e5cef828599d407c6557a7bd46ff8d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 01:24:35 +0200 Subject: [PATCH] Contracts: the id walk follows declared types; keyed structs, every map interface and AllowPrivate named Co-Authored-By: Claude Sonnet 5.5 --- .../Contracts/cultcache-persistence-format.md | 13 ++++++++++--- .../Contracts/cultcache-store-composition.md | 5 +++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md index f1335e8f..2c83b102 100644 --- a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md +++ b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md @@ -408,8 +408,9 @@ fifth slot: ## Element Ids -Every object that sits in a list, array or dictionary value of a registered document -type, union subtypes included, carries an id: one string member with an integer `[Key]` +Every object that sits in a list, array or dictionary value (`IDictionary<,>` or +`IReadOnlyDictionary<,>`) of a registered document type, or of a keyed struct a document +holds, union subtypes included, carries an id: one string member with an integer `[Key]` and `[CultElementId]`. The id identifies the element inside its list, so an override can name it (a path step is `[slot, elementId]`) and a base edit that reorders or inserts elements retargets nothing. @@ -432,7 +433,13 @@ elements retargets nothing. store; they carry ids and do not decide when a document carries them. - An object list's elements are written by their `[Key]` layout, because the id is a keyed member of the element. A list whose element type is not keyed - (`[MessagePackObject]` or `[Union]`) is a value list and is replaced whole. For an + (`[MessagePackObject]` or `[Union]`) is a value list and is replaced whole; so is a list + of keyed structs, since a struct is a value and never an element, but an object list + inside a keyed struct is walked like any other. The walk follows declared member types: + a member declared `object` is untyped on disk, and a subclass's extra members under a + non-union base-typed member are not written, so neither is walked. A non-public `[Key]` + member on the way to an object list is refused at registration, because MessagePack + writes it only under `AllowPrivate` and the element-id walk reads public members. For an object list, C# refuses at registration a concrete element type that is not keyed and `[MessagePackFormatter]` on any type, collection type or member on the way to the list or on the id member, and refuses at the first encode or decode of the document a formatter diff --git a/src/GameCult.Caching/Contracts/cultcache-store-composition.md b/src/GameCult.Caching/Contracts/cultcache-store-composition.md index 545d9a8f..c8e2f17b 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -299,8 +299,9 @@ exclusive open of the same lock file exclude each other is not established. the walk passes (`List`, `T[]`, a collection class, a dictionary on the way). Those are written by their `[Key]` layout so each element's id reaches disk, and the codec refuses the document on first encode or decode, naming the resolver and the - type. A resolver that returns MessagePack's standard formatter for a type does not - claim it. A value list (`List`, a list of a non-keyed class) is not on the + type. The formatter `StandardResolver` or `StandardResolverAllowPrivate` hands out for a + type does not claim it (registration refuses a non-public `[Key]` member on the way to + an object list, so the private-member formatter writes every id). A value list (`List`, a list of a non-keyed class) is not on the path, so a resolver may claim it. - Deserialization runs under untrusted-data security. `CultRecordRef` is a permitted dictionary key, compared by its key string.