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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,11 @@ var cachedRemoteProxy = Proxy<int, string>.Create(id => remoteProxy.Execute(id))
---

## Patterns Table
PatternKit currently tracks 118 production-readiness patterns. Each catalog pattern is represented in tests, documentation, real-world examples, IoC integration, and the BenchmarkDotNet coverage matrix.
PatternKit currently tracks 119 production-readiness patterns. Each catalog pattern is represented in tests, documentation, real-world examples, IoC integration, and the BenchmarkDotNet coverage matrix.

| Category | Count | Patterns |
| --- | ---: | --- |
| Application Architecture | 27 | Activity Tracker, Aggregate Root, Anti-Corruption Layer, Audit Log, Bounded Context, Context Map, CQRS, Data Mapper, Domain Event, Domain Service, Event Sourcing, Eventual Consistency Monitor, Feature Toggle, Identity Map, Lazy Load, Manual Task Gate, Materialized View, Repository, Service Layer, Snapshot / Checkpoint Management, Specification, Table Data Gateway, Timeout Manager, Transaction Script, Unit of Work, Value Object, Workflow Orchestration |
| Application Architecture | 28 | Activity Tracker, Aggregate Root, Anti-Corruption Layer, Audit Log, Bounded Context, Context Map, CQRS, Data Mapper, Domain Event, Domain Service, Event Sourcing, Eventual Consistency Monitor, Feature Toggle, Identity Map, Lazy Load, Manual Task Gate, Materialized View, Ports and Adapters, Repository, Service Layer, Snapshot / Checkpoint Management, Specification, Table Data Gateway, Timeout Manager, Transaction Script, Unit of Work, Value Object, Workflow Orchestration |
| Behavioral | 12 | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Null Object, Observer, State, Strategy, Template Method, Visitor |
| Cloud Architecture | 20 | Ambassador, Backends for Frontends, Bulkhead, Cache-Aside, Cache Stampede Protection, Circuit Breaker, External Configuration Store, Gateway Aggregation, Gateway Routing, Health Endpoint Monitoring, Leader Election, Priority Queue, Queue-Based Load Leveling, Rate Limiting, Read-Through Cache, Retry, Scheduler Agent Supervisor, Sidecar, Strangler Fig, Write-Through Cache |
| Creational | 6 | Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Application.PortsAndAdapters;
using PatternKit.Examples.PortsAndAdaptersDemo;

namespace PatternKit.Benchmarks.Application;

[BenchmarkCategory("ApplicationArchitecture", "PortsAndAdapters")]
public class PortsAndAdaptersBenchmarks
{
[Benchmark(Baseline = true, Description = "Fluent: create ports and adapters pipeline")]
[BenchmarkCategory("Fluent", "Construction")]
public PortsAndAdaptersPipeline<OrderEntryHttpRequest, PlaceOrderCommand, PlaceOrderResult, OrderEntryHttpResponse> Fluent_CreatePipeline()
=> OrderEntryPortsAndAdaptersPolicies.CreateFluent(new InMemoryOrderEntryApplicationPort());

[Benchmark(Description = "Generated: create ports and adapters pipeline")]
[BenchmarkCategory("Generated", "Construction")]
public PortsAndAdaptersPipeline<OrderEntryHttpRequest, PlaceOrderCommand, PlaceOrderResult, OrderEntryHttpResponse> Generated_CreatePipeline()
=> GeneratedOrderEntryPortsAndAdapters.CreateGenerated();

[Benchmark(Description = "Fluent: execute ports and adapters pipeline")]
[BenchmarkCategory("Fluent", "Execution")]
public ValueTask<OrderEntryHttpResponse> Fluent_Execute()
=> OrderEntryPortsAndAdaptersPolicies.CreateFluent(new InMemoryOrderEntryApplicationPort())
.ExecuteAsync(new("order-100", "buyer@example.com", 42m));

[Benchmark(Description = "Generated: execute ports and adapters pipeline")]
[BenchmarkCategory("Generated", "Execution")]
public ValueTask<OrderEntryHttpResponse> Generated_Execute()
{
GeneratedOrderEntryPortsAndAdapters.ApplicationPort = new InMemoryOrderEntryApplicationPort();
return GeneratedOrderEntryPortsAndAdapters.CreateGenerated()
.ExecuteAsync(new("order-100", "buyer@example.com", 42m));
}
}
2 changes: 2 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Welcome! This section collects small, focused demos that show **how to compose b

* **Customer Profile Lazy Load**
A Generic Host importable deferred profile lookup with fluent and source-generated routes, `IServiceCollection` registration, TTL caching, and invalidation. See [Customer Profile Lazy Load](customer-profile-lazy-load.md).
* **Order Entry Ports and Adapters**
A Generic Host importable hexagonal order-entry flow with fluent and source-generated adapter routes. See [Order Entry Ports and Adapters](order-entry-ports-and-adapters.md).

* **Product Catalog Change Data Capture**
A Generic Host importable mutation capture workflow with fluent and source-generated routes, ordered pending records, and post-commit publication. See [Product Catalog Change Data Capture](product-catalog-change-data-capture.md).
Expand Down
3 changes: 3 additions & 0 deletions docs/examples/order-entry-ports-and-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Order Entry Ports and Adapters

The order entry example maps HTTP-style request records into application commands, calls an application port, and maps the result back to HTTP-style response records. It includes fluent and source-generated factories plus an `AddOrderEntryPortsAndAdaptersDemo` registration method for `IServiceCollection`.
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@
- name: Product Catalog Cache Stampede Protection
href: product-catalog-cache-stampede-protection.md

- name: Order Entry Ports and Adapters
href: order-entry-ports-and-adapters.md

- name: Product Catalog Read-Through and Write-Through Cache
href: product-catalog-read-write-through-cache.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 @@ -74,6 +74,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Data Mapper**](data-mapper.md) | Domain/data model mapper factories | `[GenerateDataMapper]` |
| [**Identity Map**](identity-map.md) | Scoped object identity caches from key selectors | `[GenerateIdentityMap]` |
| [**Lazy Load**](lazy-load.md) | Deferred value factories with caching, TTL, and invalidation | `[GenerateLazyLoad]` |
| [**Ports and Adapters**](ports-and-adapters.md) | Hexagonal inbound adapters, application ports, and outbound adapters | `[GeneratePortsAndAdapters]` |
| [**Materialized View**](materialized-view.md) | Event projection read-model factories from handlers | `[GenerateMaterializedView]` |
| [**Transaction Script**](transaction-script.md) | Typed application workflow factories | `[GenerateTransactionScript]` |
| [**Service Layer**](service-layer.md) | Application operation boundary factories | `[GenerateServiceLayerOperation]` |
Expand Down
20 changes: 20 additions & 0 deletions docs/generators/ports-and-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Ports and Adapters Generator

Annotate a partial host with `[GeneratePortsAndAdapters]` and mark one inbound adapter, one application port, and one outbound adapter.

```csharp
[GeneratePortsAndAdapters(typeof(HttpRequest), typeof(Command), typeof(Result), typeof(HttpResponse))]
public static partial class OrderEntry
{
[InboundAdapter]
private static Command Inbound(HttpRequest request) => new(request.Id);

[ApplicationPort]
private static ValueTask<Result> Handle(Command command, CancellationToken ct) => new(new(command.Id));

[OutboundAdapter]
private static HttpResponse Outbound(Result result) => new(202, result.Id);
}
```

The generated `Create()` factory returns a `PortsAndAdaptersPipeline<TInbound,TCommand,TResult,TOutbound>`.
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
- name: Service Layer
href: service-layer.md

- name: Ports and Adapters
href: ports-and-adapters.md

- name: Visitor Generator
href: visitor-generator.md

Expand Down
18 changes: 11 additions & 7 deletions docs/guides/benchmark-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ The latest measured timings below were captured on Windows 11, Intel Core i9-149
| Backpressure | Execution | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Lazy Load | Construction | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Lazy Load | Execution | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Ports and Adapters | Construction | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Ports and Adapters | Execution | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Cache Stampede Protection | Construction | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Cache Stampede Protection | Execution | Pending | Pending | Pending | Pending | Covered by the BenchmarkDotNet matrix; publish measured values after the next benchmark refresh. |
| Cache-Aside | Construction | 19.91 ns | 200 B | 19.85 ns | 200 B | Effectively equivalent for this microbenchmark. |
Expand Down Expand Up @@ -258,19 +260,19 @@ The latest measured timings below were captured on Windows 11, Intel Core i9-149

## Coverage Matrix Summary

The coverage matrix currently publishes 118 catalog patterns and 472 pattern route results. Each pattern has four BenchmarkDotNet routes: fluent construction, fluent execution, source-generated construction, and source-generated execution. The reusable hosting integration matrix publishes 12 reusable hosting integration route results for package-level `IServiceCollection` registrations.
The coverage matrix currently publishes 119 catalog patterns and 476 pattern route results. Each pattern has four BenchmarkDotNet routes: fluent construction, fluent execution, source-generated construction, and source-generated execution. The reusable hosting integration matrix publishes 12 reusable hosting integration route results for package-level `IServiceCollection` registrations.

| Category | Patterns | Published route results |
| --- | ---: | ---: |
| Application Architecture | 27 | 108 |
| Application Architecture | 28 | 112 |
| Behavioral | 12 | 48 |
| Cloud Architecture | 20 | 80 |
| Creational | 6 | 24 |
| Enterprise Integration | 42 | 168 |
| Messaging Reliability | 4 | 16 |
| Structural | 7 | 28 |

The generator matrix currently publishes 113 generator source route results.
The generator matrix currently publishes 114 generator source route results.

## Hosting Integration Matrix Results

Expand Down Expand Up @@ -313,7 +315,8 @@ The generator matrix currently publishes 113 generator source route results.
| Application Architecture | Identity Map | Covered | Covered | Covered | Covered |
| Application Architecture | Lazy Load | Covered | Covered | Covered | Covered |
| Application Architecture | Materialized View | Covered | Covered | Covered | Covered |
| Application Architecture | Repository | Covered | Covered | Covered | Covered |
| Application Architecture | Ports and Adapters | Covered | Covered | Covered | Covered |
| Application Architecture | Repository | Covered | Covered | Covered | Covered |
| Application Architecture | Service Layer | Covered | Covered | Covered | Covered |
| Application Architecture | Specification | Covered | Covered | Covered | Covered |
| Application Architecture | Value Object | Covered | Covered | Covered | Covered |
Expand Down Expand Up @@ -515,9 +518,10 @@ The generator matrix currently publishes 113 generator source route results.
| RateLimitPolicyGenerator | `src/PatternKit.Generators/RateLimiting/RateLimitPolicyGenerator.cs` | Covered |
| RepositoryGenerator | `src/PatternKit.Generators/Repository/RepositoryGenerator.cs` | Covered |
| RetryPolicyGenerator | `src/PatternKit.Generators/Retry/RetryPolicyGenerator.cs` | Covered |
| SchedulerAgentSupervisorGenerator | `src/PatternKit.Generators/SchedulerAgentSupervisor/SchedulerAgentSupervisorGenerator.cs` | Covered |
| ServiceLayerOperationGenerator | `src/PatternKit.Generators/ServiceLayer/ServiceLayerOperationGenerator.cs` | Covered |
| SidecarGenerator | `src/PatternKit.Generators/Sidecar/SidecarGenerator.cs` | Covered |
| SchedulerAgentSupervisorGenerator | `src/PatternKit.Generators/SchedulerAgentSupervisor/SchedulerAgentSupervisorGenerator.cs` | Covered |
| ServiceLayerOperationGenerator | `src/PatternKit.Generators/ServiceLayer/ServiceLayerOperationGenerator.cs` | Covered |
| PortsAndAdaptersGenerator | `src/PatternKit.Generators/PortsAndAdapters/PortsAndAdaptersGenerator.cs` | Covered |
| SidecarGenerator | `src/PatternKit.Generators/Sidecar/SidecarGenerator.cs` | Covered |
| SingletonGenerator | `src/PatternKit.Generators/Singleton/SingletonGenerator.cs` | Covered |
| SpecificationGenerator | `src/PatternKit.Generators/Specification/SpecificationGenerator.cs` | Covered |
| ValueObjectGenerator | `src/PatternKit.Generators/ValueObjects/ValueObjectGenerator.cs` | Covered |
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ if (parser.Execute("123", out var value))

## 📚 Available Patterns

PatternKit covers 118 production-readiness patterns with fluent APIs, source-generated routes where applicable, IoC integration examples, TinyBDD coverage, and BenchmarkDotNet coverage-matrix validation:
PatternKit covers 119 production-readiness patterns with fluent APIs, source-generated routes where applicable, IoC integration examples, TinyBDD coverage, and BenchmarkDotNet coverage-matrix validation:

| Category | Count | Patterns |
| --- | ---: | --- |
| Application Architecture | 27 | Activity Tracker, Aggregate Root, Anti-Corruption Layer, Audit Log, Bounded Context, Context Map, CQRS, Data Mapper, Domain Event, Domain Service, Event Sourcing, Eventual Consistency Monitor, Feature Toggle, Identity Map, Lazy Load, Manual Task Gate, Materialized View, Repository, Service Layer, Snapshot / Checkpoint Management, Specification, Table Data Gateway, Timeout Manager, Transaction Script, Unit of Work, Value Object, Workflow Orchestration |
| Application Architecture | 28 | Activity Tracker, Aggregate Root, Anti-Corruption Layer, Audit Log, Bounded Context, Context Map, CQRS, Data Mapper, Domain Event, Domain Service, Event Sourcing, Eventual Consistency Monitor, Feature Toggle, Identity Map, Lazy Load, Manual Task Gate, Materialized View, Ports and Adapters, Repository, Service Layer, Snapshot / Checkpoint Management, Specification, Table Data Gateway, Timeout Manager, Transaction Script, Unit of Work, Value Object, Workflow Orchestration |
| Behavioral | 12 | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Null Object, Observer, State, Strategy, Template Method, Visitor |
| Cloud Architecture | 20 | Ambassador, Backends for Frontends, Bulkhead, Cache-Aside, Cache Stampede Protection, Circuit Breaker, External Configuration Store, Gateway Aggregation, Gateway Routing, Health Endpoint Monitoring, Leader Election, Priority Queue, Queue-Based Load Leveling, Rate Limiting, Read-Through Cache, Retry, Scheduler Agent Supervisor, Sidecar, Strangler Fig, Write-Through Cache |
| Creational | 6 | Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton |
Expand Down
14 changes: 14 additions & 0 deletions docs/patterns/application/ports-and-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Ports and Adapters

Ports and Adapters keeps application use cases independent from delivery mechanisms and infrastructure adapters.

```csharp
var pipeline = PortsAndAdaptersPipeline<OrderEntryHttpRequest, PlaceOrderCommand, PlaceOrderResult, OrderEntryHttpResponse>
.Create("order-entry")
.AdaptInboundWith(request => new PlaceOrderCommand(request.OrderId, request.CustomerEmail, request.Total))
.HandleWith((command, ct) => applicationPort.PlaceOrderAsync(command, ct))
.AdaptOutboundWith(result => new OrderEntryHttpResponse(result.Accepted ? 202 : 409, result.OrderId, result.Message))
.Build();
```

Use the fluent path when adapters are assembled at runtime from `IServiceCollection`. Use the generator path when the adapter methods are stable and you want a named factory with minimal ceremony.
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@
href: application/identity-map.md
- name: Lazy Load
href: application/lazy-load.md
- name: Ports and Adapters
href: application/ports-and-adapters.md
- name: Transaction Script
href: application/transaction-script.md
- name: Service Layer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace PatternKit.Application.PortsAndAdapters;

/// <summary>Primary port exposed by a Ports and Adapters boundary.</summary>
public interface IPortsAndAdaptersPipeline<TInbound, TCommand, TResult, TOutbound>
{
string Name { get; }

ValueTask<TOutbound> ExecuteAsync(TInbound inbound, CancellationToken cancellationToken = default);
}

/// <summary>Fluent Ports and Adapters pipeline that isolates delivery DTOs from application use cases.</summary>
public sealed class PortsAndAdaptersPipeline<TInbound, TCommand, TResult, TOutbound> : IPortsAndAdaptersPipeline<TInbound, TCommand, TResult, TOutbound>
{
private readonly Func<TInbound, TCommand> _inboundAdapter;
private readonly Func<TCommand, CancellationToken, ValueTask<TResult>> _applicationPort;
private readonly Func<TResult, TOutbound> _outboundAdapter;

private PortsAndAdaptersPipeline(
string name,
Func<TInbound, TCommand> inboundAdapter,
Func<TCommand, CancellationToken, ValueTask<TResult>> applicationPort,
Func<TResult, TOutbound> outboundAdapter)
{
Name = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("Ports and Adapters pipeline name is required.", nameof(name))
: name;
_inboundAdapter = inboundAdapter ?? throw new ArgumentNullException(nameof(inboundAdapter));
_applicationPort = applicationPort ?? throw new ArgumentNullException(nameof(applicationPort));
_outboundAdapter = outboundAdapter ?? throw new ArgumentNullException(nameof(outboundAdapter));
}

public string Name { get; }

public static Builder Create(string name = "ports-and-adapters") => new(name);

public async ValueTask<TOutbound> ExecuteAsync(TInbound inbound, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (inbound is null)
throw new ArgumentNullException(nameof(inbound));

var command = _inboundAdapter(inbound);
var result = await _applicationPort(command, cancellationToken).ConfigureAwait(false);
return _outboundAdapter(result);
}

public sealed class Builder
{
private readonly string _name;
private Func<TInbound, TCommand>? _inboundAdapter;
private Func<TCommand, CancellationToken, ValueTask<TResult>>? _applicationPort;
private Func<TResult, TOutbound>? _outboundAdapter;

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

public Builder AdaptInboundWith(Func<TInbound, TCommand> adapter)
{
_inboundAdapter = adapter ?? throw new ArgumentNullException(nameof(adapter));
return this;
}

public Builder HandleWith(Func<TCommand, CancellationToken, ValueTask<TResult>> applicationPort)
{
_applicationPort = applicationPort ?? throw new ArgumentNullException(nameof(applicationPort));
return this;
}

public Builder AdaptOutboundWith(Func<TResult, TOutbound> adapter)
{
_outboundAdapter = adapter ?? throw new ArgumentNullException(nameof(adapter));
return this;
}

public PortsAndAdaptersPipeline<TInbound, TCommand, TResult, TOutbound> Build()
{
if (_inboundAdapter is null)
throw new InvalidOperationException("Ports and Adapters pipeline requires an inbound adapter.");
if (_applicationPort is null)
throw new InvalidOperationException("Ports and Adapters pipeline requires an application port.");
if (_outboundAdapter is null)
throw new InvalidOperationException("Ports and Adapters pipeline requires an outbound adapter.");

return new(_name, _inboundAdapter, _applicationPort, _outboundAdapter);
}
}
}
Loading
Loading