The Strategy Pattern is a staple of object-oriented design. It lets you encapsulate a family of algorithms and make them interchangeable at runtime. However, traditional implementations often suffer from heavy interface hierarchies and rigid factory logic.
In modern C#, language features like switch expressions, primary constructors, collection expressions, and collectible AssemblyLoadContext allow us to build clean, extensible strategy pipelines with zero unnecessary boilerplate.
In this article, we'll walk through building a flexible shipping rate calculator that supports:
- Multiple Carrier Strategies (DHL, FedEx, UPS).
- Customer Tier Rules (VIP, Corporate, Standard) via pattern matching.
-
Dynamic Plugin Architecture to load new strategies from external
.dllfiles without recompiling. - Zero-Allocation Execution for performance-critical hot paths.
You can find the complete source code on GitHub: stevsharp/Shipping-strategy-demo.
1. Domain Contracts & Core Models
We start by defining clean, immutable records for our domain model and a clear IShippingStrategy interface.
namespace Shipping.Contracts;
public enum CustomerType { Standard, Vip, Corporate }
public record Customer(string Id, string Name, CustomerType Type, int LoyaltyPoints);
public record Order(string OrderId, decimal Subtotal, decimal WeightKg, Customer Customer);
public interface IShippingStrategy
{
string ProviderName { get; }
decimal CalculateCost(Order order);
}
2. Encapsulating Carrier Strategies with Pattern Matching
Each strategy encapsulates carrier-specific pricing. Instead of cluttering our algorithms with if/else checks for customer ranks, we leverage C# switch expressions over the Customer object.
DHL Express Strategy
public class DhlShippingStrategy : IShippingStrategy
{
public string ProviderName => "DHL Express";
public decimal CalculateCost(Order order)
{
// DHL Base International Rate
decimal baseRate = 25.00m + (order.WeightKg * 3.50m);
return order.Customer switch
{
// VIPs get priority express at a 40% discount
{ Type: CustomerType.Vip } => baseRate * 0.60m,
// Corporate accounts get a flat $15 discount
{ Type: CustomerType.Corporate } => Math.Max(5.00m, baseRate - 15.00m),
// Standard customers with high loyalty points get $5 off
{ LoyaltyPoints: > 500 } => baseRate - 5.00m,
_ => baseRate
};
}
}
FedEx & UPS Implementations
public class FedExShippingStrategy : IShippingStrategy
{
public string ProviderName => "FedEx";
public decimal CalculateCost(Order order)
{
decimal baseRate = 15.00m + (order.WeightKg * 2.50m);
return order.Customer switch
{
{ Type: CustomerType.Vip } => baseRate * 0.50m,
{ Type: CustomerType.Corporate } => baseRate * 0.80m,
_ => baseRate
};
}
}
public class UpsShippingStrategy : IShippingStrategy
{
public string ProviderName => "UPS Ground";
public decimal CalculateCost(Order order)
{
decimal baseRate = 12.00m + (order.WeightKg * 2.00m);
return order.Customer switch
{
{ Type: CustomerType.Vip } => baseRate * 0.70m,
_ => baseRate
};
}
}
3. Dynamic Strategy Factory & Plugin Loader
To keep our host application fully compliant with the Open/Closed Principle, we combine our strategies with a factory capable of scanning an external /plugins directory and instantiating .dll strategies at runtime using AssemblyLoadContext.
using System.Reflection;
using System.Runtime.Loader;
using Shipping.Contracts;
public class ShippingStrategyFactory
{
private readonly Dictionary<string, IShippingStrategy> _strategies = new(StringComparer.OrdinalIgnoreCase);
public ShippingStrategyFactory()
{
// Register built-in strategies
Register(new DhlShippingStrategy());
Register(new FedExShippingStrategy());
Register(new UpsShippingStrategy());
}
public void Register(IShippingStrategy strategy)
{
_strategies[strategy.ProviderName] = strategy;
}
public IShippingStrategy GetStrategy(string providerKey)
{
return _strategies.GetValueOrDefault(providerKey)
?? throw new NotSupportedException($"Shipping provider '{providerKey}' is not supported.");
}
/// <summary>
/// Scans a folder for external strategy DLLs and registers them dynamically.
/// </summary>
public void LoadExternalPlugins(string pluginsFolderPath)
{
if (!Directory.Exists(pluginsFolderPath)) return;
foreach (string dllPath in Directory.GetFiles(pluginsFolderPath, "*.dll"))
{
var loadContext = new AssemblyLoadContext(dllPath, isCollectible: true);
Assembly assembly = loadContext.LoadFromAssemblyPath(dllPath);
foreach (Type type in assembly.GetTypes())
{
if (typeof(IShippingStrategy).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface)
{
if (Activator.CreateInstance(type) is IShippingStrategy pluginStrategy)
{
Register(pluginStrategy);
Console.WriteLine($"[Plugin Loaded]: {pluginStrategy.ProviderName}");
}
}
}
}
}
public IEnumerable<string> AvailableProviders => _strategies.Keys;
}
4. Zero-Allocation Hot-Path Variant
In high-throughput microservices or financial engines, object allocations add GC pressure. For those hot paths, you can express the strategy directly using a stack-allocated readonly ref struct:
public readonly ref struct FastPricingContext
{
public required decimal Subtotal { get; init; }
public required Customer Customer { get; init; }
}
public static class FastPricingStrategy
{
public static decimal Calculate(in FastPricingContext ctx) => ctx switch
{
{ Customer.Type: CustomerType.Vip, Subtotal: > 500m } => ctx.Subtotal * 0.70m,
{ Customer.Type: CustomerType.Vip } => ctx.Subtotal * 0.80m,
{ Customer.Type: CustomerType.Corporate } => ctx.Subtotal * 0.85m,
_ => ctx.Subtotal
};
}
5. Putting It All Together
public static class Program
{
public static void Main()
{
var vipCustomer = new Customer("C-101", "Alice", CustomerType.Vip, LoyaltyPoints: 1200);
var order = new Order("ORD-9901", Subtotal: 200.00m, WeightKg: 4.5m, Customer: vipCustomer);
var factory = new ShippingStrategyFactory();
// Resolve and calculate using DHL Strategy
IShippingStrategy dhl = factory.GetStrategy("DHL Express");
decimal dhlCost = dhl.CalculateCost(order);
Console.WriteLine($"[DHL Express] Final Cost: ${dhlCost:F2}");
// Resolve and calculate using FedEx Strategy
IShippingStrategy fedEx = factory.GetStrategy("FedEx");
decimal fedExCost = fedEx.CalculateCost(order);
Console.WriteLine($"[FedEx] Final Cost: ${fedExCost:F2}");
}
}
Summary Matrix
| Technique | Primary Advantage | Memory Overhead | Use Case |
|---|---|---|---|
| Context Strategy Class | Highly readable, unit-testable | Standard heap allocations | Typical enterprise logic |
Plugin Factory (DLL) |
Hot-swappable providers without re-compiling | Low assembly metadata cost | Extensible SaaS / Monoliths |
ref struct Strategy |
Zero heap allocations | Zero GC cost | High-frequency / Game engines |
References & Further Reading
- 📁 Source Code Repository: stevsharp/Shipping-strategy-demo on GitHub
- 📖 Microsoft Docs: Pattern matching in C#
- 📖 Microsoft Docs: AssemblyLoadContext in .NET
- 📘 Refactoring Guru: Strategy Design Pattern Overview
Top comments (0)