Skip to content

Commit dba4342

Browse files
committed
[DEVEX-222] Nested Message type mapping into settings object
1 parent 27e1c61 commit dba4342

File tree

7 files changed

+180
-148
lines changed

7 files changed

+180
-148
lines changed

src/KurrentDB.Client/Core/KurrentDBClientSerializationSettings.cs

+142-117
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,7 @@ public class KurrentDBClientSerializationSettings {
3838
/// </summary>
3939
public IMessageTypeNamingStrategy? MessageTypeNamingStrategy { get; set; }
4040

41-
/// <summary>
42-
/// Allows to register mapping of CLR message types to their corresponding message type names used in serialized messages.
43-
/// </summary>
44-
public IDictionary<string, Type> MessageTypeMap { get; set; } = new Dictionary<string, Type>();
45-
46-
/// <summary>
47-
/// Registers CLR message types that can be appended to the specific stream category.
48-
/// Types will have message type names resolved based on the used <see cref="KurrentDB.Client.Core.Serialization.IMessageTypeNamingStrategy"/>
49-
/// </summary>
50-
public IDictionary<string, Type[]> CategoryMessageTypesMap { get; set; } = new Dictionary<string, Type[]>();
51-
52-
/// <summary>
53-
/// Specifies the CLR type that should be used when deserializing metadata for all events.
54-
/// When set, the client will attempt to deserialize event metadata into this type.
55-
/// If not provided, <see cref="Kurrent.Diagnostics.Tracing.TracingMetadata"/> will be used.
56-
/// </summary>
57-
public Type? DefaultMetadataType { get; set; }
41+
public MessageTypeMappingSettings MessageTypeMapping { get; set; } = new MessageTypeMappingSettings();
5842

5943
/// <summary>
6044
/// Creates a new instance of serialization settings with either default values or custom configuration.
@@ -181,113 +165,24 @@ IMessageTypeNamingStrategy messageTypeNamingStrategy
181165
}
182166

183167
/// <summary>
184-
/// Associates a message type with a specific stream category to enable automatic deserialization.
185-
/// In event sourcing, streams are often prefixed with a category (e.g., "user-123", "order-456").
186-
/// This method tells the client which message types can appear in streams of a given category.
187-
/// </summary>
188-
/// <typeparam name="T">The event or message type that can appear in the category's streams.</typeparam>
189-
/// <param name="categoryName">The category prefix (e.g., "user", "order", "account").</param>
190-
/// <returns>The current instance for method chaining.</returns>
191-
/// <example>
192-
/// <code>
193-
/// // Register event types that can appear in user streams
194-
/// settings.RegisterMessageTypeForCategory&lt;UserCreated&gt;("user")
195-
/// .RegisterMessageTypeForCategory&lt;UserUpdated&gt;("user")
196-
/// .RegisterMessageTypeForCategory&lt;UserDeleted&gt;("user");
197-
/// </code>
198-
/// </example>
199-
public KurrentDBClientSerializationSettings RegisterMessageTypeForCategory<T>(string categoryName) =>
200-
RegisterMessageTypeForCategory(categoryName, typeof(T));
201-
202-
/// <summary>
203-
/// Registers multiple message types for a specific stream category.
204-
/// </summary>
205-
/// <param name="categoryName">The category name to register the types with.</param>
206-
/// <param name="types">The message types to register.</param>
207-
/// <returns>The current instance for method chaining.</returns>
208-
public KurrentDBClientSerializationSettings RegisterMessageTypeForCategory(string categoryName, params Type[] types) {
209-
CategoryMessageTypesMap[categoryName] = CategoryMessageTypesMap.TryGetValue(categoryName, out var current)
210-
? [..current, ..types]
211-
: types;
212-
213-
return this;
214-
}
215-
216-
/// <summary>
217-
/// Maps a .NET type to a specific message type name that will be stored in the message metadata.
218-
/// This mapping is used during automatic deserialization, as it tells the client which CLR type
219-
/// to instantiate when encountering a message with a particular type name in the database.
220-
/// </summary>
221-
/// <typeparam name="T">The .NET type to register (typically a message class).</typeparam>
222-
/// <param name="typeName">The string identifier to use for this type in the database.</param>
223-
/// <returns>The current instance for method chaining.</returns>
224-
/// <remarks>
225-
/// The type name is often different from the .NET type name to support versioning and evolution
226-
/// of your domain model without breaking existing stored messages.
227-
/// </remarks>
228-
/// <example>
229-
/// <code>
230-
/// // Register me types with their corresponding type identifiers
231-
/// settings.RegisterMessageType&lt;UserCreated&gt;("user-created-v1")
232-
/// .RegisterMessageType&lt;OrderPlaced&gt;("order-placed-v2");
233-
/// </code>
234-
/// </example>
235-
public KurrentDBClientSerializationSettings RegisterMessageType<T>(string typeName) =>
236-
RegisterMessageType(typeName, typeof(T));
237-
238-
/// <summary>
239-
/// Registers a message type with a specific type name.
240-
/// </summary>
241-
/// <param name="type">The message type to register.</param>
242-
/// <param name="typeName">The type name to register for the message type.</param>
243-
/// <returns>The current instance for method chaining.</returns>
244-
public KurrentDBClientSerializationSettings RegisterMessageType(string typeName, Type type) {
245-
MessageTypeMap[typeName] = type;
246-
247-
return this;
248-
}
249-
250-
/// <summary>
251-
/// Registers multiple message types with their corresponding type names.
168+
/// Configures which serialization format (JSON or binary) is used by default when writing messages
169+
/// where the content type isn't explicitly specified. The default content type is "application/json"
252170
/// </summary>
253-
/// <param name="typeMap">Dictionary mapping types to their type names.</param>
171+
/// <param name="contentType">The serialization format content type</param>
254172
/// <returns>The current instance for method chaining.</returns>
255-
public KurrentDBClientSerializationSettings RegisterMessageTypes(IDictionary<string, Type> typeMap) {
256-
foreach (var map in typeMap) {
257-
MessageTypeMap[map.Key] = map.Value;
258-
}
173+
public KurrentDBClientSerializationSettings UseContentType(ContentType contentType) {
174+
DefaultContentType = contentType;
259175

260176
return this;
261177
}
262178

263179
/// <summary>
264-
/// Configures a strongly-typed metadata class for all mes in the system.
265-
/// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects.
266-
/// </summary>
267-
/// <typeparam name="T">The metadata class type containing properties matching the expected metadata fields.</typeparam>
268-
/// <returns>The current instance for method chaining.</returns>
269-
public KurrentDBClientSerializationSettings UseMetadataType<T>() =>
270-
UseMetadataType(typeof(T));
271-
272-
/// <summary>
273-
/// Configures a strongly-typed metadata class for all mes in the system.
274-
/// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects.
275-
/// </summary>
276-
/// <param name="type">The metadata class type containing properties matching the expected metadata fields.</param>
277-
/// <returns>The current instance for method chaining.</returns>
278-
public KurrentDBClientSerializationSettings UseMetadataType(Type type) {
279-
DefaultMetadataType = type;
280-
281-
return this;
282-
}
283-
/// <summary>
284-
/// Configures which serialization format (JSON or binary) is used by default when writing messages
285-
/// where the content type isn't explicitly specified. The default content type is "application/json"
180+
/// Allows to configure message type mapping
286181
/// </summary>
287-
/// <param name="contentType">The serialization format content type</param>
182+
/// <param name="configure"></param>
288183
/// <returns>The current instance for method chaining.</returns>
289-
public KurrentDBClientSerializationSettings UseContentType(ContentType contentType) {
290-
DefaultContentType = contentType;
184+
public KurrentDBClientSerializationSettings ConfigureTypeMap(Action<MessageTypeMappingSettings> configure) {
185+
configure(MessageTypeMapping);
291186

292187
return this;
293188
}
@@ -301,8 +196,7 @@ internal KurrentDBClientSerializationSettings Clone() {
301196
BytesSerializer = BytesSerializer,
302197
JsonSerializer = JsonSerializer,
303198
DefaultContentType = DefaultContentType,
304-
MessageTypeMap = new Dictionary<string, Type>(MessageTypeMap),
305-
CategoryMessageTypesMap = new Dictionary<string, Type[]>(CategoryMessageTypesMap),
199+
MessageTypeMapping = MessageTypeMapping.Clone(),
306200
MessageTypeNamingStrategy = MessageTypeNamingStrategy
307201
};
308202
}
@@ -369,3 +263,134 @@ public enum AutomaticDeserialization {
369263
/// </summary>
370264
Enabled = 1
371265
}
266+
267+
/// <summary>
268+
/// Represents message type mapping settings
269+
/// </summary>
270+
public class MessageTypeMappingSettings {
271+
/// <summary>
272+
/// Allows to register mapping of CLR message types to their corresponding message type names used in serialized messages.
273+
/// </summary>
274+
public IDictionary<string, Type> TypeMap { get; set; } = new Dictionary<string, Type>();
275+
276+
/// <summary>
277+
/// Registers CLR message types that can be appended to the specific stream category.
278+
/// Types will have message type names resolved based on the used <see cref="KurrentDB.Client.Core.Serialization.IMessageTypeNamingStrategy"/>
279+
/// </summary>
280+
public IDictionary<string, Type[]> CategoryTypesMap { get; set; } = new Dictionary<string, Type[]>();
281+
282+
/// <summary>
283+
/// Specifies the CLR type that should be used when deserializing metadata for all events.
284+
/// When set, the client will attempt to deserialize event metadata into this type.
285+
/// If not provided, <see cref="Kurrent.Diagnostics.Tracing.TracingMetadata"/> will be used.
286+
/// </summary>
287+
public Type? DefaultMetadataType { get; set; }
288+
289+
/// <summary>
290+
/// Associates a message type with a specific stream category to enable automatic deserialization.
291+
/// In event sourcing, streams are often prefixed with a category (e.g., "user-123", "order-456").
292+
/// This method tells the client which message types can appear in streams of a given category.
293+
/// </summary>
294+
/// <typeparam name="T">The event or message type that can appear in the category's streams.</typeparam>
295+
/// <param name="categoryName">The category prefix (e.g., "user", "order", "account").</param>
296+
/// <returns>The current instance for method chaining.</returns>
297+
/// <example>
298+
/// <code>
299+
/// // Register event types that can appear in user streams
300+
/// settings.RegisterMessageTypeForCategory&lt;UserCreated&gt;("user")
301+
/// .RegisterMessageTypeForCategory&lt;UserUpdated&gt;("user")
302+
/// .RegisterMessageTypeForCategory&lt;UserDeleted&gt;("user");
303+
/// </code>
304+
/// </example>
305+
public MessageTypeMappingSettings RegisterForCategory<T>(string categoryName) =>
306+
RegisterForCategory(categoryName, typeof(T));
307+
308+
/// <summary>
309+
/// Registers multiple message types for a specific stream category.
310+
/// </summary>
311+
/// <param name="categoryName">The category name to register the types with.</param>
312+
/// <param name="types">The message types to register.</param>
313+
/// <returns>The current instance for method chaining.</returns>
314+
public MessageTypeMappingSettings RegisterForCategory(string categoryName, params Type[] types) {
315+
CategoryTypesMap[categoryName] =
316+
CategoryTypesMap.TryGetValue(categoryName, out var current)
317+
? [..current, ..types]
318+
: types;
319+
320+
return this;
321+
}
322+
323+
/// <summary>
324+
/// Maps a .NET type to a specific message type name that will be stored in the message metadata.
325+
/// This mapping is used during automatic deserialization, as it tells the client which CLR type
326+
/// to instantiate when encountering a message with a particular type name in the database.
327+
/// </summary>
328+
/// <typeparam name="T">The .NET type to register (typically a message class).</typeparam>
329+
/// <param name="typeName">The string identifier to use for this type in the database.</param>
330+
/// <returns>The current instance for method chaining.</returns>
331+
/// <remarks>
332+
/// The type name is often different from the .NET type name to support versioning and evolution
333+
/// of your domain model without breaking existing stored messages.
334+
/// </remarks>
335+
/// <example>
336+
/// <code>
337+
/// // Register me types with their corresponding type identifiers
338+
/// settings.RegisterMessageType&lt;UserCreated&gt;("user-created-v1")
339+
/// .RegisterMessageType&lt;OrderPlaced&gt;("order-placed-v2");
340+
/// </code>
341+
/// </example>
342+
public MessageTypeMappingSettings Register<T>(string typeName) =>
343+
Register(typeName, typeof(T));
344+
345+
/// <summary>
346+
/// Registers a message type with a specific type name.
347+
/// </summary>
348+
/// <param name="type">The message type to register.</param>
349+
/// <param name="typeName">The type name to register for the message type.</param>
350+
/// <returns>The current instance for method chaining.</returns>
351+
public MessageTypeMappingSettings Register(string typeName, Type type) {
352+
TypeMap[typeName] = type;
353+
354+
return this;
355+
}
356+
357+
/// <summary>
358+
/// Registers multiple message types with their corresponding type names.
359+
/// </summary>
360+
/// <param name="typeMap">Dictionary mapping types to their type names.</param>
361+
/// <returns>The current instance for method chaining.</returns>
362+
public MessageTypeMappingSettings Register(IDictionary<string, Type> typeMap) {
363+
foreach (var map in typeMap) {
364+
TypeMap[map.Key] = map.Value;
365+
}
366+
367+
return this;
368+
}
369+
370+
/// <summary>
371+
/// Configures a strongly-typed metadata class for all mes in the system.
372+
/// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects.
373+
/// </summary>
374+
/// <typeparam name="T">The metadata class type containing properties matching the expected metadata fields.</typeparam>
375+
/// <returns>The current instance for method chaining.</returns>
376+
public MessageTypeMappingSettings UseMetadataType<T>() =>
377+
UseMetadataType(typeof(T));
378+
379+
/// <summary>
380+
/// Configures a strongly-typed metadata class for all mes in the system.
381+
/// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects.
382+
/// </summary>
383+
/// <param name="type">The metadata class type containing properties matching the expected metadata fields.</param>
384+
/// <returns>The current instance for method chaining.</returns>
385+
public MessageTypeMappingSettings UseMetadataType(Type type) {
386+
DefaultMetadataType = type;
387+
388+
return this;
389+
}
390+
391+
internal MessageTypeMappingSettings Clone() =>
392+
new MessageTypeMappingSettings {
393+
TypeMap = new Dictionary<string, Type>(TypeMap),
394+
CategoryTypesMap = new Dictionary<string, Type[]>(CategoryTypesMap),
395+
};
396+
}

src/KurrentDB.Client/Core/Serialization/SchemaRegistry.cs

+8-6
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,16 @@ public bool TryResolveClrMetadataType(EventRecord record, [NotNullWhen(true)] ou
5252

5353
public static SchemaRegistry From(KurrentDBClientSerializationSettings settings) {
5454
var messageTypeNamingStrategy =
55-
settings.MessageTypeNamingStrategy ?? new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType);
55+
settings.MessageTypeNamingStrategy
56+
?? new DefaultMessageTypeNamingStrategy(settings.MessageTypeMapping.DefaultMetadataType);
5657

5758
var categoriesTypeMap = ResolveMessageTypeUsingNamingStrategy(
58-
settings.CategoryMessageTypesMap,
59+
settings.MessageTypeMapping,
5960
messageTypeNamingStrategy
6061
);
6162

6263
var messageTypeRegistry = new MessageTypeRegistry();
63-
messageTypeRegistry.Register(settings.MessageTypeMap);
64+
messageTypeRegistry.Register(settings.MessageTypeMapping.TypeMap);
6465
messageTypeRegistry.Register(categoriesTypeMap);
6566

6667
var serializers = new Dictionary<ContentType, ISerializer> {
@@ -77,16 +78,17 @@ public static SchemaRegistry From(KurrentDBClientSerializationSettings settings)
7778
serializers,
7879
new MessageTypeNamingStrategyWrapper(
7980
messageTypeRegistry,
80-
settings.MessageTypeNamingStrategy ?? new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType)
81+
settings.MessageTypeNamingStrategy
82+
?? new DefaultMessageTypeNamingStrategy(settings.MessageTypeMapping.DefaultMetadataType)
8183
)
8284
);
8385
}
8486

8587
static Dictionary<string, Type> ResolveMessageTypeUsingNamingStrategy(
86-
IDictionary<string, Type[]> categoryMessageTypesMap,
88+
MessageTypeMappingSettings messageTypeMappingSettings,
8789
IMessageTypeNamingStrategy messageTypeNamingStrategy
8890
) =>
89-
categoryMessageTypesMap
91+
messageTypeMappingSettings.CategoryTypesMap
9092
.SelectMany(
9193
categoryTypes => categoryTypes.Value.Select(
9294
type =>

test/KurrentDB.Client.Tests/Core/Serialization/MessageSerializerTests.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ public void With_ConfigureSettings_CreatesNewMessageSerializer() {
276276

277277
var operationSettings = OperationSerializationSettings.Configure(
278278
s =>
279-
s.RegisterMessageType<UserRegistered>("CustomMessageName")
279+
s.MessageTypeMapping.Register<UserRegistered>("CustomMessageName")
280280
);
281281

282282
// When
@@ -309,9 +309,10 @@ public void Serialize_WithMultipleMessages_ReturnsArrayOfMessageData() {
309309

310310
static KurrentDBClientSerializationSettings CreateTestSettings() {
311311
var settings = new KurrentDBClientSerializationSettings();
312-
settings.RegisterMessageType<UserRegistered>("UserRegistered");
313-
settings.RegisterMessageType<UserAssignedToRole>("UserAssignedToRole");
314-
settings.UseMetadataType<TestMetadata>();
312+
settings.MessageTypeMapping
313+
.Register<UserRegistered>("UserRegistered")
314+
.Register<UserAssignedToRole>("UserAssignedToRole")
315+
.UseMetadataType<TestMetadata>();
315316

316317
return settings;
317318
}

test/KurrentDB.Client.Tests/Core/Serialization/SchemaRegistryTests.cs

+7-8
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ public void From_WithCustomBytesSerializer_UsesProvidedSerializer() {
101101
public void From_WithMessageTypeMap_RegistersTypes() {
102102
// Given
103103
var settings = new KurrentDBClientSerializationSettings();
104-
settings.RegisterMessageType<TestEvent1>("test-event-1");
105-
settings.RegisterMessageType<TestEvent2>("test-event-2");
104+
settings.MessageTypeMapping.Register<TestEvent1>("test-event-1");
105+
settings.MessageTypeMapping.Register<TestEvent2>("test-event-2");
106106

107107
// When
108108
var registry = SchemaRegistry.From(settings);
@@ -120,7 +120,7 @@ public void From_WithMessageTypeMap_RegistersTypes() {
120120
public void From_WithCategoryMessageTypesMap_WithDefaultMessageAutoRegistration() {
121121
// Given
122122
var settings = new KurrentDBClientSerializationSettings();
123-
var defaultMessageTypeNamingStrategy = new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType);
123+
var defaultMessageTypeNamingStrategy = new DefaultMessageTypeNamingStrategy(settings.MessageTypeMapping.DefaultMetadataType);
124124

125125
// When
126126
var registry = SchemaRegistry.From(settings);
@@ -179,9 +179,9 @@ public void From_WithCategoryMessageTypesMap_WithDefaultMessageAutoRegistration(
179179
public void From_WithCategoryMessageTypesMap_RegistersTypesWithCategories() {
180180
// Given
181181
var settings = new KurrentDBClientSerializationSettings();
182-
settings.RegisterMessageTypeForCategory<TestEvent1>("category1");
183-
settings.RegisterMessageTypeForCategory<TestEvent2>("category1");
184-
settings.RegisterMessageTypeForCategory<TestEvent3>("category2");
182+
settings.MessageTypeMapping.RegisterForCategory<TestEvent1>("category1");
183+
settings.MessageTypeMapping.RegisterForCategory<TestEvent2>("category1");
184+
settings.MessageTypeMapping.RegisterForCategory<TestEvent3>("category2");
185185

186186
// When
187187
var registry = SchemaRegistry.From(settings);
@@ -242,8 +242,7 @@ public void From_WithNoMessageTypeNamingStrategy_UsesDefaultStrategy() {
242242
// Given
243243
var settings = new KurrentDBClientSerializationSettings {
244244
MessageTypeNamingStrategy = null,
245-
DefaultMetadataType = typeof(TestMetadata)
246-
};
245+
}.ConfigureTypeMap(setting => setting.DefaultMetadataType = typeof(TestMetadata));
247246

248247
// When
249248
var registry = SchemaRegistry.From(settings);

0 commit comments

Comments
 (0)