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 @@ -47,6 +47,21 @@ public static IFeatureManagementBuilder WithTargeting<T>(this IFeatureManagement
/// <exception cref="ArgumentNullException">Thrown if feature name parameter is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if a variant service of the type has already been added.</exception>
public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatureManagementBuilder builder, string featureName) where TService : class
{
return WithVariantService<TService>(builder, featureName, VariantServiceMatchMode.Variant);
}

/// <summary>
/// Adds a <see cref="VariantServiceProvider{TService}"/> to the feature management system that selects the implementation
/// by either the assigned variant name or the enabled status of the feature flag.
/// </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.</param>
/// <param name="matchMode">Describes whether the implementation is matched by variant name or by feature flag status.</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 variant service of the type has already been added.</exception>
public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatureManagementBuilder builder, string featureName, VariantServiceMatchMode matchMode) where TService : class
{
if (string.IsNullOrEmpty(featureName))
{
Expand All @@ -63,14 +78,16 @@ public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatu
builder.Services.AddScoped<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp));
sp,
matchMode));
}
else
{
builder.Services.AddSingleton<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp));
sp,
matchMode));
}

return builder;
Expand Down
27 changes: 22 additions & 5 deletions src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Allows the name of a variant service to be customized to relate to the variant name specified in configuration.
/// Allows a variant service implementation to be associated with either the assigned variant name
/// or the enabled status of the bound feature flag.
/// </summary>
public class VariantServiceAliasAttribute : Attribute
{
/// <summary>
/// Creates a variant service alias using the provided alias.
/// Creates a variant service alias that matches the assigned variant name of the feature flag.
/// </summary>
/// <param name="alias">The alias of the variant service.</param>
/// <param name="alias">The alias of the variant service. Used to match the assigned variant name specified in the configuration.</param>
public VariantServiceAliasAttribute(string alias)
{
if (string.IsNullOrEmpty(alias))
Expand All @@ -22,11 +23,27 @@ public VariantServiceAliasAttribute(string alias)
}

Alias = alias;
MatchMode = VariantServiceMatchMode.Variant;
}

/// <summary>
/// The name that will be used to match variant name specified in the configuration.
/// Creates a variant service alias that matches the enabled status of the feature flag.
/// </summary>
/// <param name="enabled">Whether the variant service should be selected when the feature flag is enabled (<c>true</c>) or disabled (<c>false</c>).</param>
public VariantServiceAliasAttribute(bool enabled)
{
Alias = enabled ? bool.TrueString : bool.FalseString;
MatchMode = VariantServiceMatchMode.Status;
}

/// <summary>
/// The name that will be used to match either the assigned variant name or the enabled status of the feature flag.
/// </summary>
public string Alias { get; }

/// <summary>
/// Describes whether the implementation is matched by variant name or by feature flag status.
/// </summary>
public VariantServiceMatchMode MatchMode { get; }
}
}
21 changes: 21 additions & 0 deletions src/Microsoft.FeatureManagement/VariantServiceMatchMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
namespace Microsoft.FeatureManagement
{
/// <summary>
/// Describes how a variant service implementation is selected from the bound feature flag.
/// </summary>
public enum VariantServiceMatchMode
{
/// <summary>
/// The implementation is selected based on the assigned variant name of the feature flag.
/// </summary>
Variant = 0,

/// <summary>
/// The implementation is selected based on the enabled status of the feature flag.
/// </summary>
Status = 1
}
}
98 changes: 81 additions & 17 deletions src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -13,17 +13,18 @@
namespace Microsoft.FeatureManagement
{
/// <summary>
/// Used to get different implementations of TService depending on the assigned variant from a specific variant feature flag.
/// Used to get different implementations of TService depending on either the assigned variant or the enabled status of a feature flag.
/// </summary>
internal class VariantServiceProvider<TService> : IVariantServiceProvider<TService> where TService : class
{
private readonly IServiceProvider _serviceProvider;
private readonly IVariantFeatureManager _featureManager;
private readonly string _featureName;
private readonly VariantServiceMatchMode _matchMode;
private readonly ConcurrentDictionary<string, TService> _variantServiceCache;

/// <summary>
/// Creates a variant service provider.
/// Creates a variant service provider that selects an implementation based on the assigned variant of the feature flag.
/// </summary>
/// <param name="featureName">The feature flag that should be used to determine which variant of the service should be used.</param>
/// <param name="featureManager">The feature manager to get the assigned variant of the feature flag.</param>
Expand All @@ -32,37 +33,62 @@ internal class VariantServiceProvider<TService> : IVariantServiceProvider<TServi
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureManager"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="serviceProvider"/> is null.</exception>
public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider)
: this(featureName, featureManager, serviceProvider, VariantServiceMatchMode.Variant)
{
}

/// <summary>
/// Creates a variant service provider that selects an implementation based on the assigned variant or the enabled status of the feature flag.
/// </summary>
/// <param name="featureName">The feature flag that should be used to determine which variant of the service should be used.</param>
/// <param name="featureManager">The feature manager to evaluate the feature flag.</param>
/// <param name="serviceProvider">The service provider used to resolve implementation variants of TService.</param>
/// <param name="matchMode">Describes whether the implementation is matched by variant name or by feature flag status.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureName"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureManager"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="serviceProvider"/> is null.</exception>
public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider, VariantServiceMatchMode matchMode)
{
_featureName = featureName ?? throw new ArgumentNullException(nameof(featureName));
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_matchMode = matchMode;
_variantServiceCache = new ConcurrentDictionary<string, TService>();
}

/// <summary>
/// Gets implementation of TService according to the assigned variant from the feature flag.
/// Gets the implementation of TService matched against the assigned variant or the enabled status of the feature flag.
/// </summary>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>An implementation matched with the assigned variant. If there is no matched implementation, it will return null.</returns>
/// <returns>An implementation matched with the assigned variant or status. If there is no matched implementation, it will return null.</returns>
public async ValueTask<TService> GetServiceAsync(CancellationToken cancellationToken)
{
Debug.Assert(_featureName != null);

Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken);
if (_matchMode == VariantServiceMatchMode.Status)
{
bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken);

string statusKey = isEnabled ? bool.TrueString : bool.FalseString;

TService implementation = null;
return _variantServiceCache.GetOrAdd(
statusKey,
(_) => ResolveByStatus(isEnabled));
}

Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken);

if (variant != null)
if (variant == null)
{
implementation = _variantServiceCache.GetOrAdd(
variant.Name,
(variantName) => ResolveVariantService(variantName));
return null;
}

return implementation;
return _variantServiceCache.GetOrAdd(
variant.Name,
(variantName) => ResolveByVariant(variantName));
}

private TService ResolveVariantService(string variantName)
private TService ResolveByVariant(string variantName)
{
if (_serviceProvider is IKeyedServiceProvider)
{
Expand All @@ -80,16 +106,54 @@ private TService ResolveVariantService(string variantName)
service => IsMatchingVariantName(service.GetType(), variantName));
}

private bool IsMatchingVariantName(Type implementationType, string variantName)
private TService ResolveByStatus(bool enabled)
{
string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias;
if (_serviceProvider is IKeyedServiceProvider)
{
TService keyedService = _serviceProvider.GetKeyedService<TService>(enabled);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if i want to register my feature services against other keys?


if (implementationName == null)
if (keyedService != null)
{
return keyedService;
}
}

IEnumerable<TService> services = _serviceProvider.GetRequiredService<IEnumerable<TService>>();

return services.FirstOrDefault(
service => IsMatchingStatus(service.GetType(), enabled));
}

private static bool IsMatchingVariantName(Type implementationType, string variantName)
{
var attribute = (VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute));

//
// Implementations explicitly declared as status-bound do not participate in variant-name matching.
if (attribute != null && attribute.MatchMode == VariantServiceMatchMode.Status)
{
implementationName = implementationType.Name;
return false;
}

string implementationName = attribute?.Alias ?? implementationType.Name;

return string.Equals(implementationName, variantName, StringComparison.OrdinalIgnoreCase);
}

private static bool IsMatchingStatus(Type implementationType, bool enabled)
{
var attribute = (VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute));

//
// Only implementations explicitly declared as status-bound participate in status matching.
if (attribute == null || attribute.MatchMode != VariantServiceMatchMode.Status)
{
return false;
}

string expected = enabled ? bool.TrueString : bool.FalseString;

return string.Equals(attribute.Alias, expected, StringComparison.OrdinalIgnoreCase);
}
}
}
110 changes: 110 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2385,6 +2385,116 @@ public async Task VariantServiceProviderPrefersKeyedOverNonKeyed()
Assert.Equal("KeyedBeta", algorithm.Style);
}

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

IServiceCollection enabledServices = new ServiceCollection();

enabledServices.AddKeyedSingleton<IAlgorithm, EnabledAlgorithm>(true);
enabledServices.AddKeyedSingleton<IAlgorithm, DisabledAlgorithm>(false);

enabledServices.AddSingleton(configuration)
.AddFeatureManagement()
.WithVariantService<IAlgorithm>(Features.OnTestFeature, VariantServiceMatchMode.Status);

ServiceProvider enabledServiceProvider = enabledServices.BuildServiceProvider();

IVariantServiceProvider<IAlgorithm> enabledFeaturedAlgorithm = enabledServiceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>();

IAlgorithm algorithm = await enabledFeaturedAlgorithm.GetServiceAsync(CancellationToken.None);
Assert.NotNull(algorithm);
Assert.Equal("Enabled", algorithm.Style);

IServiceCollection disabledServices = new ServiceCollection();

disabledServices.AddKeyedSingleton<IAlgorithm, EnabledAlgorithm>(true);
disabledServices.AddKeyedSingleton<IAlgorithm, DisabledAlgorithm>(false);

disabledServices.AddSingleton(configuration)
.AddFeatureManagement()
.WithVariantService<IAlgorithm>(Features.OffTestFeature, VariantServiceMatchMode.Status);

ServiceProvider disabledServiceProvider = disabledServices.BuildServiceProvider();

IVariantServiceProvider<IAlgorithm> disabledFeaturedAlgorithm = disabledServiceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>();

algorithm = await disabledFeaturedAlgorithm.GetServiceAsync(CancellationToken.None);
Assert.NotNull(algorithm);
Assert.Equal("Disabled", algorithm.Style);
}

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

IServiceCollection enabledServices = new ServiceCollection();

//
// Non-keyed registrations; matching relies on [VariantServiceAlias(bool)] attributes.
enabledServices.AddSingleton<IAlgorithm, EnabledAlgorithm>();
enabledServices.AddSingleton<IAlgorithm, DisabledAlgorithm>();

enabledServices.AddSingleton(configuration)
.AddFeatureManagement()
.WithVariantService<IAlgorithm>(Features.OnTestFeature, VariantServiceMatchMode.Status);

ServiceProvider enabledServiceProvider = enabledServices.BuildServiceProvider();

IAlgorithm algorithm = await enabledServiceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>()
.GetServiceAsync(CancellationToken.None);
Assert.NotNull(algorithm);
Assert.Equal("Enabled", algorithm.Style);

IServiceCollection disabledServices = new ServiceCollection();

disabledServices.AddSingleton<IAlgorithm, EnabledAlgorithm>();
disabledServices.AddSingleton<IAlgorithm, DisabledAlgorithm>();

disabledServices.AddSingleton(configuration)
.AddFeatureManagement()
.WithVariantService<IAlgorithm>(Features.OffTestFeature, VariantServiceMatchMode.Status);

ServiceProvider disabledServiceProvider = disabledServices.BuildServiceProvider();

algorithm = await disabledServiceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>()
.GetServiceAsync(CancellationToken.None);
Assert.NotNull(algorithm);
Assert.Equal("Disabled", algorithm.Style);
}

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

IServiceCollection services = new ServiceCollection();

//
// Only variant-bound implementations registered; none of them should match in Status mode.
services.AddSingleton<IAlgorithm, AlgorithmBeta>();
services.AddSingleton<IAlgorithm, AlgorithmSigma>();

services.AddSingleton(configuration)
.AddFeatureManagement()
.WithVariantService<IAlgorithm>(Features.OnTestFeature, VariantServiceMatchMode.Status);

ServiceProvider serviceProvider = services.BuildServiceProvider();

IAlgorithm algorithm = await serviceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>()
.GetServiceAsync(CancellationToken.None);

Assert.Null(algorithm);
}

[Fact]
public async Task VariantFeatureFlagWithContextualFeatureFilter()
{
Expand Down
Loading