Skip to content

Commit cbe128e

Browse files
committed
[Host] Ability to add assembly or type redirect names in AssemblyQualifiedNameMessageTypeResolver
Signed-off-by: Tomasz Maruszak <maruszaktomasz@gmail.com>
1 parent 0cbcf2e commit cbe128e

File tree

6 files changed

+258
-28
lines changed

6 files changed

+258
-28
lines changed

docs/intro.md

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
- [Serialization](#serialization)
3131
- [Multiple message types on one topic (or queue)](#multiple-message-types-on-one-topic-or-queue)
3232
- [Message Type Resolver](#message-type-resolver)
33+
- [Custom Message Type Resolvers](#custom-message-type-resolvers)
34+
- [Use Case: Type Redirection for Evolving Services](#use-case-type-redirection-for-evolving-services)
35+
- [Using Redirects with the Default Resolver](#using-redirects-with-the-default-resolver)
3336
- [Polymorphic messages](#polymorphic-messages)
3437
- [Polymorphic producer](#polymorphic-producer)
3538
- [Polymorphic consumer](#polymorphic-consumer)
@@ -844,17 +847,84 @@ mbb.Consume<OrderEvent>(x =>
844847

845848
### Message Type Resolver
846849

847-
By default, the message header `MessageType` conveys the message type information using the assembly qualified name of the .NET type (see `AssemblyQualifiedNameMessageTypeResolver`).
850+
SlimMessageBus uses the `MessageType` header to convey the message's type information as a `string`. This allows the consumer to determine the correct .NET `Type` needed to deserialize the payload.
848851

849-
A custom resolver could be used. Some scenarios include a desire to send short type names (to optimize overall message size) or adjust interoperability with other messaging systems.
850-
The following can be used to provide a custom `IMessageTypeResolver` implementation:
852+
- **Producer Side:** Converts the `.NET Type` to a string and adds it to the message header.
853+
- **Consumer Side:** Reads the string from the header, resolves the corresponding `Type`, and passes it to the serializer to deserialize the message before handing it off to the appropriate handler.
851854

852-
```cs
853-
IMessageTypeResolver mtr = new AssemblyQualifiedNameMessageTypeResolver();
855+
Type resolution is handled via the [`IMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/IMessageTypeResolver.cs) interface, which is resolved from the dependency injection (DI) container.
856+
857+
By default, SlimMessageBus registers the [`AssemblyQualifiedNameMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs), which formats the type name as an **assembly-qualified name (without the version number)**. For example:
858+
859+
```
860+
MyNamespace.MyMessage, MyAssembly
861+
```
862+
863+
#### Custom Message Type Resolvers
864+
865+
You can provide a custom implementation of `IMessageTypeResolver` to support scenarios like:
866+
867+
- Reducing message size by using short or alias-based type names.
868+
- Integrating with other messaging systems using different naming conventions.
869+
- Supporting versioning or refactoring, such as type or namespace changes.
870+
871+
**ExampleCustom Resolver Registration**
872+
873+
```csharp
874+
public class MyShortNameMessageTypeResolver : IMessageTypeResolver
875+
{
876+
// Implement custom logic to map between Type and string.
877+
// Useful for short names, legacy mappings, etc.
878+
}
879+
880+
// Register with Microsoft DI
881+
services.AddSingleton<MyShortNameMessageTypeResolver>();
882+
883+
services.AddSlimMessageBus(mbb =>
884+
{
885+
// If this were to be a hybrid bus, each child bus can have its own type resolver
886+
mbb.WithMessageTypeResolver<MyShortNameMessageTypeResolver>();
887+
});
888+
```
854889

855-
mbb.WithMessageTypeResolver(mtr)
890+
#### Use Case: Type Redirection for Evolving Services
891+
892+
Suppose you refactor a service, moving messages to a new assembly or namespace. Since producers and consumers may be deployed independently, youll need a strategy to handle message type mismatches between versions.
893+
894+
With a custom resolver, you can:
895+
896+
- **Redirect** incoming messages that reference old type names to the updated `Type`.
897+
- **Emit** legacy type names from the producer for backward compatibility.
898+
899+
##### Using Redirects with the Default Resolver
900+
901+
If you're using the default [`AssemblyQualifiedNameMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs), you can extend it by implementing [`IAssemblyQualifiedNameMessageTypeResolverRedirect`](/src/SlimMessageBus.Host/MessageTypeResolver/IAssemblyQualifiedNameMessageTypeResolverRedirect.cs).
902+
903+
```csharp
904+
public class MyTypeResolverRedirect : IAssemblyQualifiedNameMessageTypeResolverRedirect
905+
{
906+
public Type TryGetType(string name)
907+
{
908+
if (name == "MyNamespace.MyMessage, MyAssembly")
909+
{
910+
return Type.GetType("NewNamespace.MyMessage, NewAssembly");
911+
// or return typeof(MyMessage);
912+
}
913+
return null; // no match
914+
}
915+
916+
public string TryGetName(Type messageType) => null; // No outgoing redirect
917+
}
856918
```
857919

920+
These redirect services must be registered with DI. You can register multiple for modularity:
921+
922+
```csharp
923+
services.TryAddEnumerable<IAssemblyQualifiedNameMessageTypeResolverRedirect, MyTypeResolverRedirect>();
924+
```
925+
926+
This makes it easy to maintain compatibility as your services evolve, without disrupting production systems.
927+
858928
### Polymorphic messages
859929

860930
SMB supports working with message hierarchies.

docs/intro.t.md

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
- [Serialization](#serialization)
3131
- [Multiple message types on one topic (or queue)](#multiple-message-types-on-one-topic-or-queue)
3232
- [Message Type Resolver](#message-type-resolver)
33+
- [Custom Message Type Resolvers](#custom-message-type-resolvers)
34+
- [Use Case: Type Redirection for Evolving Services](#use-case-type-redirection-for-evolving-services)
35+
- [Using Redirects with the Default Resolver](#using-redirects-with-the-default-resolver)
3336
- [Polymorphic messages](#polymorphic-messages)
3437
- [Polymorphic producer](#polymorphic-producer)
3538
- [Polymorphic consumer](#polymorphic-consumer)
@@ -844,17 +847,84 @@ mbb.Consume<OrderEvent>(x =>
844847

845848
### Message Type Resolver
846849

847-
By default, the message header `MessageType` conveys the message type information using the assembly qualified name of the .NET type (see `AssemblyQualifiedNameMessageTypeResolver`).
850+
SlimMessageBus uses the `MessageType` header to convey the message's type information as a `string`. This allows the consumer to determine the correct .NET `Type` needed to deserialize the payload.
848851

849-
A custom resolver could be used. Some scenarios include a desire to send short type names (to optimize overall message size) or adjust interoperability with other messaging systems.
850-
The following can be used to provide a custom `IMessageTypeResolver` implementation:
852+
- **Producer Side:** Converts the `.NET Type` to a string and adds it to the message header.
853+
- **Consumer Side:** Reads the string from the header, resolves the corresponding `Type`, and passes it to the serializer to deserialize the message before handing it off to the appropriate handler.
851854

852-
```cs
853-
IMessageTypeResolver mtr = new AssemblyQualifiedNameMessageTypeResolver();
855+
Type resolution is handled via the [`IMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/IMessageTypeResolver.cs) interface, which is resolved from the dependency injection (DI) container.
856+
857+
By default, SlimMessageBus registers the [`AssemblyQualifiedNameMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs), which formats the type name as an **assembly-qualified name (without the version number)**. For example:
858+
859+
```
860+
MyNamespace.MyMessage, MyAssembly
861+
```
862+
863+
#### Custom Message Type Resolvers
864+
865+
You can provide a custom implementation of `IMessageTypeResolver` to support scenarios like:
866+
867+
- Reducing message size by using short or alias-based type names.
868+
- Integrating with other messaging systems using different naming conventions.
869+
- Supporting versioning or refactoring, such as type or namespace changes.
870+
871+
**ExampleCustom Resolver Registration**
872+
873+
```csharp
874+
public class MyShortNameMessageTypeResolver : IMessageTypeResolver
875+
{
876+
// Implement custom logic to map between Type and string.
877+
// Useful for short names, legacy mappings, etc.
878+
}
879+
880+
// Register with Microsoft DI
881+
services.AddSingleton<MyShortNameMessageTypeResolver>();
882+
883+
services.AddSlimMessageBus(mbb =>
884+
{
885+
// If this were to be a hybrid bus, each child bus can have its own type resolver
886+
mbb.WithMessageTypeResolver<MyShortNameMessageTypeResolver>();
887+
});
888+
```
854889

855-
mbb.WithMessageTypeResolver(mtr)
890+
#### Use Case: Type Redirection for Evolving Services
891+
892+
Suppose you refactor a service, moving messages to a new assembly or namespace. Since producers and consumers may be deployed independently, youll need a strategy to handle message type mismatches between versions.
893+
894+
With a custom resolver, you can:
895+
896+
- **Redirect** incoming messages that reference old type names to the updated `Type`.
897+
- **Emit** legacy type names from the producer for backward compatibility.
898+
899+
##### Using Redirects with the Default Resolver
900+
901+
If you're using the default [`AssemblyQualifiedNameMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs), you can extend it by implementing [`IAssemblyQualifiedNameMessageTypeResolverRedirect`](/src/SlimMessageBus.Host/MessageTypeResolver/IAssemblyQualifiedNameMessageTypeResolverRedirect.cs).
902+
903+
```csharp
904+
public class MyTypeResolverRedirect : IAssemblyQualifiedNameMessageTypeResolverRedirect
905+
{
906+
public Type TryGetType(string name)
907+
{
908+
if (name == "MyNamespace.MyMessage, MyAssembly")
909+
{
910+
return Type.GetType("NewNamespace.MyMessage, NewAssembly");
911+
// or return typeof(MyMessage);
912+
}
913+
return null; // no match
914+
}
915+
916+
public string TryGetName(Type messageType) => null; // No outgoing redirect
917+
}
856918
```
857919

920+
These redirect services must be registered with DI. You can register multiple for modularity:
921+
922+
```csharp
923+
services.TryAddEnumerable<IAssemblyQualifiedNameMessageTypeResolverRedirect, MyTypeResolverRedirect>();
924+
```
925+
926+
This makes it easy to maintain compatibility as your services evolve, without disrupting production systems.
927+
858928
### Polymorphic messages
859929

860930
SMB supports working with message hierarchies.

src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using SlimMessageBus.Host.Collections;
66

77
/// <summary>
8-
/// <see cref="IMessageTypeResolver"/> that uses the <see cref="Type.AssemblyQualifiedName"/> for the message type string passed in the message header.
8+
/// <see cref="IMessageTypeResolver"/> that uses the <see cref="Type.AssemblyQualifiedName"/> for mapping the <see cref="Type"/ to a string header value.
99
/// </summary>
1010
public class AssemblyQualifiedNameMessageTypeResolver : IMessageTypeResolver
1111
{
@@ -16,32 +16,59 @@ public class AssemblyQualifiedNameMessageTypeResolver : IMessageTypeResolver
1616
/// </summary>
1717
public bool EmitAssemblyStrongName { get; set; } = false;
1818

19-
private readonly SafeDictionaryWrapper<Type, string> toNameCache;
20-
private readonly SafeDictionaryWrapper<string, Type> toTypeCache;
19+
private readonly SafeDictionaryWrapper<Type, string> _toNameCache;
20+
private readonly SafeDictionaryWrapper<string, Type> _toTypeCache;
21+
private readonly IAssemblyQualifiedNameMessageTypeResolverRedirect[] _items;
2122

22-
public AssemblyQualifiedNameMessageTypeResolver()
23+
public AssemblyQualifiedNameMessageTypeResolver(IEnumerable<IAssemblyQualifiedNameMessageTypeResolverRedirect> items = null)
2324
{
24-
toNameCache = new SafeDictionaryWrapper<Type, string>(ToNameInternal);
25-
toTypeCache = new SafeDictionaryWrapper<string, Type>(ToTypeInternal);
25+
_toNameCache = new SafeDictionaryWrapper<Type, string>(ToNameInternal);
26+
_toTypeCache = new SafeDictionaryWrapper<string, Type>(ToTypeInternal);
27+
_items = items is not null ? [.. items] : [];
2628
}
2729

2830
private string ToNameInternal(Type messageType)
2931
{
30-
var assemblyQualifiedName = messageType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(messageType));
32+
if (messageType is null) throw new ArgumentNullException(nameof(messageType));
3133

32-
if (EmitAssemblyStrongName)
34+
string assemblyQualifiedName = null;
35+
36+
foreach (var item in _items)
3337
{
34-
return assemblyQualifiedName;
38+
assemblyQualifiedName = item.TryGetName(messageType);
39+
if (assemblyQualifiedName is not null)
40+
{
41+
break;
42+
}
3543
}
3644

37-
var reducedName = RedundantAssemblyTokens.Replace(assemblyQualifiedName, string.Empty);
45+
assemblyQualifiedName ??= messageType.AssemblyQualifiedName;
3846

39-
return reducedName;
47+
if (!EmitAssemblyStrongName)
48+
{
49+
assemblyQualifiedName = RedundantAssemblyTokens.Replace(assemblyQualifiedName, string.Empty);
50+
}
51+
52+
return assemblyQualifiedName;
4053
}
4154

42-
private Type ToTypeInternal(string name) => Type.GetType(name ?? throw new ArgumentNullException(nameof(name)));
55+
private Type ToTypeInternal(string name)
56+
{
57+
if (name is null) throw new ArgumentNullException(nameof(name));
58+
59+
foreach (var item in _items)
60+
{
61+
var type = item.TryGetType(name);
62+
if (type is not null)
63+
{
64+
return type;
65+
}
66+
}
67+
68+
return Type.GetType(name);
69+
}
4370

44-
public string ToName(Type messageType) => toNameCache.GetOrAdd(messageType);
71+
public string ToName(Type messageType) => _toNameCache.GetOrAdd(messageType);
4572

46-
public Type ToType(string name) => toTypeCache.GetOrAdd(name);
47-
}
73+
public Type ToType(string name) => _toTypeCache.GetOrAdd(name);
74+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace SlimMessageBus.Host;
2+
3+
public interface IAssemblyQualifiedNameMessageTypeResolverRedirect
4+
{
5+
/// <summary>
6+
/// Returns the Type if it can be resolved for the name, otherwise null.
7+
/// </summary>
8+
/// <param name="name"></param>
9+
/// <returns></returns>
10+
Type TryGetType(string name);
11+
12+
/// <summary>
13+
/// Returns the name if it can be resolved for the type, otherwise null.
14+
/// </summary>
15+
/// <param name="messageType"></param>
16+
/// <returns></returns>
17+
string TryGetName(Type messageType);
18+
}

src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public MessageBusBaseTests()
2828

2929
_serviceProviderMock = new Mock<IServiceProvider>();
3030
_serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializerProvider))).Returns(new JsonMessageSerializer());
31-
_serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver());
31+
_serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver([]));
3232
_serviceProviderMock.Setup(x => x.GetService(typeof(TimeProvider))).Returns(() => _timeProvider);
3333
_serviceProviderMock.Setup(x => x.GetService(It.Is<Type>(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Array.CreateInstance(t.GetGenericArguments()[0], 0));
3434
_serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache());
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace SlimMessageBus.Host.Test.MessageTypeResolver;
2+
3+
public class AssemblyQualifiedNameMessageTypeResolverTests
4+
{
5+
private readonly Mock<IAssemblyQualifiedNameMessageTypeResolverRedirect> _redirect;
6+
private readonly AssemblyQualifiedNameMessageTypeResolver _subject;
7+
8+
public AssemblyQualifiedNameMessageTypeResolverTests()
9+
{
10+
_redirect = new Mock<IAssemblyQualifiedNameMessageTypeResolverRedirect>();
11+
_subject = new AssemblyQualifiedNameMessageTypeResolver([_redirect.Object]);
12+
}
13+
14+
[Theory]
15+
[InlineData(typeof(SomeMessage), "SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.Test")]
16+
public void When_ToName_Given_TypeIsProvided_Then_AssemblyQualifiedNameIsReturned(Type messageType, string expectedName)
17+
{
18+
// Arrange
19+
_redirect.Setup(x => x.TryGetName(messageType)).Returns(expectedName);
20+
21+
// Act
22+
var result = _subject.ToName(messageType);
23+
24+
// Assert
25+
result.Should().Be(expectedName);
26+
}
27+
28+
[Theory]
29+
[InlineData("SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.Test", typeof(SomeMessage))]
30+
[InlineData("SlimMessageBus.Host.Test.SomeMessageV1, SlimMessageBus.Host.Test", typeof(SomeMessage))]
31+
[InlineData("SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.V1", typeof(SomeMessage))]
32+
public void When_ToType_Given_NameIsValid_Then_TypeIsReturned(string name, Type expectedMessageType)
33+
{
34+
// Arrange
35+
_redirect.Setup(x => x.TryGetType("SlimMessageBus.Host.Test.SomeMessageV1, SlimMessageBus.Host.Test")).Returns(typeof(SomeMessage));
36+
_redirect.Setup(x => x.TryGetType("SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.V1")).Returns(typeof(SomeMessage));
37+
38+
// Act
39+
var type = _subject.ToType(name);
40+
41+
// Assert
42+
type.Should().Be(expectedMessageType);
43+
}
44+
45+
}

0 commit comments

Comments
 (0)