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
4 changes: 4 additions & 0 deletions docs/examples/enterprise-messaging-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Example source:
| Routing slip | `RoutingSlipExample.cs` | Ordered fulfillment steps with route progress stored in message headers. |
| Saga/process manager | `SagaExample.cs` | Typed message transitions over explicit saga state and completion rules. |
| Mailbox | `MailboxExample.cs` | Serialized async inbox processing with explicit lifecycle and error behavior. |
| Source-generated mailbox | `MailboxExample.cs` | Attribute-driven serialized inbox factories with bounded backpressure and error policy. |
| Idempotent receiver | `ReliabilityExample.cs` | Duplicate detection around at-least-once message delivery. |
| 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. |
Expand Down Expand Up @@ -69,6 +70,8 @@ The generated factories are AOT-friendly and do not scan assemblies. The runtime

Generated splitter and aggregator contracts follow the same rule: use `[GenerateSplitter]` and `[SplitterProjection]` for a stable split projection, then `[GenerateAggregator]` with `[AggregatorCorrelation]`, `[AggregatorCompletion]`, and `[AggregatorProjection]` for the matching rejoin contract.

Generated mailbox contracts use `[GenerateMailbox]` plus `[MailboxHandler]` when a serialized in-process inbox should have compile-time-validated capacity, backpressure, and error policies.

## Testing Guidance

The example tests use behavior-oriented assertions:
Expand All @@ -92,6 +95,7 @@ The example tests use behavior-oriented assertions:
- [Routing Slip](../patterns/messaging/routing-slip.md)
- [Saga / Process Manager](../patterns/messaging/saga.md)
- [Mailbox](../patterns/messaging/mailbox.md)
- [Generated Mailbox](generated-mailbox.md)
- [Idempotent Receiver, Inbox, and Outbox](../patterns/messaging/reliability.md)
- [Messaging Generators](../generators/messaging.md)
- [Generated Splitter And Aggregator](generated-splitter-aggregator.md)
Expand Down
53 changes: 53 additions & 0 deletions docs/examples/generated-mailbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated Mailbox

This example shows fluent and source-generated mailbox factories side by side. A PatternKit mailbox is a serialized in-process inbox: accepted messages are handled by one consumer pump, making stateful background work deterministic without adopting an actor framework.

Use the generated path when the payload type, handler, capacity, backpressure policy, and error policy are stable application structure. The generator emits a factory returning the same `Mailbox<TPayload>` runtime type as the fluent API.

## Source

- `src/PatternKit.Examples/Messaging/MailboxExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/MailboxExampleTests.cs`

## Fluent Path

```csharp
using var mailbox = Mailbox<MailboxWorkItem>.Create((message, context, cancellationToken) =>
{
processed.Add($"{context.Headers.GetString(MessageHeaderNames.CorrelationId)}:{message.Payload.Id}");
return default;
})
.Bounded(8, MailboxBackpressurePolicy.Wait)
.OnError(MailboxErrorPolicy.Continue)
.Build();
```

## Source-Generated Path

```csharp
[GenerateMailbox(typeof(MailboxWorkItem), FactoryName = "CreateWorkQueue", Capacity = 8, BackpressurePolicy = "Wait", ErrorPolicy = "Continue")]
public static partial class GeneratedMailboxWorkQueue
{
[MailboxHandler]
private static ValueTask Handle(Message<MailboxWorkItem> message, MessageContext context, CancellationToken cancellationToken)
{
Processed.Add($"{context.Headers.GetString(MessageHeaderNames.CorrelationId)}:{message.Payload.Id}");
return default;
}
}
```

Optional `[MailboxErrorHandler]` and `[MailboxEventSink]` methods can be added when the generated factory should wire failure handling or metrics events.

## Dependency Injection

```csharp
var services = new ServiceCollection();
services.AddGeneratedMailboxExample();

using var provider = services.BuildServiceProvider(validateScopes: true);
var example = provider.GetRequiredService<GeneratedMailboxExample>();
var processed = await example.Runner.RunGeneratedAsync();
```

In a production host, register the generated mailbox itself as a singleton or wrap it behind a typed service that owns startup and shutdown. PatternKit keeps processing serialized in process; durable queues, persistence, and restart recovery remain application infrastructure.
3 changes: 3 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **Generated Splitter and Aggregator**
Shows fluent and source-generated split/rejoin message routing side by side, with an importable `IServiceCollection` extension. See [Generated Splitter And Aggregator](generated-splitter-aggregator.md).

* **Generated Mailbox**
Shows fluent and source-generated serialized inboxes side by side, with an importable `IServiceCollection` extension. See [Generated Mailbox](generated-mailbox.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 @@ -67,6 +67,9 @@
- name: CQRS Dispatcher
href: cqrs-dispatcher.md

- name: Generated Mailbox
href: generated-mailbox.md

- name: Resilient Checkout and Collaborating Mailboxes
href: resilient-checkout-and-mailboxes.md

Expand Down
5 changes: 5 additions & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Splitter / Aggregator**](messaging.md#generated-splitter-and-aggregator) | Split/rejoin message routing factories | `[GenerateSplitter]` / `[GenerateAggregator]` |
| [**Routing Slip**](messaging.md#generated-routing-slip) | Ordered message itinerary factories | `[GenerateRoutingSlip]` |
| [**Saga**](messaging.md#generated-saga) | Typed process-manager transition factories | `[GenerateSaga]` |
| [**Mailbox**](messaging.md#generated-mailbox) | Serialized in-process inbox factories | `[GenerateMailbox]` |

## Quick Reference

Expand Down Expand Up @@ -155,6 +156,10 @@ public static partial class OrderSplitter { }
[GenerateAggregator(typeof(string), typeof(OrderLine), typeof(decimal))]
public static partial class OrderLineAggregator { }

// Mailbox - generated serialized inbox factory
[GenerateMailbox(typeof(OrderWork), Capacity = 32, BackpressurePolicy = "Wait")]
public static partial class OrderMailbox { }

// Routing slip - generated ordered itinerary factory
[GenerateRoutingSlip(typeof(Order))]
public static partial class OrderSlip { }
Expand Down
28 changes: 27 additions & 1 deletion docs/generators/messaging.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Messaging Generators

PatternKit includes seven messaging-oriented source generators:
PatternKit includes eight messaging-oriented source generators:

- <xref:PatternKit.Generators.Messaging.GenerateDispatcherAttribute> for source-generated mediator dispatchers.
- <xref:PatternKit.Generators.Messaging.GenerateMessageEnvelopeAttribute> for required message-envelope contracts.
Expand All @@ -9,6 +9,7 @@ PatternKit includes seven messaging-oriented source generators:
- <xref:PatternKit.Generators.Messaging.GenerateSplitterAttribute> and <xref:PatternKit.Generators.Messaging.GenerateAggregatorAttribute> for split/rejoin routing.
- <xref:PatternKit.Generators.Messaging.GenerateRoutingSlipAttribute> for ordered routing-slip factories.
- <xref:PatternKit.Generators.Messaging.GenerateSagaAttribute> for typed saga/process-manager factories.
- <xref:PatternKit.Generators.Messaging.GenerateMailboxAttribute> for serialized in-process inbox factories.

Use these generators when the message topology is known at compile time and should remain explicit, AOT-friendly, and validated by the compiler. They generate factories and fluent builders; they do not discover handlers from assemblies at runtime and they do not replace brokers, durable queues, or workflow engines.

Expand Down Expand Up @@ -178,6 +179,30 @@ Example files:
- `src/PatternKit.Examples/Messaging/MessageRoutingExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/MessageRoutingExampleTests.cs`

## Generated Mailbox

`[GenerateMailbox]` creates a `Mailbox<TPayload>` factory from one static `[MailboxHandler]` method. Optional `[MailboxErrorHandler]` and `[MailboxEventSink]` methods wire failure handling and metrics events:

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

[GenerateMailbox(typeof(OrderWork), FactoryName = "CreateWorker", Capacity = 32, BackpressurePolicy = "Wait", ErrorPolicy = "Continue")]
public static partial class OrderWorkMailbox
{
[MailboxHandler]
private static ValueTask Handle(Message<OrderWork> message, MessageContext context, CancellationToken cancellationToken)
=> ProcessAsync(message.Payload, cancellationToken);
}
```

Use generated mailboxes when inbox configuration is static and should be reviewed in code. Keep using fluent builders when capacity or policy is tenant- or environment-defined.

Example files:

- `src/PatternKit.Examples/Messaging/MailboxExample.cs`
- `test/PatternKit.Examples.Tests/Messaging/MailboxExampleTests.cs`

## Generated Saga

`[GenerateSaga]` emits a process-manager factory from typed transition methods:
Expand Down Expand Up @@ -217,6 +242,7 @@ Example source:
| `PKSA001`-`PKSA006` | Splitter / Aggregator | Non-partial host, missing contract methods, invalid signatures, or invalid duplicate policy. |
| `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. |
| `PKMB001`-`PKMB005` | Mailbox | Non-partial host, missing handler, invalid handler signatures, or invalid configuration. |

## Related Runtime Patterns

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 @@ -51,7 +51,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Enterprise Integration | Aggregator | `Aggregator<TKey, TIn, TOut>` | Messaging generator |
| Enterprise Integration | Routing Slip | `RoutingSlip<TPayload>` | Messaging generator |
| Enterprise Integration | Saga / Process Manager | `Saga<TState>` | Messaging generator |
| Enterprise Integration | Mailbox | `Mailbox<TPayload>` | Tracked in [#209](https://github.com/JerrettDavis/PatternKit/issues/209) |
| Enterprise Integration | Mailbox | `Mailbox<TPayload>` | Messaging generator |
| Messaging Reliability | Idempotent Receiver | `IdempotentReceiver<TPayload, TResult>` | Tracked in [#213](https://github.com/JerrettDavis/PatternKit/issues/213) |
| Messaging Reliability | Inbox | `InboxProcessor<TPayload, TResult>` | Tracked in [#213](https://github.com/JerrettDavis/PatternKit/issues/213) |
| Messaging Reliability | Outbox | `InMemoryOutbox<TPayload>` and dispatcher contracts | Tracked in [#213](https://github.com/JerrettDavis/PatternKit/issues/213) |
Expand Down
12 changes: 10 additions & 2 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, recipient lists, 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, splitter/aggregator contracts, routing-slip steps, saga transitions, or mailbox inbox policies are static enough to validate at compile time and you want AOT-friendly factories without reflection.

## Generated Content Router

Expand Down Expand Up @@ -75,7 +75,9 @@ Routing-slip generation is documented in [Routing Slip](routing-slip.md). It dis

Saga/process-manager generation is documented in [Saga / Process Manager](saga.md). It discovers `[SagaStep]` transition methods and optional `[SagaCompleteWhen]` completion checks.

Mailbox and reliability helpers stay runtime-only for now. Their registration is already small and lifecycle-sensitive, so a generator would add indirection without removing meaningful boilerplate.
Mailbox generation is documented in [Mailbox](mailbox.md). It discovers one `[MailboxHandler]` method plus optional error and event hooks, then emits a configured serialized inbox factory.

Reliability helpers stay runtime-only for now. Their registration is still lifecycle-sensitive and is tracked separately.

## Diagnostics

Expand All @@ -90,8 +92,10 @@ Mailbox and reliability helpers stay runtime-only for now. Their registration is
| `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. |
| `PKSA001`-`PKSA006` | Splitter/aggregator generator validation. |
| `PKRS001`-`PKRS003` | Routing-slip generator validation. |
| `PKSG001`-`PKSG004` | Saga generator validation. |
| `PKMB001`-`PKMB005` | Mailbox generator validation. |

## Troubleshooting

Expand All @@ -113,6 +117,10 @@ Mailbox and reliability helpers stay runtime-only for now. Their registration is
- <xref:PatternKit.Generators.Messaging.GenerateSagaAttribute>
- <xref:PatternKit.Generators.Messaging.SagaStepAttribute>
- <xref:PatternKit.Generators.Messaging.SagaCompleteWhenAttribute>
- <xref:PatternKit.Generators.Messaging.GenerateMailboxAttribute>
- <xref:PatternKit.Generators.Messaging.MailboxHandlerAttribute>
- <xref:PatternKit.Generators.Messaging.MailboxErrorHandlerAttribute>
- <xref:PatternKit.Generators.Messaging.MailboxEventSinkAttribute>
- <xref:PatternKit.Messaging.Routing.ContentRouter`2>
- <xref:PatternKit.Messaging.Routing.RecipientList`1>

Expand Down
20 changes: 20 additions & 0 deletions docs/patterns/messaging/mailbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ var mailbox = Mailbox<OrderWork>.Create(handler)
.Build();
```

## Source-Generated Mailboxes

Use `[GenerateMailbox]` when the inbox shape is stable and should be compile-time validated:

```csharp
[GenerateMailbox(typeof(OrderWork), FactoryName = "CreateWorker", Capacity = 128, BackpressurePolicy = "Wait", ErrorPolicy = "Continue")]
public static partial class OrderWorkMailbox
{
[MailboxHandler]
private static ValueTask Handle(Message<OrderWork> message, MessageContext context, CancellationToken cancellationToken)
=> ProcessAsync(message.Payload, cancellationToken);
}
```

The generated factory returns `Mailbox<OrderWork>` and applies the configured capacity, backpressure policy, error policy, optional error handler, and optional event sink.

## Choosing Related Patterns

- Use `Mailbox<TPayload>` when one in-process consumer must serialize work.
Expand All @@ -95,6 +111,10 @@ var mailbox = Mailbox<OrderWork>.Create(handler)
- <xref:PatternKit.Messaging.Mailboxes.MailboxPostStatus>
- <xref:PatternKit.Messaging.Mailboxes.MailboxEvent>
- <xref:PatternKit.Messaging.Mailboxes.MailboxEventKind>
- <xref:PatternKit.Generators.Messaging.GenerateMailboxAttribute>
- <xref:PatternKit.Generators.Messaging.MailboxHandlerAttribute>
- <xref:PatternKit.Generators.Messaging.MailboxErrorHandlerAttribute>
- <xref:PatternKit.Generators.Messaging.MailboxEventSinkAttribute>

## Example Source

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public sealed record PatternsShowcaseExample(ShowcaseFacade Facade);
public sealed record SourceGeneratorApplicationSuiteExample(Func<ValueTask<CorporateApp>> BuildProductionAsync);
public sealed record EnterpriseMessagingWorkflowSuiteExample(Func<Summary> Run);
public sealed record CqrsDispatcherExample(Func<CancellationToken, ValueTask<CqrsSummary>> RunFluentAsync, Func<IServiceProvider, CancellationToken, ValueTask<CqrsSummary>> RunSourceGeneratedAsync);
public sealed record GeneratedMailboxExample(MailboxExampleRunner Runner);
public sealed record ResilientCheckoutMailboxesExample(Func<CheckoutRequest, CheckoutServices, CheckoutResult> Run);
public sealed record MessagingBackplaneFacadeExample(Func<CancellationToken, ValueTask<BackplaneDemoSummary>> RunAsync);
public sealed record PrototypeGameCharacterFactoryExample(Prototype<string, PrototypeDemo.PrototypeDemo.GameCharacter> Factory);
Expand Down Expand Up @@ -133,6 +134,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddSourceGeneratorApplicationSuiteExample()
.AddEnterpriseMessagingWorkflowSuiteExample()
.AddCqrsDispatcherExample()
.AddGeneratedMailboxExample()
.AddResilientCheckoutMailboxesExample()
.AddMessagingBackplaneFacadeExample()
.AddPrototypeGameCharacterFactoryExample()
Expand Down Expand Up @@ -381,6 +383,13 @@ public static IServiceCollection AddCqrsDispatcherExample(this IServiceCollectio
return services.RegisterExample<CqrsDispatcherExample>("CQRS Dispatcher", ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddGeneratedMailboxExample(this IServiceCollection services)
{
services.AddSingleton(new MailboxExampleRunner(MailboxExample.RunFluentAsync, MailboxExample.RunGeneratedAsync));
services.AddSingleton<GeneratedMailboxExample>(sp => new(sp.GetRequiredService<MailboxExampleRunner>()));
return services.RegisterExample<GeneratedMailboxExample>("Generated Mailbox", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddResilientCheckoutMailboxesExample(this IServiceCollection services)
{
services.AddSingleton<CheckoutServices>();
Expand Down
40 changes: 39 additions & 1 deletion src/PatternKit.Examples/Messaging/MailboxExample.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PatternKit.Messaging;
using PatternKit.Messaging.Mailboxes;
using PatternKit.Generators.Messaging;

namespace PatternKit.Examples.Messaging;

Expand All @@ -9,7 +10,10 @@ namespace PatternKit.Examples.Messaging;
public static class MailboxExample
{
/// <summary>Runs a bounded mailbox and returns the processed work item identifiers.</summary>
public static async ValueTask<IReadOnlyList<string>> RunAsync()
public static ValueTask<IReadOnlyList<string>> RunAsync() => RunFluentAsync();

/// <summary>Runs a bounded mailbox built with the fluent runtime API.</summary>
public static async ValueTask<IReadOnlyList<string>> RunFluentAsync()
{
var processed = new List<string>();
using var mailbox = Mailbox<MailboxWorkItem>.Create((message, context, cancellationToken) =>
Expand All @@ -30,7 +34,41 @@ public static async ValueTask<IReadOnlyList<string>> RunAsync()
await mailbox.StopAsync();
return processed;
}

/// <summary>Runs a bounded mailbox built with the source-generated factory.</summary>
public static async ValueTask<IReadOnlyList<string>> RunGeneratedAsync()
{
GeneratedMailboxWorkQueue.Processed.Clear();
using var mailbox = GeneratedMailboxWorkQueue.CreateWorkQueue();

await mailbox.StartAsync();

var context = new MessageContext(MessageHeaders.Empty.WithCorrelationId("batch-42"));
await mailbox.PostAsync(Message<MailboxWorkItem>.Create(new MailboxWorkItem("prepare")), context);
await mailbox.PostAsync(Message<MailboxWorkItem>.Create(new MailboxWorkItem("ship")), context);

await mailbox.StopAsync();
return GeneratedMailboxWorkQueue.Processed.ToArray();
}
}

/// <summary>DI-friendly entry points for fluent and generated mailbox examples.</summary>
public sealed record MailboxExampleRunner(
Func<ValueTask<IReadOnlyList<string>>> RunFluentAsync,
Func<ValueTask<IReadOnlyList<string>>> RunGeneratedAsync);

/// <summary>Mailbox example payload.</summary>
public sealed record MailboxWorkItem(string Id);

[GenerateMailbox(typeof(MailboxWorkItem), FactoryName = "CreateWorkQueue", Capacity = 8, BackpressurePolicy = "Wait", ErrorPolicy = "Continue")]
public static partial class GeneratedMailboxWorkQueue
{
public static readonly List<string> Processed = [];

[MailboxHandler]
private static ValueTask Handle(Message<MailboxWorkItem> message, MessageContext context, CancellationToken cancellationToken)
{
Processed.Add($"{context.Headers.GetString(MessageHeaderNames.CorrelationId)}:{message.Payload.Id}");
return default;
}
}
Loading
Loading