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 @@ -72,6 +72,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **ERP Channel Adapter**
Shows fluent and source-generated external DTO adapters over PatternKit message channels, with an importable `IServiceCollection` extension. See [ERP Channel Adapter](erp-channel-adapter.md).

* **Payment Messaging Gateway**
Shows fluent and source-generated request/response gateways over PatternKit message channels, with an importable `IServiceCollection` extension. See [Payment Messaging Gateway](payment-messaging-gateway.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).

Expand Down
12 changes: 12 additions & 0 deletions docs/examples/payment-messaging-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Payment Messaging Gateway

The payment messaging gateway example exposes a typed authorization method while sending requests through a PatternKit message channel.

```csharp
services.AddPaymentMessagingGatewayDemo();

var service = provider.GetRequiredService<PaymentMessagingGatewayService>();
var summary = service.Authorize(new PaymentAuthorizationRequest("ORDER-100", 42.50m));
```

The example includes fluent and source-generated construction, a message-backed request boundary, and `IServiceCollection` registration for existing .NET applications.
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
- name: ERP Channel Adapter
href: erp-channel-adapter.md

- name: Payment Messaging Gateway
href: payment-messaging-gateway.md

- name: Patterns Showcase — Integrated Order Processing
href: patterns-showcase.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 @@ -84,6 +84,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Polling Consumer**](polling-consumer.md) | Pull-based message consumer factories | `[GeneratePollingConsumer]` |
| [**Event-Driven Consumer**](event-driven-consumer.md) | Push-based message consumer factories | `[GenerateEventDrivenConsumer]` |
| [**Channel Adapter**](channel-adapter.md) | External DTO to message-channel adapter factories | `[GenerateChannelAdapter]` |
| [**Messaging Gateway**](messaging-gateway.md) | Typed request/response gateway factories | `[GenerateMessagingGateway]` |
| [**Message Envelope**](messaging.md#generated-message-envelope) | Required message metadata contracts | `[GenerateMessageEnvelope]` |
| [**Message Translator**](message-translator.md) | Partner and transport event normalization | `[GenerateMessageTranslator]` |
| [**Claim Check**](claim-check.md) | External payload storage references | `[GenerateClaimCheck]` |
Expand Down
21 changes: 21 additions & 0 deletions docs/generators/messaging-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Messaging Gateway Generator

`[GenerateMessagingGateway]` creates a typed `MessagingGateway<TRequest, TResponse>` factory.

```csharp
[GenerateMessagingGateway(typeof(PaymentAuthorizationRequest), typeof(PaymentAuthorizationDecision), FactoryName = "Create", GatewayName = "payment-authorization-gateway")]
public static partial class PaymentGateway
{
[MessagingGatewayHandler]
private static Message<PaymentAuthorizationDecision> Authorize(Message<PaymentAuthorizationRequest> request, MessageContext context)
=> Message<PaymentAuthorizationDecision>.Create(new("AUTH-100", true));
}
```

The generated factory accepts a `MessageChannel<TRequest>` so the gateway can be composed through `IServiceCollection`.

Diagnostics:

- `PKGWY001`: host type must be partial.
- `PKGWY002`: exactly one messaging gateway handler is required.
- `PKGWY003`: messaging gateway handler signature is invalid.
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@
- name: Channel Adapter
href: channel-adapter.md

- name: Messaging Gateway
href: messaging-gateway.md

- name: Message Translator
href: message-translator.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 @@ -48,6 +48,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Enterprise Integration | Polling Consumer | `PollingConsumer<TPayload>` | Polling Consumer generator |
| Enterprise Integration | Event-Driven Consumer | `EventDrivenConsumer<TPayload>` | Event-Driven Consumer generator |
| Enterprise Integration | Channel Adapter | `ChannelAdapter<TExternal, TPayload>` | Channel Adapter generator |
| Enterprise Integration | Messaging Gateway | `MessagingGateway<TRequest, TResponse>` | Messaging Gateway generator |
| Enterprise Integration | Message Envelope | `Message<TPayload>`, headers, context | Messaging generator |
| Enterprise Integration | Message Translator | `MessageTranslator<TInput, TOutput>` | Message Translator generator |
| Enterprise Integration | Claim Check | `ClaimCheck<TPayload>` | Claim Check 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 @@ Channel adapters translate external transport DTOs into PatternKit message chann

[Learn More](channel-adapter.md)

## Messaging Gateway

Messaging gateways expose typed request/response methods while hiding message envelope and channel plumbing from application services.

[Learn More](messaging-gateway.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
17 changes: 17 additions & 0 deletions docs/patterns/messaging/messaging-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Messaging Gateway

Messaging Gateway exposes an application-friendly request/response method while hiding message channel and envelope plumbing.

```csharp
var gateway = MessagingGateway<PaymentAuthorizationRequest, PaymentAuthorizationDecision>
.Create("payment-authorization-gateway")
.SendTo(requestChannel)
.Handle((message, context) => Message<PaymentAuthorizationDecision>.Create(decision))
.Build();

var result = gateway.Invoke(new PaymentAuthorizationRequest("ORDER-100", 42.50m));
```

Use it when application services should call a typed API while the implementation still sends a message through a channel and handles a message-shaped response. The gateway reports channel rejection explicitly so callers can distinguish backpressure from business decisions.

The source-generated path uses `[GenerateMessagingGateway]` and `[MessagingGatewayHandler]`. Import the payment example through `AddPaymentMessagingGatewayDemo()` or `AddPatternKitExamples()`.
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@
href: messaging/event-driven-consumer.md
- name: Channel Adapter
href: messaging/channel-adapter.md
- name: Messaging Gateway
href: messaging/messaging-gateway.md
- name: Message Envelope and Context
href: messaging/message-envelope.md
- name: Message Translator
Expand Down
104 changes: 104 additions & 0 deletions src/PatternKit.Core/Messaging/Gateways/MessagingGateway.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using PatternKit.Messaging.Channels;

namespace PatternKit.Messaging.Gateways;

/// <summary>Typed request/response facade over message channels and handlers.</summary>
public sealed class MessagingGateway<TRequest, TResponse>
{
public delegate Message<TResponse> GatewayHandler(Message<TRequest> request, MessageContext context);

private readonly MessageChannel<TRequest> _requestChannel;
private readonly GatewayHandler _handler;

private MessagingGateway(string name, MessageChannel<TRequest> requestChannel, GatewayHandler handler)
=> (Name, _requestChannel, _handler) = (name, requestChannel, handler);

public string Name { get; }

public MessagingGatewayResult<TRequest, TResponse> Invoke(TRequest request, MessageContext? context = null)
{
var requestMessage = Message<TRequest>.Create(request);
var effectiveContext = context ?? MessageContext.From(requestMessage);
var send = _requestChannel.Send(requestMessage);
if (!send.Accepted)
return MessagingGatewayResult<TRequest, TResponse>.CreateRejected(Name, requestMessage, send);

var response = _handler(requestMessage, effectiveContext);
if (response is null)
throw new InvalidOperationException("Messaging gateway handler returned null.");

return MessagingGatewayResult<TRequest, TResponse>.CreateCompleted(Name, requestMessage, response, send);
}

public static Builder Create(string name = "messaging-gateway") => new(name);

public sealed class Builder
{
private readonly string _name;
private MessageChannel<TRequest>? _requestChannel;
private GatewayHandler? _handler;

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

_name = name;
}

public Builder SendTo(MessageChannel<TRequest> channel)
{
_requestChannel = channel ?? throw new ArgumentNullException(nameof(channel));
return this;
}

public Builder Handle(GatewayHandler handler)
{
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
return this;
}

public MessagingGateway<TRequest, TResponse> Build()
{
if (_requestChannel is null)
throw new InvalidOperationException("Messaging gateway requires a request channel.");
if (_handler is null)
throw new InvalidOperationException("Messaging gateway requires a handler.");

return new(_name, _requestChannel, _handler);
}
}
}

public sealed class MessagingGatewayResult<TRequest, TResponse>
{
private MessagingGatewayResult(
string gatewayName,
Message<TRequest> request,
Message<TResponse>? response,
MessageChannelSendResult channelResult)
=> (GatewayName, Request, Response, ChannelResult) = (gatewayName, request, response, channelResult);

public string GatewayName { get; }

public Message<TRequest> Request { get; }

public Message<TResponse>? Response { get; }

public MessageChannelSendResult ChannelResult { get; }

public bool Completed => ChannelResult.Accepted && Response is not null;

internal static MessagingGatewayResult<TRequest, TResponse> CreateCompleted(
string gatewayName,
Message<TRequest> request,
Message<TResponse> response,
MessageChannelSendResult channelResult)
=> new(gatewayName, request, response, channelResult);

internal static MessagingGatewayResult<TRequest, TResponse> CreateRejected(
string gatewayName,
Message<TRequest> request,
MessageChannelSendResult channelResult)
=> new(gatewayName, request, null, channelResult);
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
using PatternKit.Messaging.Channels;
using PatternKit.Messaging.Consumers;
using PatternKit.Messaging.Adapters;
using PatternKit.Messaging.Gateways;
using PatternKit.Messaging.Routing;
using PatternKit.Messaging.Storage;
using PatternKit.Messaging.ControlBus;
Expand Down Expand Up @@ -130,6 +131,7 @@ public sealed record InventoryMessageChannelExampleService(MessageChannel<Invent
public sealed record WarehousePollingConsumerExampleService(PollingConsumer<ReplenishmentRequest> Consumer, WarehousePollingConsumerService Service);
public sealed record OrderEventDrivenConsumerExampleService(EventDrivenConsumer<OrderAcceptedEvent> Consumer, OrderEventDrivenConsumerService Service);
public sealed record ErpChannelAdapterExampleService(ChannelAdapter<ErpOrderDocument, OrderIntegrationMessage> Adapter, ErpChannelAdapterService Service);
public sealed record PaymentMessagingGatewayExampleService(MessagingGateway<PaymentAuthorizationRequest, PaymentAuthorizationDecision> Gateway, PaymentMessagingGatewayService Service);
public sealed record GeneratedMessageEnvelopeExample(MessageEnvelopeExampleRunner Runner);
public sealed record GeneratedMessageTranslatorExample(PartnerEventTranslatorExampleRunner Runner, PartnerOrderImportService Service);
public sealed record GeneratedClaimCheckExample(LargeDocumentClaimCheckExampleRunner Runner, LargeDocumentWorkflow Workflow);
Expand Down Expand Up @@ -212,6 +214,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddWarehousePollingConsumerExample()
.AddOrderEventDrivenConsumerExample()
.AddErpChannelAdapterExample()
.AddPaymentMessagingGatewayExample()
.AddGeneratedMessageEnvelopeExample()
.AddGeneratedMessageTranslatorExample()
.AddGeneratedClaimCheckExample()
Expand Down Expand Up @@ -498,6 +501,15 @@ public static IServiceCollection AddErpChannelAdapterExample(this IServiceCollec
return services.RegisterExample<ErpChannelAdapterExampleService>("ERP Channel Adapter", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddPaymentMessagingGatewayExample(this IServiceCollection services)
{
services.AddPaymentMessagingGatewayDemo();
services.AddSingleton<PaymentMessagingGatewayExampleService>(sp => new(
sp.GetRequiredService<MessagingGateway<PaymentAuthorizationRequest, PaymentAuthorizationDecision>>(),
sp.GetRequiredService<PaymentMessagingGatewayService>()));
return services.RegisterExample<PaymentMessagingGatewayExampleService>("Payment Messaging Gateway", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddGeneratedMessageEnvelopeExample(this IServiceCollection services)
{
services.AddMessageEnvelopeExample();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Microsoft.Extensions.DependencyInjection;
using PatternKit.Generators.Messaging;
using PatternKit.Messaging;
using PatternKit.Messaging.Channels;
using PatternKit.Messaging.Gateways;

namespace PatternKit.Examples.Messaging;

public sealed record PaymentAuthorizationRequest(string OrderId, decimal Amount);

public sealed record PaymentAuthorizationDecision(string AuthorizationCode, bool Approved);

public sealed record PaymentGatewaySummary(bool Completed, bool Approved, string? AuthorizationCode, int RequestCount);

public sealed class PaymentMessagingGatewayService(MessagingGateway<PaymentAuthorizationRequest, PaymentAuthorizationDecision> gateway, MessageChannel<PaymentAuthorizationRequest> requests)
{
public PaymentGatewaySummary Authorize(PaymentAuthorizationRequest request)
{
var result = gateway.Invoke(request);
return new(
result.Completed,
result.Response?.Payload.Approved ?? false,
result.Response?.Payload.AuthorizationCode,
requests.Count);
}
}

public static class PaymentMessagingGateways
{
public static MessagingGateway<PaymentAuthorizationRequest, PaymentAuthorizationDecision> Create(MessageChannel<PaymentAuthorizationRequest> requests)
=> MessagingGateway<PaymentAuthorizationRequest, PaymentAuthorizationDecision>.Create("payment-authorization-gateway")
.SendTo(requests)
.Handle(Authorize)
.Build();

public static Message<PaymentAuthorizationDecision> Authorize(Message<PaymentAuthorizationRequest> request, MessageContext context)
{
var approved = request.Payload.Amount <= 500m;
var code = approved ? $"AUTH-{request.Payload.OrderId}" : "DECLINED";
return Message<PaymentAuthorizationDecision>.Create(new(code, approved));
}
}

[GenerateMessagingGateway(typeof(PaymentAuthorizationRequest), typeof(PaymentAuthorizationDecision), FactoryName = "Create", GatewayName = "payment-authorization-gateway")]
public static partial class GeneratedPaymentMessagingGateway
{
[MessagingGatewayHandler]
private static Message<PaymentAuthorizationDecision> Authorize(Message<PaymentAuthorizationRequest> request, MessageContext context)
=> PaymentMessagingGateways.Authorize(request, context);
}

public sealed class PaymentMessagingGatewayExampleRunner(PaymentMessagingGatewayService service)
{
public PaymentGatewaySummary RunGenerated(PaymentAuthorizationRequest request) => service.Authorize(request);

public static PaymentGatewaySummary RunFluent(PaymentAuthorizationRequest request)
{
var channel = MessageChannel<PaymentAuthorizationRequest>.Create("payment-requests").Build();
return new PaymentMessagingGatewayService(PaymentMessagingGateways.Create(channel), channel).Authorize(request);
}

public static PaymentGatewaySummary RunGeneratedStatic(PaymentAuthorizationRequest request)
{
var channel = MessageChannel<PaymentAuthorizationRequest>.Create("payment-requests").Build();
return new PaymentMessagingGatewayService(GeneratedPaymentMessagingGateway.Create(channel), channel).Authorize(request);
}
}

public static class PaymentMessagingGatewayExampleServiceCollectionExtensions
{
public static IServiceCollection AddPaymentMessagingGatewayDemo(this IServiceCollection services)
{
services.AddSingleton(_ => MessageChannel<PaymentAuthorizationRequest>.Create("payment-requests").Build());
services.AddSingleton(sp => GeneratedPaymentMessagingGateway.Create(sp.GetRequiredService<MessageChannel<PaymentAuthorizationRequest>>()));
services.AddSingleton<PaymentMessagingGatewayService>();
services.AddSingleton<PaymentMessagingGatewayExampleRunner>();
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ public sealed class PatternKitExampleCatalog : IPatternKitExampleCatalog
ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["ChannelAdapter", "MessageChannel"],
["external ERP DTO bridge", "source-generated adapter factory", "DI composition"]),
Descriptor(
"Payment Messaging Gateway",
"src/PatternKit.Examples/Messaging/PaymentMessagingGatewayExample.cs",
"test/PatternKit.Examples.Tests/Messaging/PaymentMessagingGatewayExampleTests.cs",
"docs/examples/payment-messaging-gateway.md",
ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["MessagingGateway", "MessageChannel"],
["typed authorization gateway", "source-generated gateway factory", "DI composition"]),
Descriptor(
"Patterns Showcase",
"src/PatternKit.Examples/PatternShowcase/PatternShowcase.cs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,19 @@ public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog
"test/PatternKit.Examples.Tests/Messaging/ErpChannelAdapterExampleTests.cs",
["fluent external channel bridge", "generated adapter factory", "DI-importable ERP integration example"]),

Pattern("Messaging Gateway", PatternFamily.EnterpriseIntegration,
"docs/patterns/messaging/messaging-gateway.md",
"src/PatternKit.Core/Messaging/Gateways/MessagingGateway.cs",
"test/PatternKit.Tests/Messaging/Gateways/MessagingGatewayTests.cs",
"docs/generators/messaging-gateway.md",
"src/PatternKit.Generators/Messaging/MessagingGatewayGenerator.cs",
"test/PatternKit.Generators.Tests/MessagingGatewayGeneratorTests.cs",
null,
"docs/examples/payment-messaging-gateway.md",
"src/PatternKit.Examples/Messaging/PaymentMessagingGatewayExample.cs",
"test/PatternKit.Examples.Tests/Messaging/PaymentMessagingGatewayExampleTests.cs",
["fluent request-response facade", "generated gateway factory", "DI-importable payment authorization example"]),

Pattern("Message Envelope", PatternFamily.EnterpriseIntegration,
"docs/patterns/messaging/message-envelope.md",
"src/PatternKit.Core/Messaging/Message.cs",
Expand Down
Loading
Loading