Implementing the Strategy Pattern with .NET Dependency Injection

Introduction

Note: some people complained that what I actually called Dependency Injection is actually Inversion of Control. I won’t get into that fight, to me, Dependency Injection is a form of Inversion of Control.

The Strategy Pattern (sometimes known as Policy) originated in the seminal book Design Patterns, by the Gang of Four. It is a behavioural design pattern that allows picking a specific strategy at runtime. The concrete strategy to use can change dramatically how the application behaves. Quoting: instead of implementing a single algorithm directly, code receives runtime instructions as to which in a family of algorithms to use.

Dependency Injection (DI), also known as Inversion of Control (IoC) - they’re not quite the same, but, for the purpose of this post, we can think of them as the same, even though this raises heated discussions - is built-in in .NET Core (now, .NET) since the start, and whilst we generally return the same implementation type for a registered service, that does not have to be the case. Let's see how we can use strategies here, but what I'm going to show is not really rocket science.

Mixing Strategy with Dependency Injection

So, let's say we have some interface that defines our strategy contract, IStrategy, and some concrete implementations, StrategyA and StrategyB. Let's not care too much about them, or what they do, but IStrategy should define some method that describes the operations to be implemented by the strategy.

//the strategy contract
public interface IStrategy { } //one strategy implementation
public class StrategyA : IStrategy { } //another strategy implementation
public class StrategyB : IStrategy { }

Now, when we register a service to DI, we can use one of the methods of IServiceCollection.AddXXX that take an IServiceProvider parameter, and we can use it to retrieve information from the context.

The idea is simple, instead of doing this:

builder.Services.AddScoped<IStrategy, StrategyA>();

We can instead have something like this:

builder.Services.AddScoped<IStrategy>(sp =>
{
//do something with sp and return the right implementation of IStrategy
});

We could also use named (keyed) registrations, but, these are not as transparent as we may want:

builder.Services.AddKeyedScoped<IStrategy, StrategyA>("A");
builder.Services.AddKeyedScoped<IStrategy, StrategyB>("B");

Because we need to ask for a particular service key (GetKeyedService/GetRequiredKeyedService):

serviceProvider.GetRequiredKeyService<IStrategy>("A");

For example, if we want to return a different implementation depending on the ASP.NET Core environment (Development or otherwise), we could have (using IWebHostEnvironment):

builder.Services.AddScoped<IStrategy>(sp => { var webHostEnv = sp.GetRequiredService<IWebHostEnvironment>(); if (webHostEnv.IsDevelopment()) { return new StrategyA(); } else { return new StrategyB(); } });

Or, also in an ASP.NET Core application, for when the current request is authenticated or not (needs AddHttpContextAccessor registration for IHttpContextAccessor):

builder.Services.AddScoped<IStrategy>(sp => { var httpContext = sp.GetRequiredService<IHttpContextAccessor>() .HttpContext; if (httpContext.User.Identity.IsAuthenticated) { return new StrategyA(); } else { return new StrategyB(); } });

Or checking if the request is local (coming from localhost), also requires AddHttpContextAccessor):

builder.Services.AddScoped<IStrategy>(sp => { var httpContext = sp.GetRequiredService<IHttpContextAccessor>() .HttpContext; if (httpContext.Connection.RemoteIpAddress is null || httpContext.Connection.RemoteIpAddress == IPAddress.Loopback || IPAddress.IsLoopback(httpContext.Connection.RemoteIpAddress)) { return new StrategyA(); } else { return new StrategyB(); } });

Or checking a feature flag (requires Microsoft.FeatureManagement.AspNetCore NuGet), assuming we have a feature flag named "SomeFeature":

builder.Services.AddScoped<IStrategy>(sp => { var featureManager = sp.GetRequiredService<IFeatureManager>(); if (featureManager.IsEnabledAsync("SomeFeature").GetAwaiter().GetResult()) { return new StrategyA(); } else { return new StrategyB(); } });

Never mind skipping the asynchronicity, this is just at startup time.

Finally, even some plain configuration setting, assuming "Some:BooleanValue" boolean value exists in configuration:

builder.Services.AddScoped<IStrategy>(sp =>
{
    var configuration = sp.GetRequiredService<IConfiguration>();

    if (configuration.GetValue<bool>("Some:BooleanValue"))
    {
        return new StrategyA();
    }
    else
    {
        return new StrategyB();
    }
});

I think you got the picture. Of course, this doesn't have to be just with two concrete implementations, and you can make the decision using other sources, it's really up to us how we want to make that decision.

Conclusion

As you can see, nothing really new under the Sun, but I hope you find this as yet another useful tool to have in your toolbox! Let me know your thouhgts, and stay tuned for more!


Comments

Popular posts from this blog

Modern Mapping with EF Core

C# Magical Syntax

.NET 10 Validation