BrandGhost
How to Implement Strategy Pattern in C#: Step-by-Step Guide

How to Implement Strategy Pattern in C#: Step-by-Step Guide

How to Implement Strategy Pattern in C#: Step-by-Step Guide

The Strategy pattern is a behavioral design pattern that enables you to define a family of algorithms, encapsulate each one, and make them interchangeable. Implementing the Strategy pattern in C# involves defining a strategy interface, creating concrete strategy classes, and implementing a context class that uses strategies. This step-by-step guide will walk you through implementing the Strategy pattern with practical C# examples.

Understanding how to implement the Strategy pattern in C# is crucial for developers who need flexible algorithm selection mechanisms. This pattern is particularly useful when you have multiple ways of performing the same operation and want to choose between them at runtime. The Strategy pattern in C# enables you to eliminate complex conditional logic and make your code more maintainable and extensible.

If you're new to design patterns, consider reading The Big List of Design Patterns - Everything You Need to Know for context on how the Strategy pattern fits into the broader design pattern landscape.

Step 1: Define the Strategy Interface

The first step in implementing the Strategy pattern in C# is defining the strategy interface. This interface represents the common contract that all concrete strategies must follow.

// Strategy interface - defines the common contract
public interface ISortStrategy
{
    void Sort(List<int> data);
    string GetAlgorithmName();
}

The strategy interface should:

  • Define methods that all concrete strategies will implement
  • Be focused on behavior, not implementation details
  • Follow interface segregation principles (keep interfaces small and focused)
  • Express what the strategy does, not how it does it

Why this matters: The strategy interface is what the context class depends on, ensuring loose coupling between the context and concrete strategy implementations. This is a fundamental principle when implementing the Strategy pattern in C#.

Step 2: Create Concrete Strategy Classes

Next, create concrete implementations of the strategy interface. Each concrete strategy represents a specific algorithm variation.

// Concrete Strategy 1: Quick Sort
public class QuickSortStrategy : ISortStrategy
{
    public void Sort(List<int> data)
    {
        if (data == null || data.Count <= 1)
        {
            return;
        }
        
        QuickSort(data, 0, data.Count - 1);
    }

    private void QuickSort(List<int> data, int low, int high)
    {
        if (low < high)
        {
            int pivotIndex = Partition(data, low, high);
            QuickSort(data, low, pivotIndex - 1);
            QuickSort(data, pivotIndex + 1, high);
        }
    }

    private int Partition(List<int> data, int low, int high)
    {
        int pivot = data[high];
        int i = low - 1;

        for (int j = low; j < high; j++)
        {
            if (data[j] <= pivot)
            {
                i++;
                (data[i], data[j]) = (data[j], data[i]);
            }
        }

        (data[i + 1], data[high]) = (data[high], data[i + 1]);
        return i + 1;
    }

    public string GetAlgorithmName()
    {
        return "Quick Sort";
    }
}

// Concrete Strategy 2: Merge Sort
public class MergeSortStrategy : ISortStrategy
{
    public void Sort(List<int> data)
    {
        if (data == null || data.Count <= 1)
        {
            return;
        }
        
        MergeSort(data, 0, data.Count - 1);
    }

    private void MergeSort(List<int> data, int left, int right)
    {
        if (left < right)
        {
            int middle = left + (right - left) / 2;
            MergeSort(data, left, middle);
            MergeSort(data, middle + 1, right);
            Merge(data, left, middle, right);
        }
    }

    private void Merge(List<int> data, int left, int middle, int right)
    {
        int n1 = middle - left + 1;
        int n2 = right - middle;

        var leftArray = new List<int>();
        var rightArray = new List<int>();

        for (int i = 0; i < n1; i++)
        {
            leftArray.Add(data[left + i]);
        }
        for (int j = 0; j < n2; j++)
        {
            rightArray.Add(data[middle + 1 + j]);
        }

        int leftIndex = 0, rightIndex = 0;
        int k = left;

        while (leftIndex < n1 && rightIndex < n2)
        {
            if (leftArray[leftIndex] <= rightArray[rightIndex])
            {
                data[k] = leftArray[leftIndex];
                leftIndex++;
            }
            else
            {
                data[k] = rightArray[rightIndex];
                rightIndex++;
            }
            k++;
        }

        while (leftIndex < n1)
        {
            data[k] = leftArray[leftIndex];
            leftIndex++;
            k++;
        }

        while (rightIndex < n2)
        {
            data[k] = rightArray[rightIndex];
            rightIndex++;
            k++;
        }
    }

    public string GetAlgorithmName()
    {
        return "Merge Sort";
    }
}

// Concrete Strategy 3: Bubble Sort
public class BubbleSortStrategy : ISortStrategy
{
    public void Sort(List<int> data)
    {
        if (data == null || data.Count <= 1)
        {
            return;
        }

        int n = data.Count;
        for (int i = 0; i < n - 1; i++)
        {
            for (int j = 0; j < n - i - 1; j++)
            {
                if (data[j] > data[j + 1])
                {
                    (data[j], data[j + 1]) = (data[j + 1], data[j]);
                }
            }
        }
    }

    public string GetAlgorithmName()
    {
        return "Bubble Sort";
    }
}

Each concrete strategy encapsulates a complete algorithm implementation. When implementing the Strategy pattern in C#, each strategy should be independent and not depend on other strategies. This independence makes strategies easy to test, maintain, and extend.

Step 3: Implement the Context Class

The context class maintains a reference to a strategy object and delegates work to it. This is the core of implementing the Strategy pattern in C#.

// Context class - uses strategies
public class Sorter
{
    private ISortStrategy _sortStrategy;

    // Constructor injection
    public Sorter(ISortStrategy sortStrategy)
    {
        _sortStrategy = sortStrategy ?? throw new ArgumentNullException(nameof(sortStrategy));
    }

    // Method to change strategy at runtime
    public void SetSortStrategy(ISortStrategy sortStrategy)
    {
        _sortStrategy = sortStrategy ?? throw new ArgumentNullException(nameof(sortStrategy));
    }

    // Context method that delegates to strategy
    public void SortData(List<int> data)
    {
        if (data == null)
        {
            throw new ArgumentNullException(nameof(data));
        }

        Console.WriteLine($"Using {_sortStrategy.GetAlgorithmName()} to sort {data.Count} items");
        _sortStrategy.Sort(data);
    }

    public string GetCurrentStrategyName()
    {
        return _sortStrategy.GetAlgorithmName();
    }
}

The context class:

  • Maintains a reference to a strategy object
  • Provides a way to set or change the strategy (constructor or setter method)
  • Delegates algorithm execution to the strategy
  • Doesn't know the concrete implementation details of strategies

Why this matters: The context class is decoupled from specific algorithm implementations, making it easy to switch strategies without modifying the context code. This is a key benefit when implementing the Strategy pattern in C#.

Step 4: Use the Strategy Pattern

Now you can use the Strategy pattern in your application code. Here's how to implement and use it:

class Program
{
    static void Main(string[] args)
    {
        var data = new List<int> { 64, 34, 25, 12, 22, 11, 90 };

        // Create context with Quick Sort strategy
        var sorter = new Sorter(new QuickSortStrategy());
        var dataCopy1 = new List<int>(data);
        sorter.SortData(dataCopy1);
        Console.WriteLine($"Sorted: {string.Join(", ", dataCopy1)}");
        Console.WriteLine();

        // Switch to Merge Sort strategy
        sorter.SetSortStrategy(new MergeSortStrategy());
        var dataCopy2 = new List<int>(data);
        sorter.SortData(dataCopy2);
        Console.WriteLine($"Sorted: {string.Join(", ", dataCopy2)}");
        Console.WriteLine();

        // Switch to Bubble Sort strategy
        sorter.SetSortStrategy(new BubbleSortStrategy());
        var dataCopy3 = new List<int>(data);
        sorter.SortData(dataCopy3);
        Console.WriteLine($"Sorted: {string.Join(", ", dataCopy3)}");
    }
}

This example demonstrates how implementing the Strategy pattern in C# allows you to switch between different sorting algorithms at runtime without modifying the Sorter context class.

Complete Example: Payment Processing System

Let's look at a more complete example that demonstrates implementing the Strategy pattern in C# for a payment processing system:

// Strategy interface
public interface IPaymentStrategy
{
    bool ProcessPayment(decimal amount, string paymentDetails);
    string GetPaymentMethodName();
    decimal GetProcessingFee();
}

// Concrete Strategy 1: Credit Card Payment
public class CreditCardPaymentStrategy : IPaymentStrategy
{
    public bool ProcessPayment(decimal amount, string paymentDetails)
    {
        // Simulate credit card processing
        Console.WriteLine($"Processing ${amount} via Credit Card");
        Console.WriteLine($"Card details: {paymentDetails}");
        
        // In real implementation, this would call a payment gateway
        return true; // Simulated success
    }

    public string GetPaymentMethodName()
    {
        return "Credit Card";
    }

    public decimal GetProcessingFee()
    {
        return 0.029m; // 2.9% processing fee
    }
}

// Concrete Strategy 2: PayPal Payment
public class PayPalPaymentStrategy : IPaymentStrategy
{
    public bool ProcessPayment(decimal amount, string paymentDetails)
    {
        // Simulate PayPal processing
        Console.WriteLine($"Processing ${amount} via PayPal");
        Console.WriteLine($"PayPal email: {paymentDetails}");
        
        // In real implementation, this would call PayPal API
        return true; // Simulated success
    }

    public string GetPaymentMethodName()
    {
        return "PayPal";
    }

    public decimal GetProcessingFee()
    {
        return 0.034m; // 3.4% processing fee
    }
}

// Concrete Strategy 3: Bank Transfer Payment
public class BankTransferPaymentStrategy : IPaymentStrategy
{
    public bool ProcessPayment(decimal amount, string paymentDetails)
    {
        // Simulate bank transfer processing
        Console.WriteLine($"Processing ${amount} via Bank Transfer");
        Console.WriteLine($"Account details: {paymentDetails}");
        
        // In real implementation, this would initiate a bank transfer
        return true; // Simulated success
    }

    public string GetPaymentMethodName()
    {
        return "Bank Transfer";
    }

    public decimal GetProcessingFee()
    {
        return 0.01m; // 1% processing fee
    }
}

// Context class
public class PaymentProcessor
{
    private IPaymentStrategy _paymentStrategy;

    public PaymentProcessor(IPaymentStrategy paymentStrategy)
    {
        _paymentStrategy = paymentStrategy ?? throw new ArgumentNullException(nameof(paymentStrategy));
    }

    public void SetPaymentStrategy(IPaymentStrategy paymentStrategy)
    {
        _paymentStrategy = paymentStrategy ?? throw new ArgumentNullException(nameof(paymentStrategy));
    }

    public PaymentResult ProcessPayment(decimal amount, string paymentDetails)
    {
        var processingFee = _paymentStrategy.GetProcessingFee();
        var totalAmount = amount + (amount * processingFee);
        
        Console.WriteLine($"Payment Method: {_paymentStrategy.GetPaymentMethodName()}");
        Console.WriteLine($"Amount: ${amount:F2}");
        Console.WriteLine($"Processing Fee ({processingFee:P2}): ${amount * processingFee:F2}");
        Console.WriteLine($"Total: ${totalAmount:F2}");
        Console.WriteLine();

        var success = _paymentStrategy.ProcessPayment(amount, paymentDetails);

        return new PaymentResult
        {
            Success = success,
            PaymentMethod = _paymentStrategy.GetPaymentMethodName(),
            Amount = amount,
            ProcessingFee = amount * processingFee,
            TotalAmount = totalAmount
        };
    }
}

// Supporting class
public class PaymentResult
{
    public bool Success { get; set; }
    public string PaymentMethod { get; set; }
    public decimal Amount { get; set; }
    public decimal ProcessingFee { get; set; }
    public decimal TotalAmount { get; set; }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        var processor = new PaymentProcessor(new CreditCardPaymentStrategy());
        
        // Process payment with credit card
        var result1 = processor.ProcessPayment(100.00m, "4111-1111-1111-1111");
        Console.WriteLine($"Payment successful: {result1.Success}");
        Console.WriteLine();

        // Switch to PayPal
        processor.SetPaymentStrategy(new PayPalPaymentStrategy());
        var result2 = processor.ProcessPayment(100.00m, "[email protected]");
        Console.WriteLine($"Payment successful: {result2.Success}");
        Console.WriteLine();

        // Switch to Bank Transfer
        processor.SetPaymentStrategy(new BankTransferPaymentStrategy());
        var result3 = processor.ProcessPayment(100.00m, "ACC123456789");
        Console.WriteLine($"Payment successful: {result3.Success}");
    }
}

This example demonstrates a complete implementation of the Strategy pattern in C# for a payment processing system. Each payment method is encapsulated in its own strategy class, making it easy to add new payment methods or modify existing ones without changing the PaymentProcessor context class.

Using Strategy Pattern with Dependency Injection

When implementing the Strategy pattern in C# with dependency injection, you can register strategies in your DI container and inject them into context classes. This approach is common in modern C# applications:

// In your startup/configuration code (e.g., Program.cs or Startup.cs)
public void ConfigureServices(IServiceCollection services)
{
    // Register strategies
    services.AddTransient<IPaymentStrategy, CreditCardPaymentStrategy>();
    services.AddTransient<IPaymentStrategy, PayPalPaymentStrategy>();
    services.AddTransient<IPaymentStrategy, BankTransferPaymentStrategy>();
    
    // Register context
    services.AddScoped<PaymentProcessor>();
    
    // Or use a factory to select strategies
    services.AddTransient<Func<string, IPaymentStrategy>>(serviceProvider => key =>
    {
        return key switch
        {
            "creditcard" => serviceProvider.GetService<CreditCardPaymentStrategy>(),
            "paypal" => serviceProvider.GetService<PayPalPaymentStrategy>(),
            "banktransfer" => serviceProvider.GetService<BankTransferPaymentStrategy>(),
            _ => throw new ArgumentException($"Unknown payment strategy: {key}")
        };
    });
}

This approach makes implementing the Strategy pattern in C# more testable and follows SOLID principles more closely. The Factory Method pattern can complement the Strategy pattern by handling strategy creation, as shown in How to Implement Factory Method Pattern in C#: Step-by-Step Guide.

Best Practices for Implementation

When implementing the Strategy pattern in C#, follow these best practices:

Keep Strategies Stateless: Prefer stateless strategies when possible. If a strategy needs configuration, pass it through the constructor rather than storing mutable state. This makes strategies thread-safe and easier to test when implementing the Strategy pattern in C#.

Use Interfaces for Strategy Contracts: Define clear interfaces that express what strategies must do, not how they do it. This makes it easy to add new strategies and ensures all strategies follow the same contract. This principle is similar to what you'll find in the Adapter Design Pattern, which also relies heavily on interfaces.

Document Strategy Selection Logic: If strategy selection is complex, document when and why each strategy should be used. Consider using configuration files or factory patterns to manage strategy selection when implementing the Strategy pattern in C#.

Test Strategies Independently: Each strategy should be testable in isolation. Write unit tests for each concrete strategy to ensure they work correctly. This is easier when implementing the Strategy pattern in C# because strategies are independent classes.

Consider Performance: The Strategy pattern adds a layer of indirection, which has a small performance cost. However, this cost is typically negligible. Only optimize if profiling shows it's a bottleneck when implementing the Strategy pattern in C#.

Common Pitfalls to Avoid

When implementing the Strategy pattern in C#, avoid these common pitfalls:

Over-Engineering Simple Cases: Don't use the Strategy pattern for simple cases where a few if statements would suffice. The pattern adds complexity, so use it when you have multiple algorithms or expect to add more in the future. If you only have two options that are unlikely to change, a simple conditional might be more appropriate.

Creating Too Many Small Strategies: While separation is good, creating too many tiny strategy classes can make code harder to navigate. Find a balance between separation and cohesion when implementing the Strategy pattern in C#.

Not Handling Strategy State Properly: If strategies need to maintain state between calls, ensure you handle this properly. Prefer stateless strategies, but when state is necessary, document it clearly and consider thread safety when implementing the Strategy pattern in C#.

Ignoring Error Handling: Ensure strategies handle errors appropriately. Consider what happens if a strategy fails and how the context should respond. This is important when implementing the Strategy pattern in C# for production systems.

Testing Strategy Pattern Implementations

When implementing the Strategy pattern in C#, testing becomes easier because strategies can be tested independently:

[TestClass]
public class PaymentStrategyTests
{
    [TestMethod]
    public void CreditCardPaymentStrategy_ProcessPayment_ReturnsSuccess()
    {
        // Arrange
        var strategy = new CreditCardPaymentStrategy();
        
        // Act
        var result = strategy.ProcessPayment(100.00m, "4111-1111-1111-1111");
        
        // Assert
        Assert.IsTrue(result);
    }

    [TestMethod]
    public void PaymentProcessor_WithCreditCardStrategy_ProcessesPayment()
    {
        // Arrange
        var strategy = new CreditCardPaymentStrategy();
        var processor = new PaymentProcessor(strategy);
        
        // Act
        var result = processor.ProcessPayment(100.00m, "4111-1111-1111-1111");
        
        // Assert
        Assert.IsTrue(result.Success);
        Assert.AreEqual("Credit Card", result.PaymentMethod);
    }

    [TestMethod]
    public void PaymentProcessor_SwitchStrategy_ChangesBehavior()
    {
        // Arrange
        var processor = new PaymentProcessor(new CreditCardPaymentStrategy());
        
        // Act
        var result1 = processor.ProcessPayment(100.00m, "test");
        processor.SetPaymentStrategy(new PayPalPaymentStrategy());
        var result2 = processor.ProcessPayment(100.00m, "test");
        
        // Assert
        Assert.AreEqual("Credit Card", result1.PaymentMethod);
        Assert.AreEqual("PayPal", result2.PaymentMethod);
    }
}

Testing is one of the key benefits when implementing the Strategy pattern in C#. Each strategy can be tested independently, and the context can be tested with mock strategies.

Conclusion

Implementing the Strategy pattern in C# provides a powerful way to manage algorithm variations in your applications. By following these steps—defining a strategy interface, creating concrete strategies, and implementing a context class—you can create flexible, maintainable code that follows SOLID principles. The Strategy pattern in C# is particularly valuable when you need runtime algorithm selection or want to eliminate complex conditional logic.

Remember that implementing the Strategy pattern in C# works exceptionally well with modern C# features like dependency injection, interfaces, and LINQ. By combining the Strategy pattern with these language features and following best practices, you can create maintainable, testable, and flexible solutions.

Frequently Asked Questions

What is the difference between Strategy pattern and if-else statements?

When implementing the Strategy pattern in C#, you use polymorphism instead of conditional statements. This makes code more maintainable and easier to extend. If-else statements are fine for simple cases with few options, but the Strategy pattern is better when you have multiple algorithms or expect to add more in the future. The pattern also makes algorithms testable independently and allows runtime selection.

Can I use Strategy pattern with async/await in C#?

Yes, when implementing the Strategy pattern in C#, you can use async/await. Simply make your strategy interface methods return Task or Task<T>, and implement them with async methods in concrete strategies. The context class can then await strategy calls. This is particularly useful for I/O-bound operations like payment processing or data retrieval.

How do I choose which strategy to use at runtime?

When implementing the Strategy pattern in C#, you can select strategies based on configuration, user input, runtime conditions, or business rules. Common approaches include factory patterns, configuration files, or conditional logic that creates the appropriate strategy. The key is that once selected, the context uses the strategy through the common interface without knowing the concrete type.

Is Strategy pattern thread-safe?

The Strategy pattern itself doesn't guarantee thread safety when implementing it in C#. If strategies maintain state, you need to ensure thread safety yourself. Prefer stateless strategies when possible, as they're naturally thread-safe. If state is necessary, use appropriate synchronization mechanisms or ensure strategies are used within thread-safe contexts.

Can strategies be nested or composed?

Yes, when implementing the Strategy pattern in C#, you can create composite strategies that delegate to other strategies. This allows you to build complex behaviors from simpler strategies. For example, a validation strategy might compose multiple validation rules, each implemented as a separate strategy. This maintains the benefits of the Strategy pattern while allowing complex behaviors.

How does Strategy pattern work with LINQ in C#?

When implementing the Strategy pattern in C#, LINQ expressions can be used as lightweight strategies. For example, different filtering or sorting strategies can be represented as LINQ expressions. However, for complex algorithms, class-based strategies are usually more maintainable and testable. LINQ works well for simple, data-oriented strategies.

What are the performance implications of Strategy pattern?

When implementing the Strategy pattern in C#, there's a small performance cost from the indirection (method calls through interfaces). However, this cost is typically negligible in most applications. The flexibility and maintainability benefits usually outweigh the small performance overhead. Only optimize if profiling shows it's a bottleneck in your specific use case.

How to Implement Abstract Factory Pattern in C#: Step-by-Step Guide

Learn how to implement Abstract Factory pattern in C# with a complete step-by-step guide. Includes code examples, best practices, and common pitfalls to avoid.

How to Implement Builder Pattern in C#: Step-by-Step Guide

Learn how to implement Builder pattern in C# with a complete step-by-step guide. Includes code examples, best practices, and common pitfalls to avoid.

Strategy Design Pattern in C#: Complete Guide with Examples

Strategy design pattern in C#: complete guide with code examples, implementation, and best practices for flexible algorithm selection.

An error has occurred. This application may no longer respond until reloaded. Reload