Manikandan — Manikandan
Updated on 8 min read Manikandan Design Patterns

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.

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.cs
builder.Services.AddScoped<IPaymentStrategy, CreditCardStrategy>();
// Or register multiple with a factory pattern
builder.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 services
public 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

ComponentPurposeExample in this article
Strategy InterfaceDefines contract all concrete strategies must followIPaymentStrategy
Concrete StrategiesImplement the strategy interface with specific algorithm logicCreditCardStrategy, BankTransferStrategy
ContextHolds a reference to a strategy and delegates work to itPaymentProcessor
ClientCreates or selects a strategy and passes it to the contextOrderService, 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 time
var 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 runtime
var bankStrategy = new BankTransferStrategy();
var processor = new DynamicPaymentProcessor(bankStrategy);
processor.ProcessPayment(100m);
// Switch strategies on the fly
var 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

AspectStatic StrategyDynamic Strategy
Strategy SelectionCompile time via genericsRuntime via constructor or method injection
FlexibilityLow; fixed per type instanceHigh; changeable during execution
PerformanceNo polymorphic overheadSlight polymorphic dispatch cost (negligible)
TestabilityRequires separate test builds for different strategiesEasy; inject mock strategies directly
ConfigurationType parameter bindingDI container registration or factory methods
Best Use CaseFixed algorithms per deploymentUser-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 --> IPaymentStrategy

A 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 code
var 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.

Share:
Back to Blog

Related Posts

View All Posts »