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/TryGetStrategyRegistryBuilder<TKey, TStrategy>— manual registration
Source generator
- Declare a partial static registry holder.
- Mark each implementation with
[RegisterStrategy(typeof(TContract), "key")]. - Generator emits
{Name}Keys,Instance(eager registry), and optionalRegisterDi.
[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:
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:
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:
[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 requestedStatus— Execution outcome (see below)Output— Strategy output when successfulException— Exception when failedElapsedMilliseconds— Resolution and execution time
var trace = await registry.ExecuteTracedAsync("double", 5);Status values
StrategyExecutionStepStatus | Meaning |
|---|---|
| Executed | Strategy resolved and executed successfully |
| KeyNotFound | Strategy key not found in registry |
| GuardRejected | Strategy found but guard predicate returned false |
| Failed | Strategy resolved but threw an exception during execution |
Observers
Implement IStrategyExecutionObserver<TInput, TOutput> to receive callbacks for side-effects like logging or metrics:
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 (中文).