C# / .NETDevOpsMisc
C# / .NET
Top 3 useful extension methods to add feature flags around dependency injection in C#
Alexandru Puiu
Alexandru Puiu
February 25, 2022
1 min

Table Of Contents

01
Extension methods to for feature flag based dependency injection
02
Decorator Pattern with Feature Flags

Feature Management in .net has functionality for most use-cases, but I needed to register a service in dependency injection based on a feature flag being enabled and swap it out at runtime if I trigger the flag. The idea is I develop a new version of a service, but allow QA and operations to start using the new service at the toggle of a switch, and if something goes wrong, switch back to the old service, without needing a new deployment or even restarting the app. For this, we need to add feature flags around dependency injection methods.

First, we have the original service registration as it was when we started

services.AddTransient<ICoolService, CurrentCoolService>();

Next, we’ll register the new service with the ServiceProvider extensions below. The new service will only be returned if the “NewCoolService” feature flag is enabled, otherwise, it will keep returning the CurrentCoolService implementation

services.AddTransientForFeature<ICoolService, NewCoolService>("NewCoolService");

Extension methods to for feature flag based dependency injection

AddTransientForFeature

public static void AddTransientForFeature<TInterface, TImplementation>(this IServiceCollection services, string featureName)
        where TInterface : class
        where TImplementation : class, TInterface
{
    // grab the existing registration if it exists
    var oldDescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(TInterface));
    if (oldDescriptor == null)
        services.Add(ServiceDescriptor.Describe(
            typeof(TInterface),
            serviceProvider => serviceProvider.GetRequiredService<IFeatureManager>().IsEnabledAsync(featureName).ConfigureAwait(false).GetAwaiter().GetResult() ?
                ActivatorUtilities.CreateInstance(serviceProvider, typeof(TImplementation)) :
                throw new NotImplementedException("Feature {featureName} is not enabled, and no other instance of the service is registered"),
            ServiceLifetime.Transient)
        );
    else
        services.Replace(ServiceDescriptor.Describe(
            typeof(TInterface),
            serviceProvider => serviceProvider.GetRequiredService<IFeatureManager>().IsEnabledAsync(featureName).ConfigureAwait(false).GetAwaiter().GetResult() ?
                ActivatorUtilities.CreateInstance(serviceProvider, typeof(TImplementation)) :
                ActivatorUtilities.CreateInstance(serviceProvider, oldDescriptor.ImplementationType!),
            ServiceLifetime.Transient)
        );
}

AddScopedForFeature

public static void AddScopedForFeature<TInterface, TImplementation>(this IServiceCollection services, string featureName)
        where TInterface : class
        where TImplementation : class, TInterface
{
    // grab the existing registration if it exists
    var oldDescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(TInterface));
    if (oldDescriptor == null)
        services.Add(ServiceDescriptor.Describe(
            typeof(TInterface),
            serviceProvider => serviceProvider.GetRequiredService<IFeatureManager>().IsEnabledAsync(featureName).ConfigureAwait(false).GetAwaiter().GetResult() ?
                ActivatorUtilities.CreateInstance(serviceProvider, typeof(TImplementation)) :
                throw new NotImplementedException("Feature {featureName} is not enabled, and no other instance of the service is registered"),
            ServiceLifetime.Scoped)
        );
    else
        services.Replace(ServiceDescriptor.Describe(
            typeof(TInterface),
            serviceProvider => serviceProvider.GetRequiredService<IFeatureManager>().IsEnabledAsync(featureName).ConfigureAwait(false).GetAwaiter().GetResult() ?
                ActivatorUtilities.CreateInstance(serviceProvider, typeof(TImplementation)) :
                ActivatorUtilities.CreateInstance(serviceProvider, oldDescriptor.ImplementationType!),
            ServiceLifetime.Scoped)
        );
}

Decorator Pattern with Feature Flags

The Decorator Pattern allows you to replace the implementation of a service with another service that takes the original service as a parameter and calls into it. Check out this post for a great implementation:

https://greatrexpectations.com/2018/10/25/decorators-in-net-core-with-dependency-injection

We can extend the decorator pattern above to be feature aware, by creating replacing the service descriptor with a factory returning the implementation based on the feature flag.

DecorateForFeature

public static void DecorateForFeature<TInterface, TDecorator>(this IServiceCollection services, string featureName)
   where TInterface : class
   where TDecorator : class, TInterface
{
   // grab the existing registration
   var wrappedDescriptor = services.FirstOrDefault(
       s => s.ServiceType == typeof(TInterface));

   // check it's valid
   if (wrappedDescriptor == null)
       throw new InvalidOperationException($"{typeof(TInterface).Name} is not registered");

   // create the object factory for our decorator type,
   // specifying that we will supply TInterface explicitly
   var objectFactory = ActivatorUtilities.CreateFactory(
       typeof(TDecorator),
       new[] { typeof(TInterface) });

   // replace the existing registration with one
   // that passes an instance of the existing registration
   // to the object factory for the decorator
   services.Replace(ServiceDescriptor.Describe(
       typeof(TInterface),
       s => s.GetRequiredService<IFeatureManager>().IsEnabledAsync(featureName).ConfigureAwait(false).GetAwaiter().GetResult() ?
               (TInterface)objectFactory(s, new[] { s.CreateInstance(wrappedDescriptor) }) :
               ActivatorUtilities.CreateInstance(s, wrappedDescriptor.ImplementationType!),
       wrappedDescriptor.Lifetime)
   );
}

This allows us to wrap our service conditionally, and turn it on at runtime. We can register our new decorator service after the registration of the decorated service.

services.AddScoped<ITaxService, LocalTaxService>();
services.DecorateForFeature<ITaxService, AvalaraTaxService>(Constants.FeatureFlags.Avalara);

Some reasons for this include wrapping 3rd party services with logging/instrumentation and in some cases fallback logic. In the above example, the AvalaraTaxService decorates the LocalTaxService, and in the catch block, it calls into the LocalTaxService. (Avalara is a 3rd party tax calculation API)


Tags

feature-flagsextension-methods
Alexandru Puiu

Alexandru Puiu

Engineer / Security Architect

Systems Engineering advocate, Software Engineer, Security Architect / Researcher, SQL/NoSQL DBA, and Certified Scrum Master with a passion for Distributed Systems, AI and IoT..

Expertise

.NET
RavenDB
Kubernetes

Social Media

githubtwitterwebsite

Related Posts

RavenDB Integration Testing
Using RavenDB in Integration Testing
December 24, 2022
2 min

Subscribe To My Newsletter

I'll only send worthwhile content I think you'll want, less than once a month, and promise to never spam or sell your information!
© 2022, All Rights Reserved.

Quick Links

Get In TouchAbout Me

Social Media