Skip to content

Commit b473b26

Browse files
committed
Merge branch 'type-id-redesign' into worktree_1
2 parents a570ccd + 90b0675 commit b473b26

98 files changed

Lines changed: 883 additions & 964 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev_docs/TypeIdentifiers/TypeIdentifier-Design.md

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
# Structural TypeId Design
1+
# TypeIdentifier Design
22

33
> **No backward compatibility constraints.** There are zero deployed applications using this system. No persisted data exists. Any format, ID, or behavior can change freely.
44
55
> **Draft.** Direction, not specification. The goal is a simpler, more transparent system — adjust as we go.
66
77
## Context
88

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*.
1610

1711
## Insight
1812

@@ -74,32 +68,32 @@ For each `TypeName, Assembly` component:
7468

7569
### Caching
7670

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.
8074

8175
Parsing and tree-walking happen only on first encounter. Container-scoped caches are naturally garbage-collected with the container.
8276

8377
---
8478

85-
## TypeId hierarchy
79+
## TypeIdentifier hierarchy
8680

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:
8882

8983
```
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.
9488
```
9589

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.
9791

98-
### MappedTypeId
92+
### MappedTypeIdentifier
9993

10094
A leaf type from a mapped assembly. Has an explicitly assigned GUID. String representation is `"GUID, 0"`.
10195

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`.
10397

10498
### StableNameTypeId
10599

@@ -115,15 +109,15 @@ Examples: `List<MyEntity>` → `"System.Collections.Generic.List`1[[e4a8c9f2-...
115109

116110
Resolution requires walking the string, resolving GUID components via the mapping dictionary, and reconstructing with `Type.MakeGenericType()` / `Type.MakeArrayType()`.
117111

118-
### TypeId is a pure identity value
112+
### TypeIdentifier is a pure identity value
119113

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`.
121115

122-
### Open generic mappings are NOT TypeIds
116+
### Open generic mappings are NOT TypeIdentifiers
123117

124118
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.
125119

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.
127121

128122
---
129123

@@ -136,7 +130,7 @@ This mapping is represented by `OpenGenericId` — a GUID-backed struct, distinc
136130
mapper.UseStableNameStrategyForAssembliesContaining<NodaTime.Instant, SomeOtherLib.Foo>();
137131
```
138132

139-
2. **Mapped assemblies**types that need rename-safety. Leaf types get GUID assignments (`MappedTypeId`). Open generic definitions get GUID assignments (not TypeIdsbuilding blocks only). Registered explicitly per-container:
133+
2. **Mapped assemblies**types that need rename-safety. Leaf types get GUID assignments (`MappedTypeIdentifier`). Open generic definitions get GUID assignments (not TypeIdentifiersbuilding blocks only). Registered explicitly per-container:
140134
```csharp
141135
mapper.MapTypesFromAssemblyContaining<MyEntity>();
142136
```
@@ -162,7 +156,7 @@ static class MyAssemblyTypeMappings : ITypeMappingDeclaration
162156
}
163157
```
164158

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.).
166160

167161
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.
168162

@@ -171,7 +165,7 @@ The framework enforces that a mapping class only maps types from its own assembl
171165
## Per-container registration (no global state)
172166

173167
```csharp
174-
container.RegisterTypeMapper(mapper =>
168+
container.RegisterTypeIdentifierMapper(mapper =>
175169
{
176170
mapper
177171
.MapTypesFromAssemblyContaining<MyEntity>()
@@ -183,7 +177,7 @@ container.RegisterTypeMapper(mapper =>
183177
- No `AppDomain.AssemblyLoad` hook
184178
- No static singleton
185179
- No auto-discovery
186-
- Each container owns its `ITypeMapper` instance with its own mappings
180+
- Each container owns its `ITypeIdentifierMapper` instance with its own mappings
187181
- If a container didn't register an assembly's mappings, serializing those types is an error
188182

189183
---
@@ -195,8 +189,8 @@ Three separate classes, each with one job:
195189
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.
196190

197191
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()`.
200194
- Caches results in both directions.
201195

202196
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:
205199

206200
## SQL storage
207201

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.
209203

210204
---
211205

@@ -220,9 +214,9 @@ TypeId columns in the event store, document DB, tessaging, and Typermedia remain
220214

221215
## What changes
222216

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
226220
- `RenamingDecorator` — delegates structural work to `TypeNameParser` + `TypeNameMapper`
227221
- Generated mapping files — only contain leaf types and open generic definitions
228222
- 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
231225

232226
- Leaf types get explicit GUID assignments
233227
- 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
235229

236230
## Resolved questions
237231

samples/AccountManagement/src/AccountManagement.API/TypeMappingDeclarations.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using Compze.TypeIdentifiers;
22

3-
[assembly: TypeMappings(typeof(AccountManagement.API.TypeMappingDeclarations))]
3+
[assembly: AssemblyTypeMapper(typeof(AccountManagement.API.AssemblyTypeMapper))]
44

55
namespace AccountManagement.API;
66

7-
class TypeMappingDeclarations : ITypeMappingDeclaration
7+
class AssemblyTypeMapper : IAssemblyTypeMapper
88
{
9-
public void DeclareMappings(ITypeMappingRegistrar map)
9+
public void Map(ITypeMappingRegistrar map)
1010
{
1111
map.Map<AccountResource>("84c1bfcd-a5dd-41e2-ade0-e25bbe0337c3")
1212
.Map<AccountResource.Tommand.ChangeEmail>("337af6fe-e645-49c7-9da1-b00dbc19cfa6")

samples/AccountManagement/src/AccountManagement.Domain.Tevents/TypeMappingDeclarations.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using Compze.TypeIdentifiers;
22

3-
[assembly: TypeMappings(typeof(AccountManagement.Domain.Tevents.TypeMappingDeclarations))]
3+
[assembly: AssemblyTypeMapper(typeof(AccountManagement.Domain.Tevents.AssemblyTypeMapper))]
44

55
namespace AccountManagement.Domain.Tevents;
66

7-
class TypeMappingDeclarations : ITypeMappingDeclaration
7+
class AssemblyTypeMapper : IAssemblyTypeMapper
88
{
9-
public void DeclareMappings(ITypeMappingRegistrar map)
9+
public void Map(ITypeMappingRegistrar map)
1010
{
1111
map.Map<IAccountTevent.Created>("ae1684ff-a150-4840-ac08-1b9d21806da6")
1212
.Map<AccountTevent.LoggedIn>("37079b83-103e-4832-a718-3ad4c71700a7")

samples/AccountManagement/src/AccountManagement.Server/TypeMappingDeclarations.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using Compze.TypeIdentifiers;
22

3-
[assembly: TypeMappings(typeof(AccountManagement.TypeMappingDeclarations))]
3+
[assembly: AssemblyTypeMapper(typeof(AccountManagement.AssemblyTypeMapper))]
44

55
namespace AccountManagement;
66

7-
class TypeMappingDeclarations : ITypeMappingDeclaration
7+
class AssemblyTypeMapper : IAssemblyTypeMapper
88
{
9-
public void DeclareMappings(ITypeMappingRegistrar map)
9+
public void Map(ITypeMappingRegistrar map)
1010
{
1111
map.Map<Domain.Account>("57fc716d-d8ca-4224-9f78-4d3b5a7f9ebd")
1212
.Map<UI.QueryModels.AccountQueryModel>("aee890be-6bb0-4301-90d5-492b0b42a4a8")

src/Compze.Abstractions/TypeMappingDeclarations.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
using Compze.Abstractions.Tessaging.Public;
22
using Compze.TypeIdentifiers;
33

4-
[assembly: TypeMappings(typeof(Compze.Abstractions.TypeMappingDeclarations))]
4+
[assembly: AssemblyTypeMapper(typeof(Compze.Abstractions.AssemblyTypeMapper))]
55

66
namespace Compze.Abstractions;
77

8-
class TypeMappingDeclarations : ITypeMappingDeclaration
8+
class AssemblyTypeMapper : IAssemblyTypeMapper
99
{
10-
public void DeclareMappings(ITypeMappingRegistrar map)
10+
public void Map(ITypeMappingRegistrar map)
1111
{
1212
map.Map<IExactlyOnceTevent>("0d68a831-87c0-4d05-8e52-bf063d51b56d")
1313
.Map<IRemotableTevent>("887aad71-52f3-46f8-a26a-e2886941758d")

src/Compze.Core/Tessaging/Internal/SqlLayer/_IServiceBusSqlLayer.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,25 @@ public enum SaveTessageResult
3030

3131
public interface IInboxSqlLayer
3232
{
33-
SaveTessageResult SaveTessage(TessageId tessageId, MappedTypeId typeId, string serializedTessage);
33+
SaveTessageResult SaveTessage(TessageId tessageId, MappedTypeIdentifier typeId, string serializedTessage);
3434
int MarkAsSucceeded(TessageId tessageId);
3535
int RecordException(TessageId tessageId, string exceptionStackTrace, string exceptionTessage, string exceptionType);
3636
int MarkAsFailed(TessageId tessageId);
3737
Task InitAsync();
3838
}
3939

40-
public class OutboxTessageWithReceivers(string serializedTessage, MappedTypeId typeId, TessageId tessageId, IEnumerable<EndpointId> receiverEndpointIds)
40+
public class OutboxTessageWithReceivers(string serializedTessage, MappedTypeIdentifier typeId, TessageId tessageId, IEnumerable<EndpointId> receiverEndpointIds)
4141
{
4242
public string SerializedTessage { get; } = serializedTessage;
43-
public MappedTypeId TypeId { get; } = typeId;
43+
public MappedTypeIdentifier TypeId { get; } = typeId;
4444
public TessageId TessageId { get; } = tessageId;
4545
public IEnumerable<EndpointId> ReceiverEndpointIds { get; } = receiverEndpointIds.ToList();
4646
}
4747

48-
public class UndeliveredTessage(TessageId tessageId, MappedTypeId typeId, string serializedTessage, EndpointId targetEndpointId, int retryCount, DateTime? lastAttemptTime)
48+
public class UndeliveredTessage(TessageId tessageId, MappedTypeIdentifier typeId, string serializedTessage, EndpointId targetEndpointId, int retryCount, DateTime? lastAttemptTime)
4949
{
5050
public TessageId TessageId { get; } = tessageId;
51-
public MappedTypeId TypeId { get; } = typeId;
51+
public MappedTypeIdentifier TypeId { get; } = typeId;
5252
public string SerializedTessage { get; } = serializedTessage;
5353
public EndpointId TargetEndpointId { get; } = targetEndpointId;
5454
public int RetryCount { get; } = retryCount;

src/Compze.Core/Tessaging/Teventive/Internal/Implementation/TaggregateTypeValidator.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ static IReadOnlyList<Type> GetAllInheritingClassesOrInterfaces(Type type) => typ
6767
{
6868
public static void RegisterWith(IComponentRegistrar registrar)
6969
=> registrar.Register(Singleton.For<ITaggregateTypeValidator>()
70-
.CreatedBy((IStructuralTypeMapper typeMapper) => new TaggregateTypeValidator(typeMapper)));
70+
.CreatedBy((ITypeMapper typeMapper) => new TaggregateTypeValidator(typeMapper)));
7171

72-
readonly IStructuralTypeMapper _typeMapper;
73-
TaggregateTypeValidator(IStructuralTypeMapper typeMapper) => _typeMapper = typeMapper;
72+
readonly ITypeMapper _typeMapper;
73+
TaggregateTypeValidator(ITypeMapper typeMapper) => _typeMapper = typeMapper;
7474

7575
public void AssertIsValid<TTaggregate>() => ValidatorFor<TTaggregate>.AssertValid(_typeMapper);
7676

@@ -79,7 +79,7 @@ static class ValidatorFor<TTaggregate>
7979
// ReSharper disable once StaticMemberInGenericType (This is exactly the effect we are after...)
8080
static bool _validated;
8181

82-
internal static void AssertValid(IStructuralTypeMapper typeMapper)
82+
internal static void AssertValid(ITypeMapper typeMapper)
8383
{
8484
if(_validated) return;
8585

@@ -88,7 +88,7 @@ internal static void AssertValid(IStructuralTypeMapper typeMapper)
8888
_validated = true;
8989
}
9090

91-
static void AssertValidInternal(IStructuralTypeMapper typeMapper)
91+
static void AssertValidInternal(ITypeMapper typeMapper)
9292
{
9393
var classInheritanceChain = typeof(TTaggregate).ClassInheritanceChain().ToList();
9494
var inheritedTaggregateType = classInheritanceChain.Single(baseClass => baseClass.IsConstructedGenericType && baseClass.GetGenericTypeDefinition() == typeof(Taggregate<,,,,>));

0 commit comments

Comments
 (0)