DEV Community

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

Posted on

1

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

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

What if your devs could deploy infrastructure like launching a game?

What if your devs could deploy infrastructure like launching a game?

In this session, we'll show how you can build a user-friendly self-service portal for deploying infrastructure with Spacelift, in the flavor of deploying Minecraft servers.

Learn More

Top comments (0)

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server 🏁

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

👋 Kindness is contagious

Dive into this thoughtful piece, beloved in the supportive DEV Community. Coders of every background are invited to share and elevate our collective know-how.

A sincere "thank you" can brighten someone's day—leave your appreciation below!

On DEV, sharing knowledge smooths our journey and tightens our community bonds. Enjoyed this? A quick thank you to the author is hugely appreciated.

Okay