Improving Code Flexibility with Strategy Pattern and Dependency Injection in .NET

Problem statement Not sure about you, but many times when I had to implement different algorithms depending on specific input I ended up with ugly switch statements that made my code bloated and, in some cases, difficult to understand. Practical example Let's say that I need to create a Calculate API method that takes two double numbers and an operation type as input, then applies that operation to the two numbers. In code this would look something like this: Get method that implements the logic above (I am using minimal APIs to illustrate this) app.MapGet("/calculate", ([FromQuery] double a, [FromQuery] double b, [FromQuery] string op) => {    try    {        if (!Enum.TryParse(op, true, out var operationType))        {            return Results.BadRequest("Invalid operation");        }          return operationType switch        {            OperationType.ADDITION => Results.Ok(a + b),            OperationType.SUBSTRACTION => Results.Ok(a - b),            OperationType.MULTIPLICATION => Results.Ok(a * b),            OperationType.DIVISION => b == 0 ? Results.BadRequest("Division by zero") : Results.Ok(a / b),            _ => Results.BadRequest("Invalid operation type"),        };    }    catch (Exception)    {        return Results.InternalServerError("Calculation error");    } }) .WithName("Calculate"); OperationType enum public enum OperationType {     ADDITION,     SUBSTRACTION,     MULTIPLICATION,     DIVISION } This doesn't look too bad, but imagine if the algorithms you need to implement were much more complex than a simple addition. Of course, you could move the code to separate classes, but the switch statement would still remain. Proposed solution This kind of problem can be easily solved using the Strategy Pattern, and this is no secret. However, we can leverage the power of dependency injection to implement this pattern in a very elegant way. Here's how this can be done: Step 1 - Create an ICalculationStrategy interface Notice that the strategy interface also contains the OperationType — this will be used to identify the correct strategy for each case. namespace DemoTechniques.Strategies { public interface ICalculationStrategy { public OperationType OperationType { get; } public double Calculate(double a, double b); } } Step 2 - Create the concrete strategies namespace DemoTechniques.Strategies { public class AdditionCalculationStrategy : ICalculationStrategy { public OperationType OperationType => OperationType.ADDITION; public double Calculate(double a, double b) => a + b; } public class SubstractionCalculationStrategy : ICalculationStrategy { public OperationType OperationType => OperationType.SUBSTRACTION; public double Calculate(double a, double b) => a - b; } public class MultiplicationCalculationStrategy : ICalculationStrategy { public OperationType OperationType => OperationType.MULTIPLICATION; public double Calculate(double a, double b) => a * b; } public class DivisionCalculationStrategy : ICalculationStrategy { public OperationType OperationType => OperationType.DIVISION; public double Calculate(double a, double b) { if (b == 0) { throw new ArgumentException("Cannot divide by zero", nameof(b)); } return a / b; } } } Step 3 - Register the strategies inside the DI container Now that we have all the strategies, we can injected them inside the DI container, like this: var builder = WebApplication.CreateBuilder(args); //other services builder.Services .AddTransient() .AddTransient() .AddTransient() .AddTransient(); Step 4 - Apply the strategies inside the Calculate method These services can be injected into the method, and OperationType can be used to identify the correct calculation service based on the user input. This allows us to eliminate the switch statement and delegate the responsibility of identifying the correct service to the DI container. The Calculate method can be refactored like this: app.MapGet("/calculate", ([FromQuery] double a, [FromQuery] double b, [FromQuery] string op, [FromServices] IEnumerable calculationStrategies) => { try { if (!Enum.TryParse(op, true, out var operationType)) { return Results.BadRequest("Invalid operation"); } var calculationStrategy = calculationStrategies .FirstOrDefault(s => s.OperationType == operationType); if (calculationStrategy is null) { return Results.BadRequest("Invalid operation type"); } return Results.Ok(calculationStrategy.Calculate(a, b)); } catch (Exception) { return Results.InternalServerError("Calculation error");

Feb 7, 2025 - 07:20
 0
Improving Code Flexibility with Strategy Pattern and Dependency Injection in .NET

Problem statement

Not sure about you, but many times when I had to implement different algorithms depending on specific input I ended up with ugly switch statements that made my code bloated and, in some cases, difficult to understand.

Practical example

Let's say that I need to create a Calculate API method that takes two double numbers and an operation type as input, then applies that operation to the two numbers.

In code this would look something like this:

  • Get method that implements the logic above (I am using minimal APIs to illustrate this)
app.MapGet("/calculate", ([FromQuery] double a, [FromQuery] double b, [FromQuery] string op) =>
{
   try
   {
       if (!Enum.TryParse<OperationType>(op, true, out var operationType))
       {
           return Results.BadRequest("Invalid operation");
       }
 
       return operationType switch
       {
           OperationType.ADDITION => Results.Ok(a + b),
           OperationType.SUBSTRACTION => Results.Ok(a - b),
           OperationType.MULTIPLICATION => Results.Ok(a * b),
           OperationType.DIVISION => b == 0 ? Results.BadRequest("Division by zero") : Results.Ok(a / b),
           _ => Results.BadRequest("Invalid operation type"),
       };
   }
   catch (Exception)
   {
       return Results.InternalServerError("Calculation error");
   }
})
.WithName("Calculate");
  • OperationType enum
public enum OperationType
{
    ADDITION,
    SUBSTRACTION,
    MULTIPLICATION,
    DIVISION
}

This doesn't look too bad, but imagine if the algorithms you need to implement were much more complex than a simple addition. Of course, you could move the code to separate classes, but the switch statement would still remain.

Proposed solution

This kind of problem can be easily solved using the Strategy Pattern, and this is no secret. However, we can leverage the power of dependency injection to implement this pattern in a very elegant way.

Here's how this can be done:

Step 1 - Create an ICalculationStrategy interface

Notice that the strategy interface also contains the OperationType — this will be used to identify the correct strategy for each case.

namespace DemoTechniques.Strategies
{
    public interface ICalculationStrategy
    {
        public OperationType OperationType { get; }
        public double Calculate(double a, double b);
    }
}

Step 2 - Create the concrete strategies

namespace DemoTechniques.Strategies
{
    public class AdditionCalculationStrategy : ICalculationStrategy
    {
        public OperationType OperationType => OperationType.ADDITION;

        public double Calculate(double a, double b) => a + b;
    }

    public class SubstractionCalculationStrategy : ICalculationStrategy
    {
        public OperationType OperationType => OperationType.SUBSTRACTION;

        public double Calculate(double a, double b) => a - b;
    }

    public class MultiplicationCalculationStrategy : ICalculationStrategy
    {
        public OperationType OperationType => OperationType.MULTIPLICATION;

        public double Calculate(double a, double b) => a * b;
    }

    public class DivisionCalculationStrategy : ICalculationStrategy
    {
        public OperationType OperationType => OperationType.DIVISION;

        public double Calculate(double a, double b)
        {
            if (b == 0)
            {
                throw new ArgumentException("Cannot divide by zero", nameof(b));
            }

            return a / b;
        }
    }
}

Step 3 - Register the strategies inside the DI container

Now that we have all the strategies, we can injected them inside the DI container, like this:

var builder = WebApplication.CreateBuilder(args);

//other services

builder.Services
    .AddTransient<ICalculationStrategy, AdditionCalculationStrategy>()
    .AddTransient<ICalculationStrategy, SubstractionCalculationStrategy>()
    .AddTransient<ICalculationStrategy, MultiplicationCalculationStrategy>()
    .AddTransient<ICalculationStrategy, DivisionCalculationStrategy>();

Step 4 - Apply the strategies inside the Calculate method

These services can be injected into the method, and OperationType can be used to identify the correct calculation service based on the user input. This allows us to eliminate the switch statement and delegate the responsibility of identifying the correct service to the DI container.

The Calculate method can be refactored like this:

app.MapGet("/calculate", ([FromQuery] double a, [FromQuery] double b, [FromQuery] string op,
    [FromServices] IEnumerable<ICalculationStrategy> calculationStrategies) =>
{
    try
    {
        if (!Enum.TryParse<OperationType>(op, true, out var operationType))
        {
            return Results.BadRequest("Invalid operation");
        }

        var calculationStrategy = calculationStrategies
            .FirstOrDefault(s => s.OperationType == operationType);

        if (calculationStrategy is null)
        {
            return Results.BadRequest("Invalid operation type");
        }

        return Results.Ok(calculationStrategy.Calculate(a, b));
    }
    catch (Exception)
    {
        return Results.InternalServerError("Calculation error");
    }
})
.WithName("Calculate");

Bonus tip

If you are using a controller or any other service, you can create a dictionary from the IEnumerable inside the constructor and then use this dictionary to quickly lookup the correct strategy throughout your controller/service methods.

Example implementation:

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

[ApiController]
[Route("api/[controller]")]
public class CalculatorController : ControllerBase
{
    private readonly Dictionary<OperationType, ICalculationStrategy> _strategyDictionary;

    public CalculatorController(IEnumerable<ICalculationStrategy> calculationStrategies)
    {
        // Create a dictionary for quick lookup
        _strategyDictionary = calculationStrategies.ToDictionary(s => s.OperationType, s => s);
    }

    [HttpGet("calculate")]
    public IActionResult Calculate([FromQuery] double a, [FromQuery] double b, [FromQuery] string op)
    {
        try
        {
            if (!Enum.TryParse<OperationType>(op, true, out var operationType))
            {
                return BadRequest("Invalid operation");
            }

            if (!_strategyDictionary.TryGetValue(operationType, out var calculationStrategy))
            {
                return BadRequest("Invalid operation type");
            }

            return Ok(calculationStrategy.Calculate(a, b));
        }
        catch (Exception)
        {
            return StatusCode(500, "Calculation error");
        }
    }
}

Conclusions

In this brief article, we tackled the problem of bloated switch statements for algorithm selection based on user input. We introduced the Strategy Pattern, a solution that helps decouple the logic, making the code cleaner and more maintainable. By combining this pattern with Dependency Injection, we effectively eliminate the switch statement, delegating the responsibility of choosing the correct strategy to the DI container.

This approach aligns with SOLID principles, particularly the Open/Closed Principle (allowing easy addition of new operations without modifying existing code) and the Dependency Inversion Principle (relying on abstractions rather than concrete implementations). By using a dictionary for quick lookup of strategies, the design becomes even more modular and scalable.