Skip to content

Generator: Create Chain Pattern #40

@JerrettDavis

Description

@JerrettDavis

Summary

Add a source generator that produces a complete implementation of the Chain of Responsibility pattern as a deterministic, composable chain with optional short-circuiting, sync + async support, and allocation-aware execution.

The generator lives in PatternKit.Generators and emits self-contained, readable C# with no runtime PatternKit dependency.

Primary goals:

  • Define chain handlers and routing rules declaratively.
  • Generate a chain builder + executor that is deterministic and testable.
  • Support both “handle-or-pass” and “pipeline transform” styles.
  • Support async chains with ValueTask.

Motivation / Problem

Chains are often hand-rolled with:

  • ad-hoc if/else branching
  • inconsistent short-circuit semantics
  • hard-to-test behavior ordering
  • sync/async duplication

We want generated chains that are:

  • deterministic in order
  • explicit about short-circuit behavior
  • allocation-light
  • easy to compose and benchmark

Supported Targets (must-have)

The generator must support:

  • partial class / partial struct
  • partial record class / partial record struct

Two chain models must be supported:

  1. Responsibility chain: each handler either handles and stops, or passes along.
  2. Pipeline chain: each handler transforms input to output, forwarding to next.

Proposed User Experience

A) Responsibility chain (try-handle)

public readonly record struct Request(string Path);
public readonly record struct Response(int Status);

[Chain]
public partial class Router
{
    [ChainHandler(Order = 0)]
    private bool TryStatic(in Request req, out Response res);

    [ChainHandler(Order = 1)]
    private bool TryApi(in Request req, out Response res);

    [ChainDefault]
    private Response NotFound(in Request req) => new(404);
}

Generated (representative shape):

public partial class Router
{
    public Response Handle(in Request req);
    public bool TryHandle(in Request req, out Response res);
}

B) Pipeline chain (next-style)

[Chain(Model = ChainModel.Pipeline)]
public partial class Middleware
{
    [ChainHandler(Order = 0)]
    private Response Auth(in Request req, Func<Request, Response> next);

    [ChainHandler(Order = 1)]
    private Response Logging(in Request req, Func<Request, Response> next);

    [ChainTerminal]
    private Response Terminal(in Request req) => new(200);
}

Async example:

  • handlers can be ValueTask<T> returning and accept Func<..., ValueTask<T>> next.

Attributes / Surface Area

Namespace: PatternKit.Generators.Chain

Core

  • [Chain] on the chain host

    • ChainModel Model (default: Responsibility)
    • string HandleMethodName = "Handle"
    • string TryHandleMethodName = "TryHandle"
    • string HandleAsyncMethodName = "HandleAsync"
    • bool GenerateAsync (default: inferred)
    • bool ForceAsync (default: false)
  • [ChainHandler] on handler methods

    • int Order
    • string? Name

Responsibility model markers:

  • [ChainDefault] for fallback (optional; if missing, default behavior is throw or return default based on config)

Pipeline model markers:

  • [ChainTerminal] for terminal handler (required)

Semantics (must-have)

Ordering

  • Execute handlers in ascending Order.
  • Duplicate orders are errors.

Responsibility model

  • Handler signature options:

    • bool TryHandle(in TIn input, out TOut output)
    • ValueTask<(bool Handled, TOut Output)> TryHandleAsync(TIn input, CancellationToken ct = default) (optional v1)
  • Chain stops at first handler that returns handled.

  • If none handle:

    • if [ChainDefault] exists, invoke it
    • else apply configured policy: throw vs default

Pipeline model

  • Handler signature options:

    • TOut Handle(in TIn input, Func<TIn, TOut> next)
    • ValueTask<TOut> HandleAsync(TIn input, Func<TIn, ValueTask<TOut>> next, CancellationToken ct = default)
  • Deterministic wrapping order must be documented:

    • Order=0 is outermost wrapper by default.

Async

  • If any handler is async, generate HandleAsync returning ValueTask<TOut>.
  • Cancellation token should be threaded through.

Allocation expectations

  • Building the chain can allocate.
  • Executing should avoid per-call allocations in sync mode.
  • Prefer static delegates or cached function pointers when possible.

Diagnostics (must-have)

Stable IDs, actionable:

  • PKCH001 Type marked [Chain] must be partial.
  • PKCH002 No [ChainHandler] methods found.
  • PKCH003 Duplicate handler order detected.
  • PKCH004 Handler method signature invalid for selected model.
  • PKCH005 Missing [ChainTerminal] for Pipeline model.
  • PKCH006 Multiple terminals declared.
  • PKCH007 Missing default behavior for Responsibility model (no [ChainDefault] and policy requires one).
  • PKCH008 Async handler detected but async generation disabled; enable GenerateAsync/ForceAsync.

Generated Code Layout

  • TypeName.Chain.g.cs

Determinism:

  • handlers ordered by Order, then by fully-qualified method name.

Testing Expectations

Responsibility model:

  • First handler wins.
  • Fall-through to default.
  • No handler + no default → policy behavior.

Pipeline model:

  • Wrapping order is correct and deterministic.
  • Terminal invoked exactly once.
  • Exception bubbling is correct.

Async:

  • Uses ValueTask.
  • Cancellation respected (best-effort).

Diagnostics:

  • Missing/duplicate orders.
  • Invalid signatures.
  • Missing terminal.

Acceptance Criteria

  • [Chain] works for class/struct/record class/record struct.
  • Responsibility model supported with deterministic short-circuit.
  • Pipeline model supported with deterministic wrapper order.
  • Async variants use ValueTask when needed.
  • Clear fallback/default policy and diagnostics.
  • Tests cover ordering, short-circuiting, wrapper semantics, and diagnostics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions