2 min read

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:

Decorators in .NET Core with Dependency Injection

The Decorator Pattern allows you to add functionality to an implementation of an interface by wrapping it in another implementation. e.g. We have a concrete implementation of an IService interface ( DbService) that returns a string value. LoggingService then decorates that implementation by wrapping invocation of the concrete instance and logging entry and exit.

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)