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
3 changes: 2 additions & 1 deletion docs/examples/enterprise-messaging-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Example source:
| Pattern | Example source | What it demonstrates |
| --- | --- | --- |
| Message envelope/context | `MessageEnvelopeExample.cs` | Correlation, causation, idempotency, headers, typed payloads, and execution-scoped context. |
| Source-generated message envelope | `MessageEnvelopeExample.cs` | Required-header contract factories for stable integration boundaries. |
| Content router | `MessageRoutingExample.cs` | First-match routing of orders to named destinations. |
| Recipient list | `MessageRoutingExample.cs` | Fan-out to multiple interested recipients. |
| Splitter | `MessageRoutingExample.cs` | Splitting one aggregate message into line-level messages. |
Expand All @@ -32,7 +33,7 @@ Example source:

A production application usually combines these primitives in layers:

1. Accept or create a `Message<TPayload>` at the boundary with correlation, causation, and idempotency headers.
1. Accept or create a `Message<TPayload>` at the boundary with correlation, causation, and idempotency headers. Use generated envelope contracts when the header set is stable.
2. Route the message through a content router, recipient list, splitter, or routing slip.
3. Serialize stateful handlers through a mailbox when concurrency must be constrained.
4. Use a saga/process manager when multiple messages update long-running state.
Expand Down
60 changes: 60 additions & 0 deletions docs/examples/generated-message-envelope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Generated Message Envelope

This example shows the runtime `Message<TPayload>` envelope path beside a source-generated contract factory. Use this shape when an application boundary must always attach the same headers before a message enters routers, routing slips, sagas, mailboxes, or reliability components.

Source:

- `src/PatternKit.Examples/Messaging/MessageEnvelopeExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/MessageEnvelopeExampleTests.cs`

## Runtime Path

```csharp
var message = Message<OrderAccepted>
.Create(new OrderAccepted("order-42", 199.95m))
.WithMessageId("msg-100")
.WithCorrelationId("order-42")
.WithCausationId("checkout-7")
.WithIdempotencyKey("order-42:accepted")
.WithContentType("application/vnd.patternkit.order+json");
```

The fluent path is useful when headers come from configuration, a transport adapter, or user-owned middleware.

## Generated Path

```csharp
[GenerateMessageEnvelope(typeof(OrderAccepted), FactoryName = "CreateAccepted")]
[MessageEnvelopeHeader("message-id", typeof(string), ParameterName = "messageId")]
[MessageEnvelopeHeader("correlation-id", typeof(string), ParameterName = "correlationId")]
[MessageEnvelopeHeader("causation-id", typeof(string), ParameterName = "causationId")]
[MessageEnvelopeHeader("idempotency-key", typeof(string), ParameterName = "idempotencyKey")]
[MessageEnvelopeHeader("content-type", typeof(string), ParameterName = "contentType")]
public static partial class GeneratedOrderAcceptedEnvelope;
```

The generated factory requires every declared header as a typed parameter:

```csharp
var message = GeneratedOrderAcceptedEnvelope.CreateAccepted(
new OrderAccepted("order-42", 199.95m),
"msg-100",
"order-42",
"checkout-7",
"order-42:accepted",
"application/vnd.patternkit.order+json");

var context = GeneratedOrderAcceptedEnvelope.CreateContext(message);
```

## DI Integration

The example is importable through the standard container:

```csharp
services.AddGeneratedMessageEnvelopeExample();
var runner = provider.GetRequiredService<GeneratedMessageEnvelopeExample>().Runner;
var generated = runner.RunGenerated();
```

The extension registers the runner and production-readiness descriptor used by the examples catalog.
3 changes: 3 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **Enterprise Messaging Workflow Suite**
End-to-end messaging examples for envelopes, content routing, recipient lists, splitters, aggregators, routing slips, sagas, mailboxes, idempotent receivers, inboxes, outboxes, and generated messaging factories. See [Enterprise Messaging Workflow Suite](enterprise-messaging-workflows.md).

* **Generated Message Envelope**
Shows fluent and source-generated message envelope contracts side by side, with an importable `IServiceCollection` extension. See [Generated Message Envelope](generated-message-envelope.md).

* **Generated Recipient List**
Shows fluent and source-generated recipient-list fan-out side by side, with an importable `IServiceCollection` extension. See [Generated Recipient List](generated-recipient-list.md).

Expand Down
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
- name: Enterprise Messaging Workflow Suite
href: enterprise-messaging-workflows.md

- name: Generated Message Envelope
href: generated-message-envelope.md

- name: Generated Recipient List
href: generated-recipient-list.md

Expand Down
6 changes: 6 additions & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| Generator | Description | Attribute |
|---|---|---|
| [**Dispatcher**](dispatcher.md) | Mediator pattern with commands, notifications, and streams | `[GenerateDispatcher]` |
| [**Message Envelope**](messaging.md#generated-message-envelope) | Required message metadata contracts | `[GenerateMessageEnvelope]` |
| [**Content Router**](messaging.md#generated-content-router) | Content-based message routing factories | `[GenerateContentRouter]` |
| [**Recipient List**](messaging.md#generated-recipient-list) | Recipient fan-out factories | `[GenerateRecipientList]` |
| [**Routing Slip**](messaging.md#generated-routing-slip) | Ordered message itinerary factories | `[GenerateRoutingSlip]` |
Expand Down Expand Up @@ -137,6 +138,11 @@ public interface IDocumentVisitor { }
// Dispatcher - mediator pattern
[assembly: GenerateDispatcher(Namespace = "MyApp", Name = "Dispatcher")]

// Message envelope - generated required-header contract
[GenerateMessageEnvelope(typeof(OrderAccepted))]
[MessageEnvelopeHeader("correlation-id", typeof(string))]
public static partial class OrderAcceptedEnvelope { }

// Content router - generated first-match route factory
[GenerateContentRouter(typeof(Order), typeof(string))]
public static partial class OrderRouter { }
Expand Down
25 changes: 24 additions & 1 deletion docs/generators/messaging.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Messaging Generators

PatternKit includes five messaging-oriented source generators:
PatternKit includes six messaging-oriented source generators:

- <xref:PatternKit.Generators.Messaging.GenerateDispatcherAttribute> for source-generated mediator dispatchers.
- <xref:PatternKit.Generators.Messaging.GenerateMessageEnvelopeAttribute> for required message-envelope contracts.
- <xref:PatternKit.Generators.Messaging.GenerateContentRouterAttribute> for content-based message routers.
- <xref:PatternKit.Generators.Messaging.GenerateRecipientListAttribute> for recipient-list fan-out.
- <xref:PatternKit.Generators.Messaging.GenerateRoutingSlipAttribute> for ordered routing-slip factories.
Expand Down Expand Up @@ -30,6 +31,27 @@ Example source:
- `src/PatternKit.Examples/MediatorComprehensiveDemo/ComprehensiveDemo.cs`
- `test/PatternKit.Examples.Tests/Messaging/DispatcherExampleTests.cs`

## Generated Message Envelope

`[GenerateMessageEnvelope]` creates typed factories for message contracts that require a stable set of headers:

```csharp
using PatternKit.Generators.Messaging;

[GenerateMessageEnvelope(typeof(OrderAccepted), FactoryName = "CreateAccepted")]
[MessageEnvelopeHeader("message-id", typeof(string), ParameterName = "messageId")]
[MessageEnvelopeHeader("correlation-id", typeof(string), ParameterName = "correlationId")]
[MessageEnvelopeHeader("tenant-id", typeof(string), ParameterName = "tenantId")]
public static partial class OrderAcceptedEnvelope;
```

The generated factory returns `Message<TPayload>` and writes every required header. It also emits a context factory so the same contract can start routing, saga, mailbox, or reliability workflows without manual `MessageContext.From(...)` boilerplate.

Example source:

- `src/PatternKit.Examples/Messaging/MessageEnvelopeExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/MessageEnvelopeExampleTests.cs`

## Generated Content Router

`[GenerateContentRouter]` creates a `ContentRouter<TPayload, TResult>` factory from static route methods:
Expand Down Expand Up @@ -148,6 +170,7 @@ Example source:
| ID | Generator | Meaning |
| --- | --- | --- |
| `PKDSP001`-`PKDSP004` | Dispatcher | Invalid dispatcher configuration or handler registration. |
| `PKME001`-`PKME004` | Message Envelope | Non-partial host, missing headers, invalid header configuration, or duplicate names. |
| `PKCR001`-`PKCR005` | Content Router | Non-partial host, missing routes, invalid signatures, duplicate defaults, or duplicate route identity. |
| `PKRL001`-`PKRL004` | Recipient List | Non-partial host, missing recipients, invalid signatures, or duplicate recipient identity. |
| `PKRS001`-`PKRS003` | Routing Slip | Non-partial host, missing steps, or invalid step signatures. |
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr

| Family | Pattern | Fluent/runtime path | Source-generated path |
| --- | --- | --- | --- |
| Enterprise Integration | Message Envelope | `Message<TPayload>`, headers, context | Tracked in [#215](https://github.com/JerrettDavis/PatternKit/issues/215) |
| Enterprise Integration | Message Envelope | `Message<TPayload>`, headers, context | Messaging generator |
| Enterprise Integration | Content-Based Router | `ContentRouter<TPayload, TResult>` | Messaging generator |
| Enterprise Integration | Recipient List | `RecipientList<TPayload>` | Messaging generator |
| Enterprise Integration | Splitter | `Splitter<TIn, TOut>` | Tracked in [#211](https://github.com/JerrettDavis/PatternKit/issues/211) |
Expand Down
30 changes: 30 additions & 0 deletions docs/patterns/messaging/message-envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,34 @@ if (context.TryGetItem<int>("attempt", out var attempt))
}
```

## Source-Generated Contracts

Use `[GenerateMessageEnvelope]` when an application boundary has a stable envelope contract and every message must include the same required headers:

```csharp
using PatternKit.Generators.Messaging;

[GenerateMessageEnvelope(typeof(OrderAccepted), FactoryName = "CreateAccepted")]
[MessageEnvelopeHeader("message-id", typeof(string), ParameterName = "messageId")]
[MessageEnvelopeHeader("correlation-id", typeof(string), ParameterName = "correlationId")]
[MessageEnvelopeHeader("idempotency-key", typeof(string), ParameterName = "idempotencyKey")]
public static partial class OrderAcceptedEnvelope;
```

The generated factory returns `Message<OrderAccepted>` and requires each header as a typed parameter. The generated context factory starts an execution context from the same contract:

```csharp
var message = OrderAcceptedEnvelope.CreateAccepted(
new OrderAccepted("order-42", 199.95m),
"msg-100",
"order-42",
"order-42:accepted");

var context = OrderAcceptedEnvelope.CreateContext(message);
```

Prefer the fluent runtime API when the header set is dynamic. Prefer the generated path when the contract is stable and should fail at compile time if a required header is omitted.

## Relationship To Other Patterns

`Message<TPayload>` and `MessageContext` are not a replacement for a mediator, observer, or broker. They are shared metadata primitives for higher-level patterns:
Expand All @@ -110,6 +138,8 @@ if (context.TryGetItem<int>("attempt", out var attempt))
- <xref:PatternKit.Messaging.MessageHeaders>
- <xref:PatternKit.Messaging.MessageContext>
- <xref:PatternKit.Messaging.MessageHeaderNames>
- <xref:PatternKit.Generators.Messaging.GenerateMessageEnvelopeAttribute>
- <xref:PatternKit.Generators.Messaging.MessageEnvelopeHeaderAttribute>

## Example Source

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public sealed record PosTenderVisitorExample(TypeDispatcher<VisitorTender, strin
public sealed record ApiExceptionMappingVisitorExample(Func<Task> RunAsync);
public sealed record EventProcessingVisitorExample(Func<Task> RunAsync);
public sealed record MessageRouterVisitorExample(Func<RoutingSummary> Run);
public sealed record GeneratedMessageEnvelopeExample(MessageEnvelopeExampleRunner Runner);
public sealed record GeneratedRecipientListExample(RecipientListGeneratorExampleRunner Runner);
public sealed record PatternsShowcaseExample(ShowcaseFacade Facade);
public sealed record SourceGeneratorApplicationSuiteExample(Func<ValueTask<CorporateApp>> BuildProductionAsync);
Expand Down Expand Up @@ -124,6 +125,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddApiExceptionMappingVisitorExample()
.AddEventProcessingVisitorExample()
.AddMessageRouterVisitorExample()
.AddGeneratedMessageEnvelopeExample()
.AddGeneratedRecipientListExample()
.AddPatternsShowcaseExample()
.AddSourceGeneratorApplicationSuiteExample()
Expand Down Expand Up @@ -324,6 +326,13 @@ public static IServiceCollection AddMessageRouterVisitorExample(this IServiceCol
return services.RegisterExample<MessageRouterVisitorExample>("Message Router Visitor", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddGeneratedMessageEnvelopeExample(this IServiceCollection services)
{
services.AddMessageEnvelopeExample();
services.AddSingleton<GeneratedMessageEnvelopeExample>(sp => new(sp.GetRequiredService<MessageEnvelopeExampleRunner>()));
return services.RegisterExample<GeneratedMessageEnvelopeExample>("Generated Message Envelope", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddGeneratedRecipientListExample(this IServiceCollection services)
{
services.AddRecipientListGeneratorExample();
Expand Down
64 changes: 63 additions & 1 deletion src/PatternKit.Examples/Messaging/MessageEnvelopeExample.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using PatternKit.Messaging;
using Microsoft.Extensions.DependencyInjection;
using PatternKit.Generators.Messaging;

namespace PatternKit.Examples.Messaging;

Expand All @@ -10,7 +12,12 @@ public static class MessageEnvelopeExample
/// <summary>
/// Builds an enriched message and context, returning the metadata needed by tests and docs.
/// </summary>
public static Summary Run()
public static Summary Run() => RunFluent();

/// <summary>
/// Builds the message envelope through the fluent runtime API.
/// </summary>
public static Summary RunFluent()
{
var message = Message<OrderAccepted>
.Create(new OrderAccepted("order-42", 199.95m))
Expand All @@ -28,6 +35,39 @@ public static Summary Run()
context.TryGetItem<int>("attempt", out var attempt);

return new Summary(
"fluent",
message.Payload.OrderId,
message.Headers.MessageId!,
message.Headers.CorrelationId!,
message.Headers.CausationId!,
message.Headers.IdempotencyKey!,
message.Headers.ContentType!,
context.Headers.GetString("route")!,
attempt);
}

/// <summary>
/// Builds the same message envelope through a generated contract factory.
/// </summary>
public static Summary RunGenerated()
{
var message = GeneratedOrderAcceptedEnvelope.CreateAccepted(
new OrderAccepted("order-42", 199.95m),
"msg-100",
"order-42",
"checkout-7",
"order-42:accepted",
"application/vnd.patternkit.order+json");

var context = GeneratedOrderAcceptedEnvelope
.CreateContext(message)
.WithHeader("route", "billing")
.WithItem("attempt", 1);

context.TryGetItem<int>("attempt", out var attempt);

return new Summary(
"source-generated",
message.Payload.OrderId,
message.Headers.MessageId!,
message.Headers.CorrelationId!,
Expand All @@ -39,11 +79,33 @@ public static Summary Run()
}
}

/// <summary>
/// Registers the message-envelope example with a standard .NET service collection.
/// </summary>
public static class MessageEnvelopeExampleServiceCollectionExtensions
{
/// <summary>Adds the generated and fluent message-envelope example runner.</summary>
public static IServiceCollection AddMessageEnvelopeExample(this IServiceCollection services)
=> services.AddSingleton(new MessageEnvelopeExampleRunner(MessageEnvelopeExample.RunFluent, MessageEnvelopeExample.RunGenerated));
}

/// <summary>DI-importable runner for the message-envelope example.</summary>
public sealed record MessageEnvelopeExampleRunner(Func<Summary> RunFluent, Func<Summary> RunGenerated);

[GenerateMessageEnvelope(typeof(OrderAccepted), FactoryName = "CreateAccepted")]
[MessageEnvelopeHeader("message-id", typeof(string), ParameterName = "messageId")]
[MessageEnvelopeHeader("correlation-id", typeof(string), ParameterName = "correlationId")]
[MessageEnvelopeHeader("causation-id", typeof(string), ParameterName = "causationId")]
[MessageEnvelopeHeader("idempotency-key", typeof(string), ParameterName = "idempotencyKey")]
[MessageEnvelopeHeader("content-type", typeof(string), ParameterName = "contentType")]
public static partial class GeneratedOrderAcceptedEnvelope;

/// <summary>Example payload for the message envelope demo.</summary>
public sealed record OrderAccepted(string OrderId, decimal Total);

/// <summary>Example output for the message envelope demo.</summary>
public sealed record Summary(
string Path,
string OrderId,
string MessageId,
string CorrelationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,16 @@ public sealed class PatternKitExampleCatalog : IPatternKitExampleCatalog
"test/PatternKit.Examples.Tests/Messaging/MessageEnvelopeExampleTests.cs",
"docs/examples/enterprise-messaging-workflows.md",
ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator,
["ContentRouter", "RecipientList", "Splitter", "Aggregator", "RoutingSlip", "Saga", "Mailbox"],
["idempotency", "inbox/outbox", "generated dispatcher"]),
["MessageEnvelope", "ContentRouter", "RecipientList", "Splitter", "Aggregator", "RoutingSlip", "Saga", "Mailbox"],
["idempotency", "inbox/outbox", "generated envelope contracts", "generated dispatcher"]),
Descriptor(
"Generated Message Envelope",
"src/PatternKit.Examples/Messaging/MessageEnvelopeExample.cs",
"test/PatternKit.Examples.Tests/Messaging/MessageEnvelopeExampleTests.cs",
"docs/examples/generated-message-envelope.md",
ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection,
["MessageEnvelope"],
["required headers", "source-generated factory", "DI composition"]),
Descriptor(
"Generated Recipient List",
"src/PatternKit.Examples/Messaging/RecipientListGeneratorExample.cs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,14 @@ public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog
"docs/patterns/messaging/message-envelope.md",
"src/PatternKit.Core/Messaging/Message.cs",
"test/PatternKit.Tests/Messaging/MessageTests.cs",
"docs/generators/messaging.md",
"src/PatternKit.Generators/Messaging/MessageEnvelopeGenerator.cs",
"test/PatternKit.Generators.Tests/MessageEnvelopeGeneratorTests.cs",
null,
null,
null,
"https://github.com/JerrettDavis/PatternKit/issues/215",
"docs/examples/enterprise-messaging-workflows.md",
"docs/examples/generated-message-envelope.md",
"src/PatternKit.Examples/Messaging/MessageEnvelopeExample.cs",
"test/PatternKit.Examples.Tests/Messaging/MessageEnvelopeExampleTests.cs",
["runtime envelope and headers", "generated contract path tracked", "enterprise workflow example"]),
["runtime envelope and headers", "generated required-header contract", "DI-importable envelope example"]),

Pattern("Content-Based Router", PatternFamily.EnterpriseIntegration,
"docs/patterns/messaging/message-routing.md",
Expand Down
Loading
Loading