Strategy Pattern in .NET Core API
A comprehensive guide to the Strategy pattern in C#, with static compile-time and dynamic runtime injection variations for algorithm selection.

Intro
The Strategy pattern is a behavioral design pattern that addresses the need to encapsulate a family of algorithms and make them interchangeable. Instead of embedding conditional logic throughout your codebase to select which algorithm to use, you define a contract that each algorithm must follow and let clients choose the appropriate implementation at runtime—or compile time in specialized scenarios.
In this article, we use a simple payment processing example to demonstrate how strategies eliminate complex conditionals and improve code flexibility.
What Is the Strategy Pattern?
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The pattern lets the algorithm vary independently from clients that use it.
In simple terms:
- The client needs to perform some operation but doesn’t care which algorithm handles it.
- Multiple algorithm implementations exist, each with different behavior or performance characteristics.
- The strategy interface provides a contract; concrete strategies implement it.
- The context delegates the work to whichever strategy is active.
Strategy Pattern Example
Suppose your e-commerce app needs to process payments. The existing contract looks like this:
public interface IPaymentStrategy{ decimal CalculateFee(decimal amount); bool IsAvailable();}
public class CreditCardStrategy : IPaymentStrategy{ public decimal CalculateFee(decimal amount) => amount * 0.029m + 0.30m; // 2.9% + $0.30
public bool IsAvailable() => true;}
public class BankTransferStrategy : IPaymentStrategy{ public decimal CalculateFee(decimal amount) => amount * 0.01m; // 1% flat fee
public bool IsAvailable() => true;}The context that uses a strategy looks like this:
public class PaymentProcessor{ private readonly IPaymentStrategy _strategy;
public PaymentProcessor(IPaymentStrategy strategy) { _strategy = strategy; }
public void ProcessPayment(decimal amount) { if (!_strategy.IsAvailable()) throw new InvalidOperationException("Strategy unavailable");
var fee = _strategy.CalculateFee(amount); Console.WriteLine($"Processing ${amount} with fee ${fee}"); }}Why This Works
- The client (
PaymentProcessor) depends on the strategy interface, not concrete implementations. - Each payment method is isolated in its own strategy class; changes to one strategy don’t affect others.
- New payment methods can be added by creating a new strategy class without modifying existing code.
- The conditional logic for algorithm selection moves out of the context and into the client or factory layer.
- Testing becomes straightforward: you can inject mock strategies to test different payment scenarios.
When to Use It
- When you have multiple related algorithms and need to choose between them at runtime.
- When you want to avoid long switch statements or cascading if/else conditionals.
- When algorithm logic is likely to change or grow over time.
- When you need to add new algorithms without modifying existing client code.
- When different algorithms have significantly different performance or resource requirements.
Common scenarios:
- Payment gateway selection (credit card, PayPal, crypto, bank transfer)
- Sorting and searching algorithms
- Compression or serialization formats
- Authentication mechanisms (OAuth, JWT, Basic Auth)
- Notification delivery methods (email, SMS, push notifications)
Important Note for ASP.NET Core
In ASP.NET Core, the Strategy pattern integrates naturally with the built-in dependency injection container:
// Register strategies in Program.csbuilder.Services.AddScoped<IPaymentStrategy, CreditCardStrategy>();
// Or register multiple with a factory patternbuilder.Services.AddScoped<Func<PaymentType, IPaymentStrategy>>(provider =>{ return paymentType => paymentType switch { PaymentType.CreditCard => provider.GetRequiredService<CreditCardStrategy>(), PaymentType.BankTransfer => provider.GetRequiredService<BankTransferStrategy>(), _ => throw new ArgumentException("Invalid payment type") };});
// Inject into servicespublic class OrderService{ private readonly Func<PaymentType, IPaymentStrategy> _strategyFactory;
public OrderService(Func<PaymentType, IPaymentStrategy> strategyFactory) { _strategyFactory = strategyFactory; }
public async Task ProcessOrderAsync(Order order, PaymentType method) { var strategy = _strategyFactory(method); var fee = strategy.CalculateFee(order.Total); // Process payment... }}This approach keeps service constructors clean and leverages ASP.NET Core’s container for declarative strategy management.
Core Components of the Pattern
| Component | Purpose | Example in this article |
|---|---|---|
| Strategy Interface | Defines contract all concrete strategies must follow | IPaymentStrategy |
| Concrete Strategies | Implement the strategy interface with specific algorithm logic | CreditCardStrategy, BankTransferStrategy |
| Context | Holds a reference to a strategy and delegates work to it | PaymentProcessor |
| Client | Creates or selects a strategy and passes it to the context | OrderService, checkout code |
Common Strategy Variations
You will typically encounter Strategy in these two primary forms:
01-StaticStrategy
Static Strategy uses generic types to bind the strategy at compile time. The algorithm choice is determined when the type is instantiated and cannot change during execution.
public class StaticPaymentProcessor<TStrategy> where TStrategy : IPaymentStrategy, new(){ private readonly TStrategy _strategy = new();
public void ProcessPayment(decimal amount) { if (!_strategy.IsAvailable()) throw new InvalidOperationException("Strategy unavailable");
var fee = _strategy.CalculateFee(amount); Console.WriteLine($"Processing ${amount} with fee ${fee}"); }}
// Usage: Strategy determined at compile timevar processor = new StaticPaymentProcessor<CreditCardStrategy>();processor.ProcessPayment(100m);Why it matters in implementation:
- No runtime overhead from polymorphic dispatch.
- Type safety enforced at compile time.
- Strategy cannot be changed after instantiation.
- Suitable for scenarios where the algorithm is fixed for a given deployment or module.
02-DynamicStrategy
Dynamic Strategy injects the strategy at runtime via constructor or method parameters. The algorithm can be selected or changed based on user input, configuration, or runtime conditions.
public class DynamicPaymentProcessor{ private IPaymentStrategy _strategy;
public DynamicPaymentProcessor(IPaymentStrategy strategy) { _strategy = strategy; }
// Allow strategy to be changed at runtime public void SetStrategy(IPaymentStrategy strategy) { _strategy = strategy; }
public void ProcessPayment(decimal amount) { if (!_strategy.IsAvailable()) throw new InvalidOperationException("Strategy unavailable");
var fee = _strategy.CalculateFee(amount); Console.WriteLine($"Processing ${amount} with fee ${fee}"); }}
// Usage: Strategy chosen and changed at runtimevar bankStrategy = new BankTransferStrategy();var processor = new DynamicPaymentProcessor(bankStrategy);processor.ProcessPayment(100m);
// Switch strategies on the flyvar creditStrategy = new CreditCardStrategy();processor.SetStrategy(creditStrategy);processor.ProcessPayment(250m);Why it matters in implementation:
- Full runtime flexibility to switch strategies based on context.
- Easy unit testing by injecting mock strategies.
- Works seamlessly with ASP.NET Core dependency injection.
- Supports dynamic algorithm selection based on user input or external conditions.
- More common in modern applications.
How the Variations Differ
| Aspect | Static Strategy | Dynamic Strategy |
|---|---|---|
| Strategy Selection | Compile time via generics | Runtime via constructor or method injection |
| Flexibility | Low; fixed per type instance | High; changeable during execution |
| Performance | No polymorphic overhead | Slight polymorphic dispatch cost (negligible) |
| Testability | Requires separate test builds for different strategies | Easy; inject mock strategies directly |
| Configuration | Type parameter binding | DI container registration or factory methods |
| Best Use Case | Fixed algorithms per deployment | User-driven selection or conditional logic |
The practical default in modern .NET Core APIs is Dynamic Strategy because it provides runtime flexibility and integrates naturally with ASP.NET Core’s dependency injection container.
SOLID Principles Behind It
SRP - Single Responsibility Principle
Each concrete strategy encapsulates a single algorithm. The context is responsible only for applying the strategy, not implementing business logic.
OCP - Open/Closed Principle
You can add new payment methods by creating new strategy classes without modifying existing context or client code.
LSP - Liskov Substitution Principle
All strategies implementing IPaymentStrategy are interchangeable; any strategy can replace another without breaking the context.
DIP - Dependency Inversion Principle
Contexts and clients depend on the strategy interface abstraction, not on concrete strategy implementations.
Advantages and Disadvantages
Advantages
- Eliminates cascading if/else conditionals and switch statements for algorithm selection.
- Makes it easy to add new algorithms without modifying existing code.
- Simplifies unit testing by allowing mock strategy injection.
- Improves code readability and maintainability.
- Supports runtime flexibility for dynamic algorithm selection.
- Each strategy is independently testable and reusable.
Disadvantages
- Adds extra classes, increasing project structure complexity.
- Overkill for very simple scenarios with only one or two algorithms.
- Runtime strategy selection is not checked at compile time; errors surface at runtime.
- Requires careful documentation of available strategies and their behavior.
- Slight polymorphic dispatch overhead (typically negligible in practice).
UML Diagram
classDiagram class IPaymentStrategy { <<interface>> +CalculateFee(decimal) decimal +IsAvailable() bool }
class CreditCardStrategy { +CalculateFee(decimal) decimal +IsAvailable() bool }
class BankTransferStrategy { +CalculateFee(decimal) decimal +IsAvailable() bool }
class PaymentProcessor { -strategy: IPaymentStrategy +ProcessPayment(decimal) +SetStrategy(IPaymentStrategy) }
IPaymentStrategy <|.. CreditCardStrategy IPaymentStrategy <|.. BankTransferStrategy PaymentProcessor --> IPaymentStrategyA Quick Usage Example
public enum PaymentType{ CreditCard, BankTransfer, PayPal}
public class CheckoutService{ private readonly Dictionary<PaymentType, IPaymentStrategy> _strategies;
public CheckoutService() { _strategies = new() { { PaymentType.CreditCard, new CreditCardStrategy() }, { PaymentType.BankTransfer, new BankTransferStrategy() }, { PaymentType.PayPal, new PayPalStrategy() } }; }
public bool Checkout(decimal amount, PaymentType paymentType) { if (!_strategies.ContainsKey(paymentType)) throw new InvalidOperationException("Payment method not supported");
var strategy = _strategies[paymentType];
if (!strategy.IsAvailable()) return false;
var fee = strategy.CalculateFee(amount); var total = amount + fee;
Console.WriteLine($"Processing ${amount} + ${fee} fee = ${total} via {paymentType}"); return true; }}
// Client codevar checkout = new CheckoutService();checkout.Checkout(99.99m, PaymentType.CreditCard);checkout.Checkout(99.99m, PaymentType.BankTransfer);How to Validate the Pattern
If you want to confirm your Strategy implementation is correct, check these points:
- All concrete strategies implement the same interface or abstract base class.
- The context delegates work to a strategy without hardcoding algorithm logic.
- No switch statements or long conditionals exist in the context.
- Each strategy can be swapped without changing context behavior.
- New strategies can be added without modifying existing code.
- Unit tests can inject different strategies to verify context behavior.
A small xUnit example:
[Fact]public void DifferentStrategiesProduceDifferentFees(){ var creditStrategy = new CreditCardStrategy(); var bankStrategy = new BankTransferStrategy();
var creditFee = creditStrategy.CalculateFee(100m); var bankFee = bankStrategy.CalculateFee(100m);
Assert.NotEqual(creditFee, bankFee);}
[Fact]public void ProcessorCanSwitchStrategiesAtRuntime(){ var processor = new DynamicPaymentProcessor(new CreditCardStrategy()); var initial = processor.GetCurrentStrategyType();
processor.SetStrategy(new BankTransferStrategy()); var switched = processor.GetCurrentStrategyType();
Assert.NotEqual(initial, switched);}Summary
The Strategy pattern is a powerful tool for managing algorithm variability and eliminating complex conditional logic. Choose Static Strategy when algorithms are compile-time constant and performance is critical. Choose Dynamic Strategy when you need runtime flexibility and integration with ASP.NET Core’s dependency injection. In most modern applications, Dynamic Strategy paired with DI provides the cleanest, most maintainable approach to algorithm selection and execution.




