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: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ PatternKit currently tracks 92 production-readiness patterns. Each catalog patte
| Behavioral | 11 | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor |
| Cloud Architecture | 17 | Ambassador, Backends for Frontends, Bulkhead, Cache-Aside, Circuit Breaker, External Configuration Store, Gateway Aggregation, Gateway Routing, Health Endpoint Monitoring, Leader Election, Priority Queue, Queue-Based Load Leveling, Rate Limiting, Retry, Scheduler Agent Supervisor, Sidecar, Strangler Fig |
| Creational | 5 | Abstract Factory, Builder, Factory Method, Prototype, Singleton |
| Enterprise Integration | 34 | Aggregator, Canonical Data Model, Channel Adapter, Channel Purger, Claim Check, Competing Consumers, Content-Based Router, Control Bus, Dead Letter Channel, Durable Subscriber, Dynamic Router, Event Notification, Event-Carried State Transfer, Event-Driven Consumer, Invalid Message Channel, Mailbox, Message Channel, Message Envelope, Message Filter, Message Store, Message Translator, Messaging Gateway, Pipes and Filters, Polling Consumer, Publish-Subscribe, Recipient List, Request-Reply, Resequencer, Routing Slip, Saga / Process Manager, Scatter-Gather, Service Activator, Splitter, Wire Tap |
| Enterprise Integration | 35 | Aggregator, Canonical Data Model, Channel Adapter, Channel Purger, Claim Check, Competing Consumers, Content-Based Router, Control Bus, Dead Letter Channel, Durable Subscriber, Dynamic Router, Event Notification, Event-Carried State Transfer, Event-Driven Consumer, Invalid Message Channel, Mailbox, Message Bus, Message Channel, Message Envelope, Message Filter, Message Store, Message Translator, Messaging Gateway, Pipes and Filters, Polling Consumer, Publish-Subscribe, Recipient List, Request-Reply, Resequencer, Routing Slip, Saga / Process Manager, Scatter-Gather, Service Activator, Splitter, Wire Tap |
| Messaging Reliability | 3 | Idempotent Receiver, Inbox, Outbox |
| Structural | 7 | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy |

Expand Down Expand Up @@ -516,6 +516,8 @@ BenchmarkDotNet guidance is documented in [docs/guides/benchmarks.md](docs/guide
| Durable Subscriber | Execution | 711.81 ns | 3,912 B | 605.65 ns | 3,128 B | Generated reduced catch-up projection replay time and allocation in this microbenchmark. |
| Dynamic Router | Construction | 213.4 ns | 1.29 KB | 214.0 ns | 1.29 KB | Fluent and generated route-table construction were effectively equivalent. |
| Dynamic Router | Execution | 406.7 ns | 2.83 KB | 415.2 ns | 2.83 KB | Same allocation; fluent was slightly faster for fulfillment route-table dispatch. |
| Message Bus | Construction | 186.7 ns | 1,264 B | 160.0 ns | 904 B | Generated reduced construction time and allocation for topic bus topology setup. |
| Message Bus | Execution | 503.8 ns | 3,192 B | 587.7 ns | 3,232 B | Fluent was faster and allocated slightly less for publishing order events to topic subscribers. |
| Decorator | Construction | 34.293 ns | 264 B | 17.669 ns | 168 B | Generated decorator composition was faster and allocated less. |
| Decorator | Execution | 60.765 ns | 384 B | 35.551 ns | 304 B | Generated decorator execution was faster and allocated less for decorated storage reads. |
| Domain Event | Construction | 199.5 ns | 1.34 KB | 157.6 ns | 1.04 KB | Generated reduced construction time and allocation in this microbenchmark. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Examples.Messaging;
using PatternKit.Messaging.Channels;

namespace PatternKit.Benchmarks.Messaging;

[BenchmarkCategory("EnterpriseIntegration", "Messaging", "MessageBus")]
public class MessageBusBenchmarks
{
private static readonly BusOrderEvent[] Events =
[
new("O-100", "accepted", 125m),
new("O-101", "paid", 250m)
];

[Benchmark(Baseline = true, Description = "Fluent: create message bus")]
[BenchmarkCategory("Fluent", "Construction")]
public MessageBus<BusOrderEvent> Fluent_CreateMessageBus()
{
var fulfillment = MessageChannel<BusOrderEvent>.Create("fulfillment-orders").Build();
var billing = MessageChannel<BusOrderEvent>.Create("billing-orders").Build();
var audit = MessageChannel<BusOrderEvent>.Create("order-audit").Build();
return OrderMessageBuses.Create(fulfillment, billing, audit);
}

[Benchmark(Description = "Generated: create message bus")]
[BenchmarkCategory("Generated", "Construction")]
public MessageBus<BusOrderEvent> Generated_CreateMessageBus()
=> GeneratedOrderMessageBus.Create();

[Benchmark(Description = "Fluent: publish order events")]
[BenchmarkCategory("Fluent", "Execution")]
public OrderMessageBusSummary Fluent_PublishOrderEvents()
=> OrderMessageBusExampleRunner.RunFluent(Events);

[Benchmark(Description = "Generated: publish order events")]
[BenchmarkCategory("Generated", "Execution")]
public OrderMessageBusSummary Generated_PublishOrderEvents()
=> OrderMessageBusExampleRunner.RunGeneratedStatic(Events);
}
19 changes: 19 additions & 0 deletions docs/examples/order-message-bus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Order Message Bus Example

The order message bus example shows a production-oriented topic bus with standard dependency injection:

- fluent `OrderMessageBuses.Create(...)` construction from application-owned channels;
- source-generated `GeneratedOrderMessageBus.Create()` topology;
- `AddOrderMessageBusDemo()` for `IServiceCollection` integration;
- TinyBDD coverage for fluent behavior, generated parity, direct DI registration, and aggregate `AddPatternKitExamples()` import.

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

using var provider = services.BuildServiceProvider(validateScopes: true);
var runner = provider.GetRequiredService<OrderMessageBusExampleRunner>();
var summary = runner.RunGenerated(orderEvents);
```

The service publishes accepted orders to fulfillment and audit subscribers, while paid orders flow to billing and audit subscribers.
1 change: 1 addition & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
|---|---|---|
| [**Dispatcher**](dispatcher.md) | Mediator pattern with commands, notifications, and streams | `[GenerateDispatcher]` |
| [**Message Channel**](message-channel.md) | Typed channel factories for in-process message queues | `[GenerateMessageChannel]` |
| [**Message Bus**](message-bus.md) | Topic bus topology factories over message channels | `[GenerateMessageBus]` |
| [**Channel Purger**](channel-purger.md) | Channel maintenance purger factories for in-process message queues | `[GenerateChannelPurger]` |
| [**Invalid Message Channel**](invalid-message-channel.md) | Invalid-message channel builder factories for validation boundaries | `[GenerateInvalidMessageChannel]` |
| [**Polling Consumer**](polling-consumer.md) | Pull-based message consumer factories | `[GeneratePollingConsumer]` |
Expand Down
23 changes: 23 additions & 0 deletions docs/generators/message-bus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Message Bus Generator

`[GenerateMessageBus]` emits a `MessageBus<TPayload>` factory from static channel factory methods marked with `[MessageBusRoute]`.

```csharp
[GenerateMessageBus(typeof(BusOrderEvent), FactoryName = "Create", BusName = "order-bus")]
public static partial class GeneratedOrderMessageBus
{
[MessageBusRoute("accepted")]
private static MessageChannel<BusOrderEvent> Fulfillment()
=> MessageChannel<BusOrderEvent>.Create("fulfillment-orders").Build();
}
```

The generated factory returns a normal `MessageBus<TPayload>`, so applications can still call `Subscribe` for runtime bolt-ons after the generated topology is created.

Diagnostics:

| ID | Meaning |
| --- | --- |
| `PKBUS001` | The host type must be partial. |
| `PKBUS002` | At least one `[MessageBusRoute]` method is required. |
| `PKBUS003` | Route methods must be static, parameterless, and return `MessageChannel<TPayload>`. |
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
- name: Message Channel
href: message-channel.md

- name: Message Bus
href: message-bus.md

- name: Channel Purger
href: channel-purger.md

Expand Down
12 changes: 8 additions & 4 deletions docs/guides/benchmark-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ The latest measured timings below were captured on Windows 11, Intel Core i9-149
| Durable Subscriber | Execution | 711.81 ns | 3,912 B | 605.65 ns | 3,128 B | Generated reduced catch-up projection replay time and allocation in this microbenchmark. |
| Dynamic Router | Construction | 213.4 ns | 1.29 KB | 214.0 ns | 1.29 KB | Fluent and generated route-table construction were effectively equivalent. |
| Dynamic Router | Execution | 406.7 ns | 2.83 KB | 415.2 ns | 2.83 KB | Same allocation; fluent was slightly faster for fulfillment route-table dispatch. |
| Message Bus | Construction | 186.7 ns | 1,264 B | 160.0 ns | 904 B | Generated reduced construction time and allocation for topic bus topology setup. |
| Message Bus | Execution | 503.8 ns | 3,192 B | 587.7 ns | 3,232 B | Fluent was faster and allocated slightly less for publishing order events to topic subscribers. |
| Decorator | Construction | 34.293 ns | 264 B | 17.669 ns | 168 B | Generated decorator composition was faster and allocated less. |
| Decorator | Execution | 60.765 ns | 384 B | 35.551 ns | 304 B | Generated decorator execution was faster and allocated less for decorated storage reads. |
| Domain Event | Construction | 199.5 ns | 1.34 KB | 157.6 ns | 1.04 KB | Generated reduced construction time and allocation in this microbenchmark. |
Expand Down Expand Up @@ -202,7 +204,7 @@ The latest measured timings below were captured on Windows 11, Intel Core i9-149

## Coverage Matrix Summary

The coverage matrix currently publishes 92 catalog patterns and 368 pattern route results. Each pattern has four BenchmarkDotNet routes: fluent construction, fluent execution, source-generated construction, and source-generated execution.
The coverage matrix currently publishes 93 catalog patterns and 372 pattern route results. Each pattern has four BenchmarkDotNet routes: fluent construction, fluent execution, source-generated construction, and source-generated execution.

| Category | Patterns | Published route results |
| --- | ---: | ---: |
Expand All @@ -214,7 +216,7 @@ The coverage matrix currently publishes 92 catalog patterns and 368 pattern rout
| Messaging Reliability | 3 | 12 |
| Structural | 7 | 28 |

The generator matrix currently publishes 88 generator source route results.
The generator matrix currently publishes 89 generator source route results.

## Pattern Matrix Results

Expand Down Expand Up @@ -280,6 +282,7 @@ The generator matrix currently publishes 88 generator source route results.
| Enterprise Integration | Dead Letter Channel | Covered | Covered | Covered | Covered |
| Enterprise Integration | Durable Subscriber | Covered | Covered | Covered | Covered |
| Enterprise Integration | Dynamic Router | Covered | Covered | Covered | Covered |
| Enterprise Integration | Message Bus | Covered | Covered | Covered | Covered |
| Enterprise Integration | Event Notification | Covered | Covered | Covered | Covered |
| Enterprise Integration | Event-Carried State Transfer | Covered | Covered | Covered | Covered |
| Enterprise Integration | Event-Driven Consumer | Covered | Covered | Covered | Covered |
Expand Down Expand Up @@ -366,8 +369,9 @@ The generator matrix currently publishes 88 generator source route results.
| DurableSubscriberGenerator | `src/PatternKit.Generators/Messaging/DurableSubscriberGenerator.cs` | Covered |
| DynamicRouterGenerator | `src/PatternKit.Generators/Messaging/DynamicRouterGenerator.cs` | Covered |
| EventDrivenConsumerGenerator | `src/PatternKit.Generators/Messaging/EventDrivenConsumerGenerator.cs` | Covered |
| MailboxGenerator | `src/PatternKit.Generators/Messaging/MailboxGenerator.cs` | Covered |
| MessageChannelGenerator | `src/PatternKit.Generators/Messaging/MessageChannelGenerator.cs` | Covered |
| MailboxGenerator | `src/PatternKit.Generators/Messaging/MailboxGenerator.cs` | Covered |
| MessageBusGenerator | `src/PatternKit.Generators/Messaging/MessageBusGenerator.cs` | Covered |
| MessageChannelGenerator | `src/PatternKit.Generators/Messaging/MessageChannelGenerator.cs` | Covered |
| MessageEnvelopeGenerator | `src/PatternKit.Generators/Messaging/MessageEnvelopeGenerator.cs` | Covered |
| MessageFilterGenerator | `src/PatternKit.Generators/Messaging/MessageFilterGenerator.cs` | Covered |
| MessageStoreGenerator | `src/PatternKit.Generators/Messaging/MessageStoreGenerator.cs` | Covered |
Expand Down
1 change: 1 addition & 0 deletions docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Enterprise Integration | Dead Letter Channel | `DeadLetterChannel<TPayload>` | Dead Letter Channel generator |
| Enterprise Integration | Content-Based Router | `ContentRouter<TPayload, TResult>` | Messaging generator |
| Enterprise Integration | Dynamic Router | `DynamicRouter<TPayload, TResult>` | Dynamic Router generator |
| Enterprise Integration | Message Bus | `MessageBus<TPayload>` | Message Bus generator |
| Enterprise Integration | Message Filter | `MessageFilter<TPayload>` | Message Filter generator |
| Enterprise Integration | Message Store | `MessageStore<TPayload>` | Message Store generator |
| Enterprise Integration | Wire Tap | `WireTap<TPayload>` | Wire Tap generator |
Expand Down
6 changes: 6 additions & 0 deletions docs/patterns/messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ Durable subscribers catch up from a message store and checkpoint the last succes

[Learn More](durable-subscriber.md)

## Message Bus

Typed message buses publish messages to named topic subscribers backed by PatternKit message channels, with fluent and source-generated topology paths.

[Learn More](message-bus.md)

## Channel Adapter

Channel adapters translate external transport DTOs into PatternKit message channels and translate outbound channel messages back to the transport shape.
Expand Down
29 changes: 29 additions & 0 deletions docs/patterns/messaging/message-bus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Message Bus

`MessageBus<TPayload>` provides a typed in-process topic bus over `MessageChannel<TPayload>` subscribers. Use it when an application needs one integration surface for publishing domain or integration events to multiple internal consumers without coupling publishers to channel instances.

```csharp
var bus = MessageBus<OrderEvent>.Create("order-bus")
.Route("accepted", fulfillmentChannel)
.Route("accepted", auditChannel)
.Route("paid", billingChannel)
.Build();

var result = bus.Publish("accepted", Message<OrderEvent>.Create(orderAccepted));
```

The fluent path supports runtime `Subscribe(topic, channel)` calls, so host bootstrapping can add tenant, module, or feature-specific routes from normal `IServiceCollection` registrations.

The source-generated path uses `[GenerateMessageBus]` and `[MessageBusRoute]` to create a strongly typed topology factory:

```csharp
[GenerateMessageBus(typeof(OrderEvent), BusName = "order-bus")]
public static partial class GeneratedOrderBus
{
[MessageBusRoute("accepted")]
private static MessageChannel<OrderEvent> Accepted()
=> MessageChannel<OrderEvent>.Create("accepted-orders").Build();
}
```

`OrderMessageBusExample` demonstrates fluent and generated publishing. Import it with `AddOrderMessageBusDemo()` or the aggregate `AddPatternKitExamples()` registration.
126 changes: 126 additions & 0 deletions src/PatternKit.Core/Messaging/Channels/MessageChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,132 @@ private MessageChannelReceiveResult(string channelName, bool received, Message<T
internal static MessageChannelReceiveResult<TPayload> Empty(string channelName) => new(channelName, false, null, 0);
}

public sealed class MessageBus<TPayload>
{
private readonly object _gate = new();
private readonly Dictionary<string, List<MessageChannel<TPayload>>> _routes;

private MessageBus(string name, Dictionary<string, List<MessageChannel<TPayload>>> routes)
=> (Name, _routes) = (name, routes);

public string Name { get; }

public IReadOnlyList<string> Topics
{
get
{
lock (_gate)
return _routes.Keys.OrderBy(static key => key, StringComparer.Ordinal).ToArray();
}
}

public void Subscribe(string topic, MessageChannel<TPayload> channel)
{
if (string.IsNullOrWhiteSpace(topic))
throw new ArgumentException("Message bus topic cannot be null, empty, or whitespace.", nameof(topic));
if (channel is null)
throw new ArgumentNullException(nameof(channel));

lock (_gate)
{
if (!_routes.TryGetValue(topic, out var channels))
{
channels = [];
_routes[topic] = channels;
}

channels.Add(channel);
}
}

public MessageBusPublishResult Publish(string topic, Message<TPayload> message)
{
if (string.IsNullOrWhiteSpace(topic))
throw new ArgumentException("Message bus topic cannot be null, empty, or whitespace.", nameof(topic));
if (message is null)
throw new ArgumentNullException(nameof(message));

MessageChannel<TPayload>[] channels;
lock (_gate)
channels = _routes.TryGetValue(topic, out var routed) ? routed.ToArray() : [];

var deliveries = new List<MessageBusDelivery>(channels.Length);
foreach (var channel in channels)
{
var result = channel.Send(message);
deliveries.Add(new(channel.Name, result.Accepted, result.Count, result.RejectionReason));
}

return new(Name, topic, deliveries);
}

public static Builder Create(string name = "message-bus") => new(name);

public sealed class Builder
{
private readonly string _name;
private readonly Dictionary<string, List<MessageChannel<TPayload>>> _routes = new(StringComparer.Ordinal);

internal Builder(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Message bus name cannot be null, empty, or whitespace.", nameof(name));

_name = name;
}

public Builder Route(string topic, MessageChannel<TPayload> channel)
{
if (string.IsNullOrWhiteSpace(topic))
throw new ArgumentException("Message bus topic cannot be null, empty, or whitespace.", nameof(topic));
if (channel is null)
throw new ArgumentNullException(nameof(channel));

if (!_routes.TryGetValue(topic, out var channels))
{
channels = [];
_routes[topic] = channels;
}

channels.Add(channel);
return this;
}

public MessageBus<TPayload> Build()
=> new(_name, _routes.ToDictionary(static route => route.Key, static route => route.Value.ToList(), StringComparer.Ordinal));
}
}

public sealed class MessageBusDelivery
{
public MessageBusDelivery(string channelName, bool accepted, int count, string? rejectionReason)
=> (ChannelName, Accepted, Count, RejectionReason) = (channelName, accepted, count, rejectionReason);

public string ChannelName { get; }

public bool Accepted { get; }

public int Count { get; }

public string? RejectionReason { get; }
}

public sealed class MessageBusPublishResult
{
public MessageBusPublishResult(string busName, string topic, IReadOnlyList<MessageBusDelivery> deliveries)
=> (BusName, Topic, Deliveries) = (busName, topic, deliveries);

public string BusName { get; }

public string Topic { get; }

public IReadOnlyList<MessageBusDelivery> Deliveries { get; }

public int AcceptedCount => Deliveries.Count(static delivery => delivery.Accepted);

public int RejectedCount => Deliveries.Count(static delivery => !delivery.Accepted);
}

public sealed class ChannelPurger<TPayload>
{
private readonly MessageChannel<TPayload> _channel;
Expand Down
Loading
Loading