🎯 A Clean Way to Inject Strategy Pattern in C# Using Factory and DI Dictionary
Ali Shahriari (MasterPars)

Ali Shahriari (MasterPars) @masterpars

About: I am a backend developer specialized in building scalable, high-performance platforms. My work primarily focuses on platform development, seamless integration with external systems, and ensuring high

Location:
Shiraz, Iran
Joined:
Sep 22, 2024

🎯 A Clean Way to Inject Strategy Pattern in C# Using Factory and DI Dictionary

Publish Date: May 27
0 0

When implementing multiple behaviors like discount strategies, the common trap is hardcoding logic into service classes or bloating factories with switchstatements. Let’s explore a cleaner, extensible way using:

✅ Strategy Pattern
✅ Factory Pattern
✅ Dependency Injection via Dictionary<Enum, Func<T>>

This approach is minimal, testable, and elegant—especially if you want to add more strategies later with zero friction.


🧠 The Scenario

Suppose you're building an e-commerce platform that supports various discount types:

  • Percentage-based discount (e.g., 20% off)
  • Fixed amount discount (e.g., $10 off)
  • Buy One Get One Free (BOGO)

Each of these follows different logic, but they all return the final price.


🧱 Step 1: Define the Strategy Interface

public interface IDiscountStrategy
{
    decimal ApplyDiscount(decimal unitPrice, int quantity);
}

Enter fullscreen mode Exit fullscreen mode

🛠 Step 2: Implement Each Strategy

PercentageDiscount:

public class PercentageDiscount : IDiscountStrategy
{
    private readonly decimal _percentage = 0.2m;

    public decimal ApplyDiscount(decimal unitPrice, int quantity)
    {
        var total = unitPrice * quantity;
        return total - (total * _percentage);
    }
}

Enter fullscreen mode Exit fullscreen mode

FixedAmountDiscount:

public class FixedAmountDiscount : IDiscountStrategy
{
    private readonly decimal _amount = 10m;

    public decimal ApplyDiscount(decimal unitPrice, int quantity)
    {
        var total = unitPrice * quantity;
        return total - _amount;
    }
}

Enter fullscreen mode Exit fullscreen mode

BuyOneGetOneDiscount:

public class BuyOneGetOneDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal unitPrice, int quantity)
    {
        var payableQuantity = (quantity / 2) + (quantity % 2);
        return unitPrice * payableQuantity;
    }
}

Enter fullscreen mode Exit fullscreen mode

🏭 Step 4: Implement the Factory

Instead of injecting IServiceProvider, we inject a dictionary of factory functions. This keeps your factory clean and testable.

IDiscountFactory:

public interface IDiscountFactory
{
    IDiscountStrategy GetStrategy(DiscountType type);
}

Enter fullscreen mode Exit fullscreen mode

DiscountFactory (with DI Dictionary)

public class DiscountFactory : IDiscountFactory
{
    private readonly IReadOnlyDictionary<DiscountType, Func<IDiscountStrategy>> _strategyResolvers;

    public DiscountFactory(IReadOnlyDictionary<DiscountType, Func<IDiscountStrategy>> strategyResolvers)
    {
        _strategyResolvers = strategyResolvers;
    }

    public IDiscountStrategy GetStrategy(DiscountType type)
    {
        if (_strategyResolvers.TryGetValue(type, out var resolver))
        {
            return resolver();
        }

        throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported discount type: {type}");
    }
}

Enter fullscreen mode Exit fullscreen mode

💼 Step 5: Define the Purchase Service Interface

public interface IPurchaseService
{
    decimal CalculateFinalPrice(decimal unitPrice, int quantity, DiscountType discountType);
}

Enter fullscreen mode Exit fullscreen mode

💳 Step 6: Implement PurchaseService Using the Factory

public class PurchaseService : IPurchaseService
{
    private readonly IDiscountFactory _discountFactory;

    public PurchaseService(IDiscountFactory discountFactory)
    {
        _discountFactory = discountFactory;
    }

    public decimal CalculateFinalPrice(decimal unitPrice, int quantity, DiscountType discountType)
    {
        var strategy = _discountFactory.GetStrategy(discountType);
        return strategy.ApplyDiscount(unitPrice, quantity);
    }
}

Enter fullscreen mode Exit fullscreen mode

🧩 Step 7: Register Everything in Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<PercentageDiscount>();
builder.Services.AddScoped<FixedAmountDiscount>();
builder.Services.AddScoped<BuyOneGetOneDiscount>();

builder.Services.AddScoped<IReadOnlyDictionary<DiscountType, Func<IDiscountStrategy>>>(sp =>
    new Dictionary<DiscountType, Func<IDiscountStrategy>>
    {
        [DiscountType.Percentage] = () => sp.GetRequiredService<PercentageDiscount>(),
        [DiscountType.FixedAmount] = () => sp.GetRequiredService<FixedAmountDiscount>(),
        [DiscountType.BuyOneGetOne] = () => sp.GetRequiredService<BuyOneGetOneDiscount>()
    });

builder.Services.AddScoped<IDiscountFactory, DiscountFactory>();
builder.Services.AddScoped<IPurchaseService, PurchaseService>();

var app = builder.Build();

Enter fullscreen mode Exit fullscreen mode

🧪 Step 8: Real-World Usage (e.g., Controller or Console Output)

Let’s demonstrate this in Program.cs using console output (ideal for early testing or debugging):

using var scope = app.Services.CreateScope();
var purchaseService = scope.ServiceProvider.GetRequiredService<IPurchaseService>();

Console.WriteLine("=== DISCOUNT CALCULATOR ===");

var price = purchaseService.CalculateFinalPrice(100m, 3, DiscountType.BuyOneGetOne);
Console.WriteLine($"Buy 3 items at $100 each with BOGO => Final price: {price:C}");

price = purchaseService.CalculateFinalPrice(100m, 3, DiscountType.Percentage);
Console.WriteLine($"Buy 3 items at $100 each with 20% discount => Final price: {price:C}");

price = purchaseService.CalculateFinalPrice(100m, 3, DiscountType.FixedAmount);
Console.WriteLine($"Buy 3 items at $100 each with $10 off => Final price: {price:C}");

app.Run();

Enter fullscreen mode Exit fullscreen mode

✅ Final Thoughts

This architecture blends clean code, testability, and maintainability. You gain the flexibility to plug in new discount logic without touching the core business service or polluting the factory with conditionals.

You can extend this pattern to other domains like:

  • Shipping methods
  • Tax strategies
  • Report generators
  • Notification channels

Comments 0 total

    Add comment