Skip to content

Commit 9744bac

Browse files
committed
feat: add message translator pattern support
1 parent 6998333 commit 9744bac

23 files changed

Lines changed: 1065 additions & 2 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Generated Message Translator
2+
3+
This example normalizes partner order events into an application-owned commerce event. It shows the fluent runtime path, the source-generated path, and the `IServiceCollection` integration an importing app can use.
4+
5+
Source:
6+
7+
- `src/PatternKit.Examples/Messaging/PartnerEventTranslatorExample.cs`
8+
- `test/PatternKit.Examples.Tests/Messaging/PartnerEventTranslatorExampleTests.cs`
9+
10+
## Runtime Path
11+
12+
```csharp
13+
var translator = PartnerOrderTranslatorPolicies.CreateFluentTranslator();
14+
var result = translator.Translate(PartnerEventTranslatorExample.CreatePartnerMessage(
15+
"partner-a",
16+
"EXT-100",
17+
125m));
18+
```
19+
20+
The translator preserves correlation headers, drops the raw partner signature header, and writes the normalized content type.
21+
22+
## Generated Path
23+
24+
```csharp
25+
[GenerateMessageTranslator(typeof(PartnerOrderAccepted), typeof(CommerceOrderAccepted), TranslatorName = "partner-order-translator")]
26+
[MessageTranslatorDropHeader("raw-signature")]
27+
[MessageTranslatorHeader(MessageHeaderNames.ContentType, "application/vnd.patternkit.commerce-order-accepted+json")]
28+
public static partial class GeneratedPartnerOrderTranslator;
29+
```
30+
31+
The generated factory returns the same runtime translator type:
32+
33+
```csharp
34+
var translator = GeneratedPartnerOrderTranslator.Create();
35+
var result = translator.Translate(partnerMessage);
36+
```
37+
38+
## DI Integration
39+
40+
```csharp
41+
services.AddPartnerEventTranslatorExample();
42+
43+
var importer = provider.GetRequiredService<PartnerOrderImportService>();
44+
var summary = importer.Import(partnerMessage);
45+
```
46+
47+
`AddPatternKitExamples()` also registers this example through `GeneratedMessageTranslatorExample` so catalog consumers can resolve it with the rest of the production-ready examples.

docs/examples/toc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
- name: Generated Message Envelope
5959
href: generated-message-envelope.md
6060

61+
- name: Generated Message Translator
62+
href: generated-message-translator.md
63+
6164
- name: Generated Recipient List
6265
href: generated-recipient-list.md
6366

docs/generators/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
6969
|---|---|---|
7070
| [**Dispatcher**](dispatcher.md) | Mediator pattern with commands, notifications, and streams | `[GenerateDispatcher]` |
7171
| [**Message Envelope**](messaging.md#generated-message-envelope) | Required message metadata contracts | `[GenerateMessageEnvelope]` |
72+
| [**Message Translator**](message-translator.md) | Partner and transport event normalization | `[GenerateMessageTranslator]` |
7273
| [**Content Router**](messaging.md#generated-content-router) | Content-based message routing factories | `[GenerateContentRouter]` |
7374
| [**Recipient List**](messaging.md#generated-recipient-list) | Recipient fan-out factories | `[GenerateRecipientList]` |
7475
| [**Splitter / Aggregator**](messaging.md#generated-splitter-and-aggregator) | Split/rejoin message routing factories | `[GenerateSplitter]` / `[GenerateAggregator]` |
@@ -159,6 +160,10 @@ public abstract partial class DataProcessor { }
159160
[GenerateAntiCorruptionLayer(typeof(LegacyOrderDto), typeof(CommerceOrder))]
160161
public static partial class LegacyOrderAcl { }
161162

163+
// Message translator - partner event normalization
164+
[GenerateMessageTranslator(typeof(PartnerOrderAccepted), typeof(CommerceOrderAccepted))]
165+
public static partial class PartnerOrderTranslator { }
166+
162167
// Visitor - type-safe double dispatch
163168
[GenerateVisitor]
164169
public interface IDocumentVisitor { }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Message Translator Generator
2+
3+
`[GenerateMessageTranslator]` creates a factory for `MessageTranslator<TInput,TOutput>`.
4+
5+
Use it when partner or transport event normalization is part of the application contract and should be validated by the compiler.
6+
7+
```csharp
8+
using PatternKit.Generators.Messaging;
9+
using PatternKit.Messaging;
10+
11+
[GenerateMessageTranslator(typeof(PartnerOrderAccepted), typeof(CommerceOrderAccepted), FactoryName = "Build")]
12+
[MessageTranslatorDropHeader("raw-signature")]
13+
[MessageTranslatorHeader(MessageHeaderNames.ContentType, "application/vnd.myapp.order+json")]
14+
public static partial class PartnerOrderTranslator
15+
{
16+
[MessageTranslatorHandler]
17+
private static CommerceOrderAccepted Translate(Message<PartnerOrderAccepted> message, MessageContext context)
18+
=> new($"commerce-{message.Payload.ExternalOrderId}", message.Payload.Amount, message.Payload.PartnerId);
19+
}
20+
```
21+
22+
Generated output:
23+
24+
```csharp
25+
var translator = PartnerOrderTranslator.Build();
26+
var result = translator.Translate(partnerMessage);
27+
```
28+
29+
## Diagnostics
30+
31+
- `PKMT001`: the translator host must be partial.
32+
- `PKMT002`: exactly one `[MessageTranslatorHandler]` method is required.
33+
- `PKMT003`: the handler must be static, return the output payload type, and accept `Message<TInput>` plus `MessageContext`.
34+
35+
## Example
36+
37+
- `src/PatternKit.Examples/Messaging/PartnerEventTranslatorExample.cs`
38+
- `test/PatternKit.Examples.Tests/Messaging/PartnerEventTranslatorExampleTests.cs`

docs/generators/messaging.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Messaging Generators
22

3-
PatternKit includes ten messaging-oriented source generators:
3+
PatternKit includes eleven messaging-oriented source generators:
44

55
- <xref:PatternKit.Generators.Messaging.GenerateDispatcherAttribute> for source-generated mediator dispatchers.
66
- <xref:PatternKit.Generators.Messaging.GenerateMessageEnvelopeAttribute> for required message-envelope contracts.
7+
- <xref:PatternKit.Generators.Messaging.GenerateMessageTranslatorAttribute> for partner and transport message normalization.
78
- <xref:PatternKit.Generators.Messaging.GenerateContentRouterAttribute> for content-based message routers.
89
- <xref:PatternKit.Generators.Messaging.GenerateRecipientListAttribute> for recipient-list fan-out.
910
- <xref:PatternKit.Generators.Messaging.GenerateSplitterAttribute> and <xref:PatternKit.Generators.Messaging.GenerateAggregatorAttribute> for split/rejoin routing.
@@ -56,6 +57,32 @@ Example source:
5657
- `src/PatternKit.Examples/Messaging/MessageEnvelopeExample.cs`
5758
- `test/PatternKit.Examples.Tests/Messaging/MessageEnvelopeExampleTests.cs`
5859

60+
## Generated Message Translator
61+
62+
`[GenerateMessageTranslator]` creates a `MessageTranslator<TInput,TOutput>` factory from one static handler method:
63+
64+
```csharp
65+
using PatternKit.Generators.Messaging;
66+
using PatternKit.Messaging;
67+
68+
[GenerateMessageTranslator(typeof(PartnerOrderAccepted), typeof(CommerceOrderAccepted))]
69+
[MessageTranslatorDropHeader("raw-signature")]
70+
[MessageTranslatorHeader(MessageHeaderNames.ContentType, "application/vnd.myapp.order+json")]
71+
public static partial class PartnerOrderTranslator
72+
{
73+
[MessageTranslatorHandler]
74+
private static CommerceOrderAccepted Translate(Message<PartnerOrderAccepted> message, MessageContext context)
75+
=> new($"commerce-{message.Payload.ExternalOrderId}", message.Payload.Amount, message.Payload.PartnerId);
76+
}
77+
```
78+
79+
The generated factory preserves headers by default, then applies declared drop/set policies. See [Message Translator Generator](message-translator.md) for diagnostics and examples.
80+
81+
Example source:
82+
83+
- `src/PatternKit.Examples/Messaging/PartnerEventTranslatorExample.cs`
84+
- `test/PatternKit.Examples.Tests/Messaging/PartnerEventTranslatorExampleTests.cs`
85+
5986
## Generated Content Router
6087

6188
`[GenerateContentRouter]` creates a `ContentRouter<TPayload, TResult>` factory from static route methods:

docs/generators/toc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
- name: Messaging Generators
6565
href: messaging.md
6666

67+
- name: Message Translator
68+
href: message-translator.md
69+
6770
- name: Prototype
6871
href: prototype.md
6972

docs/guides/pattern-coverage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
4545
| Family | Pattern | Fluent/runtime path | Source-generated path |
4646
| --- | --- | --- | --- |
4747
| Enterprise Integration | Message Envelope | `Message<TPayload>`, headers, context | Messaging generator |
48+
| Enterprise Integration | Message Translator | `MessageTranslator<TInput, TOutput>` | Message Translator generator |
4849
| Enterprise Integration | Content-Based Router | `ContentRouter<TPayload, TResult>` | Messaging generator |
4950
| Enterprise Integration | Recipient List | `RecipientList<TPayload>` | Messaging generator |
5051
| Enterprise Integration | Splitter | `Splitter<TIn, TOut>` | Messaging generator |
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Message Translator
2+
3+
`MessageTranslator<TInput,TOutput>` translates one message contract into another while applying an explicit header policy. Use it at integration boundaries where partner, vendor, or transport-specific events need to become application-owned contracts before they enter routers, sagas, mailboxes, or reliability pipelines.
4+
5+
## Runtime Path
6+
7+
```csharp
8+
using PatternKit.Messaging;
9+
using PatternKit.Messaging.Transformation;
10+
11+
var translator = MessageTranslator<PartnerOrderAccepted, CommerceOrderAccepted>
12+
.Create("partner-order-translator")
13+
.TranslateWith(static (message, context) => new CommerceOrderAccepted(
14+
$"commerce-{message.Payload.ExternalOrderId}",
15+
message.Payload.Amount,
16+
message.Payload.PartnerId))
17+
.DropHeader("raw-signature")
18+
.SetHeader(MessageHeaderNames.ContentType, "application/vnd.myapp.order+json")
19+
.Build();
20+
21+
var result = translator.Translate(partnerMessage);
22+
```
23+
24+
The translator returns `MessageTranslationResult<TOutput>` instead of leaking transformation exceptions into routing code. Invalid translator output or header policy failures produce a failed result with the captured exception.
25+
26+
## Header Policies
27+
28+
Headers are preserved by default so correlation, causation, message identifiers, and tenant metadata survive the contract change. Fluent policies can remove sensitive transport headers, keep only an allow-list, or set normalized headers:
29+
30+
```csharp
31+
var translator = MessageTranslator<RawEvent, NormalizedEvent>
32+
.Create("normalizer")
33+
.TranslateWith(static (message, _) => new NormalizedEvent(message.Payload.Id))
34+
.KeepHeaders(MessageHeaderNames.CorrelationId, "tenant-id")
35+
.SetHeader(MessageHeaderNames.ContentType, "application/vnd.myapp.normalized+json")
36+
.Build();
37+
```
38+
39+
## Source-Generated Path
40+
41+
Use `[GenerateMessageTranslator]` when a translator contract should be compile-time visible and reusable from dependency injection setup:
42+
43+
```csharp
44+
using PatternKit.Generators.Messaging;
45+
using PatternKit.Messaging;
46+
47+
[GenerateMessageTranslator(typeof(PartnerOrderAccepted), typeof(CommerceOrderAccepted), TranslatorName = "partner-order-translator")]
48+
[MessageTranslatorDropHeader("raw-signature")]
49+
[MessageTranslatorHeader(MessageHeaderNames.ContentType, "application/vnd.myapp.order+json")]
50+
public static partial class PartnerOrderTranslator
51+
{
52+
[MessageTranslatorHandler]
53+
private static CommerceOrderAccepted Translate(Message<PartnerOrderAccepted> message, MessageContext context)
54+
=> new($"commerce-{message.Payload.ExternalOrderId}", message.Payload.Amount, message.Payload.PartnerId);
55+
}
56+
```
57+
58+
The generated factory returns `MessageTranslator<PartnerOrderAccepted, CommerceOrderAccepted>` and emits the same runtime builder calls.
59+
60+
## DI Integration
61+
62+
Examples can be imported through `Microsoft.Extensions.DependencyInjection`:
63+
64+
```csharp
65+
services.AddPartnerEventTranslatorExample();
66+
67+
var service = provider.GetRequiredService<PartnerOrderImportService>();
68+
var summary = service.Import(partnerMessage);
69+
```
70+
71+
Production example:
72+
73+
- `src/PatternKit.Examples/Messaging/PartnerEventTranslatorExample.cs`
74+
- `test/PatternKit.Examples.Tests/Messaging/PartnerEventTranslatorExampleTests.cs`

docs/patterns/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@
297297
items:
298298
- name: Message Envelope and Context
299299
href: messaging/message-envelope.md
300+
- name: Message Translator
301+
href: messaging/message-translator.md
300302
- name: Enterprise Message Routing
301303
href: messaging/message-routing.md
302304
- name: Routing Slip

0 commit comments

Comments
 (0)