Intermediate

C-Sharp Design Patterns Strategy

This course focuses on the Strategy Pattern, one of the most commonly used design patterns. The course is designed for C# developers who want to understand how this pattern works and how...

Table of Contents

  1. Course Overview
  2. Introduction to Pattern Strategy
  1. First look: Sales tax calculation
  1. Alternative approach: Pass strategy as method parameter
  2. Example: Creating invoices (Invoice)
  1. Example: Different carriers (Shipping Providers)
  1. Existing implementation in .NET: Array.Sort
  1. Complete project structure (Demo 5)
  2. Advantages and trade offs of Pattern Strategy
  3. Summary and conclusion

1. Course Overview

This course focuses on the Strategy Pattern, one of the most commonly used design patterns. The course is designed for C# developers who want to understand how this pattern works and how to apply it in their .NET applications.

Course objectives

At the end of this course, you will be able to:

  • Understand what the Strategy Pattern is and what its characteristics are
  • Identify the advantages and trade-offs of using this pattern
  • Implement the Strategy Pattern in new and existing solutions
  • Identify and leverage existing implementations of the pattern in the .NET framework, in your own applications, and in third-party NuGet packages

Prerequisites

  • Be familiar with C# syntax
  • Know how to build and run .NET applications
  • No prior knowledge of design patterns is required

2. Introduction to Pattern Strategy

The Strategy Pattern is probably the pattern you will encounter most often in your career as a developer. It is crucially important to know how to apply it, how to identify it and how to exploit it in your applications.

This pattern belongs to the family of behavioral design patterns (behavioral design patterns). It defines a family of algorithms, encapsulates each of them and makes them interchangeable. The Strategy Pattern allows you to change the algorithm of an object independently of the clients that use it.

2.1 The three characteristics of the pattern

The Strategy Pattern is recognized by three fundamental components:

┌──────────────────────────────────────────────────┐
│                   CONTEXT                        │
│  (ex : Order)                                    │
│                                                  │
│  - Contient une référence à IStrategy            │
│  - Délègue l'exécution à la stratégie active     │
└────────────────────┬─────────────────────────────┘
                     │ référence
                     ▼
┌──────────────────────────────────────────────────┐
│              << interface >>                     │
│                 IStrategy                        │
│                                                  │
│  + ExecuteAlgorithm(...)                         │
└────────────────────┬─────────────────────────────┘
                     │ implémente
          ┌──────────┼──────────┐
          ▼          ▼          ▼
  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
  │ConcreteStratA│  │ConcreteStratB│  │ConcreteStratC│
  │              │  │              │  │              │
  │ + Execute()  │  │ + Execute()  │  │ + Execute()  │
  └──────────────┘  └──────────────┘  └──────────────┘
ComponentRoleExample in the course
BackgroundClass that has a reference to the active policy. She delegates work to this strategy without knowing the implementation details.Order
Strategy (interface)Contract defining how to interact with any concrete strategy.ISalesTaxStrategy, IInvoiceStrategy, IShippingStrategy
Concrete StrategyConcrete implementation of a particular algorithm.SwedenSalesTaxStrategy, USAStateSalesTaxStrategy

2.2 Concrete examples in the real world

Example 1 — Calculating sales tax

The context is the order (Order). It has a reference to ISalesTaxStrategy. It calls GetTax() on the strategy without worrying about the internal logic.

The interface ISalesTaxStrategy exposes the GetTaxFor(Order order) method.

The concrete strategies are:

  • SwedenSalesTaxStrategy — calculates Swedish VAT (25% overall, or by item type)
  • USAStateSalesTaxStrategy — calculates tax based on US state

Calculating sales tax in the United States is very different from calculating VAT in Sweden. It therefore makes sense to decouple these two algorithms from the context. This is one of the main goals of any pattern: to make applications more extensible, easier to maintain, and more testable.

Example 2 — Invoice creation

Context Order has a reference to IInvoiceStrategy. The interface exposes Generate(Order order).

The concrete strategies allow the invoice to be delivered by:

  • Email (EmailInvoiceStrategy)
  • Text file on disk (FileInvoiceStrategy)
  • Print On Demand Service (PrintOnDemandInvoiceStrategy)

The selection of the strategy is done at execution (runtime), based on the customer’s preferences at the time of the order. The context does not need to know about the PDF, email, or print implementation. He just needs to know that the active strategy will allow him to generate an invoice.

Key Principle: The policy can be selected at runtime based on user input, without changing the context class.


3. First Look: Calculating Sales Tax

3.1 Initial problem — coupled code

The demo application is a console application simulating an order processing system. The objective is to calculate the tax on an order taking into account the destination country.

Here is the initial problem: all the tax calculation logic is coupled directly into the GetTax() method of the Order class. This approach has several flaws:

// AVANT REFACTORISATION — Code couplé dans Order
public decimal GetTax()
{
    var destination = ShippingDetails.DestinationCountry.ToLowerInvariant();

    if (destination == "sweden")
    {
        if (destination == ShippingDetails.OriginCountry.ToLowerInvariant())
        {
            return TotalPrice * 0.25m;
        }
        return 0;
    }

    if (destination == "us")
    {
        switch (ShippingDetails.DestinationState.ToLowerInvariant())
        {
            case "la":  return TotalPrice * 0.095m;
            case "ny":  return TotalPrice * 0.04m;
            case "nyc": return TotalPrice * 0.045m;
            default:    return 0m;
        }
    }

    return 0m;
}

Problems with this approach:

  • Method contains all logic for Sweden AND USA
  • Adding a new country requires modifying the Order class
  • It is difficult to write tests for each calculation individually
  • The code becomes longer and longer as you add countries
  • Class Order violates the Single Responsibility Principle (SRP)

3.2 Step 1 — Create the ISalesTaxStrategy interface

The first step is to define the contract (the interface) for all sales tax policies. We create a Strategies/SalesTax folder in the business layer.

// Business/Strategies/SalesTax/ISalesTaxStrategy.cs
using Strategy_Pattern_First_Look.Business.Models;

namespace Strategy_Pattern_First_Look.Business.Strategies.SalesTax
{
    public interface ISalesTaxStrategy
    {
        public decimal GetTaxFor(Order order);
    }
}

This interface requires any concrete implementation to expose a GetTaxFor method which takes a command as a parameter and returns a decimal amount.

3.3 Step 2 — Implement concrete strategies

We extract the calculation code from Order to dedicated classes.

SwedenSalesTaxStrategy

// Business/Strategies/SalesTax/SwedenSalesTaxStrategy.cs
using Strategy_Pattern_First_Look.Business.Models;

namespace Strategy_Pattern_First_Look.Business.Strategies.SalesTax
{
    public class SwedenSalesTaxStrategy : ISalesTaxStrategy
    {
        public decimal GetTaxFor(Order order)
        {
            var destination = order.ShippingDetails.DestinationCountry.ToLowerInvariant();
            var totalPrice = order.TotalPrice;

            if (destination == order.ShippingDetails.OriginCountry.ToLowerInvariant())
            {
                return totalPrice * 0.25m;
            }

            return 0;
        }

        // Version améliorée — taux différenciés par type d'article
        public decimal GetTaxForBetter(Order order)
        {
            decimal totalTax = 0m;

            foreach (var item in order.LineItems)
            {
                switch (item.Key.ItemType)
                {
                    case ItemType.Food:
                        totalTax += (item.Key.Price * 0.06m) * item.Value;
                        break;

                    case ItemType.Literature:
                        totalTax += (item.Key.Price * 0.08m) * item.Value;
                        break;

                    case ItemType.Service:
                    case ItemType.Hardware:
                        totalTax += (item.Key.Price * 0.25m) * item.Value;
                        break;
                }
            }

            return totalTax;
        }
    }
}

VAT rate in Sweden:

  • Power supply: 6%
  • Literature: 8%
  • Services and equipment: 25%

USAStateSalesTaxStrategy

// Business/Strategies/SalesTax/USAStateSalesTaxStrategy.cs
using Strategy_Pattern_First_Look.Business.Models;

namespace Strategy_Pattern_First_Look.Business.Strategies.SalesTax
{
    public class USAStateSalesTaxStrategy : ISalesTaxStrategy
    {
        public decimal GetTaxFor(Order order)
        {
            var totalPrice = order.TotalPrice;

            switch (order.ShippingDetails.DestinationState.ToLowerInvariant())
            {
                case "la":  return totalPrice * 0.095m;
                case "ny":  return totalPrice * 0.04m;
                case "nyc": return totalPrice * 0.045m;
                default:    return 0m;
            }
        }
    }
}

Tax rate by state (USA):

  • Los Angeles (LA): 9.5%
  • New York (NY): 4%
  • New York City (NYC): 4.5%

3.4 Step 3 — Refactor Context (Order)

The Order class now exposes a property of type ISalesTaxStrategy. Its GetTax() method delegates the calculation to the active strategy:

// Business/Models/Order.cs — après refactorisation
public class Order
{
    public Dictionary<Item, int> LineItems { get; } = new Dictionary<Item, int>();
    public IList<Payment> SelectedPayments { get; } = new List<Payment>();
    public IList<Payment> FinalizedPayments { get; } = new List<Payment>();

    public decimal AmountDue => TotalPrice - FinalizedPayments.Sum(payment => payment.Amount);
    public decimal TotalPrice => LineItems.Sum(item => item.Key.Price * item.Value);

    public ShippingStatus ShippingStatus { get; set; } = ShippingStatus.WaitingForPayment;
    public ShippingDetails ShippingDetails { get; set; }

    // Propriété exposant la stratégie — le cœur du pattern
    public ISalesTaxStrategy SalesTaxStrategy { get; set; }

    public decimal GetTax()
    {
        if (SalesTaxStrategy == null)
        {
            return 0m; // ou throw new InvalidOperationException()
        }

        return SalesTaxStrategy.GetTaxFor(this);
    }
}

Important point: The Order class knows nothing about calculation logic. She delegates this responsibility entirely to the strategy assigned to her. It is the total decoupling between context and concrete implementations.

3.5 Step 4 — Connect everything in Program

At the entry point of the application, we assign the appropriate strategy depending on the destination of the command:

// Program.cs — après refactorisation
using Strategy_Pattern_First_Look.Business.Models;
using Strategy_Pattern_First_Look.Business.Strategies.SalesTax;
using System;

namespace Strategy_Pattern_First_Look
{
    class Program
    {
        static void Main(string[] args)
        {
            var order = new Order
            {
                ShippingDetails = new ShippingDetails
                {
                    OriginCountry = "Sweden",
                    DestinationCountry = "Sweden"
                },
                // Sélection de la stratégie basée sur la destination
                SalesTaxStrategy = new SwedenSalesTaxStrategy()
            };

            // Ajout d'articles de différents types
            order.LineItems.Add(
                new Item("CSHARP_SMORGASBORD", "C# Smorgasbord", 100m, ItemType.Literature), 1);

            Console.WriteLine($"Total taxe : {order.GetTax()} SEK");
        }
    }
}

Result: With SwedenSalesTaxStrategy (rate per item type), the total tax is 33 Swedish crowns instead of 50 with the fixed rate of 25%.

3.6 Results obtained after refactoring

By applying the Strategy Pattern, we obtained:

BeforeAfter
Calculation logic coupled in OrderEach strategy is in its own class
Difficult to test (everything is mixed)Each strategy can be tested in isolation
Changing Sweden’s logic risks affecting the USAPolicy-isolated changes
Add country = modify OrderAdd a country = create a new class
Order code grows indefinitelyOrder keeps it simple and clean

Key Benefits:

  • Extensibility: you can add new strategies without modifying existing ones
  • Single responsibility: each strategy is responsible for a single algorithm
  • Testability: we can test SwedenSalesTaxStrategy independently of USAStateSalesTaxStrategy
  • Decoupling: the Order context class is decoupled from concrete implementations

4. Alternative approach: Pass the strategy as a method parameter

So far the strategy is exposed as a property on the context (Order.SalesTaxStrategy). The Strategy pattern also offers an alternative approach: pass the strategy directly into method parameter.

public decimal GetTax(ISalesTaxStrategy strategy = null)
{
    // Utilise la stratégie passée en paramètre, ou celle définie sur la propriété
    var activeStrategy = strategy ?? SalesTaxStrategy;

    if (activeStrategy == null)
    {
        return 0m;
    }

    return activeStrategy.GetTaxFor(this);
}

Usage:

// Avec la stratégie par défaut (définie sur la propriété)
var tax1 = order.GetTax();

// En passant une stratégie différente pour ce calcul spécifique
var tax2 = order.GetTax(new SwedenSalesTaxStrategy());
var tax3 = order.GetTax(new USAStateSalesTaxStrategy());

Important implication: This approach demonstrates that each time a method accepts an interface as a parameter, it essentially exploits the Strategy Pattern. This means that you do not have to name your strategies with the suffix “Strategy” to exploit this pattern. You probably already use it in your applications without knowing it.

Conclusion: This pattern is extremely powerful because it allows you to change the behavior of the Order object for tax calculation at any time during runtime, depending on the behavior of the application.


5. Example: Creating invoices (Invoice)

This example illustrates how to introduce the Strategy Pattern from the beginning for a new feature, rather than refactoring existing code.

The goal is to allow the application to send invoices in different ways:

  • By email (EmailInvoiceStrategy)
  • As file on disk (FileInvoiceStrategy)
  • Via a print-on-demand service (PrintOnDemandInvoiceStrategy)

Best practice: Introducing the Strategy Pattern from the start of a new feature facilitates future extension. In 6 months, it will be easy to add new delivery methods (SMS, push notification, etc.) without changing existing code.

5.1 IInvoiceStrategy Interface

// Business/Strategies/Invoice/IInvoiceStrategy.cs
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;

namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
    public interface IInvoiceStrategy
    {
        public void Generate(Order order);
    }
}

The Generate method returns nothing (void) because depending on the implementation, it can send an email, call an API or write to the file system — no return is necessary.

5.2 InvoiceStrategy base abstract class

All billing policies share a common functionality: generating invoice text. We therefore create a basic abstract class to avoid code duplication.

// Business/Strategies/Invoice/InvoiceStrategy.cs
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
using System;

namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
    public abstract class InvoiceStrategy : IInvoiceStrategy
    {
        // Méthode abstraite — chaque sous-classe doit l'implémenter
        public abstract void Generate(Order order);

        // Fonctionnalité partagée : génère la représentation textuelle de la facture
        public string GenerateTextInvoice(Order order)
        {
            var invoice = $"INVOICE DATE: {DateTimeOffset.Now}{Environment.NewLine}{Environment.NewLine}";

            invoice += $"ID|NAME|PRICE|QUANTITY{Environment.NewLine}";

            foreach (var item in order.LineItems)
            {
                invoice += $"{item.Key.Id}|{item.Key.Name}|{item.Key.Price}|{item.Value}{Environment.NewLine}";
            }

            invoice += Environment.NewLine + Environment.NewLine;

            var tax = order.GetTax();    // Utilise la SalesTaxStrategy de l'Order !
            var total = order.TotalPrice + tax;

            invoice += $"TAX TOTAL: {tax}{Environment.NewLine}";
            invoice += $"TOTAL: {total}{Environment.NewLine}";

            return invoice;
        }
    }
}

Interesting observation: The GenerateTextInvoice method calls order.GetTax(), which itself uses the SalesTaxStrategy. We therefore have an aggregation of strategies: the invoicing strategy depends indirectly on the tax strategy.

Hierarchy structure:

IInvoiceStrategy (interface)
    └── InvoiceStrategy (abstract class — partage GenerateTextInvoice)
            ├── EmailInvoiceStrategy
            └── FileInvoiceStrategy

IInvoiceStrategy (interface)
    └── PrintOnDemandInvoiceStrategy (implémente directement l'interface — pas de texte)

5.3 Concrete billing strategies

EmailInvoiceStrategy

Send the invoice by email via SmtpClient (here with SendGrid):

// Business/Strategies/Invoice/EmailInvoiceStrategy.cs
using System;
using System.Net;
using System.Net.Mail;
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;

namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
    public class EmailInvoiceStrategy : InvoiceStrategy
    {
        public override void Generate(Order order)
        {
            using (SmtpClient client = new SmtpClient("smtp.sendgrid.net", 587))
            {
                NetworkCredential credentials = new NetworkCredential("USERNAME", "PASSWORD");
                client.Credentials = credentials;

                MailMessage mail = new MailMessage("YOUR EMAIL", "YOUR EMAIL")
                {
                    Subject = "We've created an invoice for your order",
                    Body = GenerateTextInvoice(order)  // Utilise la méthode de la classe de base
                };

                client.Send(mail);

                Console.WriteLine("Invoice for order sent over e-mail");
            }
        }
    }
}

FileInvoiceStrategy

Writes the invoice to a text file on local disk:

// Business/Strategies/Invoice/FileInvoiceStrategy.cs
using System;
using System.IO;
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;

namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
    public class FileInvoiceStrategy : InvoiceStrategy
    {
        public override void Generate(Order order)
        {
            var filename = $"invoice_{Guid.NewGuid()}.txt";
            using (var stream = new StreamWriter(filename))
            {
                stream.Write(GenerateTextInvoice(order));  // Utilise la méthode de la classe de base

                stream.Flush();

                Console.WriteLine($"Invoice for order saved to {filename}");
            }
        }
    }
}

PrintOnDemandInvoiceStrategy

Calls an external HTTP API to print and mail the invoice. This strategy does not inherit from the abstract class InvoiceStrategy because it does not need the textual representation — it sends the serialized object directly to the API:

// Business/Strategies/Invoice/PrintOnDemandInvoiceStrategy.cs
using System;
using System.Net.Http;
using Newtonsoft.Json;
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;

namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
    public class PrintOnDemandInvoiceStrategy : IInvoiceStrategy
    {
        public void Generate(Order order)
        {
            using (var client = new HttpClient())
            {
                var content = JsonConvert.SerializeObject(order);

                client.BaseAddress = new Uri("https://pluralsight.com");

                client.PostAsync("/print-on-demand", new StringContent(content));

                Console.WriteLine($"Invoice sent for printing");
            }
        }
    }
}

Note: The /print-on-demand API is purely fictitious in the context of this course. It illustrates the concept of integration with an external “print-on-demand” service.

5.4 Integration into the order (Order)

The Order class now exposes an IInvoiceStrategy property and a FinalizeOrder() method:

// Dans Order.cs — ajout de la stratégie de facturation
public IInvoiceStrategy InvoiceStrategy { get; set; }

public void FinalizeOrder()
{
    if (SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice) &&
        AmountDue > 0 &&
        ShippingStatus == ShippingStatus.WaitingForPayment)
    {
        // Génère la facture selon la stratégie définie
        InvoiceStrategy.Generate(this);

        ShippingStatus = ShippingStatus.ReadyForShippment;
    }
    else if (AmountDue > 0)
    {
        throw new Exception("Unable to finalize order");
    }
    // ... puis appelle la stratégie d'expédition
}

Use in program:

var order = new Order
{
    ShippingDetails = new ShippingDetails { ... },
    SalesTaxStrategy = new SwedenSalesTaxStrategy(),
    InvoiceStrategy = new FileInvoiceStrategy()  // ou EmailInvoiceStrategy, etc.
};

// Finalise la commande — génère la facture selon la stratégie
order.FinalizeOrder();

Future extensibility: Simply create a new class implementing IInvoiceStrategy to add a delivery method (e.g. SmsInvoiceStrategy). The Order class and all other strategies remain unchanged.


6. Example: Different carriers (Shipping Providers)

This example adds the management of different carriers. When an order is finalized, we want to ship it via the carrier chosen by the user: DHL, FedEx, PostNord (Swedish Post), UPS, or USPS (US Post).

6.1 IShippingStrategy Interface

// Business/Strategies/Shipping/IShippingStrategy.cs
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;

namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Shipping
{
    public interface IShippingStrategy
    {
        void Ship(Order order);
    }
}

6.2 Concrete shipping strategies

DhlShippingStrategy

public class DhlShippingStrategy : IShippingStrategy
{
    public void Ship(Order order)
    {
        using (var client = new HttpClient())
        {
            // TODO: Implement DHL Shipping Integration
            Console.WriteLine("Order is shipped with DHL");
        }
    }
}

FedexShippingStrategy

public class FedexShippingStrategy : IShippingStrategy
{
    public void Ship(Order order)
    {
        using (var client = new HttpClient())
        {
            // TODO: Implement Fedex Shipping Integration
            Console.WriteLine("Order is shipped with Fedex");
        }
    }
}

SwedishPostalServiceShippingStrategy (PostNord)

public class SwedishPostalServiceShippingStrategy : IShippingStrategy
{
    public void Ship(Order order)
    {
        using (var client = new HttpClient())
        {
            // TODO: Implement PostNord Shipping Integration
            Console.WriteLine("Order is shipped with PostNord");
        }
    }
}

UnitedStatesPostalServiceShippingStrategy (USPS)

public class UnitedStatesPostalServiceShippingStrategy : IShippingStrategy
{
    public void Ship(Order order)
    {
        using (var client = new HttpClient())
        {
            // TODO: Implement USPS Shipping Integration
            Console.WriteLine("Order is shipped with USPS");
        }
    }
}

UpsShippingStrategy

public class UpsShippingStrategy : IShippingStrategy
{
    public void Ship(Order order)
    {
        using (var client = new HttpClient())
        {
            // TODO: Implement UPS Shipping Integration
            Console.WriteLine("Order is shipped with UPS");
        }
    }
}

Observation: Each carrier communicates with a different API via HTTP. The Strategy Pattern is particularly beneficial here because each API has its own requirements (authentication, data format, package weight, etc.). These details remain encapsulated in each policy.

6.3 Final order — the three strategies combined

The Order class now exposes three strategies:

// Business/Models/Order.cs — version finale complète
public class Order
{
    public Dictionary<Item, int> LineItems { get; } = new Dictionary<Item, int>();
    public IList<Payment> SelectedPayments { get; } = new List<Payment>();
    public IList<Payment> FinalizedPayments { get; } = new List<Payment>();

    public decimal AmountDue => TotalPrice - FinalizedPayments.Sum(payment => payment.Amount);
    public decimal TotalPrice => LineItems.Sum(item => item.Key.Price * item.Value);

    public ShippingStatus ShippingStatus { get; set; } = ShippingStatus.WaitingForPayment;
    public ShippingDetails ShippingDetails { get; set; }

    // Les trois stratégies
    public ISalesTaxStrategy SalesTaxStrategy { get; set; }
    public IInvoiceStrategy InvoiceStrategy { get; set; }
    public IShippingStrategy ShippingStrategy { get; set; }

    public decimal GetTax()
    {
        if (SalesTaxStrategy == null)
        {
            return 0m;
        }
        return SalesTaxStrategy.GetTaxFor(this);
    }

    public void FinalizeOrder()
    {
        if (SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice) &&
            AmountDue > 0 &&
            ShippingStatus == ShippingStatus.WaitingForPayment)
        {
            InvoiceStrategy.Generate(this);
            ShippingStatus = ShippingStatus.ReadyForShippment;
        }
        else if (AmountDue > 0)
        {
            throw new Exception("Unable to finalize order");
        }

        // Expédition via la stratégie choisie
        ShippingStrategy.Ship(this);
    }
}

Associated data models:

public class ShippingDetails
{
    public string Receiver { get; set; }
    public string AddressLine1 { get; set; }
    public string AddressLine2 { get; set; }
    public string PostalCode { get; set; }
    public string DestinationCountry { get; set; }
    public string DestinationState { get; set; }
    public string OriginCountry { get; set; }
    public string OriginState { get; set; }
}

public enum ShippingStatus
{
    WaitingForPayment,
    ReadyForShippment,
    Shipped
}

public enum PaymentProvider
{
    Paypal,
    CreditCard,
    Invoice
}

public class Payment
{
    public decimal Amount { get; set; }
    public PaymentProvider PaymentProvider { get; set; }
}

public class Item
{
    public string Id { get; }
    public string Name { get; }
    public decimal Price { get; }
    public ItemType ItemType { get; set; }

    public Item(string id, string name, decimal price, ItemType type)
    {
        Id = id;
        Name = name;
        Price = price;
        ItemType = type;
    }
}

public enum ItemType
{
    Service,
    Food,
    Hardware,
    Literature
}

6.4 Program.cs — dynamic selection at runtime

In the final application (Demo 5), the program asks the user to choose their preferences and dynamically selects the appropriate strategies:

// Program.cs — sélection interactive des stratégies
// (version Array.Sort pour démonstration finale)
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
using Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice;
using Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.SalesTax;
using Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Shipping;
using System;
using System.Collections.Generic;

namespace Strategy_Pattern_Using_different_shipping_providers
{
    class Program
    {
        static void Main(string[] args)
        {
            // Scénario : sélection d'une stratégie selon l'origine
            var orders = new[] {
                new Order { ShippingDetails = new ShippingDetails { OriginCountry = "Sweden" } },
                new Order { ShippingDetails = new ShippingDetails { OriginCountry = "USA" } },
                new Order { ShippingDetails = new ShippingDetails { OriginCountry = "Sweden" } },
                new Order { ShippingDetails = new ShippingDetails { OriginCountry = "USA" } },
                new Order { ShippingDetails = new ShippingDetails { OriginCountry = "Singapore" } }
            };

            Print(orders);

            Console.WriteLine();
            Console.WriteLine("Sorting..");
            Console.WriteLine();

            // Array.Sort avec une stratégie de comparaison (IComparer)
            Array.Sort(orders, new OrderAmountComparer());

            Print(orders);
        }

        static void Print(IEnumerable<Order> orders)
        {
            foreach (var order in orders)
            {
                Console.WriteLine(order.ShippingDetails.OriginCountry);
            }
        }
    }
}

Strategy selection logic (interactive version):

// Sélection de la SalesTaxStrategy selon l'origine
ISalesTaxStrategy GetSalesTaxStrategy(string origin, string destination)
{
    if (origin.ToLower() == "sweden")
        return new SwedenSalesTaxStrategy();
    if (origin.ToLower() == "usa")
        return new USAStateSalesTaxStrategy();
    throw new NotSupportedException($"No strategy for country: {origin}");
}

// Sélection de l'InvoiceStrategy selon le choix utilisateur
IInvoiceStrategy GetInvoiceStrategy(int invoiceOption)
{
    switch (invoiceOption)
    {
        case 1: return new EmailInvoiceStrategy();
        case 2: return new FileInvoiceStrategy();
        case 3: return new PrintOnDemandInvoiceStrategy();
        default: throw new NotSupportedException($"Unknown option: {invoiceOption}");
    }
}

// Sélection de la ShippingStrategy selon le transporteur choisi
IShippingStrategy GetShippingStrategy(int provider)
{
    switch (provider)
    {
        case 1: return new SwedishPostalServiceShippingStrategy();  // PostNord
        case 2: return new DhlShippingStrategy();
        case 3: return new UnitedStatesPostalServiceShippingStrategy();  // USPS
        case 4: return new FedexShippingStrategy();
        case 5: return new UpsShippingStrategy();
        default: throw new NotSupportedException($"Unknown provider: {provider}");
    }
}

Execution example (Sweden → Sweden, PostNord, invoice on file):

Total tax: 25 SEK
Order is shipped with PostNord
Invoice for order saved to invoice_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.txt

7. Existing implementation in .NET: Array.Sort

The final module of the course illustrates that the Strategy Pattern is already present everywhere in the .NET framework. The example uses Array.Sort with an IComparer<T>.

7.1 IComparer<T> as comparison strategy

Array.Sort is a method that accepts an IComparer<T> as an optional parameter. This is precisely the Strategy Pattern: we inject a comparison strategy to modify the sorting behavior at runtime.

// Stratégie de comparaison par montant total
public class OrderAmountComparer : IComparer<Order>
{
    public int Compare(Order x, Order y)
    {
        var xTotal = x.TotalPrice;
        var yTotal = y.TotalPrice;

        if (xTotal == yTotal)   return 0;
        else if (xTotal > yTotal) return 1;
        return -1;
    }
}

// Stratégie de comparaison par pays d'origine (première lettre)
public class OrderOriginComparer : IComparer<Order>
{
    public int Compare(Order x, Order y)
    {
        var xDest = x.ShippingDetails.OriginCountry.ToLowerInvariant();
        var yDest = y.ShippingDetails.OriginCountry.ToLowerInvariant();

        if (xDest == yDest)    return 0;
        else if (xDest[0] > yDest[0]) return 1;
        return -1;
    }
}

Usage:

// Tri par montant
Array.Sort(orders, new OrderAmountComparer());

// Tri par pays d'origine
Array.Sort(orders, new OrderOriginComparer());

Result of alphabetical sorting (by first letter of country of origin):

Avant tri:  Sweden, USA, Sweden, USA, Singapore
Après tri:  Singapore, Sweden, Sweden, USA, USA

Key Insight: Even though the code does not use the words “Strategy” or “strategy”, it is indeed the Strategy Pattern. Whenever you pass an interface to a method to modify its behavior, you are using the Strategy Pattern.

Other examples of the Strategy Pattern in .NET

The pattern is omnipresent in the .NET framework:

.NET ComponentStrategy interfaceBehavior replaced
Array.Sort()ICompare<T>Comparison logic
LINQ OrderBy()ICompare<T>Sorting criteria
HttpClientHttpMessageHandlerSending HTTP requests
Dependency InjectionAny interfaceImplementing a service
IEqualityComparer<T>IEqualityComparer<T>Equality and hash

8. Complete project structure (Demo 5)

Here is the complete architecture of the final project (Demo 5 — Array.Sort, full version):

Strategy_Pattern_Using_different_shipping_providers/
│
├── Program.cs
│
└── Business/
    ├── Models/
    │   └── Order.cs                    ← Contexte (Context)
    │       ├── Item.cs (interne)
    │       ├── ShippingDetails.cs (interne)
    │       ├── Payment.cs (interne)
    │       └── Enums: ShippingStatus, PaymentProvider, ItemType
    │
    └── Strategies/
        ├── SalesTax/
        │   ├── ISalesTaxStrategy.cs          ← Interface
        │   ├── SwedenSalesTaxStrategy.cs     ← Stratégie concrète
        │   └── USAStateSalesTaxStrategy.cs   ← Stratégie concrète
        │
        ├── Invoice/
        │   ├── IInvoiceStrategy.cs           ← Interface
        │   ├── InvoiceStrategy.cs            ← Classe abstraite de base
        │   ├── EmailInvoiceStrategy.cs       ← Stratégie concrète
        │   ├── FileInvoiceStrategy.cs        ← Stratégie concrète
        │   └── PrintOnDemandInvoiceStrategy.cs ← Stratégie concrète
        │
        └── Shipping/
            ├── IShippingStrategy.cs                      ← Interface
            ├── DhlShippingStrategy.cs                    ← Stratégie concrète
            ├── FedexShippingStrategy.cs                  ← Stratégie concrète
            ├── SwedishPostalServiceShippingStrategy.cs   ← Stratégie concrète
            ├── UnitedStatesPostalServiceShippingStrategy.cs ← Stratégie concrète
            └── UpsShippingStrategy.cs                    ← Stratégie concrète

Simplified class diagram:

Order (Context)
 ├── ISalesTaxStrategy  ─── SwedenSalesTaxStrategy
 │                      └── USAStateSalesTaxStrategy
 │
 ├── IInvoiceStrategy   ─── InvoiceStrategy (abstract)
 │                      │       ├── EmailInvoiceStrategy
 │                      │       └── FileInvoiceStrategy
 │                      └── PrintOnDemandInvoiceStrategy
 │
 └── IShippingStrategy  ─── DhlShippingStrategy
                        ├── FedexShippingStrategy
                        ├── SwedishPostalServiceShippingStrategy
                        ├── UnitedStatesPostalServiceShippingStrategy
                        └── UpsShippingStrategy

9. Advantages and trade offs of Pattern Strategy

Advantages

1. Extensibility without modification

It is possible to add new policies without modifying existing policies or the context. This principle is consistent with the Open/Closed principle (open to extension, closed to modification).

Ajouter GermanyVATStrategy ?
→ Créer la classe, implémenter ISalesTaxStrategy
→ Rien d'autre à modifier !

2. Single Responsibility Principle (SRP)

Each strategy is responsible for a single algorithm. The Order class is only responsible for managing the order.

3. Improved testability

Each strategy can be tested totally isolated:

// Test unitaire de SwedenSalesTaxStrategy
[Test]
public void SwedenSalesTaxStrategy_ShouldCalculateCorrectTax()
{
    var strategy = new SwedenSalesTaxStrategy();
    var order = new Order { /* ... */ };
    
    var tax = strategy.GetTaxFor(order);
    
    Assert.AreEqual(25m, tax);
}

We can also inject mocked strategies to test the context:

// Mock de la stratégie pour tester Order
var mockStrategy = new Mock<ISalesTaxStrategy>();
mockStrategy.Setup(s => s.GetTaxFor(It.IsAny<Order>())).Returns(10m);

var order = new Order { SalesTaxStrategy = mockStrategy.Object };
Assert.AreEqual(10m, order.GetTax());

4. Dynamic selection at runtime

The active policy can be chosen based on user behavior, database data, configuration, or any other runtime criteria.

5. Cleaner code in context

The Order class remains simple and readable. She delegates complex algorithms to strategies and focuses on her role: managing an order.

Compromise

1. Increased complexity

Introducing Strategy Pattern creates more classes and files. For simple algorithms that are not likely to change, this may be over-engineering.

2. Clients should be aware of available policies

The caller (the Program, a service, a controller) must know and instantiate the correct strategy. This can be mitigated by dependency injection or a Factory pattern.

3. Additional indirection

There is a layer of indirection between the caller and the actual algorithm. This can make debugging slightly more complex for developers unfamiliar with the pattern.

General rule: Always make sure that the code you add makes working with the application easier and does not complicate it unnecessarily. The Strategy Pattern is an excellent tool, but like any pattern, it must be applied thoughtfully.


10. Summary and conclusion

What you learned

This course covered the following aspects of the Strategy Pattern:

ConceptDescription
DefinitionBehavioral pattern allowing to define a family of interchangeable algorithms
ComponentsContext, Interface (Strategy), Concrete Strategies
RefactoringHow to extract coupled algorithms to decoupled strategies
New featureHow to start a new feature by applying the pattern from the start
Alternative approachPass strategy as method parameter
In .NETArray.Sort + IComparer<T> is an implementation of the Strategy Pattern

Key Takeaways

  1. The Strategy Pattern is everywhere. If you’ve ever written code that accepts an interface to change a behavior, you’ve used the Strategy Pattern — even without naming it.

  2. Context does not know details. The context class (Order) knows nothing about how the tax is calculated, how the invoice is generated, or how the package is shipped. She delegates everything to strategies.

  3. Names are not required. You are not required to name your classes with the “Strategy” suffix. If you accept an interface to replace a behavior, that’s Strategy Pattern.

  4. Strong coupling is the enemy. Coupling algorithms directly in context makes code difficult to test, difficult to extend, and difficult to maintain.

  5. Start with the interface. When implementing a new feature that might have multiple implementations, start by defining the interface. This forces you to think about the contract before the implementation details.

Pattern application checklist

Before applying the Strategy Pattern, ask yourself these questions:

  • Are there several variations of the same algorithm?
  • Should the active algorithm be able to change at runtime?
  • Is the algorithm code bloating in the context class?
  • Do you want to isolate the algorithm logic for easier testing?
  • Do you plan to add new variations of the algorithm in the future?

If you answered yes to one or more of these questions, the Strategy Pattern is probably the right approach.

Application in a real project

Contexte réel → Appliquer Strategy Pattern

Calcul de prix/taxes par région     → ITaxStrategy
Envoi de notifications              → INotificationStrategy (Email, SMS, Push)
Export de données                   → IExportStrategy (CSV, Excel, PDF)
Authentification                    → IAuthStrategy (JWT, OAuth, Basic)
Paiement                            → IPaymentStrategy (Stripe, PayPal, Virement)
Mise en cache                       → ICacheStrategy (Memory, Redis, File)
Compression de fichiers             → ICompressionStrategy (ZIP, GZIP, LZ4)

“My name is Filip Ekberg, and thank you so much for taking this course. We’ve covered this amazing pattern and how to apply that in your applications. Hopefully by now, you get some ideas of how you can refactor your applications, and this will take your development experience to a whole new level.”


Search Terms

c-sharp · design · patterns · strategy · testing · architecture · c# · .net · development · pattern · concrete · strategies · interface · order · advantages · application · calculating · context · dynamic · invoice · runtime · sales · selection · shipping

Interested in this course?

Contact us to book it or get a custom training plan for your team.