Strategy Pattern in C# for Interchangeable Algorithms

The Strategy design pattern is a behavioral pattern that allows you to define a family of algorithms, encapsulate each one inside a separate class, and make them interchangeable at runtime. This article provides a direct, practical guide to implementing the Strategy pattern in C#. You will learn how to decouple algorithm logic from business logic using interfaces, build concrete strategy classes, implement a context class, and dynamically switch behaviors using modern C# practices.


Core Components of the Strategy Pattern

The pattern consists of three primary components:

  1. Strategy Interface: Declares a common interface for all supported algorithms.
  2. Concrete Strategies: Classes that implement the strategy interface with specific algorithmic logic.
  3. Context: A class that maintains a reference to a strategy object and delegates the algorithmic task to it instead of implementing the behavior directly.

Step 1: Define the Strategy Interface

Create an interface that standardizes the algorithm's execution signature. For this example, consider a payment processing scenario where different payment methods require different processing logic.

public interface IPaymentStrategy
{
    void Pay(decimal amount);
}

Step 2: Implement Concrete Strategies

Create concrete classes that implement the interface. Each class encapsulates an isolated algorithm or mechanism.

public class CreditCardPayment : IPaymentStrategy
{
    private readonly string _cardNumber;

    public CreditCardPayment(string cardNumber)
    {
        _cardNumber = cardNumber;
    }

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount:C} using Credit Card ending in {_cardNumber[^4..]}.");
    }
}

public class PayPalPayment : IPaymentStrategy
{
    private readonly string _email;

    public PayPalPayment(string email)
    {
        _email = email;
    }

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount:C} using PayPal account ({_email}).");
    }
}

public class CryptoPayment : IPaymentStrategy
{
    private readonly string _walletAddress;

    public CryptoPayment(string walletAddress)
    {
        _walletAddress = walletAddress;
    }

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount:C} using Crypto Wallet: {_walletAddress}.");
    }
}

Step 3: Implement the Context Class

The context class references the strategy interface. It can receive the strategy via constructor injection or allow runtime mutation through a setter method.

public class CheckoutContext
{
    private IPaymentStrategy _paymentStrategy;

    public CheckoutContext(IPaymentStrategy initialStrategy)
    {
        _paymentStrategy = initialStrategy;
    }

    // Allows swapping the algorithm dynamically at runtime
    public void SetPaymentStrategy(IPaymentStrategy strategy)
    {
        _paymentStrategy = strategy;
    }

    public void ExecuteCheckout(decimal totalAmount)
    {
        if (_paymentStrategy == null)
        {
            throw new InvalidOperationException("Payment strategy is not set.");
        }

        _paymentStrategy.Pay(totalAmount);
    }
}

Step 4: Execute the Pattern

Instantiate the context and dynamically swap strategies based on runtime conditions or user choices.

class Program
{
    static void Main()
    {
        decimal orderAmount = 149.99m;

        // Initialize with Credit Card strategy
        var checkout = new CheckoutContext(new CreditCardPayment("1234567812345678"));
        checkout.ExecuteCheckout(orderAmount);

        // Switch algorithm to PayPal at runtime
        checkout.SetPaymentStrategy(new PayPalPayment("user@example.com"));
        checkout.ExecuteCheckout(orderAmount);

        // Switch algorithm to Crypto at runtime
        checkout.SetPaymentStrategy(new CryptoPayment("0x123abc456def"));
        checkout.ExecuteCheckout(orderAmount);
    }
}

Using Dependency Injection in .NET

In modern .NET applications, you can register multiple concrete strategies with the built-in Dependency Injection (DI) container and resolve them using a factory delegate or a key:

// Program.cs / Service Registration
builder.Services.AddTransient<CreditCardPayment>();
builder.Services.AddTransient<PayPalPayment>();

// Factory delegate for strategy resolution
builder.Services.AddTransient<Func<string, IPaymentStrategy>>(serviceProvider => key =>
{
    return key switch
    {
        "CreditCard" => serviceProvider.GetRequiredService<CreditCardPayment>(),
        "PayPal" => serviceProvider.GetRequiredService<PayPalPayment>(),
        _ => throw new KeyNotFoundException("Strategy not found.")
    };
});

When to Use the Strategy Pattern

  • Multiple algorithmic variants: When you need different variations of an algorithm (e.g., sorting, compression, data export).
  • Eliminating conditional logic: When code uses large switch or if-else blocks to select behaviors.
  • Isolating implementation details: When business logic should remain agnostic of complex, proprietary, or frequently changing algorithmic code.