Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature-based Injection #335

Merged
merged 31 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
57d2243
init
zhiyuanliang-ms Dec 19, 2023
f9506c9
draft
zhiyuanliang-ms Dec 19, 2023
f9c1484
Merge branch 'preview' into zhiyuanliang/feature-based-injection
zhiyuanliang-ms Dec 19, 2023
5edd9be
use ValueTask
zhiyuanliang-ms Dec 19, 2023
06f410b
Merge branch 'preview' of https://github.com/microsoft/FeatureManagem…
zhiyuanliang-ms Dec 20, 2023
e1b2450
support factory method
zhiyuanliang-ms Dec 25, 2023
f497405
add example
zhiyuanliang-ms Dec 25, 2023
2b7ad46
Merge branch 'preview' of https://github.com/microsoft/FeatureManagem…
zhiyuanliang-ms Jan 8, 2024
d89abc8
update
zhiyuanliang-ms Jan 9, 2024
ca772ec
Update
zhiyuanliang-ms Jan 9, 2024
3710dab
Merge branch 'preview' into zhiyuanliang/feature-based-injection
zhiyuanliang-ms Jan 9, 2024
b92fefb
Update
zhiyuanliang-ms Jan 9, 2024
f3926f8
update example
zhiyuanliang-ms Jan 9, 2024
0d0f3f3
match variant name or configuration value
zhiyuanliang-ms Jan 9, 2024
176cf78
update to the latest design
zhiyuanliang-ms Jan 10, 2024
a7b8f0f
merge with preview branch
zhiyuanliang-ms Jan 18, 2024
73aac10
resolve comments
zhiyuanliang-ms Jan 18, 2024
25f91e9
remove check for variant value
zhiyuanliang-ms Jan 19, 2024
3ed4c4c
rename to VariantService
zhiyuanliang-ms Jan 19, 2024
941c7a1
update & add comments
zhiyuanliang-ms Jan 19, 2024
18ae0eb
remove POC example
zhiyuanliang-ms Jan 19, 2024
f48767c
add testcases & use method name GetServiceAsync
zhiyuanliang-ms Jan 22, 2024
1556c0d
update comments
zhiyuanliang-ms Jan 22, 2024
7120abd
add variant service cache
zhiyuanliang-ms Jan 29, 2024
1a4e0fa
resolve comments
zhiyuanliang-ms Jan 31, 2024
926182a
throw exception for duplicated registration
zhiyuanliang-ms Feb 1, 2024
1065d76
add testcase
zhiyuanliang-ms Feb 1, 2024
37cda1b
remove unused package
zhiyuanliang-ms Feb 7, 2024
df47105
update comment
zhiyuanliang-ms Feb 7, 2024
cbda0c6
set feature name in constructor
zhiyuanliang-ms Feb 9, 2024
6488a81
Merge branch 'preview' into zhiyuanliang/feature-based-injection
zhiyuanliang-ms Feb 9, 2024
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 @@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved

namespace Microsoft.FeatureManagement
{
Expand Down Expand Up @@ -39,6 +40,48 @@ public static class FeatureManagementBuilderExtensions
return builder;
}

/// <summary>
/// Adds a <see cref="VariantServiceProvider{TService}"/> 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 variant of the service should be used. The <see cref="VariantServiceProvider{TService}"/> will return different implementations of TService according to the assigned variant.</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 the variant service of the type has been added.</exception>
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatureManagementBuilder builder, string featureName) where TService : class
{
if (string.IsNullOrEmpty(featureName))
{
throw new ArgumentNullException(nameof(featureName));
}

if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IVariantServiceProvider<TService>)))
{
throw new InvalidOperationException($"Variant services of {typeof(TService)} has been added.");
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
}

if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped))
{
builder.Services.AddScoped<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved
sp.GetRequiredService<IEnumerable<TService>>(),
sp.GetRequiredService<IVariantFeatureManager>())
{
FeatureName = featureName,
});
}
else
{
builder.Services.AddSingleton<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
sp.GetRequiredService<IEnumerable<TService>>(),
sp.GetRequiredService<IVariantFeatureManager>())
{
FeatureName = featureName,
});
}

return builder;
}

/// <summary>
/// Adds a telemetry publisher to the feature management system.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.FeatureManagement/IVariantServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Used to get different implementation variants of TService.
/// </summary>
public interface IVariantServiceProvider<TService> where TService : class
{
/// <summary>
/// Gets an implementation variant of TService.
/// </summary>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>An implementation of TService.</returns>
ValueTask<TService> GetServiceAsync(CancellationToken cancellationToken);
}
}
32 changes: 32 additions & 0 deletions src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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.
/// </summary>
public class VariantServiceAliasAttribute : Attribute
{
/// <summary>
/// Creates a variant service alias using the provided alias.
/// </summary>
/// <param name="alias">The alias of the variant service.</param>
public VariantServiceAliasAttribute(string alias)
{
if (string.IsNullOrEmpty(alias))
{
throw new ArgumentNullException(nameof(alias));
}

Alias = alias;
}

/// <summary>
/// The name that will be used to match variant name specified in the configuration.
/// </summary>
public string Alias { get; }
}
}
90 changes: 90 additions & 0 deletions src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Used to get different implementations of TService depending on the assigned variant from a specific variant feature flag.
/// </summary>
internal class VariantServiceProvider<TService> : IVariantServiceProvider<TService> where TService : class
{
private readonly IEnumerable<TService> _services;
private readonly IVariantFeatureManager _featureManager;
private readonly string _featureName;
private readonly ConcurrentDictionary<string, TService> _variantServiceCache;

/// <summary>
/// Creates a variant service provider.
/// </summary>
/// <param name="services">Implementation variants of TService.</param>
/// <param name="featureManager">Feature manager to get the assigned variant of the variant feature flag.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="services"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureManager"/> is null.</exception>
public VariantServiceProvider(IEnumerable<TService> services, IVariantFeatureManager featureManager)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
_variantServiceCache = new ConcurrentDictionary<string, TService>();
}

/// <summary>
/// The variant feature flag used to assign variants.
/// </summary>
public string FeatureName
{
get => _featureName;

init
{
_featureName = value ?? throw new ArgumentNullException(nameof(value));
}
}

/// <summary>
/// Gets implementation of TService according to the assigned variant from 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>
public async ValueTask<TService> GetServiceAsync(CancellationToken cancellationToken)
{
Debug.Assert(_featureName != null);

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

TService implementation = null;

if (variant != null)
{
implementation = _variantServiceCache.GetOrAdd(
variant.Name,
(_) => _services.FirstOrDefault(
service => IsMatchingVariantName(
service.GetType(),
variant.Name))
);
}

return implementation;
}

private bool IsMatchingVariantName(Type implementationType, string variantName)
{
string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias;

if (implementationName == null)
{
implementationName = implementationType.Name;
}

return string.Equals(implementationName, variantName, StringComparison.OrdinalIgnoreCase);
}
}
}
77 changes: 77 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1262,5 +1262,82 @@ public async Task VariantsInvalidScenarios()
Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error);
Assert.Contains(ConfigurationFields.PercentileAllocationFrom, e.Message);
}

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

IServiceCollection services = new ServiceCollection();

services.AddSingleton<IAlgorithm, AlgorithmBeta>();
services.AddSingleton<IAlgorithm, AlgorithmSigma>();
services.AddSingleton<IAlgorithm>(sp => new AlgorithmOmega("OMEGA"));

services.AddSingleton(configuration)
.AddFeatureManagement()
.AddFeatureFilter<TargetingFilter>()
.WithVariantService<IAlgorithm>(Features.VariantImplementationFeature);

var targetingContextAccessor = new OnDemandTargetingContextAccessor();

services.AddSingleton<ITargetingContextAccessor>(targetingContextAccessor);

ServiceProvider serviceProvider = services.BuildServiceProvider();

IVariantFeatureManager featureManager = serviceProvider.GetRequiredService<IVariantFeatureManager>();

IVariantServiceProvider<IAlgorithm> featuredAlgorithm = serviceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>();

targetingContextAccessor.Current = new TargetingContext
{
UserId = "Guest"
};

IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);

Assert.Null(algorithm);

targetingContextAccessor.Current = new TargetingContext
{
UserId = "UserSigma"
};

algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);

Assert.Null(algorithm);

targetingContextAccessor.Current = new TargetingContext
{
UserId = "UserBeta"
};

algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);

Assert.NotNull(algorithm);
Assert.Equal("Beta", algorithm.Style);

targetingContextAccessor.Current = new TargetingContext
{
UserId = "UserOmega"
};

algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);

Assert.NotNull(algorithm);
Assert.Equal("OMEGA", algorithm.Style);

services = new ServiceCollection();

Assert.Throws<InvalidOperationException>(() =>
{
services.AddFeatureManagement()
.WithVariantService<IAlgorithm>("DummyFeature1")
.WithVariantService<IAlgorithm>("DummyFeature2");
}
);
}
}
}
1 change: 1 addition & 0 deletions tests/Tests.FeatureManagement/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ static class Features
public const string VariantFeatureBothConfigurations = "VariantFeatureBothConfigurations";
public const string VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride";
public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo";
public const string VariantImplementationFeature = "VariantImplementationFeature";
}
}
40 changes: 40 additions & 0 deletions tests/Tests.FeatureManagement/VariantServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.FeatureManagement;

namespace Tests.FeatureManagement
{
interface IAlgorithm
{
public string Style { get; }
}

class AlgorithmBeta : IAlgorithm
{
public string Style { get; set; }

public AlgorithmBeta()
{
Style = "Beta";
}
}

class AlgorithmSigma : IAlgorithm
{
public string Style { get; set; }

public AlgorithmSigma()
{
Style = "Sigma";
}
}

[VariantServiceAlias("Omega")]
class AlgorithmOmega : IAlgorithm
{
public string Style { get; set; }

public AlgorithmOmega(string style)
{
Style = style;
}
}
}
48 changes: 48 additions & 0 deletions tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,54 @@
"Name": "On"
}
]
},
"VariantImplementationFeature": {
"EnabledFor": [
{
"Name": "Targeting",
"Parameters": {
"Audience": {
"Users": [
"UserOmega", "UserSigma", "UserBeta"
]
}
}
}
],
"Variants": [
{
"Name": "AlgorithmBeta"
},
{
"Name": "Sigma",
"ConfigurationValue": "AlgorithmSigma"
},
{
"Name": "Omega"
}
],
"Allocation": {
"User": [
{
"Variant": "AlgorithmBeta",
"Users": [
"UserBeta"
]
},
{
"Variant": "Omega",
"Users": [
"UserOmega"
]
},
{
"Variant": "Sigma",
"Users": [
"UserSigma"
]
}
]
}
}
}
}