Leveraging Dependency Injection with the Strategy Pattern and Factory Method
Elevate Your .NET Core Applications...
In this blog, we’ll explore how you can enhance your .NET Core applications by combining Dependency Injection (DI) with design patterns like the Strategy Pattern and Factory Method. This approach not only improves the flexibility and scalability of your code but also fosters better maintainability and testability. We’ll walk you through step-by-step, using simple and practical examples, so you can start implementing these patterns today!
🚀 Why Dependency Injection?
Before diving into the patterns, let’s recap why Dependency Injection is such a core part of .NET Core architecture.
Loose Coupling: By injecting dependencies rather than hard-coding them, your application becomes more modular.
Easier Testing: Mocking dependencies becomes much simpler, facilitating more robust unit testing.
Better Extensibility: Easily swap out implementations, especially when combining DI with design patterns.
However, what happens when you need to handle complex logic like selecting strategies dynamically or constructing objects with varied configurations? That’s where the Strategy Pattern and Factory Method shine!
🎯 Scenario: Dynamic Pricing Calculation for an E-Commerce System
Imagine you’re building an e-commerce application where different products require different pricing strategies. For example, some products have discounts, others have surge pricing, and some use seasonal pricing. Instead of having a large switch-case block that is difficult to maintain, let’s implement a clean, extensible solution using Dependency Injection, Strategy Pattern, and Factory Method.
🛠 Step 1: Define the Strategy Interface
We’ll start by defining an interface, IPricingStrategy
, which will represent different pricing strategies.
public interface IPricingStrategy
{
decimal CalculatePrice(decimal basePrice);
}
Each concrete class will implement its own pricing logic based on the strategy it represents.
🏗 Step 2: Implement Concrete Strategies
Let’s create different pricing strategies that implement IPricingStrategy
.
public class DiscountPricingStrategy : IPricingStrategy
{
public decimal CalculatePrice(decimal basePrice)
{
return basePrice * 0.9m; // 10% discount
}
}
public class SurgePricingStrategy : IPricingStrategy
{
public decimal CalculatePrice(decimal basePrice)
{
return basePrice * 1.5m; // 50% surge
}
}
public class SeasonalPricingStrategy : IPricingStrategy
{
public decimal CalculatePrice(decimal basePrice)
{
return basePrice * 0.8m; // 20% seasonal discount
}
}
Each strategy focuses solely on its specific price calculation logic. This keeps the code clean and modular.
🔄 Step 3: Create the Strategy Factory
Here’s where the Factory Method comes into play. We’ll create a PricingStrategyFactory
that will decide which strategy to instantiate based on some conditions or input.
public interface IPricingStrategyFactory
{
IPricingStrategy GetPricingStrategy(string strategyType);
}
public class PricingStrategyFactory : IPricingStrategyFactory
{
private readonly IServiceProvider _serviceProvider;
public PricingStrategyFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IPricingStrategy GetPricingStrategy(string strategyType)
{
return strategyType switch
{
"Discount" => _serviceProvider.GetService<DiscountPricingStrategy>(),
"Surge" => _serviceProvider.GetService<SurgePricingStrategy>(),
"Seasonal" => _serviceProvider.GetService<SeasonalPricingStrategy>(),
_ => throw new NotImplementedException($"Strategy {strategyType} not implemented.")
};
}
}
📦 Step 4: Register Strategies and Factory in DI Container
In your Startup.cs
or Program.cs
, register the strategies and factory with the DI container.
public void ConfigureServices(IServiceCollection services)
{
// Register concrete strategies
services.AddTransient<DiscountPricingStrategy>();
services.AddTransient<SurgePricingStrategy>();
services.AddTransient<SeasonalPricingStrategy>();
// Register factory
services.AddSingleton<IPricingStrategyFactory, PricingStrategyFactory>();
services.AddControllers();
}
⚙️ Step 5: Inject and Use the Strategy in Your Controller
Let’s see how you can use this in a real scenario. In your controller, you can inject the factory and select the strategy dynamically based on input.
[ApiController]
[Route("api/[controller]")]
public class PricingController : ControllerBase
{
private readonly IPricingStrategyFactory _pricingStrategyFactory;
public PricingController(IPricingStrategyFactory pricingStrategyFactory)
{
_pricingStrategyFactory = pricingStrategyFactory;
}
[HttpGet("calculate")]
public IActionResult CalculatePrice(string strategyType, decimal basePrice)
{
var strategy = _pricingStrategyFactory.GetPricingStrategy(strategyType);
var finalPrice = strategy.CalculatePrice(basePrice);
return Ok(new { Strategy = strategyType, Price = finalPrice });
}
}
When the CalculatePrice
method is called, the pricing strategy will be selected dynamically, allowing different pricing logic without modifying the controller or the core logic.
🔑 Key Benefits of Combining DI with Strategy and Factory Patterns
Flexibility: Easily add new pricing strategies without touching existing code.
Testability: Each strategy can be tested individually and mocked during unit testing.
Separation of Concerns: Business logic related to pricing is encapsulated within strategy classes, keeping the controller light and focused on handling requests.
Decoupling: By using
IServiceProvider
with Dependency Injection, the controller has no direct knowledge of concrete strategy implementations.
🧑💻 Testing the Implementation
Now, let’s test how the system dynamically handles pricing strategies.
For example:
/api/pricing/calculate?strategyType=Discount&basePrice=100
will return90.00
with a 10% discount./api/pricing/calculate?strategyType=Surge&basePrice=100
will return150.00
with a 50% surge pricing.
🎉 Wrapping It Up
By combining Dependency Injection with the Strategy Pattern and Factory Method, you unlock a flexible and scalable architecture for handling complex logic. This approach allows you to dynamically select strategies, swap out implementations easily, and keep your code clean, testable, and maintainable.
🌟 Ready to Implement?
Give your .NET Core applications a boost by leveraging this powerful combination of design patterns and DI. Whether you're working on pricing, logging, or other dynamic behaviours, this approach ensures your code stays clean and adaptable as your system evolves.
Do you have a real-world use case in mind where this architecture could shine? Comment below and let’s discuss how we can optimize it for your specific needs!