.NET Tip: Feature toggling inside DI-containers

A pivotal moment in the history of an application is when a service changes drastically and deployment becomes a subject of stress and uncertainty. The service might be deemed as unhealthy in a production environment whereas it worked just fine in the test environment. The rollback and further development workflow become tiresome. Your team gets sad 😨. How can we continuously deploy and make the team happy 😀 again? With feature flags, of course!

In the spirit of clean code and smooth deployments, feature toggles are a great way to toggle between functionality at runtime. I want to show a neat trick that you can use with dotnet's built-in DI-container, Azure Feature Management, and Azure App Configuration.

Let's say that we have two services. OldBankService as the existing service and NewBankService as the new service. Both are expected to deliver bank transactions, but they get that information from different places. Maybe the information that the NewBankService wants to get, might not even be ready yet. When doing this approach it's important that the two implementations of your service share the same interface.

public interface IBankService
{
    Task<TransactionResponse> GetTransactions(string customerId);
}

When I refer to IBankService in another part of my application, I don't care what service gets called as long as I get my bank transactions. How can we switch between these two in an already running application? Through our DI-container, of course!

The .AddScoped method in the service provider exposes a Func<IServiceProvider, TService> that allows us to do some switching logic to resolve the correct instance. We can do that with ease using IFeatureManager.

services.AddScoped<OldBankService>();
services.AddScoped<NewBankService>();
services.AddScoped(factory =>
{
    var featureManager = factory.GetRequiredService<IFeatureManager>();
    return featureManager.IsEnabledAsync("UseNewBankService").GetAwaiter().GetResult() ?
        factory.GetRequiredService<NewBankService>() as IBankService :
        factory.GetRequiredService<OldBankService>() as IBankService;
});

First adding the two services and then resolving by the UseNewBankService key when calling IBankService. The functionality can easily be switched on or off inside the Azure Portal without the need for a new deployment. And your team is happy once again! I really like this approach. This method allows you to write code in isolated instances without interfering with the legacy functionality and vice versa. .AddScoped doesn't allow async resolving. That explains the .GetAwaiter().GetResult() when getting the flag from the app configuration.