Skip to content

Generator: Create Proxy Pattern #39

@JerrettDavis

Description

@JerrettDavis

Summary

Add a source generator that produces boilerplate-free, GoF-consistent Proxy pattern implementations for interfaces and abstract classes.

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

Primary goals:

  • Generate typed proxies without runtime reflection/dynamic dispatch.
  • Support common proxy use cases (logging, timing, retries, caching, auth, circuit breaker).
  • Provide deterministic interceptor ordering and clear semantics.
  • Support sync + async methods (favoring ValueTask where possible).

Motivation / Problem

Proxies are commonly implemented via:

  • runtime dynamic proxies/reflection emit
  • hand-written wrapper classes that drift and break
  • inconsistent async handling and exception semantics

We want a generator that:

  • works with trimming/AOT constraints
  • keeps proxies debuggable (readable output)
  • is deterministic and testable

Supported Targets (must-have)

The generator must support proxying:

  • interface
  • abstract class (virtual/abstract members only)

Proxy generation should be opt-in via attribute on the contract type.


Proposed User Experience

A) Simple proxy generation

[GenerateProxy]
public partial interface IUserService
{
    User Get(Guid id);
    ValueTask<User> GetAsync(Guid id, CancellationToken ct = default);
}

Generated (representative shape):

public sealed partial class UserServiceProxy : IUserService
{
    public UserServiceProxy(IUserService inner, IUserServiceInterceptor? interceptor = null);

    public User Get(Guid id);
    public ValueTask<User> GetAsync(Guid id, CancellationToken ct = default);
}

public interface IUserServiceInterceptor
{
    // Sync
    void Before(MethodContext context);
    void After(MethodContext context);
    void OnException(MethodContext context, Exception ex);

    // Async
    ValueTask BeforeAsync(MethodContext context, CancellationToken ct);
    ValueTask AfterAsync(MethodContext context, CancellationToken ct);
    ValueTask OnExceptionAsync(MethodContext context, Exception ex, CancellationToken ct);
}

B) Multiple interceptors with deterministic ordering

[GenerateProxy(InterceptorMode = ProxyInterceptorMode.Pipeline)]
public partial interface IUserService { /* ... */ }

Generated supports:

  • IReadOnlyList<IUserServiceInterceptor>
  • deterministic pipeline order (documented)

Attributes / Surface Area

Namespace: PatternKit.Generators.Proxy

Core

  • [GenerateProxy] on contract type

    • string? ProxyTypeName (default: <ContractName>Proxy)
    • ProxyInterceptorMode InterceptorMode (default: Single)
    • bool GenerateAsync (default: inferred)
    • bool ForceAsync (default: false)
    • ProxyExceptionPolicy Exceptions (default: Rethrow)

Enums:

  • ProxyInterceptorMode: None, Single, Pipeline
  • ProxyExceptionPolicy: Rethrow, Swallow (discouraged; require explicit opt-in)

Optional:

  • [ProxyIgnore] for members that must not be proxied.

Semantics (must-have)

Member coverage

  • Interfaces: proxy all methods + properties.
  • Abstract classes: proxy only virtual/abstract members.
  • Events are v2 (unless trivial).

Ordering

For pipeline mode:

  • interceptors[0] is outermost by default.

  • Before/After semantics must be deterministic:

    • Before called in ascending order.
    • After called in descending order.

Exceptions

  • Default: rethrow.
  • OnException invoked deterministically.

Async

  • If any member returns Task/ValueTask OR has CancellationToken, generator emits async-capable interceptor path.
  • Prefer ValueTask in generated interceptors and wrappers.
  • Do not “rewrite” consumer signatures (proxy implements exact contract).

Context

Generated MethodContext must include:

  • method identifier (stable string or enum)
  • arguments (typed accessors preferred; object[] fallback allowed)
  • optional return value

V1 recommendation:

  • generate a strongly-typed context per method to avoid object[] allocations in hot paths.

Diagnostics (must-have)

Stable IDs, actionable:

  • PKPRX001 Type marked [GenerateProxy] must be partial.
  • PKPRX002 Unsupported member kind (e.g., event) for v1.
  • PKPRX003 Member not accessible for proxy generation.
  • PKPRX004 Proxy type name conflicts with existing type.
  • PKPRX005 Async member detected but async interception disabled (enable GenerateAsync/ForceAsync).

Generated Code Layout

  • ContractName.Proxy.g.cs
  • ContractName.Proxy.Interceptor.g.cs (if generating interceptor interfaces/types)

Determinism:

  • stable emission order by fully-qualified member name.

Testing Expectations

  • Proxy delegates calls to inner.

  • Interceptor ordering is correct (before asc, after desc).

  • Exception behavior:

    • OnException called
    • exception rethrown by default
  • Async proxy:

    • awaits properly
    • cancellation token flows
  • Diagnostics:

    • unsupported members
    • conflicts

Acceptance Criteria

  • [GenerateProxy] works for interfaces and abstract classes.
  • Proxy implements exact contract signatures.
  • Optional interceptor support with deterministic ordering.
  • Async path supported, favoring ValueTask for generated hooks.
  • Reflection-free implementation.
  • Diagnostics cover unsupported/invalid usage.
  • Tests cover delegation, ordering, exceptions, and async.

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