How to Implement Strategy Pattern in ASP.NET Core

Imagine you are developing the checkout page for an online store where you sell goods and services, and you have been instructed to include a feature that will let customers select from a variety of shipping options. Each shipping method may use a different logic to determine shipping costs, and there may be instances where various developers must use various algorithms. Writing a single class that contains all the calculations and logic would be a bad idea. You must divide your code in a way that makes it simple for various developers to maintain it and simple to scale should more shipping methods ever be added. In this tutorial, I’ll demonstrate how to use a design pattern called the Strategy Pattern to implement several algorithms independently and switch between them as needed.

What is a Strategy Pattern?

A behavioral design pattern called the strategy pattern (also referred to as the policy pattern) enables us to implement a family of algorithms (or strategies) into distinct classes and then switch from one algorithm (or strategy) to another during runtime. Developers can isolate the code, internal logic, and dependencies of different algorithms using this pattern, making the application code simple to scale and maintain. By switching from one algorithm to another at runtime, this pattern also enables us to change the behavior of the application.

Pros of Strategy Pattern

  1. Different algorithms’ implementation specifics can be separated from the business logic that employs them.
  2. At runtime, we can change from one algorithm to another.
  3. Algorithms can be added or removed without changing the context.

Cons of Strategy Pattern

  1. You don’t need to overcomplicate your code by adding new classes and interfaces that go along with this pattern if you only have a few algorithms that rarely change.
  2. To choose the most appropriate strategy, the client needs to understand how various strategies differ from one another.

Setting Up an ASP.NET Core Demo App

When dealing with multiple algorithms, strategy patterns can be used in a variety of real-world use cases, including

  1. An online store checkout page with various shipping options.
  2. A checkout page for an online store that accepts various forms of payment.
  3. A program that produces images in a variety of formats.
  4. A program that authenticates users through OpenID, OAuth, and Basic authentication.

In order to demonstrate how a Strategy pattern can help us switch from one shipping method to another shipping method dynamically, I’ll implement a sample eCommerce checkout page that will let the user select one of the shipping methods.

Create the ShippingMethod class below in a new ASP.NET Core MVC 5 Web Application.

ShippingMethod.cs

public class ShippingMethod
{
    public int Id { get; set; }
    public string Name { get; set; }
}
The next step is to create the CheckoutModel class, which will assist in the creation of a demo checkout page.
CheckoutModel.cs
public class CheckoutModel
{
    public int SelectedMethod { get; set; }
    public decimal OrderTotal { get; set; }
    public decimal FinalTotal { get; set; }
    public List<ShippingMethod> ShippingMethods { get; set; }
}
Add the next private method by opening the HomeController and doing so. For our demo app, the GetShippingMethods method will return a list of shipping options.
private List<ShippingMethod> GetShippingMethods()
{
    return new List<ShippingMethod>()
    {
        new ShippingMethod()
        {
            Id = 1,
            Name="Free Shipping ($0.00)"
        },
        new ShippingMethod() {
            Id = 2,
            Name="Local Shipping ($10.00)"
        },
        new ShippingMethod() {
            Id = 3,
            Name="Worldwide Shipping ($50.00)"
        }
    };
}
Make a second private method GetOrderDetails creates and returns an object of the class CheckoutModel.
private CheckoutModel GetOrderDetails()
{
    var model = new CheckoutModel()
    {
        OrderTotal = 100.00m,
        ShippingMethods = GetShippingMethods()
    };
    return model;
}
As demonstrated in the code snippet below, call the GetOrderDetails method inside the HomeController’s Index action. Simply put, we are sending the model back to the Razar view page.
public IActionResult Index()
{
    var model = GetOrderDetails();
    return View(model);
}
The code for the razor view page, where we’re building a straightforward checkout form, is shown below. The shipping options are presented in a dropdown menu so that the user can select one and we can then use that information to run one of the shipping calculation algorithms.
Index.cshtml
@model CheckoutModel
@{
    ViewData["Title"] = "Home Page";
}
<br />
<div>
    <h3 class="display-4 m-3 text-center">Checkout</h3>
    <br />
    @using (Html.BeginForm("Index", "Home", null, FormMethod.Post))
    {
        @Html.HiddenFor(x => x.OrderTotal);
        <table cellpadding="0" cellspacing="0" class="table">
            <tr>
                <th>Order Total: </th>
                <th>[email protected]</th>
            </tr>
            <tr>
                <th>Shipping Method: </th>
                <td>
                    @Html.DropDownListFor(x => x.SelectedMethod,
                        new SelectList(Model.ShippingMethods, "Id", "Name"),
                        "- Select -",
                        new { @class = "form-control" })
                </td>
                <td class="text-left">
                    <input class="btn btn-primary" type="submit" value="Calculate" />
                </td>
            </tr>
            @if (Model.FinalTotal > 0)
            {
                <tr>
                    <th>Final Total: </th>
                    <th>[email protected]</th>
                </tr>
            }
        </table>
    }
</div>

Getting Started with Strategy Pattern

Declaring a common interface that will be implemented by all the algorithms is the first step in putting the Strategy pattern into practice. Let’s call the interface for our test app the IShippingStrategy interface. The interface only has one method, CalculateFinalTotal, that receives as a parameter the orderTotal and returns the final sum.

IShippingStragegy.cs

public interface IShippingStrategy
{
    decimal CalculateFinalTotal(decimal orderTotal);
}
The implementation of the aforementioned interface on all algorithms is the next step. Three algorithms—Free Shipping, Local Shipping, and Worldwide Shipping—are required for our demo app in order to calculate the final sum using various shipping costs. Let’s implement each of the three algorithms separately.
FreeShippingStrategy.cs
public class FreeShippingStrategy : IShippingStrategy
{
    public decimal CalculateFinalTotal(decimal orderTotal)
    {
        return orderTotal;
    }
}
LocalShippingStrategy.cs
public class LocalShippingStrategy : IShippingStrategy
{
    public decimal CalculateFinalTotal(decimal orderTotal)
    {
        return orderTotal + 10;
    }
}

WorldwideShippingStrategy.cs

public class WorldwideShippingStrategy : IShippingStrategy
{
    public decimal CalculateFinalTotal(decimal orderTotal)
    {
        return orderTotal + 50;
    }
}
In the aforementioned classes, I only included very basic calculations for shipping rates; however, in real-world applications, these classes may contain a wide range of complex logics. To determine shipping costs for various destinations, they can also use shipping APIs provided by third parties.

Continue implementing the Strategy pattern by declaring the IShippingContext interface below.

IShippingContext.cs

public interface IShippingContext
{
    void SetStrategy(IShippingStrategy strategy);
    decimal ExecuteStrategy(decimal orderTotal);
}
Next, add the aforementioned interface to the ShippingContext class.
ShippingContext.cs
public class ShippingContext : IShippingContext
{
    private IShippingStrategy _strategy;
    public ShippingContext()
    { }
    public ShippingContext(IShippingStrategy strategy)
    {
        this._strategy = strategy;
    }
    public void SetStrategy(IShippingStrategy strategy)
    {
        this._strategy = strategy;
    }
    public decimal ExecuteStrategy(decimal orderTotal)
    {
        return this._strategy.CalculateFinalTotal(orderTotal);
    }
}
For the purpose of storing the reference to a strategy object, we declared the field _strategy. We can swap out the current strategy with the one passed as a parameter by using the SetStrategy method. The ExecuteStrategy method’s implementation, which carries out the CalculateFinalTotal for the current strategy, is the last step.

Using Strategy Pattern in ASP.NET Core

With the help of the aforementioned ShippingContext class, we are now prepared to use various shipping algorithms in our HomeController, but first we must register it with the.NET Core DI Container. This can be done by including the following line in the Startup.cs file’s ConfigureService method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddScoped<IShippingContext, ShippingContext>();
}

The IShippingContext must then be added to our HomeController.

public class HomeController : Controller
{
    private readonly IShippingContext _shippingContext;
    public HomeController(IShippingContext shippingContext)
    {
        _shippingContext = shippingContext;
    }
}

The Index action method, which will be invoked when the user clicks the Calculate button, can now be put into practice. Take note of how we are utilizing the SetStrategy method to associate the proper strategy with the user-selected shipping method. Once the strategy is chosen, the ExecuteStrategy will automatically determine the final cost using the appropriate shipping strategy.

[HttpPost]
public IActionResult Index(CheckoutModel model)
{
    model.ShippingMethods = GetShippingMethods();
    switch (model.SelectedMethod)
    {
        case 1:
            _shippingContext.SetStrategy(new FreeShippingStrategy());
            break;
        case 2:
            _shippingContext.SetStrategy(new LocalShippingStrategy());
            break;
        case 3:
            _shippingContext.SetStrategy(new WorldwideShippingStrategy());
            break;
    }
    model.FinalTotal = _shippingContext.ExecuteStrategy(model.OrderTotal);
    return View(model);
}

Start the demo app and experiment with calculating the final sum by choosing various shipping options. Your final order total will reflect this.

The following is the complete source code of our HomeController.

HomeController.cs

public class HomeController : Controller
{
    private readonly IShippingContext _shippingContext;
    public HomeController(IShippingContext shippingContext)
    {
        _shippingContext = shippingContext;
    }
    public IActionResult Index()
    {
        var model = GetOrderDetails();
        return View(model);
    }
    [HttpPost]
    public IActionResult Index(CheckoutModel model)
    {
        model.ShippingMethods = GetShippingMethods();
        switch (model.SelectedMethod)
        {
            case 1:
                _shippingContext.SetStrategy(new FreeShippingStrategy());
                break;
            case 2:
                _shippingContext.SetStrategy(new LocalShippingStrategy());
                break;
            case 3:
                _shippingContext.SetStrategy(new WorldwideShippingStrategy());
                break;
        }
        model.FinalTotal = _shippingContext.ExecuteStrategy(model.OrderTotal);
        return View(model);
    }
    private CheckoutModel GetOrderDetails()
    {
        var model = new CheckoutModel()
        {
            OrderTotal = 100.00m,
            ShippingMethods = GetShippingMethods()
        };
        return model;
    }
    private List<ShippingMethod> GetShippingMethods()
    {
        return new List<ShippingMethod>()
        {
            new ShippingMethod()
            {
                Id = 1,
                Name="Free Shipping ($0.00)"
            },
            new ShippingMethod() {
                Id = 2,
                Name="Local Shipping ($10.00)"
            },
            new ShippingMethod() {
                Id = 3,
                Name="Worldwide Shipping ($50.00)"
            }
        };
    }
}

Summary

We can create software that is more effective, readable, and simple to maintain by using strategy patterns. It enables us to build software with reusable and interchangeable parts that can be dynamically added to or taken away at runtime. I hope you’ve enjoyed this tutorial and the example I used to clearly explain this pattern to everyone.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *