Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,51 @@ public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatu

return builder;
}

/// <summary>
/// Adds a <see cref="FeatureServiceProvider{TService,TEnabled,TDisabled}"/> to the feature management system.
/// </summary>
/// <param name="builder">The <see cref="IFeatureManagementBuilder"/> used to customize feature management functionality.</param>
/// <param name="featureName">The feature flag that should be used to determine which implementation of the service should be used. The <see cref="IFeatureServiceProvider{TService}"/> will return different implementations of TService according to the feature status.</param>
/// <param name="options">Options used to configure the feature service provider.</param>
/// <returns>A <see cref="IFeatureManagementBuilder"/> that can be used to customize feature management functionality.</returns>
/// <exception cref="ArgumentNullException">Thrown if feature name parameter is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if a feature service of the type has already been added.</exception>
public static IFeatureManagementBuilder WithFeatureService<TService, TEnabled, TDisabled>(this IFeatureManagementBuilder builder, string featureName, FeatureServiceProviderOptions options = null)
where TService : class
where TEnabled : class, TService
where TDisabled : class, TService
{
if (string.IsNullOrEmpty(featureName))
{
throw new ArgumentNullException(nameof(featureName));
}

options ??= new FeatureServiceProviderOptions();

if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureServiceProvider<TService>)))
{
throw new InvalidOperationException($"A feature service of {typeof(TService).FullName} has already been added.");
}

if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped))
{
builder.Services.AddScoped<IFeatureServiceProvider<TService>>(sp => new FeatureServiceProvider<TService, TEnabled, TDisabled>(
sp,
sp.GetRequiredService<IFeatureManager>(),
featureName,
options));
}
else
{
builder.Services.AddSingleton<IFeatureServiceProvider<TService>>(sp => new FeatureServiceProvider<TService, TEnabled, TDisabled>(
sp,
sp.GetRequiredService<IFeatureManager>(),
featureName,
options));
}

return builder;
}
}
}
78 changes: 78 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Microsoft.FeatureManagement
{
/// <inheritdoc/>
internal class FeatureServiceProvider<TService, TEnabled, TDisabled> : IFeatureServiceProvider<TService>
where TService : class
where TEnabled : class, TService
where TDisabled : class, TService
{
private readonly IServiceProvider _serviceProvider;
private readonly IFeatureManager _featureManager;
private readonly string _featureName;
private readonly FeatureServiceProviderOptions _options;
private TService _enabledService;
private TService _disabledService;

/// <summary>
/// Creates a feature service provider.
/// </summary>
/// <param name="serviceProvider">The service provider used to resolve implementation variants of TService. If it implements <see cref="IKeyedServiceProvider"/>, keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated.</param>
/// <param name="featureManager">The feature manager to get the assigned variant of the feature flag.</param>
/// <param name="featureName">The feature flag that should be used to determine which variant of the service should be used.</param>
/// <param name="options">Options used to configure the feature service provider.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="serviceProvider"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureManager"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureName"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="options"/> is null.</exception>
public FeatureServiceProvider(IServiceProvider serviceProvider, IFeatureManager featureManager, string featureName, FeatureServiceProviderOptions options)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
_featureName = featureName ?? throw new ArgumentNullException(nameof(featureName));
_options = options ?? throw new ArgumentNullException(nameof(options));
}

/// <inheritdoc/>
public ValueTask<TService> GetServiceAsync()
{
return GetServiceAsync<object>(null);
}

/// <inheritdoc/>
public async ValueTask<TService> GetServiceAsync<TContext>(TContext context)
{
var isEnabled = await _featureManager.IsEnabledAsync(_featureName, context);

var implementation = isEnabled ? _enabledService : _disabledService;
if (implementation != null)
{
return implementation;
}

//
// If the service provider supports keyed services, try to resolve the implementation by the configured key first.
// This allows lazy instantiation of the feature service.
if (_serviceProvider is IKeyedServiceProvider keyedServiceProvider)
{
implementation = isEnabled
? _enabledService ??= keyedServiceProvider.GetKeyedService<TService>(_options.EnabledKey)
: _disabledService ??= keyedServiceProvider.GetKeyedService<TService>(_options.DisabledKey);
if (implementation != null)
{
return implementation;
}
}

//
// Fall back to enumerating all non-keyed registrations of TService and matching by the implementation type.
return isEnabled
? _enabledService ??= _serviceProvider.GetServices<TService>().OfType<TEnabled>().FirstOrDefault()
: _disabledService ??= _serviceProvider.GetServices<TService>().OfType<TDisabled>().FirstOrDefault();
}
}
}
18 changes: 18 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureServiceProviderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Microsoft.FeatureManagement
{
/// <summary>
/// Specifies the keys used by a feature service provider to resolve an implementation based on the feature flag status when keyed di is available.
/// </summary>
public class FeatureServiceProviderOptions
{
/// <summary>
/// The key used to resolve the service when the feature flag is enabled and keyed di is available.
/// </summary>
public object EnabledKey { get; set; } = true;

/// <summary>
/// The alias used to resolve the service when the feature flag is disabled and keyed di is available.
/// </summary>
public object DisabledKey { get; set; } = false;
}
}
23 changes: 23 additions & 0 deletions src/Microsoft.FeatureManagement/IFeatureServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Threading.Tasks;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Used to get TService implementation based on the feature status.
/// </summary>
public interface IFeatureServiceProvider<TService> where TService : class
{
/// <summary>
/// Gets an implementation of TService.
/// </summary>
/// <returns>An implementation of TService.</returns>
ValueTask<TService> GetServiceAsync();

/// <summary>
/// Gets an implementation of TService with additional usage of the feature filters.
/// </summary>
/// <param name="context">A context that provides information to evaluate whether a feature should be on or off.</param>
/// <returns>An implementation of TService.</returns>
ValueTask<TService> GetServiceAsync<TContext>(TContext context);
}
}
115 changes: 115 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2385,6 +2385,121 @@ public async Task VariantServiceProviderPrefersKeyedOverNonKeyed()
Assert.Equal("KeyedBeta", algorithm.Style);
}

[Fact]
public async Task VariantServiceProviderStatusEagerResolution()
{
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("DotnetFeatureManagementSchema.json")
.Build();

//
// OnTestFeature has no variants and is always enabled; OffTestFeature has none and is always disabled.
// The provider should fall back to the EnabledKey / DisabledKey respectively.
IServiceCollection services = new ServiceCollection();

services.AddSingleton<IAlgorithm, AlgorithmBeta>();
services.AddSingleton<IAlgorithm, AlgorithmSigma>();

services.AddSingleton(configuration)
.AddFeatureManagement()
.WithFeatureService<IAlgorithm, AlgorithmBeta, AlgorithmSigma>(Features.OnTestFeature);

IAlgorithm algorithm = await services.BuildServiceProvider()
.GetRequiredService<IFeatureServiceProvider<IAlgorithm>>()
.GetServiceAsync();
Assert.Equal("Beta", algorithm.Style);

services = new ServiceCollection();

services.AddSingleton<IAlgorithm, AlgorithmBeta>();
services.AddSingleton<IAlgorithm, AlgorithmSigma>();

services.AddSingleton(configuration)
.AddFeatureManagement()
.WithFeatureService<IAlgorithm, AlgorithmBeta, AlgorithmSigma>(Features.OffTestFeature);

algorithm = await services.BuildServiceProvider()
.GetRequiredService<IFeatureServiceProvider<IAlgorithm>>()
.GetServiceAsync();
Assert.Equal("Sigma", algorithm.Style);
}

[Fact]
public async Task VariantServiceProviderStatusLazyResolution()
{
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("DotnetFeatureManagementSchema.json")
.Build();

//
// OnTestFeature has no variants and is always enabled; OffTestFeature has none and is always disabled.
// The provider should fall back to the EnabledKey / DisabledKey respectively.
IServiceCollection services = new ServiceCollection();

services.AddKeyedSingleton<IAlgorithm, AlgorithmBeta>(true);
services.AddKeyedSingleton<IAlgorithm, AlgorithmSigma>(false);

services.AddSingleton(configuration)
.AddFeatureManagement()
.WithFeatureService<IAlgorithm, AlgorithmBeta, AlgorithmSigma>(Features.OnTestFeature);

IAlgorithm algorithm = await services.BuildServiceProvider()
.GetRequiredService<IFeatureServiceProvider<IAlgorithm>>()
.GetServiceAsync();
Assert.Equal("Beta", algorithm.Style);

services = new ServiceCollection();

services.AddKeyedSingleton<IAlgorithm, AlgorithmBeta>(true);
services.AddKeyedSingleton<IAlgorithm, AlgorithmSigma>(false);

services.AddSingleton(configuration)
.AddFeatureManagement()
.WithFeatureService<IAlgorithm, AlgorithmBeta, AlgorithmSigma>(Features.OffTestFeature);

algorithm = await services.BuildServiceProvider()
.GetRequiredService<IFeatureServiceProvider<IAlgorithm>>()
.GetServiceAsync();
Assert.Equal("Sigma", algorithm.Style);
}

[Fact]
public async Task VariantServiceProviderStatusLazyResolutionWithContextualFeatureFilter()
{
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("DotnetFeatureManagementSchema.json")
.Build();

IServiceCollection services = new ServiceCollection();

services.AddSingleton<IAlgorithm, AlgorithmBeta>();
services.AddSingleton<IAlgorithm, AlgorithmSigma>();

services.AddSingleton(configuration)
.AddFeatureManagement()
.AddFeatureFilter<StringContextualTestFilter>()
.WithFeatureService<IAlgorithm, AlgorithmBeta, AlgorithmSigma>(Features.ConditionalFeature);

ServiceProvider provider = services.BuildServiceProvider();

StringContextualTestFilter contextualTestFeatureFilter = provider.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>().OfType<StringContextualTestFilter>().First();

contextualTestFeatureFilter.ContextualCallback = (ctx, stringContext) =>
{
var stringValue = ctx.Parameters.GetValue<string>("P1");

return stringValue == stringContext;
};

IFeatureServiceProvider<IAlgorithm> featureAlgorithm = provider.GetRequiredService<IFeatureServiceProvider<IAlgorithm>>();

IAlgorithm algorithm = await featureAlgorithm.GetServiceAsync("V1");
Assert.Equal("Beta", algorithm.Style);

algorithm = await featureAlgorithm.GetServiceAsync("V2");
Assert.Equal("Sigma", algorithm.Style);
}

[Fact]
public async Task VariantFeatureFlagWithContextualFeatureFilter()
{
Expand Down
20 changes: 20 additions & 0 deletions tests/Tests.FeatureManagement/StringContextualTestFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.FeatureManagement;
using System;
using System.Threading.Tasks;

namespace Tests.FeatureManagement
{
[FilterAlias("Test")]
class StringContextualTestFilter : IContextualFeatureFilter<string>
{
public Func<FeatureFilterEvaluationContext, string, bool> ContextualCallback { get; set; }

public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, string stringContext)
{
return Task.FromResult(ContextualCallback?.Invoke(context, stringContext) ?? false);
}
}
}