BrandGhost
Strategy Pattern Real-World Example in C#: Complete Implementation

Strategy Pattern Real-World Example in C#: Complete Implementation

Strategy Pattern Real-World Example in C#: Complete Implementation

This article presents a complete real-world example of the Strategy pattern in C#: an e-commerce discount system that handles multiple discount types. This Strategy pattern real-world example in C# demonstrates how to implement the pattern in a production-ready application with proper error handling, testing, and maintainability.

The Strategy pattern real-world example in C# we'll build shows how different discount calculation algorithms can be encapsulated as strategies, allowing the pricing system to switch between discount types without modifying core logic. This example demonstrates practical implementation techniques you can apply in your own C# applications.

If you're new to the Strategy pattern, understanding the fundamentals is essential before implementing it in production systems.

Problem Statement: E-Commerce Discount System

Our Strategy pattern real-world example in C# addresses a common e-commerce problem: calculating discounts for orders. The system needs to support multiple discount types:

  • Percentage discounts: "20% off your order"
  • Fixed amount discounts: "$10 off your order"
  • Buy-one-get-one (BOGO) discounts: "Buy one, get one free"
  • Tiered discounts: Different percentages based on order total
  • Seasonal discounts: Time-based discount calculations

Without the Strategy pattern, this would require complex conditional logic that becomes difficult to maintain. Our Strategy pattern real-world example in C# shows how to solve this elegantly.

Solution Architecture

Our Strategy pattern real-world example in C# uses the following architecture:

  1. IDiscountStrategy interface: Defines the contract for all discount strategies
  2. Concrete strategy classes: Implement specific discount calculation algorithms
  3. PriceCalculator context: Uses strategies to calculate final prices
  4. DiscountFactory: Creates appropriate strategies based on discount type
  5. Supporting classes: Order, DiscountResult, and validation logic

This architecture demonstrates how Strategy pattern real-world example in C# provides flexibility and maintainability.

Step 1: Define the Strategy Interface

The first step in our Strategy pattern real-world example in C# is defining the discount strategy interface:

/// <summary>
/// Strategy interface for discount calculations.
/// All discount strategies must implement this interface.
/// </summary>
public interface IDiscountStrategy
{
    /// <summary>
    /// Calculates the discount amount for the given order total.
    /// </summary>
    /// <param name="orderTotal">The original order total before discount</param>
    /// <returns>The discount amount to be subtracted</returns>
    decimal CalculateDiscount(decimal orderTotal);

    /// <summary>
    /// Gets a human-readable description of the discount.
    /// </summary>
    string GetDiscountDescription();

    /// <summary>
    /// Validates whether this discount can be applied to the given order total.
    /// </summary>
    /// <param name="orderTotal">The order total to validate</param>
    /// <returns>True if discount can be applied, false otherwise</returns>
    bool CanApply(decimal orderTotal);
}

This interface is the foundation of our Strategy pattern real-world example in C#. It defines what all discount strategies must do, not how they do it.

Step 2: Implement Concrete Strategies

Now we implement concrete strategies for our Strategy pattern real-world example in C#. Each strategy encapsulates a specific discount calculation algorithm.

Percentage Discount Strategy

/// <summary>
/// Calculates discount as a percentage of the order total.
/// Example: 20% off means 20% of the order total is discounted.
/// </summary>
public class PercentageDiscountStrategy : IDiscountStrategy
{
    private readonly decimal _percentage;

    public PercentageDiscountStrategy(decimal percentage)
    {
        if (percentage < 0 || percentage > 100)
        {
            throw new ArgumentOutOfRangeException(
                nameof(percentage),
                "Percentage must be between 0 and 100");
        }
        _percentage = percentage;
    }

    public decimal CalculateDiscount(decimal orderTotal)
    {
        if (orderTotal < 0)
        {
            throw new ArgumentException("Order total cannot be negative", nameof(orderTotal));
        }

        if (!CanApply(orderTotal))
        {
            return 0m;
        }

        return orderTotal * (_percentage / 100m);
    }

    public string GetDiscountDescription()
    {
        return $"{_percentage}% off";
    }

    public bool CanApply(decimal orderTotal)
    {
        return orderTotal > 0;
    }
}

Fixed Amount Discount Strategy

/// <summary>
/// Calculates discount as a fixed amount subtracted from the order total.
/// Example: $10 off means $10 is subtracted from the order total.
/// </summary>
public class FixedAmountDiscountStrategy : IDiscountStrategy
{
    private readonly decimal _fixedAmount;
    private readonly decimal? _minimumOrderTotal;

    public FixedAmountDiscountStrategy(decimal fixedAmount, decimal? minimumOrderTotal = null)
    {
        if (fixedAmount < 0)
        {
            throw new ArgumentException("Fixed amount cannot be negative", nameof(fixedAmount));
        }
        _fixedAmount = fixedAmount;
        _minimumOrderTotal = minimumOrderTotal;
    }

    public decimal CalculateDiscount(decimal orderTotal)
    {
        if (orderTotal < 0)
        {
            throw new ArgumentException("Order total cannot be negative", nameof(orderTotal));
        }

        if (!CanApply(orderTotal))
        {
            return 0m;
        }

        // Don't discount more than the order total
        return Math.Min(_fixedAmount, orderTotal);
    }

    public string GetDiscountDescription()
    {
        var minOrderText = _minimumOrderTotal.HasValue 
            ? $" (minimum order: ${_minimumOrderTotal.Value:F2})" 
            : "";
        return $"${_fixedAmount:F2} off{minOrderText}";
    }

    public bool CanApply(decimal orderTotal)
    {
        if (orderTotal <= 0)
        {
            return false;
        }

        if (_minimumOrderTotal.HasValue && orderTotal < _minimumOrderTotal.Value)
        {
            return false;
        }

        return true;
    }
}

Buy-One-Get-One Discount Strategy

/// <summary>
/// Calculates discount for buy-one-get-one (BOGO) promotions.
/// Example: Buy one item at full price, get the second item free.
/// </summary>
public class BogoDiscountStrategy : IDiscountStrategy
{
    private readonly decimal _itemPrice;
    private readonly int _quantity;

    public BogoDiscountStrategy(decimal itemPrice, int quantity)
    {
        if (itemPrice < 0)
        {
            throw new ArgumentException("Item price cannot be negative", nameof(itemPrice));
        }
        if (quantity < 2)
        {
            throw new ArgumentException("Quantity must be at least 2 for BOGO", nameof(quantity));
        }
        _itemPrice = itemPrice;
        _quantity = quantity;
    }

    public decimal CalculateDiscount(decimal orderTotal)
    {
        if (!CanApply(orderTotal))
        {
            return 0m;
        }

        // Calculate how many free items the customer gets
        int freeItems = _quantity / 2;
        return freeItems * _itemPrice;
    }

    public string GetDiscountDescription()
    {
        return $"Buy {_quantity / 2}, Get {_quantity / 2} Free";
    }

    public bool CanApply(decimal orderTotal)
    {
        // BOGO applies if order contains at least the required quantity
        return orderTotal >= (_itemPrice * _quantity);
    }
}

Tiered Discount Strategy

/// <summary>
/// Calculates discount based on order total tiers.
/// Example: 10% off orders over $100, 20% off orders over $200.
/// </summary>
public class TieredDiscountStrategy : IDiscountStrategy
{
    private readonly List<DiscountTier> _tiers;

    public TieredDiscountStrategy(List<DiscountTier> tiers)
    {
        if (tiers == null || tiers.Count == 0)
        {
            throw new ArgumentException("Tiers cannot be null or empty", nameof(tiers));
        }
        _tiers = tiers.OrderByDescending(t => t.MinimumOrderTotal).ToList();
    }

    public decimal CalculateDiscount(decimal orderTotal)
    {
        if (!CanApply(orderTotal))
        {
            return 0m;
        }

        // Find the highest tier that applies
        var applicableTier = _tiers.FirstOrDefault(t => orderTotal >= t.MinimumOrderTotal);
        if (applicableTier == null)
        {
            return 0m;
        }

        return orderTotal * (applicableTier.DiscountPercentage / 100m);
    }

    public string GetDiscountDescription()
    {
        var tierDescriptions = _tiers.Select(t => 
            $"{t.DiscountPercentage}% off orders over ${t.MinimumOrderTotal:F2}");
        return string.Join(", ", tierDescriptions);
    }

    public bool CanApply(decimal orderTotal)
    {
        return orderTotal > 0 && _tiers.Any(t => orderTotal >= t.MinimumOrderTotal);
    }
}

public class DiscountTier
{
    public decimal MinimumOrderTotal { get; set; }
    public decimal DiscountPercentage { get; set; }
}

Step 3: Implement the Context Class

The context class in our Strategy pattern real-world example in C# uses strategies to calculate final prices:

/// <summary>
/// Context class that uses discount strategies to calculate final order prices.
/// </summary>
public class PriceCalculator
{
    private readonly IDiscountStrategy _discountStrategy;
    private readonly ILogger<PriceCalculator> _logger;

    public PriceCalculator(IDiscountStrategy discountStrategy, ILogger<PriceCalculator> logger = null)
    {
        _discountStrategy = discountStrategy ?? throw new ArgumentNullException(nameof(discountStrategy));
        _logger = logger;
    }

    /// <summary>
    /// Calculates the final price after applying the discount strategy.
    /// </summary>
    public PriceCalculationResult CalculateFinalPrice(decimal orderTotal)
    {
        if (orderTotal < 0)
        {
            throw new ArgumentException("Order total cannot be negative", nameof(orderTotal));
        }

        try
        {
            if (!_discountStrategy.CanApply(orderTotal))
            {
                _logger?.LogInformation(
                    "Discount {DiscountType} cannot be applied to order total ${OrderTotal}",
                    _discountStrategy.GetDiscountDescription(),
                    orderTotal);

                return new PriceCalculationResult
                {
                    OriginalPrice = orderTotal,
                    DiscountAmount = 0m,
                    FinalPrice = orderTotal,
                    DiscountDescription = "No discount applied",
                    IsDiscountApplied = false
                };
            }

            var discountAmount = _discountStrategy.CalculateDiscount(orderTotal);
            var finalPrice = Math.Max(0, orderTotal - discountAmount); // Ensure price doesn't go negative

            _logger?.LogInformation(
                "Applied discount {DiscountType}: ${DiscountAmount} off ${OrderTotal} = ${FinalPrice}",
                _discountStrategy.GetDiscountDescription(),
                discountAmount,
                orderTotal,
                finalPrice);

            return new PriceCalculationResult
            {
                OriginalPrice = orderTotal,
                DiscountAmount = discountAmount,
                FinalPrice = finalPrice,
                DiscountDescription = _discountStrategy.GetDiscountDescription(),
                IsDiscountApplied = true
            };
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Error calculating discount for order total ${OrderTotal}", orderTotal);
            throw;
        }
    }
}

public class PriceCalculationResult
{
    public decimal OriginalPrice { get; set; }
    public decimal DiscountAmount { get; set; }
    public decimal FinalPrice { get; set; }
    public string DiscountDescription { get; set; }
    public bool IsDiscountApplied { get; set; }
}

Step 4: Create Strategy Factory

A factory helps manage strategy creation in our Strategy pattern real-world example in C#:

/// <summary>
/// Factory for creating discount strategies based on discount type and parameters.
/// </summary>
public class DiscountStrategyFactory
{
    public IDiscountStrategy CreateStrategy(DiscountType discountType, Dictionary<string, object> parameters)
    {
        return discountType switch
        {
            DiscountType.Percentage => new PercentageDiscountStrategy(
                Convert.ToDecimal(parameters["percentage"])),

            DiscountType.FixedAmount => new FixedAmountDiscountStrategy(
                Convert.ToDecimal(parameters["amount"]),
                parameters.ContainsKey("minimumOrderTotal") 
                    ? Convert.ToDecimal(parameters["minimumOrderTotal"]) 
                    : null),

            DiscountType.Bogo => new BogoDiscountStrategy(
                Convert.ToDecimal(parameters["itemPrice"]),
                Convert.ToInt32(parameters["quantity"])),

            DiscountType.Tiered => new TieredDiscountStrategy(
                (List<DiscountTier>)parameters["tiers"]),

            _ => throw new ArgumentException($"Unknown discount type: {discountType}")
        };
    }
}

public enum DiscountType
{
    Percentage,
    FixedAmount,
    Bogo,
    Tiered
}

Step 5: Complete Usage Example

Here's how to use our Strategy pattern real-world example in C#:

class Program
{
    static void Main(string[] args)
    {
        // Example 1: Percentage discount
        var percentageStrategy = new PercentageDiscountStrategy(20m); // 20% off
        var calculator1 = new PriceCalculator(percentageStrategy);
        var result1 = calculator1.CalculateFinalPrice(100m);
        Console.WriteLine($"Original: ${result1.OriginalPrice:F2}, " +
                         $"Discount: ${result1.DiscountAmount:F2}, " +
                         $"Final: ${result1.FinalPrice:F2}");
        // Output: Original: $100.00, Discount: $20.00, Final: $80.00

        // Example 2: Fixed amount discount with minimum order
        var fixedStrategy = new FixedAmountDiscountStrategy(15m, minimumOrderTotal: 50m);
        var calculator2 = new PriceCalculator(fixedStrategy);
        var result2 = calculator2.CalculateFinalPrice(60m);
        Console.WriteLine($"Original: ${result2.OriginalPrice:F2}, " +
                         $"Discount: ${result2.DiscountAmount:F2}, " +
                         $"Final: ${result2.FinalPrice:F2}");
        // Output: Original: $60.00, Discount: $15.00, Final: $45.00

        // Example 3: Tiered discount
        var tiers = new List<DiscountTier>
        {
            new DiscountTier { MinimumOrderTotal = 100m, DiscountPercentage = 10m },
            new DiscountTier { MinimumOrderTotal = 200m, DiscountPercentage = 20m },
            new DiscountTier { MinimumOrderTotal = 500m, DiscountPercentage = 30m }
        };
        var tieredStrategy = new TieredDiscountStrategy(tiers);
        var calculator3 = new PriceCalculator(tieredStrategy);
        var result3 = calculator3.CalculateFinalPrice(250m);
        Console.WriteLine($"Original: ${result3.OriginalPrice:F2}, " +
                         $"Discount: ${result3.DiscountAmount:F2}, " +
                         $"Final: ${result3.FinalPrice:F2}");
        // Output: Original: $250.00, Discount: $50.00, Final: $200.00

        // Example 4: Using factory
        var factory = new DiscountStrategyFactory();
        var strategy = factory.CreateStrategy(
            DiscountType.Percentage,
            new Dictionary<string, object> { ["percentage"] = 15m });
        var calculator4 = new PriceCalculator(strategy);
        var result4 = calculator4.CalculateFinalPrice(200m);
        Console.WriteLine($"Original: ${result4.OriginalPrice:F2}, " +
                         $"Discount: ${result4.DiscountAmount:F2}, " +
                         $"Final: ${result4.FinalPrice:F2}");
        // Output: Original: $200.00, Discount: $30.00, Final: $170.00
    }
}

Testing the Implementation

Testing is crucial for our Strategy pattern real-world example in C#. Here are unit tests:

[TestClass]
public class DiscountStrategyTests
{
    [TestMethod]
    public void PercentageDiscountStrategy_CalculateDiscount_ReturnsCorrectAmount()
    {
        // Arrange
        var strategy = new PercentageDiscountStrategy(20m);
        
        // Act
        var discount = strategy.CalculateDiscount(100m);
        
        // Assert
        Assert.AreEqual(20m, discount);
    }

    [TestMethod]
    public void FixedAmountDiscountStrategy_WithMinimumOrder_AppliesCorrectly()
    {
        // Arrange
        var strategy = new FixedAmountDiscountStrategy(10m, minimumOrderTotal: 50m);
        
        // Act & Assert
        Assert.IsFalse(strategy.CanApply(40m)); // Below minimum
        Assert.IsTrue(strategy.CanApply(60m)); // Above minimum
        Assert.AreEqual(10m, strategy.CalculateDiscount(60m));
    }

    [TestMethod]
    public void PriceCalculator_WithPercentageStrategy_CalculatesCorrectFinalPrice()
    {
        // Arrange
        var strategy = new PercentageDiscountStrategy(15m);
        var calculator = new PriceCalculator(strategy);
        
        // Act
        var result = calculator.CalculateFinalPrice(100m);
        
        // Assert
        Assert.AreEqual(100m, result.OriginalPrice);
        Assert.AreEqual(15m, result.DiscountAmount);
        Assert.AreEqual(85m, result.FinalPrice);
        Assert.IsTrue(result.IsDiscountApplied);
    }
}

Integration with Dependency Injection

Our Strategy pattern real-world example in C# integrates with dependency injection:

// In Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Register strategies
    services.AddTransient<IDiscountStrategy, PercentageDiscountStrategy>();
    services.AddTransient<IDiscountStrategy, FixedAmountDiscountStrategy>();
    
    // Register factory
    services.AddSingleton<DiscountStrategyFactory>();
    
    // Register context
    services.AddScoped<PriceCalculator>();
}

Conclusion

This Strategy pattern real-world example in C# demonstrates a complete, production-ready implementation of an e-commerce discount system. The example shows how Strategy pattern provides flexibility, maintainability, and testability. By encapsulating each discount calculation algorithm in its own strategy class, we can easily add new discount types, modify existing ones, and test each strategy independently.

The key benefits demonstrated in this Strategy pattern real-world example in C# include: elimination of complex conditional logic, easy extension with new discount types, independent testing of strategies, and clean separation of concerns.

Frequently Asked Questions

How does this Strategy pattern real-world example in C# handle edge cases?

This Strategy pattern real-world example in C# handles edge cases through input validation, the CanApply method that checks if discounts can be applied, and ensuring final prices never go negative. Each strategy validates its own requirements before calculating discounts.

Can I add new discount types to this Strategy pattern real-world example in C#?

Yes, adding new discount types to this Strategy pattern real-world example in C# is straightforward: create a new class implementing IDiscountStrategy, add it to the factory if needed, and use it with the PriceCalculator. No existing code needs modification, demonstrating the Open/Closed Principle.

How do I test this Strategy pattern real-world example in C#?

Test this Strategy pattern real-world example in C# by testing each strategy independently with unit tests, testing the PriceCalculator context with mock strategies, and testing the factory's strategy creation logic. Dependency injection makes testing easier.

What are the performance implications of this Strategy pattern real-world example in C#?

This Strategy pattern real-world example in C# adds minimal performance overhead from interface method calls. The performance cost is negligible for discount calculations, and the flexibility benefits typically outweigh any small performance cost.

How does this Strategy pattern real-world example in C# compare to using if-else statements?

This Strategy pattern real-world example in C# eliminates complex if-else chains, makes it easy to add new discount types without modifying existing code, and allows independent testing of each discount algorithm. If-else statements would require modifying the calculator class for each new discount type.

Can I use this Strategy pattern real-world example in C# with async operations?

Yes, you can extend this Strategy pattern real-world example in C# for async operations by making strategy methods return Task<decimal> instead of decimal. The context class would then await strategy calls, making it suitable for I/O-bound discount calculations.

How do I handle discount validation in this Strategy pattern real-world example in C#?

This Strategy pattern real-world example in C# handles validation through the CanApply method in each strategy, input validation in strategy constructors and methods, and the context class checking CanApply before calculating discounts. This ensures discounts are only applied when appropriate.

Builder Pattern Real-World Example in C#: Complete Implementation

See Builder pattern in action with a complete real-world C# example. Step-by-step implementation of a configuration system demonstrating step-by-step object construction.

Abstract Factory Pattern Real-World Example in C#: Complete Implementation

See Abstract Factory pattern in action with a complete real-world C# example. Step-by-step implementation of a furniture shop system demonstrating families of related objects.

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