Skip to content

Commit 35c820d

Browse files
author
Sharifuzzaman Nakib
committed
Overview and Versioning section on Message.
1 parent 8cb284d commit 35c820d

6 files changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using MassTransit;
2+
using Shared;
3+
4+
namespace OrderService.Consumers;
5+
6+
public class SubmitOrderConsumer : IConsumer<SubmitOrder>
7+
{
8+
public async Task Consume(ConsumeContext<SubmitOrder> context)
9+
{
10+
Console.WriteLine($"Inside SubmitOrderConsumer. Consuming the Message, OrderId: {context.Message.OrderId}");
11+
12+
await Task.Delay(5000);
13+
14+
var processOrder = new ProcessOrder
15+
{
16+
OrderId = context.Message.OrderId,
17+
ExpectedShippingDate = context.Message.ExpectedShippingDate //Manually propagating the property
18+
};
19+
await context.Publish(processOrder);
20+
Console.WriteLine("ProcessOrder Message is published.");
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using MassTransit;
2+
using Shared;
3+
4+
namespace OrderService.Consumers;
5+
6+
public class SubmitOrderConsumer : IConsumer<SubmitOrder>
7+
{
8+
public async Task Consume(ConsumeContext<SubmitOrder> context)
9+
{
10+
Console.WriteLine($"Inside SubmitOrderConsumer. Consuming the Message, OrderId: {context.Message.OrderId}");
11+
12+
await Task.Delay(5000);
13+
14+
var processOrder = new ProcessOrder
15+
{
16+
OrderId = context.Message.OrderId,
17+
ExtensionData = context.Message.ExtensionData // Propagating additional properties via ExtensionData
18+
};
19+
await context.Publish(processOrder);
20+
Console.WriteLine("ProcessOrder Message is published.");
21+
}
22+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Messages are the means of Communication between services. Publishers publish messages, and Consumers consume.
2+
3+
---
4+
5+
### Defining Message Contracts with CLR Types
6+
7+
OpenTransit supports **Classes**, **Records**, and **Interfaces** to define message Contracts.
8+
9+
In message broker systems, internally, the message is serialized (e.g., to JSON) and transmitted over the network.
10+
11+
However, since Opentransit provides strongly typed representations via Classes, Records, or Interfaces,
12+
it allows us to establish a clear contract between services and effectively manage [message versions](./versioning).
13+
14+
15+
<br/>
16+
17+
Under the hood, OpenTransit not only handles serialization and deserialization but also performs the additional tasks required for distributed-system communication.
18+
This includes **wrapping messages in envelopes** and **attaching metadata** such as **source**, **destination**, **message type**, **correlation ID**, etc.
19+
20+
These responsibilities are encapsulated within the infrastructure layer, so in most cases, publishers and subscribers do not need to be concerned with them.
21+
By abstracting away these infrastructure details, OpenTransit provides a clean, method-like syntax for publishing and consuming messages.
22+
We will explore this in more detail in the Message Envelope section.
23+
24+
---
25+
26+
### Message Design Guidelines
27+
28+
When defining message contracts, what follows is general guidance based upon years of using MassTransit(till V8) combined with continued questions raised by developers new to
29+
MassTransit(till V8).
30+
31+
As we continue to evolve OpenTransit, we will refine these guidelines further.
32+
33+
> [!TIP]
34+
> Use records, define properties as `public` and specify `{ get; init; }` accessors. Create messages using the constructor/object initializer or a message initializer.
35+
36+
> [!TIP]
37+
> Use interfaces, specify only `{ get; }` accessors. Create messages using message initializers and use the Roslyn Analyzer to identify missing or incompatible properties.
38+
39+
> [!WARNING]
40+
> Message design is not object-oriented design. Messages should contain state, not behavior. Behavior should be in a separate class or service.
41+
42+
> [!WARNING]
43+
> Limit the use of interface inheritance, pay attention to polymorphic message routing. A message type containing a dozen interfaces is a bit annoying to untangle if you need to delve deep into message routing to troubleshoot an issue.
44+
45+
> [!CAUTION]
46+
> * Class inheritance has the same guidance as interfaces, but with more caution.
47+
> * Consuming a base class type, and expecting polymorphic method behavior almost always leads to problems.
48+
> * A big base class may cause pain down the road as changes are made, particularly when supporting multiple message versions.
49+
50+
---
51+
52+
### Sharing Messages Between Services
53+
54+
<br/>
55+
56+
> [!IMPORTANT]
57+
> When two applications or services exchange the same message(for example, when one application publishes a message and another consumes it) OpenTransit has a single strict requirement for correct operation:
58+
the message must use **the same namespace** in both systems.
59+
60+
OpenTransit uses the combination of **message name** and **namespace** to define the broker topology, which is why this requirement is enforced.
61+
62+
OpenTransit does not require the publisher and subscriber to share a NuGet package, a common project reference, or any shared code. Message contracts may be defined independently in each application, provided the same message uses the same namespace in both contexts.
63+
64+
However, it is recommended to keep the Message Contracts in a class Library and share them between the applications via Nuget Reference(especially if you need Versioning).
65+
66+
---
67+
68+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- name: Overview
2+
href: overview.md
3+
- name: Versioning
4+
href: versioning.md
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Message Versioning
2+
3+
<br/>
4+
5+
> [!Note]
6+
> OpenTransit does not impose any opinion on how message contracts should be handled as they evolve over time. That responsibility is entirely yours.
7+
That said, this documentation outlines recommended guidelines you may choose to follow.
8+
9+
As an application’s business logic evolves, its message contracts may need to evolve as well.
10+
For example, properties may be added, removed, or renamed within a message type.
11+
12+
When such changes occur, the class library containing the message contract is updated and a new NuGet version is published.
13+
The updated message contract package is then referenced **only** by the producer and/or consumer applications that **require** the new behavior,
14+
along with the corresponding business logic changes, then they are deployed.
15+
16+
> [!Important]
17+
> For proper versioning and long-term maintainability, it is recommended to use a **shared class library as NuGet package** that contains the message contracts.
18+
Throughout this documentation, we assume this approach is being followed.
19+
20+
Since not all applications are updated at the same time, the system can end up running with ***multiple versions*** of the same message contract simultaneously.
21+
22+
This concept, **message multi-versioning** must be handled with great care, as it can easily lead to system failures.
23+
For example, a newer consumer may treat a property as required, while an older producer does not send that property at all.
24+
In such cases, the consumer may fail at runtime due to missing data.
25+
26+
This is why proper message versioning is critical in distributed systems.
27+
28+
In the following sections, we will analyze several hypothetical examples.
29+
By carefully reviewing these examples, you should be able to handle message versioning correctly in most real-world situations.
30+
31+
If you encounter an edge case or scenario that is not covered here, feel free to discuss it with us. We will incorporate relevant findings into the documentation.
32+
33+
---
34+
35+
<br/>
36+
37+
## The Scenarios
38+
39+
There is a Producer Application P1 that publishes an Event Message of Type M. Two Consumer Applications, C1 and C2, both consume the Event Message M.
40+
41+
We will explain 3 scenarios: **adding**, **renaming**, and **removing** a property on a Message Contract.
42+
43+
<br/>
44+
45+
### Scenario 1: Adding an extra Property
46+
47+
Let’s say the business evolves, and the C1 consumer needs an extra parameter.
48+
So a new property is added to M, NuGet is published, both P1 and C1’s NuGet packages are updated, business logic is added, and deployment is done.
49+
50+
However, C2 has no business with the new property, so it isn’t redeployed and continues to use the old contract version, which doesn't include the newly added property.
51+
52+
So what could go wrong after P1 and C1’s new deployment? And how to solve. Let’s see.
53+
54+
<br/>
55+
56+
##### The Publisher P1(with updated Message Contract)
57+
58+
Publishing isn’t an issue. It always works. Problems happen with Consumers.
59+
60+
<br/>
61+
62+
##### The Consumer C2(with old Message Contract)
63+
64+
The Consumer C2 will work perfectly fine.
65+
Yes, it will receive the message M with an extra property. However, getting extra property also isn’t an issue. The serializer will simply ignore it.
66+
Since C2 has no business with that extra property, everything works fine.
67+
68+
<br/>
69+
70+
##### The Consumer C1(with updated Message Contract)
71+
72+
If, after the deployment, the Consumer C1 gets any **Old** message(that doesn’t contain the new property),
73+
it will break unless the newly added Property on the Message Type is **nullable**.
74+
75+
**How C1 may get an old message?**
76+
77+
- There were already some old messages in the broker. As soon as the new C1 deployment starts consuming, it will retrieve those old messages.
78+
- Even if there are no old messages on the broker, when you deploy two applications in parallel, for example, in Kubernetes, the P1 rollout may complete later than C1.
79+
80+
<br/>
81+
<br/>
82+
83+
84+
### Scenario 2: Renaming a Property
85+
86+
Now imagine the same Scenario as before, however, instead of adding a new property, an existing property is renamed.
87+
88+
What could go wrong after P1 and C1’s new deployment? And how to solve it. Let’s see.
89+
90+
##### The Publisher P1(with updated Message Contract)
91+
92+
Publishing, as always, works fine.
93+
94+
<br/>
95+
96+
##### The Consumer C2(with old Message Contract)
97+
98+
The Consumer C2 is using the old contract. So it won’t recognize the **new** property name and will simply ignore it on deserialization.
99+
100+
However, the **old** property value will be null for new messages, which will cause an issue.
101+
102+
(However, if the **old** property value was **nullable** and sending null doesn’t hamper the business, then you are okay.)
103+
104+
<br/>
105+
106+
##### The Consumer C1(with updated Message Contract)
107+
108+
The Consumer C1 will break if it gets any **old** message because the old message doesn’t contain the renamed property.
109+
110+
However, ([if the serializer supports](https://stackoverflow.com/questions/66416800/mapping-multiple-json-property-names-to-the-same-property-in-system-text-json))
111+
this can be easily solved by instructing the serializer to look for both the old and the renamed value when deserializing by adding a private property on the message.
112+
113+
If the serializer trick isn't possible then the renamed property should be made **nullable**(if business allows) to avoid breaking C1 when it gets old messages.
114+
115+
<br/>
116+
<br/>
117+
118+
### Scenario 3: Removing a Property
119+
120+
Same scenario as above, but here we will **remove** a property rather than add or rename one. Let’s see what happens.
121+
122+
<br/>
123+
124+
##### The Publisher P1(with updated Message Contract)
125+
126+
Publishing, as always, works fine.
127+
128+
<br/>
129+
130+
##### The Consumer C2(with old Message Contract)
131+
132+
Will break (unless the removed property was **nullable** in the old contract and a null value doesn’t affect the business).
133+
134+
<br/>
135+
136+
##### The Consumer C1(with updated Message Contract)
137+
138+
Old messages will have the removed property; however, the serializer will simply ignore it.
139+
So if the removed property was not critical to the business logic for the old messages, everything will work fine.
140+
141+
<br/>
142+
<br/>
143+
144+
### Conclusion
145+
146+
So here we tried to cover some common scenarios that may arise when versioning message contracts and possible solutions(if existing) to avoid breaking consumers
147+
so that in the production environment, you can have some idea of what could go wrong and what measures you need to take.
148+
149+
---
150+
151+
<br/>
152+
153+
## Propagation Trick
154+
155+
It’s a serialization trick that allows you to propagate a new message version without requiring changes/NuGet update in the propagator applications.
156+
157+
So what are propagator applications?
158+
159+
Well, in OpenTransit, some ***producers*** act as **initiators**, while others act as **propagators**.
160+
Propagators are producers that publish or send messages from **within** a Consumer.
161+
162+
Examples are better than definitions,
163+
so we’ll explain this propagation technique using the [Basic Communication Tutorial](../../tutorials/basic-communication.md) example.
164+
However, just imagine that the **Shared** class library containing the MessageContract isn’t a project reference; instead, it is published as a NuGet package.
165+
(In the example, we added it as a project reference for simplicity).
166+
167+
If you recall the [Basic Communication Tutorial](../../tutorials/basic-communication.md#2-publishing-messages),
168+
the `ProcessOrder` message is published from within the **SubmitOrderConsumer**.
169+
170+
So here **SubmitOrderConsumer’s** application is a **Propagator** for the `ProcessOrder` message.
171+
172+
**Now let’s look at the trick.**
173+
174+
Assume the business evolves and, when placing an order, we also capture an **expected shipping date**,
175+
which is ultimately used by the **InventoryService** (inside **ProcessOrderConsumer**).
176+
177+
This new requirement impacts only the **Client** and the **InventoryService**.
178+
So we add a new nullable property, `DateTime? ExpectedShippingDate`, to both the `SubmitOrder` and `ProcessOrder` message types and publish a new version of the contract.
179+
We then update and deploy the **Client** and the **InventoryService**.
180+
181+
At this point, if the Client publishes a message that includes `ExpectedShippingDate`, will **ProcessOrderConsumer** eventually receive it?
182+
183+
**The answer is 'No'.**
184+
185+
Because the **OrderService** Propagator Application has not been updated and is still using the older version of the `SubmitOrder` contract,
186+
the `ExpectedShippingDate` value is discarded during deserialization and never makes it downstream.
187+
188+
### The naive solution
189+
190+
The naive approach is to update the Message Contract of **OrderService** and manually propagate the `ExpectedShippingDate` property and redeploy.
191+
192+
[!code-csharp[](code-samples/OrderService.SubmitOrderConsumer-Naive.cs#L6-L122)]
193+
194+
While this approach works, it introduces unnecessary overhead. Even though no business logic of **OrderService** has changed,
195+
the application still needs to be redeployed just to propagate a new property.
196+
197+
In real-world systems, the message propagation chain can be much longer. In such cases, every intermediary (propagator) application would also need to be updated and redeployed,
198+
amplifying the operational complexity.
199+
200+
201+
### The Propagation Trick Solution
202+
203+
We can solve this by doing the Propagation trick. With this trick,
204+
we can design the messages and propagators from the beginning in a way that propagators need not be updated to propagate newly added properties.
205+
206+
It is actually a serialization trick.
207+
208+
If we use System.Text.Json, we can add an additional property `ExtensionData` of type `Dictionary<string, JsonElement>?` with `JsonExtensionData` attirbute to both the
209+
`SubmitOrder` and `ProcessOrder` classes.
210+
211+
And then propagate the ExtensionData property on the propagators.
212+
213+
[!code-csharp[](code-samples/OrderService.SubmitOrderConsumer-Trick.cs#L6-L122)]
214+
215+
If we design the messages and propgators this way from the beginning, then when a new property is added later, the propagator applications need **not** be updated to propagate that new property.
216+
217+
That is because on Deserialization, System.Text.Json will put all unknown properties in the `ExtensionData` dictionary, and on Serialization, it will write all key-value pairs from the `ExtensionData` dictionary as normal properties.
218+
So here all we need to do is copy the `ExtensionData` dictionary from the consumed message to the propagated message.
219+
220+
You may know more about `JsonExtensionData` [here](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/handle-overflow).
221+
222+
Chris Patterson has similar example [here](https://www.youtube.com/watch?v=PNNxJthctgk) where the data is sent to the frontend .
223+
224+
---
225+
226+
<br/>
227+
228+
## Enum issues
229+
230+
Enums should be used with caution, as they aren’t backward compatible. If you add a new key to the enum but the Consumer isn’t updated, it will throw an exception.
231+
232+
Chris Patterson has explained this [here](https://www.youtube.com/watch?v=PNNxJthctgk&t=589s).
233+
234+
---
235+
236+

documentation/docs/concepts/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
href: generic-broker.md
44
- name: Topology
55
href: topology.md
6+
- name: Messages
7+
href: messages/toc.yml

0 commit comments

Comments
 (0)