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
28 changes: 28 additions & 0 deletions docs/examples/order-transaction-script-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Order Transaction Script Pattern

This production-shaped example shows a submit-order application operation as a Transaction Script.

It demonstrates:

- fluent `TransactionScript<SubmitOrderRequest,SubmitOrderReceipt>` construction
- generated script factory with `[GenerateTransactionScript]`
- repository and unit-of-work coordination inside the script handler
- scoped `ITransactionScript<SubmitOrderRequest,SubmitOrderReceipt>` registration through `IServiceCollection`

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

using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();

var workflow = scope.ServiceProvider.GetRequiredService<OrderTransactionScriptWorkflow>();
var summary = await workflow.SubmitAsync(new SubmitOrderRequest("order-100", "customer-1", 125m));
```

The registered script is scoped so importing applications can safely compose it with request-scoped repositories, units of work, database sessions, or tenant services.

Files:

- `src/PatternKit.Examples/TransactionScriptDemo/OrderTransactionScriptDemo.cs`
- `test/PatternKit.Examples.Tests/TransactionScriptDemo/OrderTransactionScriptDemoTests.cs`
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
- name: Order Identity Map Pattern
href: order-identity-map-pattern.md

- name: Order Transaction Script Pattern
href: order-transaction-script-pattern.md

- name: Generated Mailbox
href: generated-mailbox.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 @@ -64,6 +64,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Unit of Work**](unit-of-work.md) | Ordered commit and rollback units | `[GenerateUnitOfWork]` |
| [**Data Mapper**](data-mapper.md) | Domain/data model mapper factories | `[GenerateDataMapper]` |
| [**Identity Map**](identity-map.md) | Scoped object identity caches from key selectors | `[GenerateIdentityMap]` |
| [**Transaction Script**](transaction-script.md) | Typed application workflow factories | `[GenerateTransactionScript]` |
| [**Template Method**](template-method-generator.md) | Template method skeletons with hook points | `[Template]` |
| [**Visitor**](visitor-generator.md) | Type-safe visitor implementations | `[GenerateVisitor]` |

Expand Down
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
- name: Unit of Work
href: unit-of-work.md

- name: Transaction Script
href: transaction-script.md

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

Expand Down
36 changes: 36 additions & 0 deletions docs/generators/transaction-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Transaction Script Generator

`GenerateTransactionScriptAttribute` creates a typed `TransactionScript<TRequest,TResponse>` factory from a static partial host.

```csharp
[GenerateTransactionScript(typeof(SubmitOrderRequest), typeof(SubmitOrderReceipt), FactoryName = "CreateScript", ScriptName = "submit-order")]
public static partial class GeneratedSubmitOrderScript
{
[TransactionScriptValidator]
private static IEnumerable<TransactionScriptError> Validate(SubmitOrderRequest request)
=> request.Total <= 0m
? [new TransactionScriptError("total", "Order total must be positive.")]
: [];

[TransactionScriptHandler]
private static ValueTask<SubmitOrderReceipt> Handle(SubmitOrderRequest request, CancellationToken cancellationToken)
=> new(new SubmitOrderReceipt(request.OrderId, request.Total));
}
```

The generated factory is equivalent to:

```csharp
TransactionScript<SubmitOrderRequest, SubmitOrderReceipt>
.Create("submit-order")
.Validate(Validate)
.Execute(Handle)
.Build();
```

Diagnostics:

- `PKTS001`: host type must be partial.
- `PKTS002`: exactly one `[TransactionScriptHandler]` method is required.
- `PKTS003`: handler must be static and return `ValueTask<TResponse>` from `(TRequest, CancellationToken)`.
- `PKTS004`: validator must be a single static method returning `IEnumerable<TransactionScriptError>` from `TRequest`.
1 change: 1 addition & 0 deletions docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Application Architecture | Unit of Work | `UnitOfWork` | Unit of Work generator |
| Application Architecture | Data Mapper | `DataMapper<TDomain,TData>` | Data Mapper generator |
| Application Architecture | Identity Map | `IdentityMap<TEntity,TKey>` | Identity Map generator |
| Application Architecture | Transaction Script | `TransactionScript<TRequest,TResponse>` | Transaction Script generator |
| Application Architecture | Anti-Corruption Layer | `AntiCorruptionLayer<TExternal, TDomain>` | Anti-Corruption Layer generator |

## Research Baselines
Expand Down
30 changes: 30 additions & 0 deletions docs/patterns/application/transaction-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Transaction Script

Transaction Script models one application operation as an explicit workflow: validate input, coordinate persistence or integrations, and return a typed result. Use it when the business transaction is procedural and does not need a rich domain object to own the behavior.

PatternKit provides `TransactionScript<TRequest,TResponse>` in `PatternKit.Application.TransactionScript`.

```csharp
var script = TransactionScript<SubmitOrderRequest, SubmitOrderReceipt>
.Create("submit-order")
.Validate(request => request.Total <= 0m
? [new TransactionScriptError("total", "Order total must be positive.")]
: [])
.Execute(async (request, ct) =>
{
await repository.AddAsync(new SubmittedOrder(request.OrderId, request.CustomerId, request.Total), ct);
return new SubmitOrderReceipt(request.OrderId, request.Total);
})
.Build();

var result = await script.ExecuteAsync(request, cancellationToken);
```

The runtime path returns `TransactionScriptResult<TResponse>` so callers can distinguish completed, rejected, and failed executions without manual assertions or exception-only control flow.

Use the source-generated path when the script handler and validator are stable application code. Register `ITransactionScript<TRequest,TResponse>` as scoped when the script depends on repositories, unit-of-work state, or request-scoped infrastructure.

See also:

- [Transaction Script generator](../../generators/transaction-script.md)
- [Order Transaction Script example](../../examples/order-transaction-script-pattern.md)
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@
href: application/data-mapper.md
- name: Identity Map
href: application/identity-map.md
- name: Transaction Script
href: application/transaction-script.md
- name: Specification
href: application/specification.md
- name: Type-Dispatcher
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
namespace PatternKit.Application.TransactionScript;

/// <summary>Application service operation that executes one request workflow as an explicit transaction script.</summary>
public interface ITransactionScript<TRequest, TResponse>
{
string Name { get; }

ValueTask<TransactionScriptResult<TResponse>> ExecuteAsync(TRequest request, CancellationToken cancellationToken = default);
}

/// <summary>Fluent Transaction Script implementation for request/application workflows.</summary>
public sealed class TransactionScript<TRequest, TResponse> : ITransactionScript<TRequest, TResponse>
{
private readonly Func<TRequest, IEnumerable<TransactionScriptError>> _validator;
private readonly Func<TRequest, CancellationToken, ValueTask<TResponse>> _handler;

private TransactionScript(
string name,
Func<TRequest, IEnumerable<TransactionScriptError>> validator,
Func<TRequest, CancellationToken, ValueTask<TResponse>> handler)
{
Name = name;
_validator = validator;
_handler = handler;
}

public string Name { get; }

public static Builder Create(string name)
=> new(name);

public async ValueTask<TransactionScriptResult<TResponse>> ExecuteAsync(TRequest request, CancellationToken cancellationToken = default)
{
if (request is null)
throw new ArgumentNullException(nameof(request));

cancellationToken.ThrowIfCancellationRequested();
var errors = (_validator(request) ?? Array.Empty<TransactionScriptError>())
.Where(static error => error is not null)
.ToArray();
if (errors.Length > 0)
return TransactionScriptResult<TResponse>.Rejected(errors);

try
{
var response = await _handler(request, cancellationToken).ConfigureAwait(false);
return TransactionScriptResult<TResponse>.Completed(response);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return TransactionScriptResult<TResponse>.Failed(ex);
}
}

public sealed class Builder
{
private readonly string _name;
private Func<TRequest, IEnumerable<TransactionScriptError>> _validator = static _ => Array.Empty<TransactionScriptError>();
private Func<TRequest, CancellationToken, ValueTask<TResponse>>? _handler;

internal Builder(string name)
{
_name = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("Transaction script name is required.", nameof(name))
: name;
}

public Builder Validate(Func<TRequest, IEnumerable<TransactionScriptError>> validator)
{
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
return this;
}

public Builder Execute(Func<TRequest, CancellationToken, ValueTask<TResponse>> handler)
{
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
return this;
}

public TransactionScript<TRequest, TResponse> Build()
=> new(_name, _validator, _handler ?? throw new InvalidOperationException("Transaction script handler is required."));
}
}

/// <summary>Result returned by a Transaction Script execution.</summary>
public sealed class TransactionScriptResult<TResponse>
{
private TransactionScriptResult(
TResponse? response,
TransactionScriptStatus status,
IReadOnlyList<TransactionScriptError> errors,
Exception? exception)
{
Response = response;
Status = status;
Errors = errors;
Exception = exception;
}

public TResponse? Response { get; }

public TransactionScriptStatus Status { get; }

public IReadOnlyList<TransactionScriptError> Errors { get; }

public Exception? Exception { get; }

public bool Succeeded => Status == TransactionScriptStatus.Completed;

public static TransactionScriptResult<TResponse> Completed(TResponse response)
=> new(response, TransactionScriptStatus.Completed, Array.Empty<TransactionScriptError>(), null);

public static TransactionScriptResult<TResponse> Rejected(IReadOnlyList<TransactionScriptError> errors)
{
if (errors is null)
throw new ArgumentNullException(nameof(errors));
if (errors.Count == 0)
throw new ArgumentException("Transaction script rejection requires at least one error.", nameof(errors));

return new(default, TransactionScriptStatus.Rejected, errors, null);
}

public static TransactionScriptResult<TResponse> Failed(Exception exception)
=> new(default, TransactionScriptStatus.Failed, Array.Empty<TransactionScriptError>(), exception ?? throw new ArgumentNullException(nameof(exception)));
}

/// <summary>Execution status for a Transaction Script.</summary>
public enum TransactionScriptStatus
{
Completed,
Rejected,
Failed
}

/// <summary>Validation or precondition failure reported before a Transaction Script handler runs.</summary>
public sealed class TransactionScriptError
{
public TransactionScriptError(string code, string message)
{
Code = string.IsNullOrWhiteSpace(code)
? throw new ArgumentException("Transaction script error code is required.", nameof(code))
: code;
Message = string.IsNullOrWhiteSpace(message)
? throw new ArgumentException("Transaction script error message is required.", nameof(message))
: message;
}

public string Code { get; }

public string Message { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
using PatternKit.Examples.Strategies.Coercion;
using PatternKit.Examples.Strategies.Composed;
using PatternKit.Examples.TemplateDemo;
using PatternKit.Examples.TransactionScriptDemo;
using PatternKit.Examples.UnitOfWorkDemo;
using PatternKit.Examples.VisitorDemo;
using PatternKit.Messaging.Routing;
Expand Down Expand Up @@ -128,6 +129,7 @@ public sealed record OrderRepositoryPatternExample(OrderRepositoryDemoRunner Run
public sealed record CheckoutUnitOfWorkPatternExample(CheckoutUnitOfWorkDemoRunner Runner, CheckoutUnitOfWorkWorkflow Workflow);
public sealed record OrderDataMapperPatternExample(OrderDataMapperDemoRunner Runner, OrderDataMapperWorkflow Workflow);
public sealed record OrderIdentityMapPatternExample(OrderIdentityMapDemoRunner Runner);
public sealed record OrderTransactionScriptPatternExample(OrderTransactionScriptDemoRunner Runner);
public sealed record PrototypeGameCharacterFactoryExample(Prototype<string, PrototypeDemo.PrototypeDemo.GameCharacter> Factory);
public sealed record ProxyPatternDemonstrationsExample(Proxy<int, string> RemoteProxy, Proxy<(string To, string Subject, string Body), bool> EmailProxy);
public sealed record FlyweightGlyphCacheExample(Func<string, IReadOnlyList<(FlyweightDemo.FlyweightDemo.Glyph Glyph, int X)>> RenderSentence);
Expand Down Expand Up @@ -188,6 +190,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddCheckoutUnitOfWorkPatternExample()
.AddOrderDataMapperPatternExample()
.AddOrderIdentityMapPatternExample()
.AddOrderTransactionScriptPatternExample()
.AddPrototypeGameCharacterFactoryExample()
.AddProxyPatternDemonstrationsExample()
.AddFlyweightGlyphCacheExample()
Expand Down Expand Up @@ -554,6 +557,13 @@ public static IServiceCollection AddOrderIdentityMapPatternExample(this IService
return services.RegisterExample<OrderIdentityMapPatternExample>("Order Identity Map Pattern", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddOrderTransactionScriptPatternExample(this IServiceCollection services)
{
services.AddOrderTransactionScriptDemo();
services.AddSingleton<OrderTransactionScriptPatternExample>(sp => new(sp.GetRequiredService<OrderTransactionScriptDemoRunner>()));
return services.RegisterExample<OrderTransactionScriptPatternExample>("Order Transaction Script Pattern", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddPrototypeGameCharacterFactoryExample(this IServiceCollection services)
{
services.AddSingleton(_ => PrototypeDemo.PrototypeDemo.CreateCharacterFactory());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ public sealed class PatternKitExampleCatalog : IPatternKitExampleCatalog
ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["IdentityMap"],
["request-scoped identity reuse", "source-generated map factory", "DI composition"]),
Descriptor(
"Order Transaction Script Pattern",
"src/PatternKit.Examples/TransactionScriptDemo/OrderTransactionScriptDemo.cs",
"test/PatternKit.Examples.Tests/TransactionScriptDemo/OrderTransactionScriptDemoTests.cs",
"docs/examples/order-transaction-script-pattern.md",
ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["TransactionScript"],
["explicit application workflow", "source-generated script factory", "DI composition"]),
Descriptor(
"Generated Mailbox",
"src/PatternKit.Examples/Messaging/MailboxExample.cs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,19 @@ public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog
"test/PatternKit.Examples.Tests/IdentityMapDemo/OrderIdentityMapDemoTests.cs",
["fluent scoped identity map", "generated identity-map factory", "DI-importable request scope example"]),

Pattern("Transaction Script", PatternFamily.ApplicationArchitecture,
"docs/patterns/application/transaction-script.md",
"src/PatternKit.Core/Application/TransactionScript/TransactionScript.cs",
"test/PatternKit.Tests/Application/TransactionScript/TransactionScriptTests.cs",
"docs/generators/transaction-script.md",
"src/PatternKit.Generators/TransactionScript/TransactionScriptGenerator.cs",
"test/PatternKit.Generators.Tests/TransactionScriptGeneratorTests.cs",
null,
"docs/examples/order-transaction-script-pattern.md",
"src/PatternKit.Examples/TransactionScriptDemo/OrderTransactionScriptDemo.cs",
"test/PatternKit.Examples.Tests/TransactionScriptDemo/OrderTransactionScriptDemoTests.cs",
["fluent application workflow", "generated script factory", "DI-importable service operation"]),

Pattern("Anti-Corruption Layer", PatternFamily.ApplicationArchitecture,
"docs/patterns/application/anti-corruption-layer.md",
"src/PatternKit.Core/Application/AntiCorruption/AntiCorruptionLayer.cs",
Expand Down
Loading
Loading