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 @@ -75,6 +75,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **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).

* **Inventory Service Activator**
Shows fluent and source-generated message-to-service activation with an importable `IServiceCollection` extension. See [Inventory Service Activator](inventory-service-activator.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/inventory-service-activator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Inventory Service Activator

The inventory service activator example turns an inbound reservation message into a container-owned application service call.

```csharp
services.AddInventoryServiceActivatorDemo();

var service = provider.GetRequiredService<InventoryServiceActivatorService>();
var summary = service.Reserve(new InventoryReservationRequest("SKU-100", 5));
```

The example includes fluent and source-generated construction, typed message activation, 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 @@ -61,6 +61,9 @@
- name: Payment Messaging Gateway
href: payment-messaging-gateway.md

- name: Inventory Service Activator
href: inventory-service-activator.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 @@ -85,6 +85,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**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]` |
| [**Service Activator**](service-activator.md) | Message-to-service operation factories | `[GenerateServiceActivator]` |
| [**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/service-activator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Service Activator Generator

`[GenerateServiceActivator]` creates a typed `ServiceActivator<TRequest, TResponse>` factory.

```csharp
[GenerateServiceActivator(typeof(InventoryReservationRequest), typeof(InventoryReservationResult), FactoryName = "Create", ActivatorName = "inventory-reservation-activator")]
public static partial class InventoryActivator
{
[ServiceActivatorHandler]
private static Message<InventoryReservationResult> Reserve(Message<InventoryReservationRequest> request, MessageContext context)
=> Message<InventoryReservationResult>.Create(new(request.Payload.Sku, true, "allocated"));
}
```

The generated factory has no parameters, so the activator can be registered directly in `IServiceCollection` and injected into application services.

Diagnostics:

- `PKSVA001`: host type must be partial.
- `PKSVA002`: exactly one service activator handler is required.
- `PKSVA003`: service activator 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 @@ -100,6 +100,9 @@
- name: Messaging Gateway
href: messaging-gateway.md

- name: Service Activator
href: service-activator.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 @@ -49,6 +49,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| 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 | Service Activator | `ServiceActivator<TRequest, TResponse>` | Service Activator 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 @@ -50,6 +50,12 @@ Messaging gateways expose typed request/response methods while hiding message en

[Learn More](messaging-gateway.md)

## Service Activator

Service activators invoke application service operations from typed messages while preserving message context and response envelopes.

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

Service Activator invokes an application service operation from a typed message and returns a typed response message.

```csharp
var activator = ServiceActivator<InventoryReservationRequest, InventoryReservationResult>
.Create("inventory-reservation-activator")
.Handle((message, context) => Message<InventoryReservationResult>.Create(result))
.Build();

var response = activator.Activate(Message<InventoryReservationRequest>.Create(request));
```

Use it when a message endpoint should hand work to a domain or application service while preserving message context and typed request/response contracts. The activator keeps handler validation explicit, making it suitable for container-owned services in Generic Host or ASP.NET Core applications.

The source-generated path uses `[GenerateServiceActivator]` and `[ServiceActivatorHandler]`. Import the inventory example through `AddInventoryServiceActivatorDemo()` or `AddPatternKitExamples()`.
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@
href: messaging/channel-adapter.md
- name: Messaging Gateway
href: messaging/messaging-gateway.md
- name: Service Activator
href: messaging/service-activator.md
- name: Message Envelope and Context
href: messaging/message-envelope.md
- name: Message Translator
Expand Down
74 changes: 74 additions & 0 deletions src/PatternKit.Core/Messaging/Activation/ServiceActivator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace PatternKit.Messaging.Activation;

/// <summary>Activates an application service operation from a typed message.</summary>
public sealed class ServiceActivator<TRequest, TResponse>
{
public delegate Message<TResponse> ServiceHandler(Message<TRequest> request, MessageContext context);

private readonly ServiceHandler _handler;

private ServiceActivator(string name, ServiceHandler handler)
=> (Name, _handler) = (name, handler);

public string Name { get; }

public ServiceActivatorResult<TRequest, TResponse> Activate(Message<TRequest> request, MessageContext? context = null)
{
if (request is null)
throw new ArgumentNullException(nameof(request));

var effectiveContext = context ?? MessageContext.From(request);
var response = _handler(request, effectiveContext);
if (response is null)
throw new InvalidOperationException("Service activator handler returned null.");

return new(Name, request, response);
}

public static Builder Create(string name = "service-activator") => new(name);

public sealed class Builder
{
private readonly string _name;
private ServiceHandler? _handler;

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

_name = name;
}

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

public ServiceActivator<TRequest, TResponse> Build()
{
if (_handler is null)
throw new InvalidOperationException("Service activator requires a handler.");

return new(_name, _handler);
}
}
}

public sealed class ServiceActivatorResult<TRequest, TResponse>
{
public ServiceActivatorResult(
string activatorName,
Message<TRequest> request,
Message<TResponse> response)
=> (ActivatorName, Request, Response) = (activatorName, request, response);

Comment on lines +59 to +66
public string ActivatorName { get; }

public Message<TRequest> Request { get; }

public Message<TResponse> Response { get; }

public bool Completed => Response is not null;
}
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.Activation;
using PatternKit.Messaging.Gateways;
using PatternKit.Messaging.Routing;
using PatternKit.Messaging.Storage;
Expand Down Expand Up @@ -132,6 +133,7 @@ public sealed record WarehousePollingConsumerExampleService(PollingConsumer<Repl
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 InventoryServiceActivatorExampleService(ServiceActivator<InventoryReservationRequest, InventoryReservationResult> Activator, InventoryServiceActivatorService 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 @@ -215,6 +217,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddOrderEventDrivenConsumerExample()
.AddErpChannelAdapterExample()
.AddPaymentMessagingGatewayExample()
.AddInventoryServiceActivatorExample()
.AddGeneratedMessageEnvelopeExample()
.AddGeneratedMessageTranslatorExample()
.AddGeneratedClaimCheckExample()
Expand Down Expand Up @@ -510,6 +513,15 @@ public static IServiceCollection AddPaymentMessagingGatewayExample(this IService
return services.RegisterExample<PaymentMessagingGatewayExampleService>("Payment Messaging Gateway", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddInventoryServiceActivatorExample(this IServiceCollection services)
{
services.AddInventoryServiceActivatorDemo();
services.AddSingleton<InventoryServiceActivatorExampleService>(sp => new(
sp.GetRequiredService<ServiceActivator<InventoryReservationRequest, InventoryReservationResult>>(),
sp.GetRequiredService<InventoryServiceActivatorService>()));
return services.RegisterExample<InventoryServiceActivatorExampleService>("Inventory Service Activator", 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,66 @@
using Microsoft.Extensions.DependencyInjection;
using PatternKit.Generators.Messaging;
using PatternKit.Messaging;
using PatternKit.Messaging.Activation;

namespace PatternKit.Examples.Messaging;

public sealed record InventoryReservationRequest(string Sku, int Quantity);

public sealed record InventoryReservationResult(string Sku, bool Reserved, string Reason);

public sealed record InventoryServiceActivatorSummary(bool Completed, bool Reserved, string Reason);

public sealed class InventoryServiceActivatorService(ServiceActivator<InventoryReservationRequest, InventoryReservationResult> activator)
{
public InventoryServiceActivatorSummary Reserve(InventoryReservationRequest request)
{
var result = activator.Activate(Message<InventoryReservationRequest>.Create(request));
return new(result.Completed, result.Response.Payload.Reserved, result.Response.Payload.Reason);
}
}

public static class InventoryServiceActivators
{
public static ServiceActivator<InventoryReservationRequest, InventoryReservationResult> Create()
=> ServiceActivator<InventoryReservationRequest, InventoryReservationResult>.Create("inventory-reservation-activator")
.Handle(Reserve)
.Build();

public static Message<InventoryReservationResult> Reserve(Message<InventoryReservationRequest> request, MessageContext context)
{
var reserved = request.Payload.Quantity <= 25;
var reason = reserved ? "allocated" : "insufficient-stock";
return Message<InventoryReservationResult>.Create(new(request.Payload.Sku, reserved, reason));
}
}

[GenerateServiceActivator(typeof(InventoryReservationRequest), typeof(InventoryReservationResult), FactoryName = "Create", ActivatorName = "inventory-reservation-activator")]
public static partial class GeneratedInventoryServiceActivator
{
[ServiceActivatorHandler]
private static Message<InventoryReservationResult> Reserve(Message<InventoryReservationRequest> request, MessageContext context)
=> InventoryServiceActivators.Reserve(request, context);
}

public sealed class InventoryServiceActivatorExampleRunner(InventoryServiceActivatorService service)
{
public InventoryServiceActivatorSummary RunGenerated(InventoryReservationRequest request) => service.Reserve(request);

public static InventoryServiceActivatorSummary RunFluent(InventoryReservationRequest request)
=> new InventoryServiceActivatorService(InventoryServiceActivators.Create()).Reserve(request);

public static InventoryServiceActivatorSummary RunGeneratedStatic(InventoryReservationRequest request)
=> new InventoryServiceActivatorService(GeneratedInventoryServiceActivator.Create()).Reserve(request);
}

public static class InventoryServiceActivatorExampleServiceCollectionExtensions
{
public static IServiceCollection AddInventoryServiceActivatorDemo(this IServiceCollection services)
{
services.AddSingleton(_ => GeneratedInventoryServiceActivator.Create());
services.AddSingleton<InventoryServiceActivatorService>();
services.AddSingleton<InventoryServiceActivatorExampleRunner>();
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ public sealed class PatternKitExampleCatalog : IPatternKitExampleCatalog
ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["MessagingGateway", "MessageChannel"],
["typed authorization gateway", "source-generated gateway factory", "DI composition"]),
Descriptor(
"Inventory Service Activator",
"src/PatternKit.Examples/Messaging/InventoryServiceActivatorExample.cs",
"test/PatternKit.Examples.Tests/Messaging/InventoryServiceActivatorExampleTests.cs",
"docs/examples/inventory-service-activator.md",
ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["ServiceActivator"],
["message-to-service operation", "source-generated activator 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 @@ -428,6 +428,19 @@ public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog
"test/PatternKit.Examples.Tests/Messaging/PaymentMessagingGatewayExampleTests.cs",
["fluent request-response facade", "generated gateway factory", "DI-importable payment authorization example"]),

Pattern("Service Activator", PatternFamily.EnterpriseIntegration,
"docs/patterns/messaging/service-activator.md",
"src/PatternKit.Core/Messaging/Activation/ServiceActivator.cs",
"test/PatternKit.Tests/Messaging/Activation/ServiceActivatorTests.cs",
"docs/generators/service-activator.md",
"src/PatternKit.Generators/Messaging/ServiceActivatorGenerator.cs",
"test/PatternKit.Generators.Tests/ServiceActivatorGeneratorTests.cs",
null,
"docs/examples/inventory-service-activator.md",
"src/PatternKit.Examples/Messaging/InventoryServiceActivatorExample.cs",
"test/PatternKit.Examples.Tests/Messaging/InventoryServiceActivatorExampleTests.cs",
["fluent message-to-service activation", "generated activator factory", "DI-importable inventory reservation example"]),

Pattern("Message Envelope", PatternFamily.EnterpriseIntegration,
"docs/patterns/messaging/message-envelope.md",
"src/PatternKit.Core/Messaging/Message.cs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace PatternKit.Generators.Messaging;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class GenerateServiceActivatorAttribute : Attribute
{
public GenerateServiceActivatorAttribute(Type requestType, Type responseType)
{
RequestType = requestType ?? throw new ArgumentNullException(nameof(requestType));
ResponseType = responseType ?? throw new ArgumentNullException(nameof(responseType));
}

public Type RequestType { get; }

public Type ResponseType { get; }

public string FactoryName { get; set; } = "Create";

public string ActivatorName { get; set; } = "service-activator";
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class ServiceActivatorHandlerAttribute : Attribute
{
}
3 changes: 3 additions & 0 deletions src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,6 @@ PKCAD005 | PatternKit.Generators.Messaging | Error | Channel Adapter outbound tr
PKGWY001 | PatternKit.Generators.Messaging | Error | Messaging Gateway host type must be partial.
PKGWY002 | PatternKit.Generators.Messaging | Error | Messaging Gateway must declare exactly one handler.
PKGWY003 | PatternKit.Generators.Messaging | Error | Messaging Gateway handler signature is invalid.
PKSVA001 | PatternKit.Generators.Messaging | Error | Service Activator host type must be partial.
PKSVA002 | PatternKit.Generators.Messaging | Error | Service Activator must declare exactly one handler.
PKSVA003 | PatternKit.Generators.Messaging | Error | Service Activator handler signature is invalid.
Loading
Loading