Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 87 additions & 8 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
- [Message Scope Accessor](#message-scope-accessor)
- [Serialization](#serialization)
- [Multiple message types on one topic (or queue)](#multiple-message-types-on-one-topic-or-queue)
- [Message Type Resolver](#message-type-resolver)
- [Message Type Resolver](#message-type-resolver)
- [Custom Message Type Resolver](#custom-message-type-resolver)
- [Example: Registering a Custom Message Type Resolver](#example-registering-a-custom-message-type-resolver)
- [Use Case: Type Redirection for Evolving Services](#use-case-type-redirection-for-evolving-services)
- [Extending the Default Resolver with Redirects](#extending-the-default-resolver-with-redirects)
- [Polymorphic messages](#polymorphic-messages)
- [Polymorphic producer](#polymorphic-producer)
- [Polymorphic consumer](#polymorphic-consumer)
Expand Down Expand Up @@ -842,18 +846,93 @@ mbb.Consume<OrderEvent>(x =>

![Multiple messages types on one topic](/docs/images/SlimMessageBus%20-%20Multiple%20message%20types%20on%20one%20topic.jpg)

### Message Type Resolver
## Message Type Resolver

By default, the message header `MessageType` conveys the message type information using the assembly qualified name of the .NET type (see `AssemblyQualifiedNameMessageTypeResolver`).
SlimMessageBus uses a message header named `MessageType` to communicate the type information of messages as a `string`.
This header allows consumers to identify the appropriate .NET `Type` required to deserialize the message payload correctly.

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.
The following can be used to provide a custom `IMessageTypeResolver` implementation:
- **Producer side**: Converts the .NET `Type` into a string and adds it to the message header.
- **Consumer side**: Reads the type information from the header, resolves it back to the corresponding .NET `Type`, and then passes this type information to the serializer. Once deserialized, the message is passed to its registered handler.

```cs
IMessageTypeResolver mtr = new AssemblyQualifiedNameMessageTypeResolver();
The type resolution logic is encapsulated within the [`IMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/IMessageTypeResolver.cs) interface, typically provided via dependency injection (DI).

By default, SlimMessageBus uses the built-in [`AssemblyQualifiedNameMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs). This resolver formats type names as assembly-qualified names without version information.
For example:

mbb.WithMessageTypeResolver(mtr)
```
MyNamespace.MyMessage, MyAssembly
```

> **Note:**
> If a `MessageType` header is missing—such as when messages originate from non-SMB producers—SlimMessageBus assumes the message type based on the consumer definition for that specific Topic or Queue. However, if the consumer handles multiple message types, resolution fails, and the message processing will be unsuccessful.

### Custom Message Type Resolver

SlimMessageBus allows you to implement custom message type resolvers by creating your own implementations of `IMessageTypeResolver`. Common scenarios for custom resolvers include:

- Reducing message overhead by utilizing short or alias-based type identifiers.
- Integrating with external messaging platforms that use different naming conventions.
- Managing versioning and structural refactorings, such as changes in namespaces or assemblies.

#### Example: Registering a Custom Message Type Resolver

Below is an example of how to create and register a custom resolver:

```csharp
public class MyShortNameMessageTypeResolver : IMessageTypeResolver
{
// Custom logic to map .NET Type to string representation and vice versa.
// Useful for shorter identifiers or legacy mappings.
}

// Registration with Microsoft's DI container
services.AddSingleton<MyShortNameMessageTypeResolver>();

services.AddSlimMessageBus(mbb =>
{
// Hybrid buses can specify distinct resolvers per child bus
mbb.WithMessageTypeResolver<MyShortNameMessageTypeResolver>();
});
```

### Use Case: Type Redirection for Evolving Services

When refactoring or evolving your service—such as moving message types to new assemblies or namespaces—consumers and producers might temporarily reference different versions of a message type. In this scenario, type redirection becomes necessary to ensure compatibility.

Custom resolvers help you:

- **Redirect** legacy message type names to their new corresponding `Type`.
- **Emit** legacy type names for backward compatibility with older consumers.

#### Extending the Default Resolver with Redirects

If you prefer to retain the default resolver (`AssemblyQualifiedNameMessageTypeResolver`) but need type redirection capability, implement the [`IAssemblyQualifiedNameMessageTypeResolverRedirect`](/src/SlimMessageBus.Host/MessageTypeResolver/IAssemblyQualifiedNameMessageTypeResolverRedirect.cs) interface:

```csharp
public class MyTypeResolverRedirect : IAssemblyQualifiedNameMessageTypeResolverRedirect
{
public Type TryGetType(string name)
{
if (name == "MyNamespace.MyMessage, MyAssembly")
{
return Type.GetType("NewNamespace.MyMessage, NewAssembly");
// or directly: return typeof(NewNamespace.MyMessage);
}
return null; // Return null if there's no redirect match.
}

public string TryGetName(Type messageType) => null; // No outgoing redirects
}
```

These redirect implementations must also be registered in the DI container.
You can register multiple redirectors for flexibility and modularity:

```csharp
services.TryAddEnumerable<IAssemblyQualifiedNameMessageTypeResolverRedirect, MyTypeResolverRedirect>();
```

This approach simplifies maintaining compatibility during service evolution, avoiding disruptions to running production systems.

### Polymorphic messages

Expand Down
95 changes: 87 additions & 8 deletions docs/intro.t.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
- [Message Scope Accessor](#message-scope-accessor)
- [Serialization](#serialization)
- [Multiple message types on one topic (or queue)](#multiple-message-types-on-one-topic-or-queue)
- [Message Type Resolver](#message-type-resolver)
- [Message Type Resolver](#message-type-resolver)
- [Custom Message Type Resolver](#custom-message-type-resolver)
- [Example: Registering a Custom Message Type Resolver](#example-registering-a-custom-message-type-resolver)
- [Use Case: Type Redirection for Evolving Services](#use-case-type-redirection-for-evolving-services)
- [Extending the Default Resolver with Redirects](#extending-the-default-resolver-with-redirects)
- [Polymorphic messages](#polymorphic-messages)
- [Polymorphic producer](#polymorphic-producer)
- [Polymorphic consumer](#polymorphic-consumer)
Expand Down Expand Up @@ -842,18 +846,93 @@ mbb.Consume<OrderEvent>(x =>

![Multiple messages types on one topic](/docs/images/SlimMessageBus%20-%20Multiple%20message%20types%20on%20one%20topic.jpg)

### Message Type Resolver
## Message Type Resolver

By default, the message header `MessageType` conveys the message type information using the assembly qualified name of the .NET type (see `AssemblyQualifiedNameMessageTypeResolver`).
SlimMessageBus uses a message header named `MessageType` to communicate the type information of messages as a `string`.
This header allows consumers to identify the appropriate .NET `Type` required to deserialize the message payload correctly.

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.
The following can be used to provide a custom `IMessageTypeResolver` implementation:
- **Producer side**: Converts the .NET `Type` into a string and adds it to the message header.
- **Consumer side**: Reads the type information from the header, resolves it back to the corresponding .NET `Type`, and then passes this type information to the serializer. Once deserialized, the message is passed to its registered handler.

```cs
IMessageTypeResolver mtr = new AssemblyQualifiedNameMessageTypeResolver();
The type resolution logic is encapsulated within the [`IMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/IMessageTypeResolver.cs) interface, typically provided via dependency injection (DI).

By default, SlimMessageBus uses the built-in [`AssemblyQualifiedNameMessageTypeResolver`](/src/SlimMessageBus.Host/MessageTypeResolver/AssemblyQualifiedNameMessageTypeResolver.cs). This resolver formats type names as assembly-qualified names without version information.
For example:

mbb.WithMessageTypeResolver(mtr)
```
MyNamespace.MyMessage, MyAssembly
```

> **Note:**
> If a `MessageType` header is missing—such as when messages originate from non-SMB producers—SlimMessageBus assumes the message type based on the consumer definition for that specific Topic or Queue. However, if the consumer handles multiple message types, resolution fails, and the message processing will be unsuccessful.

### Custom Message Type Resolver

SlimMessageBus allows you to implement custom message type resolvers by creating your own implementations of `IMessageTypeResolver`. Common scenarios for custom resolvers include:

- Reducing message overhead by utilizing short or alias-based type identifiers.
- Integrating with external messaging platforms that use different naming conventions.
- Managing versioning and structural refactorings, such as changes in namespaces or assemblies.

#### Example: Registering a Custom Message Type Resolver

Below is an example of how to create and register a custom resolver:

```csharp
public class MyShortNameMessageTypeResolver : IMessageTypeResolver
{
// Custom logic to map .NET Type to string representation and vice versa.
// Useful for shorter identifiers or legacy mappings.
}

// Registration with Microsoft's DI container
services.AddSingleton<MyShortNameMessageTypeResolver>();

services.AddSlimMessageBus(mbb =>
{
// Hybrid buses can specify distinct resolvers per child bus
mbb.WithMessageTypeResolver<MyShortNameMessageTypeResolver>();
});
```

### Use Case: Type Redirection for Evolving Services

When refactoring or evolving your service—such as moving message types to new assemblies or namespaces—consumers and producers might temporarily reference different versions of a message type. In this scenario, type redirection becomes necessary to ensure compatibility.

Custom resolvers help you:

- **Redirect** legacy message type names to their new corresponding `Type`.
- **Emit** legacy type names for backward compatibility with older consumers.

#### Extending the Default Resolver with Redirects

If you prefer to retain the default resolver (`AssemblyQualifiedNameMessageTypeResolver`) but need type redirection capability, implement the [`IAssemblyQualifiedNameMessageTypeResolverRedirect`](/src/SlimMessageBus.Host/MessageTypeResolver/IAssemblyQualifiedNameMessageTypeResolverRedirect.cs) interface:

```csharp
public class MyTypeResolverRedirect : IAssemblyQualifiedNameMessageTypeResolverRedirect
{
public Type TryGetType(string name)
{
if (name == "MyNamespace.MyMessage, MyAssembly")
{
return Type.GetType("NewNamespace.MyMessage, NewAssembly");
// or directly: return typeof(NewNamespace.MyMessage);
}
return null; // Return null if there's no redirect match.
}

public string TryGetName(Type messageType) => null; // No outgoing redirects
}
```

These redirect implementations must also be registered in the DI container.
You can register multiple redirectors for flexibility and modularity:

```csharp
services.TryAddEnumerable<IAssemblyQualifiedNameMessageTypeResolverRedirect, MyTypeResolverRedirect>();
```

This approach simplifies maintaining compatibility during service evolution, avoiding disruptions to running production systems.

### Polymorphic messages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using SlimMessageBus.Host.Collections;

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

private readonly SafeDictionaryWrapper<Type, string> toNameCache;
private readonly SafeDictionaryWrapper<string, Type> toTypeCache;
private readonly SafeDictionaryWrapper<Type, string> _toNameCache;
private readonly SafeDictionaryWrapper<string, Type> _toTypeCache;
private readonly IAssemblyQualifiedNameMessageTypeResolverRedirect[] _items;

public AssemblyQualifiedNameMessageTypeResolver()
public AssemblyQualifiedNameMessageTypeResolver(IEnumerable<IAssemblyQualifiedNameMessageTypeResolverRedirect> items = null)
{
toNameCache = new SafeDictionaryWrapper<Type, string>(ToNameInternal);
toTypeCache = new SafeDictionaryWrapper<string, Type>(ToTypeInternal);
_toNameCache = new SafeDictionaryWrapper<Type, string>(ToNameInternal);
_toTypeCache = new SafeDictionaryWrapper<string, Type>(ToTypeInternal);
_items = items is not null ? [.. items] : [];
}

private string ToNameInternal(Type messageType)
{
var assemblyQualifiedName = messageType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(messageType));
if (messageType is null) throw new ArgumentNullException(nameof(messageType));

if (EmitAssemblyStrongName)
string assemblyQualifiedName = null;

foreach (var item in _items)
{
return assemblyQualifiedName;
assemblyQualifiedName = item.TryGetName(messageType);
if (assemblyQualifiedName is not null)
{
break;
}
}

var reducedName = RedundantAssemblyTokens.Replace(assemblyQualifiedName, string.Empty);
assemblyQualifiedName ??= messageType.AssemblyQualifiedName;

return reducedName;
if (!EmitAssemblyStrongName)
{
assemblyQualifiedName = RedundantAssemblyTokens.Replace(assemblyQualifiedName, string.Empty);
}

return assemblyQualifiedName;
}

private Type ToTypeInternal(string name) => Type.GetType(name ?? throw new ArgumentNullException(nameof(name)));
private Type ToTypeInternal(string name)
{
if (name is null) throw new ArgumentNullException(nameof(name));

foreach (var item in _items)
{
var type = item.TryGetType(name);
if (type is not null)
{
return type;
}
}

return Type.GetType(name);
}

public string ToName(Type messageType) => toNameCache.GetOrAdd(messageType);
public string ToName(Type messageType) => _toNameCache.GetOrAdd(messageType);

public Type ToType(string name) => toTypeCache.GetOrAdd(name);
}
public Type ToType(string name) => _toTypeCache.GetOrAdd(name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace SlimMessageBus.Host;

public interface IAssemblyQualifiedNameMessageTypeResolverRedirect
{
/// <summary>
/// Returns the Type if it can be resolved for the name, otherwise null.
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
Type TryGetType(string name);

/// <summary>
/// Returns the name if it can be resolved for the type, otherwise null.
/// </summary>
/// <param name="messageType"></param>
/// <returns></returns>
string TryGetName(Type messageType);
}
2 changes: 1 addition & 1 deletion src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public MessageBusBaseTests()

_serviceProviderMock = new Mock<IServiceProvider>();
_serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializerProvider))).Returns(new JsonMessageSerializer());
_serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver());
_serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver([]));
_serviceProviderMock.Setup(x => x.GetService(typeof(TimeProvider))).Returns(() => _timeProvider);
_serviceProviderMock.Setup(x => x.GetService(It.Is<Type>(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Array.CreateInstance(t.GetGenericArguments()[0], 0));
_serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace SlimMessageBus.Host.Test.MessageTypeResolver;

public class AssemblyQualifiedNameMessageTypeResolverTests
{
private readonly Mock<IAssemblyQualifiedNameMessageTypeResolverRedirect> _redirect;
private readonly AssemblyQualifiedNameMessageTypeResolver _subject;

public AssemblyQualifiedNameMessageTypeResolverTests()
{
_redirect = new Mock<IAssemblyQualifiedNameMessageTypeResolverRedirect>();
_subject = new AssemblyQualifiedNameMessageTypeResolver([_redirect.Object]);
}

[Theory]
[InlineData(typeof(SomeMessage), "SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.Test")]
public void When_ToName_Given_TypeIsProvided_Then_AssemblyQualifiedNameIsReturned(Type messageType, string expectedName)
{
// Arrange
_redirect.Setup(x => x.TryGetName(messageType)).Returns(expectedName);

// Act
var result = _subject.ToName(messageType);

// Assert
result.Should().Be(expectedName);
}

[Theory]
[InlineData("SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.Test", typeof(SomeMessage))]
[InlineData("SlimMessageBus.Host.Test.SomeMessageV1, SlimMessageBus.Host.Test", typeof(SomeMessage))]
[InlineData("SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.V1", typeof(SomeMessage))]
public void When_ToType_Given_NameIsValid_Then_TypeIsReturned(string name, Type expectedMessageType)
{
// Arrange
_redirect.Setup(x => x.TryGetType("SlimMessageBus.Host.Test.SomeMessageV1, SlimMessageBus.Host.Test")).Returns(typeof(SomeMessage));
_redirect.Setup(x => x.TryGetType("SlimMessageBus.Host.Test.SomeMessage, SlimMessageBus.Host.V1")).Returns(typeof(SomeMessage));

// Act
var type = _subject.ToType(name);

// Assert
type.Should().Be(expectedMessageType);
}

}
Loading