You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: dev_docs/TypeIdentifiers/TypeIdentifier-Design.md
+29-35Lines changed: 29 additions & 35 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,18 +1,12 @@
1
-
# Structural TypeId Design
1
+
# TypeIdentifier Design
2
2
3
3
> **No backward compatibility constraints.** There are zero deployed applications using this system. No persisted data exists. Any format, ID, or behavior can change freely.
4
4
5
5
> **Draft.** Direction, not specification. The goal is a simpler, more transparent system — adjust as we go.
6
6
7
7
## Context
8
8
9
-
The current TypeMapper replaces entire `$type` strings (e.g. `"System.Collections.Generic.List`1[[MyApp.MyEntity, MyApp]], System.Private.CoreLib"`) with opaque GUIDs. This requires:
10
-
- Scanning every loaded assembly to discover all closed generic instantiations
11
-
- Computing deterministic GUIDs for every closed generic and array type
12
-
- Maintaining a global bidirectional dictionary of every type ever seen
13
-
- A static singleton with assembly-load hooks
14
-
15
-
All of this exists because the system flattens the *structurally recursive* type name into a *single opaque GUID*, then needs to reconstruct the full type from that GUID on deserialization.
9
+
The previous TypeMapper replaced entire `$type` strings (e.g. `"System.Collections.Generic.List`1[[MyApp.MyEntity, MyApp]], System.Private.CoreLib"`) with opaque GUIDs. This required scanning every loaded assembly to discover all closed generic instantiations, computing deterministic GUIDs for every closed generic and array type, maintaining a global bidirectional dictionary of every type ever seen, and a static singleton with assembly-load hooks — all because the system flattened the *structurally recursive* type name into a *single opaque GUID*.
16
10
17
11
## Insight
18
12
@@ -74,32 +68,32 @@ For each `TypeName, Assembly` component:
74
68
75
69
### Caching
76
70
77
-
Both directions are cached on the `ITypeMapper` instance:
78
-
-**Serialize**: `Type` → `TypeId`. Once computed, subsequent serializations of the same type are a dictionary lookup.
79
-
-**Deserialize**: `TypeId` string → `Type`. Once resolved, subsequent deserializations of the same string are a dictionary lookup.
71
+
Both directions are cached on the `ITypeIdentifierMapper` instance:
72
+
-**Serialize**: `Type` → `TypeIdentifier`. Once computed, subsequent serializations of the same type are a dictionary lookup.
73
+
-**Deserialize**: `TypeIdentifier` string → `Type`. Once resolved, subsequent deserializations of the same string are a dictionary lookup.
80
74
81
75
Parsing and tree-walking happen only on first encounter. Container-scoped caches are naturally garbage-collected with the container.
82
76
83
77
---
84
78
85
-
## TypeId hierarchy
79
+
## TypeIdentifier hierarchy
86
80
87
-
`TypeId` is the identity of a fully constructed type. It is an abstract base with three concrete subtypes, each representing a different kind of identity:
81
+
`TypeIdentifier` is the identity of a fully constructed type. It is an abstract base with three concrete subtypes, each representing a different kind of identity:
88
82
89
83
```
90
-
TypeId (abstract)
91
-
├── MappedTypeId — leaf type from a mapped assembly. Has a GUID. SQL-storable.
92
-
├── StableNameTypeId — type(s) entirely from stable assemblies. Untouched AssemblyQualifiedName.
93
-
└── ConstructedTypeId — mixed: AssemblyQualifiedName with some GUID, 0 components.
84
+
TypeIdentifier (abstract)
85
+
├── MappedTypeIdentifier — leaf type from a mapped assembly. Has a GUID. SQL-storable.
86
+
├── StableNameTypeId — type(s) entirely from stable assemblies. Untouched AssemblyQualifiedName.
87
+
└── ConstructedTypeId — mixed: AssemblyQualifiedName with some GUID, 0 components.
94
88
```
95
89
96
-
All three expose a **string representation** — the `AssemblyQualifiedName`-format string used by serialization. Only `MappedTypeId` additionally exposes a **GUID** for SQL storage.
90
+
All three expose a **string representation** — the `AssemblyQualifiedName`-format string used by serialization. Only `MappedTypeIdentifier` additionally exposes a **GUID** for SQL storage.
97
91
98
-
### MappedTypeId
92
+
### MappedTypeIdentifier
99
93
100
94
A leaf type from a mapped assembly. Has an explicitly assigned GUID. String representation is `"GUID, 0"`.
101
95
102
-
This is the only subtype that can be stored in SQL GUID columns. The event store, document DB, tessaging outbox/inbox, and Typermedia routing all deal in `MappedTypeId`.
96
+
This is the only subtype that can be stored in SQL GUID columns. The event store, document DB, tessaging outbox/inbox, and Typermedia routing all deal in `MappedTypeIdentifier`.
Resolution requires walking the string, resolving GUID components via the mapping dictionary, and reconstructing with `Type.MakeGenericType()` / `Type.MakeArrayType()`.
117
111
118
-
### TypeId is a pure identity value
112
+
### TypeIdentifier is a pure identity value
119
113
120
-
`TypeId` subtypes are dumb value objects — they hold only the string representation (and GUID for `MappedTypeId`). They do not participate in parsing or resolution. That logic lives in the `TypeNameMapper`.
114
+
`TypeIdentifier` subtypes are dumb value objects — they hold only the string representation (and GUID for `MappedTypeIdentifier`). They do not participate in parsing or resolution. That logic lives in the `TypeNameMapper`.
121
115
122
-
### Open generic mappings are NOT TypeIds
116
+
### Open generic mappings are NOT TypeIdentifiers
123
117
124
118
An open generic like `List<>` is not a type — it's a template. It never exists at runtime, never gets serialized, never gets stored, never crosses the wire. Its GUID mapping exists solely as a building block for constructing and parsing `ConstructedTypeId` strings.
125
119
126
-
This mapping is represented by `OpenGenericId` — a GUID-backed struct, distinct from `TypeId`. The `ITypeMapper` may handle the `OpenGenericId` ↔ open generic `Type` mapping internally, but `OpenGenericId` is never returned where a `TypeId` is expected. TypeIds name fully constructed types; `OpenGenericId` names a template.
120
+
This mapping is represented by `OpenGenericId` — a GUID-backed struct, distinct from `TypeIdentifier`. The `ITypeIdentifierMapper` may handle the `OpenGenericId` ↔ open generic `Type` mapping internally, but `OpenGenericId` is never returned where a `TypeIdentifier` is expected. TypeIdentifiers name fully constructed types; `OpenGenericId` names a template.
127
121
128
122
---
129
123
@@ -136,7 +130,7 @@ This mapping is represented by `OpenGenericId` — a GUID-backed struct, distinc
@@ -162,7 +156,7 @@ static class MyAssemblyTypeMappings : ITypeMappingDeclaration
162
156
}
163
157
```
164
158
165
-
`MapOpenGeneric` is a separate method from `Map` to make it explicit that open generics are not types — they are templates. This mapping produces an `OpenGenericId`, not a `TypeId`. In practice, `MapOpenGeneric` is rarely needed — most generics used in serialized data come from stable assemblies (BCL collections, etc.).
159
+
`MapOpenGeneric` is a separate method from `Map` to make it explicit that open generics are not types — they are templates. This mapping produces an `OpenGenericId`, not a `TypeIdentifier`. In practice, `MapOpenGeneric` is rarely needed — most generics used in serialized data come from stable assemblies (BCL collections, etc.).
166
160
167
161
The framework enforces that a mapping class only maps types from its own assembly. Mapping a type from another assembly is an error at registration time.
168
162
@@ -171,7 +165,7 @@ The framework enforces that a mapping class only maps types from its own assembl
- Each container owns its `ITypeMapper` instance with its own mappings
180
+
- Each container owns its `ITypeIdentifierMapper` instance with its own mappings
187
181
- If a container didn't register an assembly's mappings, serializing those types is an error
188
182
189
183
---
@@ -195,8 +189,8 @@ Three separate classes, each with one job:
195
189
1.**`TypeNameParser`** — parses a standard `AssemblyQualifiedName`-format string into a tree of `TypeName, Assembly` components with nested type arguments. Works on both Newtonsoft's raw output and our persisted form (same structure). Independently testable, separate class, no mapping knowledge.
196
190
197
191
2.**`TypeNameMapper`** — walks a parsed tree and transforms it:
198
-
-**Serialize direction**: Given a .NET `Type`, produces a `TypeId` (choosing the correct subtype). For mapped types: replaces type name with GUID, assembly with `0`. For stable types: passes through unchanged. For composites: walks components recursively.
199
-
-**Deserialize direction**: Given a `TypeId`, resolves it back to a .NET `Type`. `MappedTypeId` → dictionary lookup. `StableNameTypeId` → `Type.GetType()`. `ConstructedTypeId` → parse, resolve components recursively, `MakeGenericType()` / `MakeArrayType()`.
192
+
-**Serialize direction**: Given a .NET `Type`, produces a `TypeIdentifier` (choosing the correct subtype). For mapped types: replaces type name with GUID, assembly with `0`. For stable types: passes through unchanged. For composites: walks components recursively.
193
+
-**Deserialize direction**: Given a `TypeIdentifier`, resolves it back to a .NET `Type`. `MappedTypeIdentifier` → dictionary lookup. `StableNameTypeId` → `Type.GetType()`. `ConstructedTypeId` → parse, resolve components recursively, `MakeGenericType()` / `MakeArrayType()`.
200
194
- Caches results in both directions.
201
195
202
196
3.**`RenamingDecorator`** — finds `$type` values in JSON, delegates to `TypeNameMapper`, puts the result back. Stays trivial (~20 lines).
@@ -205,7 +199,7 @@ Three separate classes, each with one job:
205
199
206
200
## SQL storage
207
201
208
-
TypeId columns in the event store, document DB, tessaging, and Typermedia remain GUID columns. These accept only `MappedTypeId` — enforced at the type level. This is not a limitation in practice: events and documents are concrete leaf domain types, not generic collections.
202
+
TypeIdentifier columns in the event store, document DB, tessaging, and Typermedia remain GUID columns. These accept only `MappedTypeIdentifier` — enforced at the type level. This is not a limitation in practice: events and documents are concrete leaf domain types, not generic collections.
209
203
210
204
---
211
205
@@ -220,9 +214,9 @@ TypeId columns in the event store, document DB, tessaging, and Typermedia remain
220
214
221
215
## What changes
222
216
223
-
-`TypeId` — from a single GUID-backed type to an abstract base with three subtypes
224
-
-`ITypeMapper` — becomes container-scoped, explicitly configured. Produces `MappedTypeId` for leaf types. May handle `OpenGenericId` ↔ `Type` mappings internally, but `OpenGenericId` is not a `TypeId`.
225
-
-`TypeMapper` — no longer a static singleton
217
+
-`TypeIdentifier` — from a single GUID-backed type to an abstract base with three subtypes
218
+
-`ITypeIdentifierMapper` — becomes container-scoped, explicitly configured. Produces `MappedTypeIdentifier` for leaf types. May handle `OpenGenericId` ↔ `Type` mappings internally, but `OpenGenericId` is not a `TypeIdentifier`.
219
+
-`TypeIdentifierMapper` — no longer a static singleton
226
220
-`RenamingDecorator` — delegates structural work to `TypeNameParser` + `TypeNameMapper`
227
221
- Generated mapping files — only contain leaf types and open generic definitions
228
222
- Persisted `$type` strings — structural `AssemblyQualifiedName` format with GUIDs for mapped components
@@ -231,7 +225,7 @@ TypeId columns in the event store, document DB, tessaging, and Typermedia remain
231
225
232
226
- Leaf types get explicit GUID assignments
233
227
- SQL schema stays GUID-based
234
-
-`GetType(MappedTypeId)` / `GetId(Type)` for leaf types — same dictionary lookup
228
+
-`GetType(MappedTypeIdentifier)` / `GetId(Type)` for leaf types — same dictionary lookup
0 commit comments