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: 3 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **Inventory Event-Carried State Transfer**
Shows fluent and source-generated inventory projection events that carry enough state to update a local read model, with an importable `IServiceCollection` extension. See [Inventory Event-Carried State Transfer](inventory-event-carried-state-transfer.md).

* **Order Event Notification**
Shows fluent and source-generated compact order notifications with correlation metadata, dispatch rules, and an importable `IServiceCollection` extension. See [Order Event Notification](order-event-notification.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
12 changes: 12 additions & 0 deletions docs/examples/order-event-notification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Order Event Notification

The order event notification example publishes compact `OrderAcceptedNotificationEvent` notifications through an importable service.

```csharp
services.AddOrderEventNotificationDemo();

var runner = provider.GetRequiredService<OrderEventNotificationDemoRunner>();
var summary = runner.RunGenerated(new OrderAcceptedNotificationEvent("O-100", "C-900", "web", true));
```

The example includes fluent and source-generated construction plus an `IServiceCollection` extension for standard .NET hosts.
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
- name: Inventory Event-Carried State Transfer
href: inventory-event-carried-state-transfer.md

- name: Order Event Notification
href: order-event-notification.md

- name: Generated Claim Check
href: generated-claim-check.md

Expand Down
28 changes: 28 additions & 0 deletions docs/generators/event-notification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Event Notification Generator

`[GenerateEventNotification]` creates a typed `EventNotification<TEvent, TKey>` factory from key, rule, correlation, and metadata methods.

```csharp
[GenerateEventNotification(typeof(OrderAccepted), typeof(string), NotificationName = "order-accepted")]
public static partial class OrderAcceptedNotification
{
[EventNotificationRule]
private static bool ShouldNotify(OrderAccepted evt) => evt.NotifySubscribers;

[EventNotificationKey]
private static string Key(OrderAccepted evt) => evt.OrderId;

[EventNotificationCorrelation]
private static string Correlation(OrderAccepted evt) => evt.CorrelationId;

[EventNotificationMetadata("source")]
private static string Source(OrderAccepted evt) => evt.Source;
}
```

Diagnostics:

- `PKEN001`: host type must be partial.
- `PKEN002`: exactly one key selector is required.
- `PKEN003`: selector, rule, correlation, or metadata signature is invalid.
- `PKEN004`: metadata names must be unique.
5 changes: 5 additions & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Message Translator**](message-translator.md) | Partner and transport event normalization | `[GenerateMessageTranslator]` |
| [**Canonical Data Model**](canonical-data-model.md) | Source-to-canonical contract normalization | `[GenerateCanonicalDataModel]` |
| [**Event-Carried State Transfer**](event-carried-state-transfer.md) | State-rich event projection factories | `[GenerateEventCarriedStateTransfer]` |
| [**Event Notification**](event-notification.md) | Compact event notification factories | `[GenerateEventNotification]` |
| [**Claim Check**](claim-check.md) | External payload storage references | `[GenerateClaimCheck]` |
| [**Dead Letter Channel**](dead-letter-channel.md) | Failed-message capture and replay handoff | `[GenerateDeadLetterChannel]` |
| [**Content Router**](messaging.md#generated-content-router) | Content-based message routing factories | `[GenerateContentRouter]` |
Expand Down Expand Up @@ -200,6 +201,10 @@ public static partial class PartnerOrderTranslator { }
[GenerateEventCarriedStateTransfer(typeof(InventoryAdjustedEvent), typeof(string), typeof(InventoryReadModel))]
public static partial class InventoryStateTransfer { }

// Event notification - compact event signals
[GenerateEventNotification(typeof(OrderAccepted), typeof(string))]
public static partial class OrderAcceptedNotification { }

// Claim check - external payload storage reference
[GenerateClaimCheck(typeof(LargeOrderDocument), StoreName = "document-archive")]
public static partial class LargeDocumentClaims { }
Expand Down
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
- name: Event-Carried State Transfer
href: event-carried-state-transfer.md

- name: Event Notification
href: event-notification.md

- name: Chain
href: chain.md

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 @@ -54,6 +54,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Enterprise Integration | Message Translator | `MessageTranslator<TInput, TOutput>` | Message Translator generator |
| Enterprise Integration | Canonical Data Model | `CanonicalDataModel<TCanonical>` | Canonical Data Model generator |
| Enterprise Integration | Event-Carried State Transfer | `EventCarriedStateTransfer<TEvent,TKey,TState>` | Event-Carried State Transfer generator |
| Enterprise Integration | Event Notification | `EventNotification<TEvent,TKey>` | Event Notification generator |
| Enterprise Integration | Claim Check | `ClaimCheck<TPayload>` | Claim Check generator |
| Enterprise Integration | Dead Letter Channel | `DeadLetterChannel<TPayload>` | Dead Letter Channel generator |
| Enterprise Integration | Content-Based Router | `ContentRouter<TPayload, TResult>` | Messaging 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 @@ -62,6 +62,12 @@ Event-carried state transfer publishes enough state in an event for subscribers

[Learn More](event-carried-state-transfer.md)

## Event Notification

Event notification publishes compact event signals with keys, correlation IDs, and metadata when subscribers do not need a full state payload.

[Learn More](event-notification.md)

## Idempotent Receiver, Inbox, and Outbox

Idempotency and handoff helpers compose message handlers with pluggable stores, inbox boundaries, and outbox records without claiming broker durability or exactly-once delivery.
Expand Down
19 changes: 19 additions & 0 deletions docs/patterns/messaging/event-notification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Event Notification

Event Notification publishes a compact signal that something happened, usually with an identifier and correlation metadata rather than the full state payload.

```csharp
var notification = EventNotification<OrderAccepted, string>
.Create("order-accepted")
.When(evt => evt.NotifySubscribers)
.WithKey(evt => evt.OrderId)
.WithCorrelation(evt => evt.CorrelationId)
.WithMetadata("source", evt => evt.Source)
.Build();

var result = notification.Notify(orderAccepted);
```

Use it when subscribers can react to a lightweight event and only fetch more detail when their workflow requires it. The runtime path supports dispatch predicates, correlation IDs, metadata, and explicit skipped or failed results.

The source-generated path uses `[GenerateEventNotification]`, `[EventNotificationKey]`, `[EventNotificationRule]`, `[EventNotificationCorrelation]`, and `[EventNotificationMetadata]`. Import the example through `AddOrderEventNotificationDemo()` or `AddPatternKitExamples()`.
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@
href: messaging/canonical-data-model.md
- name: Event-Carried State Transfer
href: messaging/event-carried-state-transfer.md
- name: Event Notification
href: messaging/event-notification.md
- name: Claim Check
href: messaging/claim-check.md
- name: Dead Letter Channel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
namespace PatternKit.EnterpriseIntegration.EventNotification;

public sealed class EventNotificationResult<TKey>
{
private EventNotificationResult(
string notificationName,
TKey? key,
string correlationId,
IReadOnlyDictionary<string, string> metadata,
Exception? exception,
bool published,
bool skipped)
=> (NotificationName, Key, CorrelationId, Metadata, Exception, Published, Skipped) = (notificationName, key, correlationId, metadata, exception, published, skipped);

public string NotificationName { get; }

public TKey? Key { get; }

public string CorrelationId { get; }

public IReadOnlyDictionary<string, string> Metadata { get; }

public Exception? Exception { get; }

public bool Published { get; }

public bool Skipped { get; }

public bool Failed => !Published && !Skipped;

public static EventNotificationResult<TKey> Publish(string notificationName, TKey key, string correlationId, IReadOnlyDictionary<string, string> metadata)
=> new(notificationName, key, correlationId, metadata, null, true, false);

public static EventNotificationResult<TKey> Skip(string notificationName)
=> new(notificationName, default, string.Empty, new Dictionary<string, string>(), null, false, true);

public static EventNotificationResult<TKey> Failure(string notificationName, Exception exception)
=> new(notificationName, default, string.Empty, new Dictionary<string, string>(), exception ?? throw new ArgumentNullException(nameof(exception)), false, false);
}

public sealed class EventNotification<TEvent, TKey>
{
private readonly Func<TEvent, bool> _predicate;
private readonly Func<TEvent, TKey> _keySelector;
private readonly Func<TEvent, string>? _correlationSelector;
private readonly IReadOnlyList<MetadataSelector> _metadataSelectors;

private EventNotification(
string name,
Func<TEvent, bool>? predicate,
Func<TEvent, TKey>? keySelector,
Func<TEvent, string>? correlationSelector,
IReadOnlyList<MetadataSelector> metadataSelectors)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Event notification name is required.", nameof(name));

Name = name;
_predicate = predicate ?? (_ => true);
_keySelector = keySelector ?? throw new InvalidOperationException("Event notification requires a key selector.");
_correlationSelector = correlationSelector;
_metadataSelectors = metadataSelectors ?? throw new ArgumentNullException(nameof(metadataSelectors));
}

public string Name { get; }

public EventNotificationResult<TKey> Notify(TEvent @event)
{
if (@event is null)
throw new ArgumentNullException(nameof(@event));

try
{
if (!_predicate(@event))
return EventNotificationResult<TKey>.Skip(Name);

var key = _keySelector(@event);
if (key is null)
return EventNotificationResult<TKey>.Failure(Name, new InvalidOperationException("Event notification key selector returned null."));

var correlationId = _correlationSelector?.Invoke(@event) ?? string.Empty;
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var selector in _metadataSelectors)
{
var value = selector.Select(@event);
if (!string.IsNullOrWhiteSpace(value))
metadata[selector.Name] = value!;
}

return EventNotificationResult<TKey>.Publish(Name, key, correlationId, metadata);
}
catch (Exception ex)
{
return EventNotificationResult<TKey>.Failure(Name, ex);
}
}

public static Builder Create(string name = "event-notification") => new(name);

public sealed class Builder
{
private readonly string _name;
private readonly List<MetadataSelector> _metadataSelectors = [];
private Func<TEvent, bool>? _predicate;
private Func<TEvent, TKey>? _keySelector;
private Func<TEvent, string>? _correlationSelector;

internal Builder(string name) => _name = name;

public Builder When(Func<TEvent, bool> predicate)
{
_predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
return this;
}

public Builder WithKey(Func<TEvent, TKey> keySelector)
{
_keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
return this;
}

public Builder WithCorrelation(Func<TEvent, string> correlationSelector)
{
_correlationSelector = correlationSelector ?? throw new ArgumentNullException(nameof(correlationSelector));
return this;
}

public Builder WithMetadata(string name, Func<TEvent, string?> metadataSelector)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Event notification metadata name is required.", nameof(name));
if (metadataSelector is null)
throw new ArgumentNullException(nameof(metadataSelector));
if (_metadataSelectors.Any(selector => string.Equals(selector.Name, name, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"Event notification metadata '{name}' is already registered.");

_metadataSelectors.Add(new(name, metadataSelector));
return this;
}

public EventNotification<TEvent, TKey> Build()
=> new(_name, _predicate, _keySelector, _correlationSelector, _metadataSelectors.ToArray());
}

private sealed class MetadataSelector
{
public MetadataSelector(string name, Func<TEvent, string?> select)
=> (Name, Select) = (name, select);

public string Name { get; }

public Func<TEvent, string?> Select { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
using PatternKit.Examples.EnterpriseFeatureSlices;
using PatternKit.Examples.EventSourcingDemo;
using PatternKit.Examples.EventCarriedStateTransferDemo;
using PatternKit.Examples.EventNotificationDemo;
using PatternKit.Examples.ExternalConfigurationStoreDemo;
using PatternKit.Examples.FeatureToggleDemo;
using PatternKit.Examples.FlyweightDemo;
Expand Down Expand Up @@ -144,6 +145,7 @@ public sealed record GeneratedMessageEnvelopeExample(MessageEnvelopeExampleRunne
public sealed record GeneratedMessageTranslatorExample(PartnerEventTranslatorExampleRunner Runner, PartnerOrderImportService Service);
public sealed record CanonicalOrderDataModelExample(CanonicalOrderDemoRunner Runner, CanonicalOrderImportService Service);
public sealed record InventoryEventCarriedStateTransferExample(InventoryEventCarriedStateTransferDemoRunner Runner, InventoryProjectionService Service);
public sealed record OrderEventNotificationExample(OrderEventNotificationDemoRunner Runner, OrderNotificationService Service);
public sealed record GeneratedClaimCheckExample(LargeDocumentClaimCheckExampleRunner Runner, LargeDocumentWorkflow Workflow);
public sealed record GeneratedDeadLetterChannelExample(FulfillmentDeadLetterChannelExampleRunner Runner, FulfillmentDeadLetterWorkflow Workflow);
public sealed record GeneratedRecipientListExample(RecipientListGeneratorExampleRunner Runner);
Expand Down Expand Up @@ -232,6 +234,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddGeneratedMessageTranslatorExample()
.AddCanonicalOrderDataModelExample()
.AddInventoryEventCarriedStateTransferExample()
.AddOrderEventNotificationExample()
.AddGeneratedClaimCheckExample()
.AddGeneratedDeadLetterChannelExample()
.AddGeneratedRecipientListExample()
Expand Down Expand Up @@ -570,6 +573,15 @@ public static IServiceCollection AddInventoryEventCarriedStateTransferExample(th
return services.RegisterExample<InventoryEventCarriedStateTransferExample>("Inventory Event-Carried State Transfer", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddOrderEventNotificationExample(this IServiceCollection services)
{
services.AddOrderEventNotificationDemo();
services.AddSingleton<OrderEventNotificationExample>(sp => new(
sp.GetRequiredService<OrderEventNotificationDemoRunner>(),
sp.GetRequiredService<OrderNotificationService>()));
return services.RegisterExample<OrderEventNotificationExample>("Order Event Notification", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddGeneratedClaimCheckExample(this IServiceCollection services)
{
services.AddLargeDocumentClaimCheckExample();
Expand Down
Loading
Loading