Skip to content

Strategy ​

Namespace: DesignPatterns.Behavioral

Overview ​

Register algorithm implementations by key at compile time. Avoid large switch blocks while keeping selection logic in your application code.

Runtime ​

  • IStrategyRegistry<TKey, TStrategy> — Get / TryGet
  • StrategyRegistryBuilder<TKey, TStrategy> — manual registration

Source generator ​

  1. Declare a partial static registry holder.
  2. Mark each implementation with [RegisterStrategy(typeof(TContract), "key")].
  3. Generator emits {Name}Keys, Instance (eager registry), and optional RegisterDi.
csharp
[RegisterStrategy(typeof(IPaymentStrategy), "alipay")]
public sealed class AlipayPayment : IPaymentStrategy { ... }

public static partial class PaymentStrategyRegistry { }

Optional marker interfaces: IStrategy<TIn, TOut>, IAsyncStrategy<TIn, TOut> — not required by the generator.

Async resolution ​

IAsyncStrategy contracts use the same Keys / Registry / RegisterDi pipeline. StrategyRegistryExtensions adds ExecuteAsync and TryExecuteAsync:

csharp
public interface ITextProcessor : IAsyncStrategy<string, int> { }

// Registry value is IAsyncStrategy<TIn, TOut>
await registry.ExecuteAsync(key, input);

// Derived contract (specify TContract, TOutput, TInput)
await registry.ExecuteAsync<ITextProcessor, int, string>(TextProcessorKeys.Length, "hello");

// Equivalent
await registry.Get(TextProcessorKeys.Length).ExecuteAsync("hello");

Diagnostics ​

DP003–DP007 — duplicate keys, contract mismatch, unregistered types (DP006 + CodeFix), missing ctor. DP025 for unknown literal keys at lookup sites. DP047–DP049 for guard method validation. See Registry key conventions.

Guard predicates ​

Conditionally enable or disable strategies at resolution time using guard predicates.

Runtime API ​

TryGetWithGuard evaluates a guard predicate when resolving a strategy. When the guard returns false, the strategy is treated as unregistered:

csharp
var builder = new StrategyRegistryBuilder<string, IPaymentStrategy>()
    .Register("alipay", new AlipayPayment(), guard: key => IsPaymentEnabled("alipay"));
var registry = builder.Build();

if (registry.TryGetWithGuard("alipay", out var strategy))
{
    await strategy.ProcessAsync(payment);
}

Design constraint: Guard signatures are Func<TKey, bool> (key only), not Func<TInput, bool>. The registry layer does not know TInput (strategies are not required to implement IStrategy<TInput, TOutput>), so input-based dynamic routing is business logic outside this library's scope.

Source generator ​

Use the Guard property on [RegisterStrategy] to reference a static guard method:

csharp
[RegisterStrategy<IPaymentStrategy>("alipay", Guard = nameof(IsEnabled))]
public sealed class AlipayPayment : IPaymentStrategy
{
    private static bool IsEnabled(string key) => IsPaymentEnabled(key);
}

The generator validates the guard method signature (DP047–DP049).

Execution tracing ​

Trace strategy execution outcomes for debugging, logging, or metrics without changing your strategy implementations.

ExecuteTracedAsync extension methods return a StrategyExecutionTrace<TOutput> containing:

  • Key — The strategy key requested
  • Status — Execution outcome (see below)
  • Output — Strategy output when successful
  • Exception — Exception when failed
  • ElapsedMilliseconds — Resolution and execution time
csharp
var trace = await registry.ExecuteTracedAsync("double", 5);

Status values ​

StrategyExecutionStepStatusMeaning
ExecutedStrategy resolved and executed successfully
KeyNotFoundStrategy key not found in registry
GuardRejectedStrategy found but guard predicate returned false
FailedStrategy resolved but threw an exception during execution

Observers ​

Implement IStrategyExecutionObserver<TInput, TOutput> to receive callbacks for side-effects like logging or metrics:

csharp
public sealed class LoggingObserver<TInput, TOutput> : IStrategyExecutionObserver<TInput, TOutput>
{
    public void OnExecutionCompleted(string key, TInput input, TOutput output, long elapsedMs)
        => Console.WriteLine($"Strategy {key} succeeded in {elapsedMs}ms");

    public void OnExecutionFailed(string key, TInput input, StrategyExecutionTrace<TOutput> trace)
        => Console.WriteLine($"Strategy {key} failed: {trace.Exception?.Message}");
}

var trace = await registry.ExecuteTracedAsync("double", 5, new LoggingObserver<int, int>());

Sample ​

DesignPatterns.Samples.Strategy — sync payment strategies plus async IRefundProcessor with ExecuteAsync (RefundProcessors.cs).

DI ​

See Dependency Injection for RegisterDi.

Maintainer doc: docs/Strategy.md (中文).

Released under the MIT License.