Skip to content

Commit d2efb27

Browse files
committed
[Host.Memory] mbb.AutoDeclareFrom() to use message type FullName by default
Signed-off-by: Tomasz Maruszak <[email protected]>
1 parent 975aa6d commit d2efb27

File tree

5 files changed

+195
-139
lines changed

5 files changed

+195
-139
lines changed

docs/provider_memory.md

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Please read the [Introduction](intro.md) before reading this provider documentat
66
- [Configuration](#configuration)
77
- [Virtual Topics](#virtual-topics)
88
- [Auto Declaration](#auto-declaration)
9-
- [Polymorphic message support](#polymorphic-message-support)
9+
- [Polymorphic Message Support](#polymorphic-message-support)
1010
- [Serialization](#serialization)
1111
- [Headers](#headers)
1212
- [Lifecycle](#lifecycle)
@@ -19,125 +19,153 @@ Please read the [Introduction](intro.md) before reading this provider documentat
1919

2020
## Introduction
2121

22-
The Memory transport provider can be used for internal communication within the same process. It is the simplest transport provider and does not require any external messaging infrastructure.
22+
The **Memory transport provider** enables message-based communication within a single process. It’s the simplest transport option in SlimMessageBus and doesn’t require any external infrastructure like Kafka or Azure Service Bus.
2323

24-
> Since messages are passed in memory and never persisted, they will be lost if the application process terminates while consuming these messages.
24+
> ⚠️ Messages are passed entirely in memory and are never persisted. If the application process terminates while messages are in-flight, those messages will be lost.
2525
26-
Good use case for in memory communication is:
26+
**Common use cases for in-memory transport include:**
2727

28-
- to integrate the domain layer with other application layers via domain events pattern,
29-
- to implement mediator pattern (when combined with [interceptors](intro.md#interceptors)),
30-
- to run unit tests against application code that normally runs with an out of process transport provider (Kafka, Azure Service Bus, etc),
31-
- to start simple messaging without having to provision messaging infrastructure, but when time comes reconfigure SMB to leverage messaging infrastructure.
28+
- Integrating the domain layer with other application layers using the **domain events** pattern.
29+
- Implementing the **mediator pattern** (especially when combined with [interceptors](intro.md#interceptors)).
30+
- Writing **unit tests** for messaging logic without requiring a full transport setup.
31+
- Starting with messaging quickly and easily — no infrastructure required — and switching to an external provider later by reconfiguring SlimMessageBus.
32+
33+
---
3234

3335
## Configuration
3436

35-
The memory transport is configured using the `.WithProviderMemory()`:
37+
First, install the NuGet package:
3638

37-
```cs
39+
```bash
40+
dotnet add package SlimMessageBus.Host.Memory
41+
```
42+
43+
Then, configure the memory transport using `.WithProviderMemory()`:
44+
45+
```csharp
3846
using SlimMessageBus.Host.Memory;
3947

4048
services.AddSlimMessageBus(mbb =>
4149
{
42-
// Bus configuration happens here (...)
43-
mbb.WithProviderMemory(); // requires SlimMessageBus.Host.Memory package
50+
// Configure your bus here...
51+
mbb.WithProviderMemory(); // Requires SlimMessageBus.Host.Memory package
4452
});
4553
```
4654

55+
> 💡 No serializer is needed by default for the in-memory transport — unless you [opt in to serialization](#serialization).
56+
4757
### Virtual Topics
4858

49-
Unlike other transport providers, memory transport does not have true notion of topics (or queues). However, it is still required to use topic names. This is required, so that the bus knows on which virtual topic to deliver the message to, and from what virtual topic to consume from.
59+
The in-memory transport doesn't use real topics or queues like other transport providers. However, **virtual topic names** are still required. These names allow the bus to correctly route messages to and from the appropriate consumers.
5060

51-
The consumer configuration side should use `.Topic()` to set the virtual topic name:
61+
On the **consumer side**, use `.Topic()` to specify the virtual topic:
5262

53-
```cs
54-
// declare that OrderSubmittedEvent will be consumed
63+
```csharp
64+
// Register a consumer for OrderSubmittedEvent
5565
mbb.Consume<OrderSubmittedEvent>(x => x.Topic(x.MessageType.Name).WithConsumer<OrderSubmittedHandler>());
5666

57-
// alternatively
67+
// Or use a hardcoded topic name
5868
mbb.Consume<OrderSubmittedEvent>(x => x.Topic("OrderSubmittedEvent").WithConsumer<OrderSubmittedHandler>());
5969
```
6070

61-
The producer configuration side should use `.DefaultTopic()` to set the virtual topic name:
71+
On the **producer side**, use `.DefaultTopic()` to define where messages should be published:
6272

63-
```cs
73+
```csharp
6474
mbb.Produce<OrderSubmittedEvent>(x => x.DefaultTopic("OrderSubmittedEvent"));
6575
```
6676

67-
> The virtual topic name can be any string. It helps to connect the relevant producers and consumers together.
77+
> Virtual topic names can be any string, as long as producers and consumers use the same value. This is what links them together internally.
6878
6979
### Auto Declaration
7080

71-
For bus configuration, we can leverage `.AutoDeclareFrom()` method to discover all the consumers (`IConsumer<T>`) and handlers (`IRequestHandler<T,R>`) types in an assembly and auto declare the respective producers and consumers/handlers in the bus.
72-
This can be useful to auto declare all of the domain event handlers in an application layer.
81+
You can simplify your bus configuration using the `.AutoDeclareFrom()` method, which scans an assembly to automatically discover all consumers (`IConsumer<T>`) and handlers (`IRequestHandler<T,R>`). It then declares the corresponding producers and consumers/handlers on the bus for you.
82+
This is especially useful for automatically registering all domain event handlers within an application layer.
7383

74-
```cs
84+
Example usage:
85+
86+
```csharp
7587
mbb
7688
.WithProviderMemory()
7789
.AutoDeclareFrom(Assembly.GetExecutingAssembly());
78-
// Alternatively specify the type of which assembly should be scanned
79-
// .AutoDeclareFromAssemblyContaining<CustomerCreateHandler>();
8090

81-
// If we want to filter to specific consumer/handler types then we can supply an additional filter:
82-
// .AutoDeclareFrom(Assembly.GetExecutingAssembly(), consumerTypeFilter: (consumerType) => consumerType.Name.EndsWith("Handler"));
83-
// .AutoDeclareFromAssemblyContaining<CustomerCreateHandler>(Assembly.GetExecutingAssembly(), consumerTypeFilter: (consumerType) => consumerType.Name.EndsWith("Handler"));
91+
// Alternatively, specify a type from the assembly you want to scan:
92+
// .AutoDeclareFromAssemblyContaining<CustomerCreateHandler>();
93+
94+
// You can also filter which consumers/handlers are picked up:
95+
// .AutoDeclareFrom(Assembly.GetExecutingAssembly(), consumerTypeFilter: consumerType => consumerType.Name.EndsWith("Handler"));
96+
// .AutoDeclareFromAssemblyContaining<CustomerCreateHandler>(consumerTypeFilter: consumerType => consumerType.Name.EndsWith("Handler"));
8497
```
8598

86-
For example, assuming this is the discovered handler type:
99+
For example, given a handler like this:
87100

88-
```cs
101+
```csharp
89102
public class EchoRequestHandler : IRequestHandler<EchoRequest, EchoResponse>
90103
{
91-
public Task<EchoResponse> OnHandle(EchoRequest request, CancellationToken cancellationToken) { /* ... */ }
104+
public Task<EchoResponse> OnHandle(EchoRequest request, CancellationToken cancellationToken)
105+
{
106+
// ...
107+
}
92108
}
93109
```
94110

95-
The bus auto registrations will set-up the producer and handler to the equivalent:
111+
The bus will automatically configure registrations equivalent to:
96112

97-
```cs
98-
mbb.Produce<EchoRequest>(x => x.DefaultTopic(x.MessageType.Name));
99-
mbb.Handle<EchoRequest, EchoResponse>(x => x.Topic(x.MessageType.Name).WithConsumer<EchoRequestHandler>());
113+
```csharp
114+
mbb.Produce<EchoRequest>(x => x.DefaultTopic(x.MessageType.FullName));
115+
mbb.Handle<EchoRequest, EchoResponse>(x => x.Topic(x.MessageType.FullName).WithConsumer<EchoRequestHandler>());
116+
```
117+
118+
By default, the virtual topic name is derived from the message type’s [`FullName`](https://learn.microsoft.com/en-us/dotnet/api/system.type.fullname?view=net-9.0) — meaning it includes both the namespace and any outer class names.
119+
You can customize this by providing your own `messageTypeToTopicConverter`:
120+
121+
```csharp
122+
services.AddSlimMessageBus(builder =>
123+
builder.WithProviderMemory()
124+
.AutoDeclareFrom(Assembly.GetExecutingAssembly(), messageTypeToTopicConverter: messageType => messageType.FullName)
125+
);
100126
```
101127

102-
The virtual topic name will be derived from the message type name by default. This can be customized by passing an additional parameter to the `AutoDeclareFrom()` method.
128+
Using `.AutoDeclareFrom()` is highly recommended when configuring the memory bus, as it ensures producers and consumers are kept up to date automatically as your application evolves.
103129

104-
Using `.AutoDeclareFrom()` to configure the memory bus is recommended, as it will declare the producers and consumers automatically as consumer types are added over time.
130+
> Note: `.AutoDeclareFrom()` and `.AutoDeclareFromAssemblyContaining<T>()` will also register discovered consumers and handlers into the Microsoft Dependency Injection (MSDI) container. [Learn more here](intro.md#autoregistration-of-consumers-interceptors-and-configurators).
105131
106-
> The `.AutoDeclareFrom()` and `.AutoDeclareFromAssembly<T>()` will also register the found consumers/handlers into MSDI (see [here](intro.md#autoregistration-of-consumers-interceptors-and-configurators)).
132+
#### Polymorphic Message Support
107133

108-
#### Polymorphic message support
134+
The memory transport supports **polymorphic message types**—messages that share a common base class or interface—via the `AutoDeclareFrom()` method.
109135

110-
The polymorphic message types (message that share a common ancestry) are supported by the `AutoDeclareFrom()`:
136+
Here’s how it works:
111137

112-
- for every consumer / handler implementation found it analyzes the message type inheritance tree,
113-
- it declares a producer in the bus for the oldest ancestor of the message type hierarchy,
114-
- it declares a consumer in the bus for the oldest ancestor message type and within that, configures a consumer for derived message type,
115-
- topic names are derived from the ancestor message type.
138+
- For each discovered consumer or handler, the message type's inheritance hierarchy is analyzed.
139+
- A producer is registered for the **root ancestor** of the message type.
140+
- A consumer is registered for the **root ancestor**, and configured to handle the specific **derived** message type.
141+
- The topic name is based on the **ancestor message type**, ensuring consistent routing for all related messages.
142+
143+
This allows you to declare base-level contracts and consume polymorphic messages seamlessly.
116144

117145
### Serialization
118146

119-
Since messages are passed within the same process, serializing and deserializing them is redundant. Also, disabling serialization gives a performance improvement.
147+
Because messages are exchanged entirely within the same process, serialization is typically unnecessary—and skipping it can yield performance benefits.
120148

121-
> Serialization is disabled by default for memory bus.
149+
> 🟢 **Serialization is disabled by default** for the memory transport.
122150
123-
Serialization can be disabled or enabled:
151+
However, you can opt-in to serialization if needed—for example, to simulate production scenarios during testing or to enforce immutability:
124152

125-
```cs
153+
```csharp
126154
services.AddSlimMessageBus(mbb =>
127155
{
128-
// Bus configuration happens here (...)
129-
mbb.WithProviderMemory(cfg =>
130-
{
131-
// Serialize the domain events instead of passing the same instance across to handlers/consumers
132-
cfg.EnableMessageSerialization = true
133-
});
134-
// Serializer not needed if EnableMessageSerialization = false
135-
mbb.AddJsonSerializer();
156+
mbb.WithProviderMemory(cfg =>
157+
{
158+
// Enable serialization for in-memory messages
159+
cfg.EnableMessageSerialization = true;
160+
});
161+
162+
// Required only if serialization is enabled
163+
mbb.AddJsonSerializer();
136164
});
137165
```
138166

139-
> When serialization is disabled for in memory passed messages, the exact same object instance send by the producer will be received by the consumer. Therefore state changes on the consumer end will be visible by the producer.
140-
> Consider making the messages immutable (read only) in that case.
167+
> ⚠️ When serialization is disabled, the **same object instance** sent by the producer is received by the consumer.
168+
> This means any changes made by the consumer will be visible to the producer. To avoid side effects, it’s recommended to use **immutable message types** in such cases.
141169
142170
### Headers
143171

0 commit comments

Comments
 (0)