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");
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) ); }
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) ); }
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.
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)
Quick Links
Legal Stuff