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
1 change: 1 addition & 0 deletions docs/examples/enterprise-messaging-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Example source:
| Inbox/outbox | `ReliabilityExample.cs` | Explicit handoff records for durable integration boundaries owned by the application. |
| Source-generated dispatcher | `DispatcherExample.cs` | Compile-time mediator commands, notifications, streams, and paging. |
| Source-generated content router | `ContentRouterGeneratorExample.cs` | Attribute-driven content routing without runtime scanning. |
| Source-generated recipient list | `RecipientListGeneratorExample.cs` | Attribute-driven fan-out without runtime scanning. |
| Resilient checkout orchestration | `ResilientCheckoutDemo.cs` | Route selection, routing-slip execution, command compensation, and fallback routes. |
| Collaborating service mailboxes | `ServiceCollaborationMailboxDemo.cs` | Inventory, payment, shipping, and notification mailboxes collaborating over correlated messages. |
| Backplane facade | `BackplaneFacadeDemo.cs` | MassTransit/MediatR-shaped host builder, typed client, request/reply, and pub/sub over an application-owned transport boundary. |
Expand Down
41 changes: 41 additions & 0 deletions docs/examples/generated-recipient-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated Recipient List

The generated recipient-list example shows event fan-out with both runtime fluent composition and an attribute-driven source-generated factory.

## Register

```csharp
using Microsoft.Extensions.DependencyInjection;
using PatternKit.Examples.DependencyInjection;

var services = new ServiceCollection()
.AddGeneratedRecipientListExample();

using var provider = services.BuildServiceProvider(validateScopes: true);
var example = provider.GetRequiredService<GeneratedRecipientListExample>();
```

## Fluent Path

```csharp
var fluent = example.Runner.RunFluent();
```

The fluent path builds a `RecipientList<GeneratedShipmentEvent>` with predicates and handlers registered in code.

## Source-Generated Path

```csharp
var generated = example.Runner.RunGenerated();
```

The generated path uses `[GenerateRecipientList]` on a partial type and `[RecipientListRecipient]` on static recipient handlers. The generator emits a strongly typed factory that builds the same `RecipientList<TPayload>` used by the fluent API.

## Production Shape

The TinyBDD scenarios validate that:

- fluent and generated paths deliver the same recipients in deterministic order
- handler side effects are visible through a scoped `MessageContext`
- the example is importable through `IServiceCollection`
- the example advertises dependency injection, messaging, and source-generation integration surfaces
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 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).

* **Resilient Checkout and Collaborating Mailboxes**
Application-shaped messaging demos: checkout route selection, routing-slip execution, command compensation, fallback routes, and service mailboxes collaborating over correlated messages. See [Resilient Checkout and Collaborating Mailboxes](resilient-checkout-and-mailboxes.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 Recipient List
href: generated-recipient-list.md

- name: CQRS Dispatcher
href: cqrs-dispatcher.md

Expand Down
1 change: 1 addition & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
|---|---|---|
| [**Dispatcher**](dispatcher.md) | Mediator pattern with commands, notifications, and streams | `[GenerateDispatcher]` |
| [**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]` |
| [**Saga**](messaging.md#generated-saga) | Typed process-manager transition factories | `[GenerateSaga]` |

Expand Down
30 changes: 29 additions & 1 deletion docs/generators/messaging.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Messaging Generators

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

- <xref:PatternKit.Generators.Messaging.GenerateDispatcherAttribute> for source-generated mediator dispatchers.
- <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.
- <xref:PatternKit.Generators.Messaging.GenerateSagaAttribute> for typed saga/process-manager factories.

Expand Down Expand Up @@ -88,6 +89,32 @@ Example source:
- `src/PatternKit.Examples/Messaging/RoutingSlipExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/RoutingSlipExampleTests.cs`

## Generated Recipient List

`[GenerateRecipientList]` creates a `RecipientList<TPayload>` or `AsyncRecipientList<TPayload>` factory from static recipient methods:

```csharp
[GenerateRecipientList(typeof(Order))]
public static partial class OrderRecipients
{
private static bool IsPriority(Message<Order> message, MessageContext context)
=> message.Payload.Priority == "priority";

[RecipientListRecipient("priority-audit", 10, nameof(IsPriority))]
private static void PriorityAudit(Message<Order> message, MessageContext context)
{
// deliver to audit sink
}
}
```

The generator orders recipients by `RecipientListRecipientAttribute.Order`, validates predicates and handlers, and emits deterministic diagnostics for missing recipients, non-partial host types, invalid signatures, and duplicate names or order values.

Example files:

- `src/PatternKit.Examples/Messaging/RecipientListGeneratorExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/RecipientListGeneratorExampleTests.cs`

## Generated Saga

`[GenerateSaga]` emits a process-manager factory from typed transition methods:
Expand Down Expand Up @@ -122,6 +149,7 @@ Example source:
| --- | --- | --- |
| `PKDSP001`-`PKDSP004` | Dispatcher | Invalid dispatcher configuration or handler registration. |
| `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. |
| `PKSG001`-`PKSG004` | Saga | Non-partial host, missing transitions, invalid transition signatures, or invalid completion checks. |

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 @@ -46,7 +46,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| --- | --- | --- | --- |
| Enterprise Integration | Message Envelope | `Message<TPayload>`, headers, context | Tracked in [#215](https://github.com/JerrettDavis/PatternKit/issues/215) |
| Enterprise Integration | Content-Based Router | `ContentRouter<TPayload, TResult>` | Messaging generator |
| Enterprise Integration | Recipient List | `RecipientList<TPayload>` | Tracked in [#210](https://github.com/JerrettDavis/PatternKit/issues/210) |
| Enterprise Integration | Recipient List | `RecipientList<TPayload>` | Messaging generator |
| Enterprise Integration | Splitter | `Splitter<TIn, TOut>` | Tracked in [#211](https://github.com/JerrettDavis/PatternKit/issues/211) |
| Enterprise Integration | Aggregator | `Aggregator<TKey, TIn, TOut>` | Tracked in [#211](https://github.com/JerrettDavis/PatternKit/issues/211) |
| Enterprise Integration | Routing Slip | `RoutingSlip<TPayload>` | Messaging generator |
Expand Down
40 changes: 37 additions & 3 deletions docs/patterns/messaging/enterprise-generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

PatternKit source generators remove repetitive registration code for explicit enterprise integration patterns. They do not scan assemblies implicitly; each generated factory is opt-in through attributes on a partial type.

Use generators when routes, routing-slip steps, or saga transitions are static enough to validate at compile time and you want AOT-friendly factories without reflection.
Use generators when routes, recipient lists, routing-slip steps, or saga transitions are static enough to validate at compile time and you want AOT-friendly factories without reflection.

## Generated Content Router

Expand Down Expand Up @@ -44,6 +44,31 @@ bool Predicate(Message<TPayload> message, MessageContext context)

The generator orders routes by `ContentRouteAttribute.Order`, then by route name. Route names and orders must be unique so the generated first-match behavior is clear.

## Generated Recipient List

`[GenerateRecipientList]` creates a `RecipientList<TPayload>` or `AsyncRecipientList<TPayload>` factory from static recipient predicates and handlers.

```csharp
using PatternKit.Generators.Messaging;
using PatternKit.Messaging;

[GenerateRecipientList(typeof(Order))]
public static partial class OrderRecipients
{
private static bool IsPriority(Message<Order> message, MessageContext context)
=> message.Payload.Priority == "priority";

[RecipientListRecipient("priority-audit", 10, nameof(IsPriority))]
private static void PriorityAudit(Message<Order> message, MessageContext context)
{
}
}

var result = OrderRecipients.Create().Dispatch(Message<Order>.Create(order));
```

The generator orders recipients by `RecipientListRecipientAttribute.Order`, then by recipient name. Recipient names and orders must be unique so fan-out order stays deterministic.

## Existing Enterprise Generators

Routing-slip generation is documented in [Routing Slip](routing-slip.md). It discovers `[RoutingSlipStep]` methods and emits sync or async itinerary factories.
Expand All @@ -61,30 +86,39 @@ Mailbox and reliability helpers stay runtime-only for now. Their registration is
| `PKCR003` | A route handler or referenced predicate has an invalid signature. |
| `PKCR004` | The default route handler has an invalid signature or more than one default handler is declared. |
| `PKCR005` | A route name or route order is duplicated. |
| `PKRL001` | `[GenerateRecipientList]` was placed on a non-partial type. |
| `PKRL002` | The generated recipient list has no `[RecipientListRecipient]` methods. |
| `PKRL003` | A recipient handler or referenced predicate has an invalid signature. |
| `PKRL004` | A recipient name or recipient order is duplicated. |
| `PKRS001`-`PKRS003` | Routing-slip generator validation. |
| `PKSG001`-`PKSG004` | Saga generator validation. |

## Troubleshooting

- Make the generated host type `partial`.
- Keep route, step, and saga methods `static`; generated factories reference them directly.
- Use `nameof(PredicateMethod)` in `[ContentRoute]` so renames remain compile-time safe.
- Use unique route names and orders. Content routers are first-match, so ambiguous ordering should fail at build time.
- Use `nameof(PredicateMethod)` in `[ContentRoute]` and `[RecipientListRecipient]` so renames remain compile-time safe.
- Use unique route and recipient names and orders. Content routers are first-match, and recipient lists are ordered fan-out, so ambiguous ordering should fail at build time.
- Ensure generated code builds under nullable enabled; the tests compile generated examples with Release settings.

## API

- <xref:PatternKit.Generators.Messaging.GenerateContentRouterAttribute>
- <xref:PatternKit.Generators.Messaging.ContentRouteAttribute>
- <xref:PatternKit.Generators.Messaging.ContentRouteDefaultAttribute>
- <xref:PatternKit.Generators.Messaging.GenerateRecipientListAttribute>
- <xref:PatternKit.Generators.Messaging.RecipientListRecipientAttribute>
- <xref:PatternKit.Generators.Messaging.GenerateRoutingSlipAttribute>
- <xref:PatternKit.Generators.Messaging.RoutingSlipStepAttribute>
- <xref:PatternKit.Generators.Messaging.GenerateSagaAttribute>
- <xref:PatternKit.Generators.Messaging.SagaStepAttribute>
- <xref:PatternKit.Generators.Messaging.SagaCompleteWhenAttribute>
- <xref:PatternKit.Messaging.Routing.ContentRouter`2>
- <xref:PatternKit.Messaging.Routing.RecipientList`1>

## Example Source

- `src/PatternKit.Examples/Messaging/ContentRouterGeneratorExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/ContentRouterGeneratorExampleTests.cs`
- `src/PatternKit.Examples/Messaging/RecipientListGeneratorExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/RecipientListGeneratorExampleTests.cs`
21 changes: 21 additions & 0 deletions docs/patterns/messaging/message-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ var result = recipients.Dispatch(Message<Order>.Create(new Order("order-1", 150m

Use `AsyncRecipientList<TPayload>` for async recipient handlers.

Use `[GenerateRecipientList]` when the recipient map is part of application structure and should be compiled into a strongly typed factory:

```csharp
[GenerateRecipientList(typeof(Order))]
public static partial class OrderRecipients
{
private static bool IsPriority(Message<Order> message, MessageContext context)
=> message.Payload.Priority == "priority";

[RecipientListRecipient("priority-audit", 10, nameof(IsPriority))]
private static void PriorityAudit(Message<Order> message, MessageContext context)
{
// deliver to audit sink
}
}
```

The generated factory returns the same `RecipientList<TPayload>` runtime type as the fluent API.

## Splitter

`Splitter<TPayload, TItem>` turns one message into item messages. Child messages preserve the parent headers. When the parent has a message id and no causation id, child messages set `CausationId` to the parent `MessageId`.
Expand Down Expand Up @@ -95,6 +114,8 @@ Use external infrastructure for:
- <xref:PatternKit.Messaging.Routing.AsyncContentRouter`2>
- <xref:PatternKit.Messaging.Routing.RecipientList`1>
- <xref:PatternKit.Messaging.Routing.AsyncRecipientList`1>
- <xref:PatternKit.Generators.Messaging.GenerateRecipientListAttribute>
- <xref:PatternKit.Generators.Messaging.RecipientListRecipientAttribute>
- <xref:PatternKit.Messaging.Routing.Splitter`2>
- <xref:PatternKit.Messaging.Routing.Aggregator`3>
- <xref:PatternKit.Messaging.Routing.AggregationResult`2>
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 GeneratedRecipientListExample(RecipientListGeneratorExampleRunner Runner);
public sealed record PatternsShowcaseExample(ShowcaseFacade Facade);
public sealed record SourceGeneratorApplicationSuiteExample(Func<ValueTask<CorporateApp>> BuildProductionAsync);
public sealed record EnterpriseMessagingWorkflowSuiteExample(Func<Summary> Run);
Expand Down Expand Up @@ -123,6 +124,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddApiExceptionMappingVisitorExample()
.AddEventProcessingVisitorExample()
.AddMessageRouterVisitorExample()
.AddGeneratedRecipientListExample()
.AddPatternsShowcaseExample()
.AddSourceGeneratorApplicationSuiteExample()
.AddEnterpriseMessagingWorkflowSuiteExample()
Expand Down Expand Up @@ -322,6 +324,13 @@ public static IServiceCollection AddMessageRouterVisitorExample(this IServiceCol
return services.RegisterExample<MessageRouterVisitorExample>("Message Router Visitor", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddGeneratedRecipientListExample(this IServiceCollection services)
{
services.AddRecipientListGeneratorExample();
services.AddSingleton<GeneratedRecipientListExample>(sp => new(sp.GetRequiredService<RecipientListGeneratorExampleRunner>()));
return services.RegisterExample<GeneratedRecipientListExample>("Generated Recipient List", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddPatternsShowcaseExample(this IServiceCollection services)
{
services.AddSingleton(_ => PatternShowcase.PatternShowcase.Build());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Microsoft.Extensions.DependencyInjection;
using PatternKit.Generators.Messaging;
using PatternKit.Messaging;

namespace PatternKit.Examples.Messaging;

/// <summary>
/// Demonstrates fluent and source-generated Recipient List integration for event fan-out.
/// </summary>
public static class RecipientListGeneratorExample
{
public static RecipientListSummary RunFluent()
{
var deliveries = new List<string>();
var message = Message<GeneratedShipmentEvent>.Create(new("order-42", "priority", 125m));
var context = MessageContext.From(message).WithItem(GeneratedShipmentRecipients.DeliveryLogKey, deliveries);

var list = PatternKit.Messaging.Routing.RecipientList<GeneratedShipmentEvent>.Create()
.When("priority-audit", static (msg, _) => msg.Payload.Priority == "priority")
.Then(static (_, ctx) => GeneratedShipmentRecipients.Record(ctx, "priority-audit"))
.When("billing-ledger", static (msg, _) => msg.Payload.Total >= 100m)
.Then(static (_, ctx) => GeneratedShipmentRecipients.Record(ctx, "billing-ledger"))
.Build();

var result = list.Dispatch(message, context);
return new RecipientListSummary("fluent", result.DeliveredRecipients, deliveries);
}

public static RecipientListSummary RunGenerated()
{
var deliveries = new List<string>();
var message = Message<GeneratedShipmentEvent>.Create(new("order-42", "priority", 125m));
var context = MessageContext.From(message).WithItem(GeneratedShipmentRecipients.DeliveryLogKey, deliveries);

var result = GeneratedShipmentRecipients.Create().Dispatch(message, context);
return new RecipientListSummary("source-generated", result.DeliveredRecipients, deliveries);
}

public static IServiceCollection AddRecipientListGeneratorExample(this IServiceCollection services)
=> services.AddSingleton(new RecipientListGeneratorExampleRunner(RunFluent, RunGenerated));
}

public sealed record RecipientListGeneratorExampleRunner(
Func<RecipientListSummary> RunFluent,
Func<RecipientListSummary> RunGenerated);

public sealed record GeneratedShipmentEvent(string OrderId, string Priority, decimal Total);

public sealed record RecipientListSummary(
string Path,
IReadOnlyList<string> DeliveredRecipients,
IReadOnlyList<string> DeliveryLog);

[GenerateRecipientList(typeof(GeneratedShipmentEvent))]
public static partial class GeneratedShipmentRecipients
{
internal const string DeliveryLogKey = "recipient-list-deliveries";

internal static void Record(MessageContext context, string recipient)
{
if (context.TryGetItem<List<string>>(DeliveryLogKey, out var deliveries) && deliveries is not null)
deliveries.Add(recipient);
}

private static bool IsPriority(Message<GeneratedShipmentEvent> message, MessageContext context)
=> message.Payload.Priority == "priority";

private static bool IsBillable(Message<GeneratedShipmentEvent> message, MessageContext context)
=> message.Payload.Total >= 100m;

[RecipientListRecipient("priority-audit", 10, nameof(IsPriority))]
private static void PriorityAudit(Message<GeneratedShipmentEvent> message, MessageContext context)
=> Record(context, "priority-audit");

[RecipientListRecipient("billing-ledger", 20, nameof(IsBillable))]
private static void BillingLedger(Message<GeneratedShipmentEvent> message, MessageContext context)
=> Record(context, "billing-ledger");
}
Loading
Loading