From 57d224312678fed9fac8d7c3342ea84b51e7896b Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 19 Dec 2023 17:26:32 +0800 Subject: [PATCH 01/25] init --- .../FeaturedImplementationWrapper.cs | 16 +++++ .../FeaturedService.cs | 71 +++++++++++++++++++ .../IFeaturedService.cs | 13 ++++ .../ServiceCollectionExtensions.cs | 24 +++++++ 4 files changed, 124 insertions(+) create mode 100644 src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs create mode 100644 src/Microsoft.FeatureManagement/FeaturedService.cs create mode 100644 src/Microsoft.FeatureManagement/IFeaturedService.cs diff --git a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs b/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs new file mode 100644 index 00000000..479f7947 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + internal class FeaturedServiceImplementationWrapper + { + public TService Implementation { get; init; } + + public string FeatureName { get; init; } + + public string VariantName { get; init; } + + public bool ForVariant { get; init; } + } +} diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs new file mode 100644 index 00000000..773b37da --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + internal class FeaturedService : IFeaturedService + { + TService _defaultImplementation; + + IEnumerable> _services; + + FeaturedServiceImplementationWrapper _featureBasedImplementation; + + IEnumerable> _variantBasedImplementation; + + IVariantFeatureManager _featureManager; + + string _feature; + + public FeaturedService(TService defaultImplementation, IEnumerable> services, IVariantFeatureManager featureManager) + { + if (!services.All(s => s.FeatureName == services.First().FeatureName)) + { + throw new ArgumentException(); + } + + _defaultImplementation = defaultImplementation; + + _feature = services.First()?.FeatureName; + + _services = services; + + _featureBasedImplementation = _services.FirstOrDefault(s => s.ForVariant == false); + + _variantBasedImplementation = _services.Where(s => s.ForVariant); + + _featureManager = featureManager; + } + + public async Task GetAsync(CancellationToken cancellationToken) + { + bool isEnabled = await _featureManager.IsEnabledAsync(_feature, cancellationToken); + + Variant variant = await _featureManager.GetVariantAsync(_feature, cancellationToken); + + if (variant != null) + { + foreach (var item in _variantBasedImplementation) + { + if(item.VariantName == variant.Name) + { + return item.Implementation; + } + } + } + + if (isEnabled) + { + return _featureBasedImplementation.Implementation; + } + + return _defaultImplementation; + } + } +} diff --git a/src/Microsoft.FeatureManagement/IFeaturedService.cs b/src/Microsoft.FeatureManagement/IFeaturedService.cs new file mode 100644 index 00000000..a4a9f194 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IFeaturedService.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + public interface IFeaturedService + { + Task GetAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index a6651e59..89fabe42 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -193,5 +193,29 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService return services.AddScopedFeatureManagement(); } + + public static IServiceCollection OverrideForFeature(this IServiceCollection services, string featureName) + where TService : class + where TImplementation : class, TService + { + services.AddSingleton(); + + services.AddSingleton>(sp => new FeaturedServiceImplementationWrapper() + { + Implementation = sp.GetRequiredService(), + FeatureName = featureName, + }); + + services.TryAddSingleton, FeaturedService>(); + + return services; + } + + public static IServiceCollection OverrideForFeatureVariant(this IServiceCollection services, string featureName, string variantName) + where TService : class + where TImplementation : class, TService + { + return services; + } } } From f9506c90c9183a762251fdedf91ffed01ab6ab05 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 19 Dec 2023 21:48:00 +0800 Subject: [PATCH 02/25] draft --- .../FeaturedImplementationWrapper.cs | 2 +- .../FeaturedService.cs | 4 ++-- .../ServiceCollectionExtensions.cs | 22 ++++++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs b/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs index 479f7947..aa67bc99 100644 --- a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs +++ b/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs @@ -11,6 +11,6 @@ internal class FeaturedServiceImplementationWrapper public string VariantName { get; init; } - public bool ForVariant { get; init; } + public bool VariantBased { get; init; } } } diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index 773b37da..f3bc6a2b 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -36,9 +36,9 @@ public FeaturedService(TService defaultImplementation, IEnumerable s.ForVariant == false); + _featureBasedImplementation = _services.FirstOrDefault(s => s.VariantBased == false); - _variantBasedImplementation = _services.Where(s => s.ForVariant); + _variantBasedImplementation = _services.Where(s => s.VariantBased); _featureManager = featureManager; } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 89fabe42..0cb5a925 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -198,7 +198,9 @@ public static IServiceCollection OverrideForFeature(t where TService : class where TImplementation : class, TService { - services.AddSingleton(); + // + // lifetime should be same as feature manager + services.TryAddSingleton(); services.AddSingleton>(sp => new FeaturedServiceImplementationWrapper() { @@ -206,6 +208,8 @@ public static IServiceCollection OverrideForFeature(t FeatureName = featureName, }); + // + // lifetime should be same as feature manager services.TryAddSingleton, FeaturedService>(); return services; @@ -215,6 +219,22 @@ public static IServiceCollection OverrideForFeatureVariant(); + + services.AddSingleton>(sp => new FeaturedServiceImplementationWrapper() + { + Implementation = sp.GetRequiredService(), + FeatureName = featureName, + VariantName = variantName, + VariantBased = true + }); + + // + // lifetime should be same as feature manager + services.TryAddSingleton, FeaturedService>(); + return services; } } From 5edd9be9bd1c00e253858c2a8f0f2b46679155fc Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 19 Dec 2023 22:38:52 +0800 Subject: [PATCH 03/25] use ValueTask --- src/Microsoft.FeatureManagement/FeaturedService.cs | 2 +- src/Microsoft.FeatureManagement/IFeaturedService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index f3bc6a2b..a4a45dd4 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -43,7 +43,7 @@ public FeaturedService(TService defaultImplementation, IEnumerable GetAsync(CancellationToken cancellationToken) + public async ValueTask GetAsync(CancellationToken cancellationToken) { bool isEnabled = await _featureManager.IsEnabledAsync(_feature, cancellationToken); diff --git a/src/Microsoft.FeatureManagement/IFeaturedService.cs b/src/Microsoft.FeatureManagement/IFeaturedService.cs index a4a9f194..903bf6a2 100644 --- a/src/Microsoft.FeatureManagement/IFeaturedService.cs +++ b/src/Microsoft.FeatureManagement/IFeaturedService.cs @@ -8,6 +8,6 @@ namespace Microsoft.FeatureManagement { public interface IFeaturedService { - Task GetAsync(CancellationToken cancellationToken); + ValueTask GetAsync(CancellationToken cancellationToken); } } From e1b2450514f48d3c1df409fa66a9a6900aa19816 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 25 Dec 2023 11:41:31 +0800 Subject: [PATCH 04/25] support factory method --- .../FeaturedService.cs | 18 +++---- .../ServiceCollectionExtensions.cs | 48 ++++++++++++++++++- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index a4a45dd4..1b8330ed 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -11,17 +11,17 @@ namespace Microsoft.FeatureManagement { internal class FeaturedService : IFeaturedService { - TService _defaultImplementation; + private TService _defaultImplementation; - IEnumerable> _services; + private IEnumerable> _services; - FeaturedServiceImplementationWrapper _featureBasedImplementation; + private FeaturedServiceImplementationWrapper _featureBasedImplementation; - IEnumerable> _variantBasedImplementation; + private IEnumerable> _variantBasedImplementation; - IVariantFeatureManager _featureManager; + private IVariantFeatureManager _featureManager; - string _feature; + private string _featureName; public FeaturedService(TService defaultImplementation, IEnumerable> services, IVariantFeatureManager featureManager) { @@ -32,7 +32,7 @@ public FeaturedService(TService defaultImplementation, IEnumerable GetAsync(CancellationToken cancellationToken) { - bool isEnabled = await _featureManager.IsEnabledAsync(_feature, cancellationToken); + bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); - Variant variant = await _featureManager.GetVariantAsync(_feature, cancellationToken); + Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); if (variant != null) { diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 0cb5a925..6620149a 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -202,7 +202,28 @@ public static IServiceCollection OverrideForFeature(t // lifetime should be same as feature manager services.TryAddSingleton(); - services.AddSingleton>(sp => new FeaturedServiceImplementationWrapper() + services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() + { + Implementation = sp.GetRequiredService(), + FeatureName = featureName, + }); + + // + // lifetime should be same as feature manager + services.TryAddSingleton, FeaturedService>(); + + return services; + } + + public static IServiceCollection OverrideForFeature(this IServiceCollection services,string featureName, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // + // lifetime should be same as feature manager + services.TryAddSingleton(implementationFactory); + + services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() { Implementation = sp.GetRequiredService(), FeatureName = featureName, @@ -223,7 +244,30 @@ public static IServiceCollection OverrideForFeatureVariant(); - services.AddSingleton>(sp => new FeaturedServiceImplementationWrapper() + services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() + { + Implementation = sp.GetRequiredService(), + FeatureName = featureName, + VariantName = variantName, + VariantBased = true + }); + + // + // lifetime should be same as feature manager + services.TryAddSingleton, FeaturedService>(); + + return services; + } + + public static IServiceCollection OverrideForFeatureVariant(this IServiceCollection services, string featureName, string variantName, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // + // lifetime should be same as feature manager + services.TryAddSingleton(implementationFactory); + + services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() { Implementation = sp.GetRequiredService(), FeatureName = featureName, From f4974054e4a726622003eb56554070fa4243aba2 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 25 Dec 2023 13:52:30 +0800 Subject: [PATCH 05/25] add example --- Microsoft.FeatureManagement.sln | 7 ++ .../AlgorithmAlpha.cs | 12 ++++ .../FeatureBasedInjectionPOC/AlgorithmBeta.cs | 12 ++++ .../AlgorithmOmega.cs | 12 ++++ .../AlgorithmSigma.cs | 12 ++++ .../FeatureBasedInjectionPOC.csproj | 25 +++++++ .../FeatureBasedInjectionPOC/IAlgorithm.cs | 7 ++ .../OnDemandTargetingContextAccessor.cs | 14 ++++ examples/FeatureBasedInjectionPOC/Program.cs | 72 +++++++++++++++++++ .../FeatureBasedInjectionPOC/appsettings.json | 42 +++++++++++ 10 files changed, 215 insertions(+) create mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs create mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs create mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs create mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs create mode 100644 examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj create mode 100644 examples/FeatureBasedInjectionPOC/IAlgorithm.cs create mode 100644 examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs create mode 100644 examples/FeatureBasedInjectionPOC/Program.cs create mode 100644 examples/FeatureBasedInjectionPOC/appsettings.json diff --git a/Microsoft.FeatureManagement.sln b/Microsoft.FeatureManagement.sln index 8963b77b..5880e42d 100644 --- a/Microsoft.FeatureManagement.sln +++ b/Microsoft.FeatureManagement.sln @@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.FeatureManagement EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EvaluationDataToApplicationInsights", "examples\EvaluationDataToApplicationInsights\EvaluationDataToApplicationInsights.csproj", "{1502529E-47E9-4306-98C4-BF6CF7C7C275}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureBasedInjectionPOC", "examples\FeatureBasedInjectionPOC\FeatureBasedInjectionPOC.csproj", "{6655016D-900B-42D2-AA43-C33B0E161D6B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {1502529E-47E9-4306-98C4-BF6CF7C7C275}.Debug|Any CPU.Build.0 = Debug|Any CPU {1502529E-47E9-4306-98C4-BF6CF7C7C275}.Release|Any CPU.ActiveCfg = Release|Any CPU {1502529E-47E9-4306-98C4-BF6CF7C7C275}.Release|Any CPU.Build.0 = Release|Any CPU + {6655016D-900B-42D2-AA43-C33B0E161D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6655016D-900B-42D2-AA43-C33B0E161D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6655016D-900B-42D2-AA43-C33B0E161D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6655016D-900B-42D2-AA43-C33B0E161D6B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -85,6 +91,7 @@ Global {DACAB624-4611-42E8-844C-529F93A54980} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} {283D3EBB-4716-4F1D-BA51-A435F7E2AB82} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} {1502529E-47E9-4306-98C4-BF6CF7C7C275} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} + {6655016D-900B-42D2-AA43-C33B0E161D6B} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs b/examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs new file mode 100644 index 00000000..ebd563e5 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs @@ -0,0 +1,12 @@ +namespace FeatureBasedInjectionPOC +{ + internal class AlgorithmAlpha : IAlgorithm + { + public string Name { get; set; } + + public AlgorithmAlpha() + { + Name = "Alpha"; + } + } +} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs b/examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs new file mode 100644 index 00000000..3f9b75bf --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs @@ -0,0 +1,12 @@ +namespace FeatureBasedInjectionPOC +{ + internal class AlgorithmBeta : IAlgorithm + { + public string Name { get; set; } + + public AlgorithmBeta() + { + Name = "Beta"; + } + } +} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs new file mode 100644 index 00000000..216b1666 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs @@ -0,0 +1,12 @@ +namespace FeatureBasedInjectionPOC +{ + internal class AlgorithmOmega : IAlgorithm + { + public string Name { get; set; } + + public AlgorithmOmega(string name) + { + Name = name; + } + } +} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs b/examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs new file mode 100644 index 00000000..dc19ba5c --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs @@ -0,0 +1,12 @@ +namespace FeatureBasedInjectionPOC +{ + internal class AlgorithmSigma : IAlgorithm + { + public string Name { get; set; } + + public AlgorithmSigma() + { + Name = "Sigma"; + } + } +} diff --git a/examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj b/examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj new file mode 100644 index 00000000..83e2d4f1 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + Always + + + + diff --git a/examples/FeatureBasedInjectionPOC/IAlgorithm.cs b/examples/FeatureBasedInjectionPOC/IAlgorithm.cs new file mode 100644 index 00000000..a4cf4596 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/IAlgorithm.cs @@ -0,0 +1,7 @@ +namespace FeatureBasedInjectionPOC +{ + public interface IAlgorithm + { + public string Name { get; } + } +} diff --git a/examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs b/examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs new file mode 100644 index 00000000..acd78ca7 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs @@ -0,0 +1,14 @@ +using Microsoft.FeatureManagement.FeatureFilters; + +namespace FeatureBasedInjectionPOC +{ + class OnDemandTargetingContextAccessor : ITargetingContextAccessor + { + public TargetingContext Current { get; set; } + + public ValueTask GetContextAsync() + { + return new ValueTask(Current); + } + } +} diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs new file mode 100644 index 00000000..df943110 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.FeatureFilters; +using FeatureBasedInjectionPOC; + + +IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +IServiceCollection services = new ServiceCollection(); + +services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter(); + +services.AddSingleton() + .OverrideForFeature("MyFeature") + .OverrideForFeatureVariant("MyFeature", "Sigma") + .OverrideForFeatureVariant("MyFeature", "Omega", sp => new AlgorithmOmega("OMEGA")); + +var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + +services.AddSingleton(targetingContextAccessor); + +using (ServiceProvider serviceProvider = services.BuildServiceProvider()) +{ + IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + + IFeaturedService featuredAlgorithm = serviceProvider.GetRequiredService>(); + + targetingContextAccessor.Current = new TargetingContext + { + UserId = "Guest" + }; + + IAlgorithm algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); + + Console.WriteLine($"Get algorithm {algorithm.Name} because the feature flag is {await featureManager.IsEnabledAsync("MyFeature", CancellationToken.None)}"); + + targetingContextAccessor.Current = new TargetingContext + { + UserId = "UserBeta" + }; + + algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); + + Console.WriteLine($"Get algorithm {algorithm.Name} because the feature flag is {await featureManager.IsEnabledAsync("MyFeature", CancellationToken.None)}"); + + targetingContextAccessor.Current = new TargetingContext + { + UserId = "UserSigma" + }; + + algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); + + Variant variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); + + Console.WriteLine($"Get algorithm {algorithm.Name} because the feature variant is {variant.Name}"); + + targetingContextAccessor.Current = new TargetingContext + { + UserId = "UserOmega" + }; + + algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); + + variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); + + Console.WriteLine($"Get algorithm {algorithm.Name} because the feature variant is {variant.Name}"); +} diff --git a/examples/FeatureBasedInjectionPOC/appsettings.json b/examples/FeatureBasedInjectionPOC/appsettings.json new file mode 100644 index 00000000..6bf13114 --- /dev/null +++ b/examples/FeatureBasedInjectionPOC/appsettings.json @@ -0,0 +1,42 @@ +{ + "FeatureManagement": { + "MyFeature": { + "EnabledFor": [ + { + "Name": "Targeting", + "Parameters": { + "Audience": { + "Users": [ + "UserOmega", "UserSigma", "UserBeta" + ] + } + } + } + ], + "Variants": [ + { + "Name": "Omega" + }, + { + "Name": "Sigma" + } + ], + "Allocation": { + "User": [ + { + "Variant": "Omega", + "Users": [ + "UserOmega" + ] + }, + { + "Variant": "Sigma", + "Users": [ + "UserSigma" + ] + } + ] + } + } + } +} \ No newline at end of file From d89abc88fab68f95010a690436f822ec9d9f3566 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 9 Jan 2024 10:06:12 +0800 Subject: [PATCH 06/25] update --- examples/FeatureBasedInjectionPOC/Program.cs | 5 +- .../FeaturedImplementationWrapper.cs | 2 - .../FeaturedService.cs | 36 +----- .../ServiceCollectionExtensions.cs | 112 ++++++------------ .../VariantAliasAttribute.cs | 22 ++++ 5 files changed, 65 insertions(+), 112 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/VariantAliasAttribute.cs diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index df943110..82a426c1 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -15,10 +15,7 @@ .AddFeatureManagement() .AddFeatureFilter(); -services.AddSingleton() - .OverrideForFeature("MyFeature") - .OverrideForFeatureVariant("MyFeature", "Sigma") - .OverrideForFeatureVariant("MyFeature", "Omega", sp => new AlgorithmOmega("OMEGA")); +services.AddSingleton(); var targetingContextAccessor = new OnDemandTargetingContextAccessor(); diff --git a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs b/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs index aa67bc99..1e8942d7 100644 --- a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs +++ b/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs @@ -10,7 +10,5 @@ internal class FeaturedServiceImplementationWrapper public string FeatureName { get; init; } public string VariantName { get; init; } - - public bool VariantBased { get; init; } } } diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index 1b8330ed..f61843ec 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -11,47 +11,28 @@ namespace Microsoft.FeatureManagement { internal class FeaturedService : IFeaturedService { - private TService _defaultImplementation; - private IEnumerable> _services; - private FeaturedServiceImplementationWrapper _featureBasedImplementation; - - private IEnumerable> _variantBasedImplementation; - private IVariantFeatureManager _featureManager; - private string _featureName; + private string _featureName { get; init; } - public FeaturedService(TService defaultImplementation, IEnumerable> services, IVariantFeatureManager featureManager) + public FeaturedService(string featureName, IEnumerable> services, IVariantFeatureManager featureManager) { - if (!services.All(s => s.FeatureName == services.First().FeatureName)) - { - throw new ArgumentException(); - } - - _defaultImplementation = defaultImplementation; - - _featureName = services.First()?.FeatureName; + _featureName = featureName; - _services = services; - - _featureBasedImplementation = _services.FirstOrDefault(s => s.VariantBased == false); - - _variantBasedImplementation = _services.Where(s => s.VariantBased); + _services = services.Where(s => s.FeatureName.Equals(featureName)); _featureManager = featureManager; } public async ValueTask GetAsync(CancellationToken cancellationToken) { - bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); - Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); if (variant != null) { - foreach (var item in _variantBasedImplementation) + foreach (var item in _services) { if(item.VariantName == variant.Name) { @@ -60,12 +41,7 @@ public async ValueTask GetAsync(CancellationToken cancellationToken) } } - if (isEnabled) - { - return _featureBasedImplementation.Implementation; - } - - return _defaultImplementation; + return default; } } } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 6620149a..8a5a7614 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Microsoft.FeatureManagement { @@ -194,90 +195,49 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService return services.AddScopedFeatureManagement(); } - public static IServiceCollection OverrideForFeature(this IServiceCollection services, string featureName) - where TService : class - where TImplementation : class, TService + public static IServiceCollection AddSingletonForFeature(this IServiceCollection services, string featureName) { - // - // lifetime should be same as feature manager - services.TryAddSingleton(); - - services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() + var implementationTypes = Assembly.GetAssembly(typeof(TService)) + .GetTypes() + .Where(type => + typeof(TService).IsAssignableFrom(type) && + !type.IsInterface && + !type.IsAbstract); + + foreach (var implementationType in implementationTypes) { - Implementation = sp.GetRequiredService(), - FeatureName = featureName, - }); - - // - // lifetime should be same as feature manager - services.TryAddSingleton, FeaturedService>(); + services.TryAddSingleton(implementationType); - return services; - } - - public static IServiceCollection OverrideForFeature(this IServiceCollection services,string featureName, Func implementationFactory) - where TService : class - where TImplementation : class, TService - { - // - // lifetime should be same as feature manager - services.TryAddSingleton(implementationFactory); + var attribute = (VariantAliasAttribute) Attribute.GetCustomAttribute(implementationType, typeof(VariantAliasAttribute)); - services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() - { - Implementation = sp.GetRequiredService(), - FeatureName = featureName, - }); - - // - // lifetime should be same as feature manager - services.TryAddSingleton, FeaturedService>(); + if (attribute != null) + { + string variantName = attribute.Alias; + + services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() + { + Implementation = (TService) sp.GetRequiredService(implementationType), + FeatureName = featureName, + VariantName = variantName, + }); + } + } - return services; - } - public static IServiceCollection OverrideForFeatureVariant(this IServiceCollection services, string featureName, string variantName) - where TService : class - where TImplementation : class, TService - { - // - // lifetime should be same as feature manager - services.TryAddSingleton(); - - services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() + if (services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) { - Implementation = sp.GetRequiredService(), - FeatureName = featureName, - VariantName = variantName, - VariantBased = true - }); - - // - // lifetime should be same as feature manager - services.TryAddSingleton, FeaturedService>(); - - return services; - } - - public static IServiceCollection OverrideForFeatureVariant(this IServiceCollection services, string featureName, string variantName, Func implementationFactory) - where TService : class - where TImplementation : class, TService - { - // - // lifetime should be same as feature manager - services.TryAddSingleton(implementationFactory); - - services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() + services.AddScoped>(sp => new FeaturedService( + featureName, + sp.GetRequiredService>>(), + sp.GetRequiredService())); + } + else { - Implementation = sp.GetRequiredService(), - FeatureName = featureName, - VariantName = variantName, - VariantBased = true - }); - - // - // lifetime should be same as feature manager - services.TryAddSingleton, FeaturedService>(); + services.AddSingleton>(sp => new FeaturedService( + featureName, + sp.GetRequiredService>>(), + sp.GetRequiredService())); + } return services; } diff --git a/src/Microsoft.FeatureManagement/VariantAliasAttribute.cs b/src/Microsoft.FeatureManagement/VariantAliasAttribute.cs new file mode 100644 index 00000000..d733c19e --- /dev/null +++ b/src/Microsoft.FeatureManagement/VariantAliasAttribute.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; + +namespace Microsoft.FeatureManagement +{ + public class VariantAliasAttribute : Attribute + { + public VariantAliasAttribute(string alias) + { + if (string.IsNullOrEmpty(alias)) + { + throw new ArgumentNullException(nameof(alias)); + } + + Alias = alias; + } + + public string Alias { get; } + } +} From ca772ec74ddfeb4bcee75390a23eab42e6086dc1 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 9 Jan 2024 16:21:11 +0800 Subject: [PATCH 07/25] Update --- .../AlgorithmOmega.cs | 9 ++-- examples/FeatureBasedInjectionPOC/Program.cs | 16 +++++--- .../FeatureBasedInjectionPOC/appsettings.json | 5 ++- .../FeaturedService.cs | 41 +++++++++++++------ ...te.cs => FeaturedServiceAliasAttribute.cs} | 4 +- ...> FeaturedServiceImplementationWrapper.cs} | 6 +-- .../IFeaturedService.cs | 2 +- .../ServiceCollectionExtensions.cs | 22 ++++------ 8 files changed, 59 insertions(+), 46 deletions(-) rename src/Microsoft.FeatureManagement/{VariantAliasAttribute.cs => FeaturedServiceAliasAttribute.cs} (75%) rename src/Microsoft.FeatureManagement/{FeaturedImplementationWrapper.cs => FeaturedServiceImplementationWrapper.cs} (85%) diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs index 216b1666..cae5e405 100644 --- a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs +++ b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs @@ -1,12 +1,15 @@ -namespace FeatureBasedInjectionPOC +using Microsoft.FeatureManagement; + +namespace FeatureBasedInjectionPOC { + [FeaturedServiceAlias("Omega")] internal class AlgorithmOmega : IAlgorithm { public string Name { get; set; } - public AlgorithmOmega(string name) + public AlgorithmOmega() { - Name = name; + Name = "Omega"; } } } diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index 82a426c1..85ce6823 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -15,7 +15,7 @@ .AddFeatureManagement() .AddFeatureFilter(); -services.AddSingleton(); +services.AddSingletonForFeature("MyFeature"); var targetingContextAccessor = new OnDemandTargetingContextAccessor(); @@ -34,7 +34,9 @@ IAlgorithm algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - Console.WriteLine($"Get algorithm {algorithm.Name} because the feature flag is {await featureManager.IsEnabledAsync("MyFeature", CancellationToken.None)}"); + Variant variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); + + Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); targetingContextAccessor.Current = new TargetingContext { @@ -43,7 +45,9 @@ algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - Console.WriteLine($"Get algorithm {algorithm.Name} because the feature flag is {await featureManager.IsEnabledAsync("MyFeature", CancellationToken.None)}"); + variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); + + Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); targetingContextAccessor.Current = new TargetingContext { @@ -52,9 +56,9 @@ algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - Variant variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); + variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - Console.WriteLine($"Get algorithm {algorithm.Name} because the feature variant is {variant.Name}"); + Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant.Name}"); targetingContextAccessor.Current = new TargetingContext { @@ -65,5 +69,5 @@ variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - Console.WriteLine($"Get algorithm {algorithm.Name} because the feature variant is {variant.Name}"); + Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant.Name}"); } diff --git a/examples/FeatureBasedInjectionPOC/appsettings.json b/examples/FeatureBasedInjectionPOC/appsettings.json index 6bf13114..d64e00c4 100644 --- a/examples/FeatureBasedInjectionPOC/appsettings.json +++ b/examples/FeatureBasedInjectionPOC/appsettings.json @@ -15,10 +15,11 @@ ], "Variants": [ { - "Name": "Omega" + "Name": "Sigma", + "ConfigurationValue": "AlgorithmSigma" }, { - "Name": "Sigma" + "Name": "Omega" } ], "Allocation": { diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index f61843ec..3230db14 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -3,26 +3,23 @@ // using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement { - internal class FeaturedService : IFeaturedService + internal class FeaturedService : IFeaturedService where TService : class { - private IEnumerable> _services; - + private string _featureName; private IVariantFeatureManager _featureManager; - - private string _featureName { get; init; } + private IEnumerable> _services; public FeaturedService(string featureName, IEnumerable> services, IVariantFeatureManager featureManager) { _featureName = featureName; - _services = services.Where(s => s.FeatureName.Equals(featureName)); - _featureManager = featureManager; } @@ -32,16 +29,34 @@ public async ValueTask GetAsync(CancellationToken cancellationToken) if (variant != null) { - foreach (var item in _services) + FeaturedServiceImplementationWrapper implementationWrapper = _services.FirstOrDefault(s => + IsMatchingVariant( + s.Implementation.GetType(), + variant)); + + if (implementationWrapper != null) { - if(item.VariantName == variant.Name) - { - return item.Implementation; - } + return implementationWrapper.Implementation; } } - return default; + return null; + } + + private bool IsMatchingVariant(Type implementationType, Variant variant) + { + Debug.Assert(variant != null); + + string implementationName = ((FeaturedServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(FeaturedServiceAliasAttribute)))?.Alias; + + if (implementationName == null) + { + implementationName = implementationType.Name; + } + + string variantConfiguration = variant.Configuration?.Value ?? variant.Name; + + return string.Equals(implementationName, variantConfiguration, StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/Microsoft.FeatureManagement/VariantAliasAttribute.cs b/src/Microsoft.FeatureManagement/FeaturedServiceAliasAttribute.cs similarity index 75% rename from src/Microsoft.FeatureManagement/VariantAliasAttribute.cs rename to src/Microsoft.FeatureManagement/FeaturedServiceAliasAttribute.cs index d733c19e..454d37c0 100644 --- a/src/Microsoft.FeatureManagement/VariantAliasAttribute.cs +++ b/src/Microsoft.FeatureManagement/FeaturedServiceAliasAttribute.cs @@ -5,9 +5,9 @@ namespace Microsoft.FeatureManagement { - public class VariantAliasAttribute : Attribute + public class FeaturedServiceAliasAttribute : Attribute { - public VariantAliasAttribute(string alias) + public FeaturedServiceAliasAttribute(string alias) { if (string.IsNullOrEmpty(alias)) { diff --git a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs b/src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs similarity index 85% rename from src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs rename to src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs index 1e8942d7..5a2fda94 100644 --- a/src/Microsoft.FeatureManagement/FeaturedImplementationWrapper.cs +++ b/src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs @@ -3,12 +3,10 @@ // namespace Microsoft.FeatureManagement { - internal class FeaturedServiceImplementationWrapper + internal class FeaturedServiceImplementationWrapper where TService : class { - public TService Implementation { get; init; } - public string FeatureName { get; init; } - public string VariantName { get; init; } + public TService Implementation { get; init; } } } diff --git a/src/Microsoft.FeatureManagement/IFeaturedService.cs b/src/Microsoft.FeatureManagement/IFeaturedService.cs index 903bf6a2..a5eac2b8 100644 --- a/src/Microsoft.FeatureManagement/IFeaturedService.cs +++ b/src/Microsoft.FeatureManagement/IFeaturedService.cs @@ -6,7 +6,7 @@ namespace Microsoft.FeatureManagement { - public interface IFeaturedService + public interface IFeaturedService where TService : class { ValueTask GetAsync(CancellationToken cancellationToken); } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 8a5a7614..b4b5a898 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -195,9 +195,9 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService return services.AddScopedFeatureManagement(); } - public static IServiceCollection AddSingletonForFeature(this IServiceCollection services, string featureName) + public static IServiceCollection AddSingletonForFeature(this IServiceCollection services, string featureName) where TService : class { - var implementationTypes = Assembly.GetAssembly(typeof(TService)) + IEnumerable implementationTypes = Assembly.GetAssembly(typeof(TService)) .GetTypes() .Where(type => typeof(TService).IsAssignableFrom(type) && @@ -208,19 +208,11 @@ public static IServiceCollection AddSingletonForFeature(this IServiceC { services.TryAddSingleton(implementationType); - var attribute = (VariantAliasAttribute) Attribute.GetCustomAttribute(implementationType, typeof(VariantAliasAttribute)); - - if (attribute != null) + services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() { - string variantName = attribute.Alias; - - services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() - { - Implementation = (TService) sp.GetRequiredService(implementationType), - FeatureName = featureName, - VariantName = variantName, - }); - } + FeatureName = featureName, + Implementation = (TService) sp.GetRequiredService(implementationType) + }); } @@ -228,7 +220,7 @@ public static IServiceCollection AddSingletonForFeature(this IServiceC { services.AddScoped>(sp => new FeaturedService( featureName, - sp.GetRequiredService>>(), + sp.GetRequiredService>>(), sp.GetRequiredService())); } else From b92fefb9e2f81e326863e23cb439d00b998a765c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 9 Jan 2024 16:48:24 +0800 Subject: [PATCH 08/25] Update --- examples/FeatureBasedInjectionPOC/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index 85ce6823..67a254d5 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -58,7 +58,7 @@ variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant.Name}"); + Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); targetingContextAccessor.Current = new TargetingContext { @@ -69,5 +69,5 @@ variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant.Name}"); + Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); } From f3926f8e76e958ef8785057e53ccaca013e74a1c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 9 Jan 2024 16:52:04 +0800 Subject: [PATCH 09/25] update example --- examples/FeatureBasedInjectionPOC/appsettings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/FeatureBasedInjectionPOC/appsettings.json b/examples/FeatureBasedInjectionPOC/appsettings.json index d64e00c4..2f398eea 100644 --- a/examples/FeatureBasedInjectionPOC/appsettings.json +++ b/examples/FeatureBasedInjectionPOC/appsettings.json @@ -14,6 +14,9 @@ } ], "Variants": [ + { + "Name": "AlgorithmBeta" + }, { "Name": "Sigma", "ConfigurationValue": "AlgorithmSigma" @@ -24,6 +27,12 @@ ], "Allocation": { "User": [ + { + "Variant": "AlgorithmBeta", + "Users": [ + "UserBeta" + ] + }, { "Variant": "Omega", "Users": [ From 0d0f3f3f91d6d4398503ffe3ca996204b94f3519 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 9 Jan 2024 17:14:31 +0800 Subject: [PATCH 10/25] match variant name or configuration value --- examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs | 2 +- src/Microsoft.FeatureManagement/FeaturedService.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs index cae5e405..a2da47f7 100644 --- a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs +++ b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs @@ -3,7 +3,7 @@ namespace FeatureBasedInjectionPOC { [FeaturedServiceAlias("Omega")] - internal class AlgorithmOmega : IAlgorithm + class AlgorithmOmega : IAlgorithm { public string Name { get; set; } diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index 3230db14..d1f7bd35 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -54,9 +54,14 @@ private bool IsMatchingVariant(Type implementationType, Variant variant) implementationName = implementationType.Name; } - string variantConfiguration = variant.Configuration?.Value ?? variant.Name; + string variantConfiguration = variant.Configuration?.Value; - return string.Equals(implementationName, variantConfiguration, StringComparison.OrdinalIgnoreCase); + if (variantConfiguration != null && string.Equals(implementationName, variantConfiguration, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return string.Equals(implementationName, variant.Name, StringComparison.OrdinalIgnoreCase); } } } From 176cf78616366b29309c80c3cf1dad9ddb18b3e6 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 10 Jan 2024 23:16:12 +0800 Subject: [PATCH 11/25] update to the latest design --- .../AlgorithmOmega.cs | 4 +- examples/FeatureBasedInjectionPOC/Program.cs | 10 +++-- .../FeatureManagementBuilderExtensions.cs | 27 +++++++++++++ .../FeaturedService.cs | 23 +++++------ .../FeaturedServiceImplementationWrapper.cs | 12 ------ .../ServiceCollectionExtensions.cs | 39 ------------------- 6 files changed, 46 insertions(+), 69 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs index a2da47f7..eb13fce7 100644 --- a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs +++ b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs @@ -7,9 +7,9 @@ class AlgorithmOmega : IAlgorithm { public string Name { get; set; } - public AlgorithmOmega() + public AlgorithmOmega(string name) { - Name = "Omega"; + Name = name; } } } diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index 67a254d5..00dafe16 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -11,11 +11,15 @@ IServiceCollection services = new ServiceCollection(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(sp => new AlgorithmOmega("Omega")); + services.AddSingleton(configuration) .AddFeatureManagement() - .AddFeatureFilter(); - -services.AddSingletonForFeature("MyFeature"); + .AddFeatureFilter() + .AddFeaturedService("MyFeature"); var targetingContextAccessor = new OnDemandTargetingContextAccessor(); diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 162a6cce..3db6707b 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Microsoft.FeatureManagement { @@ -16,6 +17,32 @@ namespace Microsoft.FeatureManagement /// public static class FeatureManagementBuilderExtensions { + /// + /// Adds a to the feature management system. + /// + /// The used to customize feature management functionality. + /// The variant feature flag used to assign variants. The will return different implementations of TService according to the assigned variant. + /// A that can be used to customize feature management functionality. + public static IFeatureManagementBuilder AddFeaturedService(this IFeatureManagementBuilder builder, string featureName) where TService : class + { + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + builder.Services.AddScoped>(sp => new FeaturedService( + featureName, + sp.GetRequiredService>(), + sp.GetRequiredService())); + } + else + { + builder.Services.AddSingleton>(sp => new FeaturedService( + featureName, + sp.GetRequiredService>(), + sp.GetRequiredService())); + } + + return builder; + } + /// /// Adds a telemetry publisher to the feature management system. /// diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/FeaturedService.cs index d1f7bd35..f22c01d3 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/FeaturedService.cs @@ -12,14 +12,14 @@ namespace Microsoft.FeatureManagement { internal class FeaturedService : IFeaturedService where TService : class { - private string _featureName; - private IVariantFeatureManager _featureManager; - private IEnumerable> _services; + private readonly string _featureName; + private readonly IVariantFeatureManager _featureManager; + private readonly IEnumerable _services; - public FeaturedService(string featureName, IEnumerable> services, IVariantFeatureManager featureManager) + public FeaturedService(string featureName, IEnumerable services, IVariantFeatureManager featureManager) { _featureName = featureName; - _services = services.Where(s => s.FeatureName.Equals(featureName)); + _services = services; _featureManager = featureManager; } @@ -27,20 +27,17 @@ public async ValueTask GetAsync(CancellationToken cancellationToken) { Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); + TService implementation = null; + if (variant != null) { - FeaturedServiceImplementationWrapper implementationWrapper = _services.FirstOrDefault(s => + implementation = _services.FirstOrDefault(service => IsMatchingVariant( - s.Implementation.GetType(), + service.GetType(), variant)); - - if (implementationWrapper != null) - { - return implementationWrapper.Implementation; - } } - return null; + return implementation; } private bool IsMatchingVariant(Type implementationType, Variant variant) diff --git a/src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs b/src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs deleted file mode 100644 index 5a2fda94..00000000 --- a/src/Microsoft.FeatureManagement/FeaturedServiceImplementationWrapper.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Microsoft.FeatureManagement -{ - internal class FeaturedServiceImplementationWrapper where TService : class - { - public string FeatureName { get; init; } - - public TService Implementation { get; init; } - } -} diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index b4b5a898..70fcd605 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -194,44 +194,5 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService return services.AddScopedFeatureManagement(); } - - public static IServiceCollection AddSingletonForFeature(this IServiceCollection services, string featureName) where TService : class - { - IEnumerable implementationTypes = Assembly.GetAssembly(typeof(TService)) - .GetTypes() - .Where(type => - typeof(TService).IsAssignableFrom(type) && - !type.IsInterface && - !type.IsAbstract); - - foreach (var implementationType in implementationTypes) - { - services.TryAddSingleton(implementationType); - - services.AddSingleton(sp => new FeaturedServiceImplementationWrapper() - { - FeatureName = featureName, - Implementation = (TService) sp.GetRequiredService(implementationType) - }); - } - - - if (services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) - { - services.AddScoped>(sp => new FeaturedService( - featureName, - sp.GetRequiredService>>(), - sp.GetRequiredService())); - } - else - { - services.AddSingleton>(sp => new FeaturedService( - featureName, - sp.GetRequiredService>>(), - sp.GetRequiredService())); - } - - return services; - } } } From a7b8f0ffd37d96b2e5fdfa1a798670a6e64f1190 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 18 Jan 2024 23:57:03 +0800 Subject: [PATCH 12/25] merge with preview branch --- .../TelemetryClientExtensions.cs | 57 +++++++++++++++++++ .../FeatureManagementBuilderExtensions.cs | 46 +++++++-------- 2 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs new file mode 100644 index 00000000..b397519b --- /dev/null +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.FeatureManagement.FeatureFilters; + +namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights +{ + /// + /// Provides extension methods for tracking events with . + /// + public static class TelemetryClientExtensions + { + /// + /// Extension method to track an event with . + /// + public static void TrackEvent(this TelemetryClient telemetryClient, string eventName, TargetingContext targetingContext, IDictionary properties = null, IDictionary metrics = null) + { + ValidateTargetingContext(targetingContext); + + if (properties == null) + { + properties = new Dictionary(); + } + + properties["TargetingId"] = targetingContext.UserId; + + telemetryClient.TrackEvent(eventName, properties, metrics); + } + + /// + /// Extension method to track an with . + /// + public static void TrackEvent(this TelemetryClient telemetryClient, EventTelemetry telemetry, TargetingContext targetingContext) + { + ValidateTargetingContext(targetingContext); + + if (telemetry == null) + { + telemetry = new EventTelemetry(); + } + + telemetry.Properties["TargetingId"] = targetingContext.UserId; + + telemetryClient.TrackEvent(telemetry); + } + + private static void ValidateTargetingContext(TargetingContext targetingContext) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 3db6707b..8e746b40 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -17,6 +17,29 @@ namespace Microsoft.FeatureManagement /// public static class FeatureManagementBuilderExtensions { + /// + /// Adds an to be used for targeting and registers the targeting filter to the feature management system. + /// + /// The used to customize feature management functionality. + /// A that can be used to customize feature management functionality. + public static IFeatureManagementBuilder WithTargeting(this IFeatureManagementBuilder builder) where T : ITargetingContextAccessor + { + // + // Register the targeting context accessor with the same lifetime as the feature manager + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + builder.Services.TryAddScoped(typeof(ITargetingContextAccessor), typeof(T)); + } + else + { + builder.Services.TryAddSingleton(typeof(ITargetingContextAccessor), typeof(T)); + } + + builder.AddFeatureFilter(); + + return builder; + } + /// /// Adds a to the feature management system. /// @@ -69,28 +92,5 @@ private static IFeatureManagementBuilder AddTelemetryPublisher(this IFeatureMana return builder; } - - /// - /// Adds an to be used for targeting and registers the targeting filter to the feature management system. - /// - /// The used to customize feature management functionality. - /// A that can be used to customize feature management functionality. - public static IFeatureManagementBuilder WithTargeting(this IFeatureManagementBuilder builder) where T : ITargetingContextAccessor - { - // - // Register the targeting context accessor with the same lifetime as the feature manager - if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) - { - builder.Services.TryAddScoped(typeof(ITargetingContextAccessor), typeof(T)); - } - else - { - builder.Services.TryAddSingleton(typeof(ITargetingContextAccessor), typeof(T)); - } - - builder.AddFeatureFilter(); - - return builder; - } } } From 73aac1019e9917873ad50af2d9a5e889b36e2171 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 19 Jan 2024 01:01:51 +0800 Subject: [PATCH 13/25] resolve comments --- examples/FeatureBasedInjectionPOC/Program.cs | 47 ++++---------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index 00dafe16..2b6b56ff 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -25,53 +25,24 @@ services.AddSingleton(targetingContextAccessor); -using (ServiceProvider serviceProvider = services.BuildServiceProvider()) -{ - IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); - - IFeaturedService featuredAlgorithm = serviceProvider.GetRequiredService>(); - - targetingContextAccessor.Current = new TargetingContext - { - UserId = "Guest" - }; - - IAlgorithm algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - - Variant variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - - Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); - - targetingContextAccessor.Current = new TargetingContext - { - UserId = "UserBeta" - }; - - algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - - variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - - Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); - - targetingContextAccessor.Current = new TargetingContext - { - UserId = "UserSigma" - }; +using ServiceProvider serviceProvider = services.BuildServiceProvider(); - algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); +IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); - variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); +IFeaturedService featuredAlgorithm = serviceProvider.GetRequiredService>(); - Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); +string[] userIds = { "Guest", "UserBeta", "UserSigma", "UserOmega" }; +foreach (string userId in userIds) +{ targetingContextAccessor.Current = new TargetingContext { - UserId = "UserOmega" + UserId = userId }; - algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); + IAlgorithm algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); + Variant variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); } From 3ed4c4cd086eb6d2f2aa68f8279930ea0bfe3083 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Fri, 19 Jan 2024 12:34:57 +0800 Subject: [PATCH 14/25] rename to VariantService --- examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs | 2 +- examples/FeatureBasedInjectionPOC/Program.cs | 2 +- .../FeatureManagementBuilderExtensions.cs | 8 ++++---- .../{IFeaturedService.cs => IVariantServiceProvider.cs} | 2 +- .../ServiceCollectionExtensions.cs | 1 - ...eAliasAttribute.cs => VariantServiceAliasAttribute.cs} | 4 ++-- .../{FeaturedService.cs => VariantServiceProvider.cs} | 6 +++--- 7 files changed, 12 insertions(+), 13 deletions(-) rename src/Microsoft.FeatureManagement/{IFeaturedService.cs => IVariantServiceProvider.cs} (77%) rename src/Microsoft.FeatureManagement/{FeaturedServiceAliasAttribute.cs => VariantServiceAliasAttribute.cs} (75%) rename src/Microsoft.FeatureManagement/{FeaturedService.cs => VariantServiceProvider.cs} (79%) diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs index eb13fce7..d2d60730 100644 --- a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs +++ b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs @@ -2,7 +2,7 @@ namespace FeatureBasedInjectionPOC { - [FeaturedServiceAlias("Omega")] + [VariantServiceAlias("Omega")] class AlgorithmOmega : IAlgorithm { public string Name { get; set; } diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index 2b6b56ff..e8a3ce7e 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -29,7 +29,7 @@ IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); -IFeaturedService featuredAlgorithm = serviceProvider.GetRequiredService>(); +IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); string[] userIds = { "Guest", "UserBeta", "UserSigma", "UserOmega" }; diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 8e746b40..12ac6703 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -41,23 +41,23 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement } /// - /// Adds a to the feature management system. + /// Adds a to the feature management system. /// /// The used to customize feature management functionality. - /// The variant feature flag used to assign variants. The will return different implementations of TService according to the assigned variant. + /// The variant feature flag used to assign variants. The will return different implementations of TService according to the assigned variant. /// A that can be used to customize feature management functionality. public static IFeatureManagementBuilder AddFeaturedService(this IFeatureManagementBuilder builder, string featureName) where TService : class { if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) { - builder.Services.AddScoped>(sp => new FeaturedService( + builder.Services.AddScoped>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService>(), sp.GetRequiredService())); } else { - builder.Services.AddSingleton>(sp => new FeaturedService( + builder.Services.AddSingleton>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService>(), sp.GetRequiredService())); diff --git a/src/Microsoft.FeatureManagement/IFeaturedService.cs b/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs similarity index 77% rename from src/Microsoft.FeatureManagement/IFeaturedService.cs rename to src/Microsoft.FeatureManagement/IVariantServiceProvider.cs index a5eac2b8..9b21b1b3 100644 --- a/src/Microsoft.FeatureManagement/IFeaturedService.cs +++ b/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs @@ -6,7 +6,7 @@ namespace Microsoft.FeatureManagement { - public interface IFeaturedService where TService : class + public interface IVariantServiceProvider where TService : class { ValueTask GetAsync(CancellationToken cancellationToken); } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 70fcd605..a6651e59 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -12,7 +12,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Microsoft.FeatureManagement { diff --git a/src/Microsoft.FeatureManagement/FeaturedServiceAliasAttribute.cs b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs similarity index 75% rename from src/Microsoft.FeatureManagement/FeaturedServiceAliasAttribute.cs rename to src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs index 454d37c0..499f7896 100644 --- a/src/Microsoft.FeatureManagement/FeaturedServiceAliasAttribute.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs @@ -5,9 +5,9 @@ namespace Microsoft.FeatureManagement { - public class FeaturedServiceAliasAttribute : Attribute + public class VariantServiceAliasAttribute : Attribute { - public FeaturedServiceAliasAttribute(string alias) + public VariantServiceAliasAttribute(string alias) { if (string.IsNullOrEmpty(alias)) { diff --git a/src/Microsoft.FeatureManagement/FeaturedService.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs similarity index 79% rename from src/Microsoft.FeatureManagement/FeaturedService.cs rename to src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 3a134fc4..b4f50661 100644 --- a/src/Microsoft.FeatureManagement/FeaturedService.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -10,13 +10,13 @@ namespace Microsoft.FeatureManagement { - internal class FeaturedService : IFeaturedService where TService : class + internal class VariantServiceProvider : IFeaturedService where TService : class { private readonly string _featureName; private readonly IVariantFeatureManager _featureManager; private readonly IEnumerable _services; - public FeaturedService(string featureName, IEnumerable services, IVariantFeatureManager featureManager) + public VariantServiceProvider(string featureName, IEnumerable services, IVariantFeatureManager featureManager) { _featureName = featureName; _services = services; @@ -44,7 +44,7 @@ private bool IsMatchingVariant(Type implementationType, Variant variant) { Debug.Assert(variant != null); - string implementationName = ((FeaturedServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(FeaturedServiceAliasAttribute)))?.Alias; + string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; if (implementationName == null) { From 941c7a1b1a41f8964c23b6ecd119cced995aad7f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Fri, 19 Jan 2024 13:21:38 +0800 Subject: [PATCH 15/25] update & add comments --- examples/FeatureBasedInjectionPOC/Program.cs | 2 +- .../FeatureManagementBuilderExtensions.cs | 19 ++++++--- .../IVariantServiceProvider.cs | 8 ++++ .../VariantServiceAliasAttribute.cs | 10 +++++ .../VariantServiceProvider.cs | 39 +++++++++++++++---- 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs index e8a3ce7e..bdea8711 100644 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ b/examples/FeatureBasedInjectionPOC/Program.cs @@ -19,7 +19,7 @@ services.AddSingleton(configuration) .AddFeatureManagement() .AddFeatureFilter() - .AddFeaturedService("MyFeature"); + .AddVariantServiceProvider("MyFeature"); var targetingContextAccessor = new OnDemandTargetingContextAccessor(); diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 12ac6703..c42e5ae6 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -46,21 +46,30 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement /// The used to customize feature management functionality. /// The variant feature flag used to assign variants. The will return different implementations of TService according to the assigned variant. /// A that can be used to customize feature management functionality. - public static IFeatureManagementBuilder AddFeaturedService(this IFeatureManagementBuilder builder, string featureName) where TService : class + public static IFeatureManagementBuilder AddVariantServiceProvider(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(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) { builder.Services.AddScoped>(sp => new VariantServiceProvider( - featureName, sp.GetRequiredService>(), - sp.GetRequiredService())); + sp.GetRequiredService()) + { + VariantFeatureName = featureName, + }); } else { builder.Services.AddSingleton>(sp => new VariantServiceProvider( - featureName, sp.GetRequiredService>(), - sp.GetRequiredService())); + sp.GetRequiredService()) + { + VariantFeatureName = featureName, + }); } return builder; diff --git a/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs b/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs index 9b21b1b3..24638940 100644 --- a/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs @@ -6,8 +6,16 @@ namespace Microsoft.FeatureManagement { + /// + /// Used to get different implementation variants of TService. + /// public interface IVariantServiceProvider where TService : class { + /// + /// Gets an implementation variant of TService. + /// + /// The cancellation token to cancel the operation. + /// An implementation of TService. ValueTask GetAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs index 499f7896..beebb88b 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs @@ -5,8 +5,15 @@ namespace Microsoft.FeatureManagement { + /// + /// Allows the name of a variant service to be customized to relate to the variant name specified in configuration. + /// public class VariantServiceAliasAttribute : Attribute { + /// + /// Creates a variant service alias using the provided alias. + /// + /// The alias of the variant service. public VariantServiceAliasAttribute(string alias) { if (string.IsNullOrEmpty(alias)) @@ -17,6 +24,9 @@ public VariantServiceAliasAttribute(string alias) Alias = alias; } + /// + /// The name that will be used to match variant name specified in the configuration. + /// public string Alias { get; } } } diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index b4f50661..193c22a8 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -10,22 +10,45 @@ namespace Microsoft.FeatureManagement { - internal class VariantServiceProvider : IFeaturedService where TService : class + /// + /// Used to get different implementations of TService depending on the assigned variant from a specific variant feature flag. + /// + internal class VariantServiceProvider : IVariantServiceProvider where TService : class { - private readonly string _featureName; - private readonly IVariantFeatureManager _featureManager; private readonly IEnumerable _services; + private readonly IVariantFeatureManager _featureManager; + private readonly string _variantFeatureName; + + + public VariantServiceProvider(IEnumerable services, IVariantFeatureManager featureManager) + { + _services = services ?? throw new ArgumentNullException(nameof(services)); + _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); + } - public VariantServiceProvider(string featureName, IEnumerable services, IVariantFeatureManager featureManager) + /// + /// The variant feature flag used to assign variants. + /// + public string VariantFeatureName { - _featureName = featureName; - _services = services; - _featureManager = featureManager; + get => _variantFeatureName; + + init + { + _variantFeatureName = value ?? throw new ArgumentNullException(nameof(value)); + } } + /// + /// Gets implementation of TService according to the assigned variant from the feature flag. + /// + /// The cancellation token to cancel the operation. + /// An implementation matched with the assigned variant. If there is no matched implementation, it will return null. public async ValueTask GetAsync(CancellationToken cancellationToken) { - Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); + Debug.Assert(_variantFeatureName != null); + + Variant variant = await _featureManager.GetVariantAsync(_variantFeatureName, cancellationToken); TService implementation = null; From 18ae0ebec38066138faf101be3002cdac7c676ef Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Fri, 19 Jan 2024 13:25:19 +0800 Subject: [PATCH 16/25] remove POC example --- Microsoft.FeatureManagement.sln | 7 --- .../AlgorithmAlpha.cs | 12 ----- .../FeatureBasedInjectionPOC/AlgorithmBeta.cs | 12 ----- .../AlgorithmOmega.cs | 15 ------ .../AlgorithmSigma.cs | 12 ----- .../FeatureBasedInjectionPOC.csproj | 25 --------- .../FeatureBasedInjectionPOC/IAlgorithm.cs | 7 --- .../OnDemandTargetingContextAccessor.cs | 14 ----- examples/FeatureBasedInjectionPOC/Program.cs | 48 ----------------- .../FeatureBasedInjectionPOC/appsettings.json | 52 ------------------- 10 files changed, 204 deletions(-) delete mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs delete mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs delete mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs delete mode 100644 examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs delete mode 100644 examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj delete mode 100644 examples/FeatureBasedInjectionPOC/IAlgorithm.cs delete mode 100644 examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs delete mode 100644 examples/FeatureBasedInjectionPOC/Program.cs delete mode 100644 examples/FeatureBasedInjectionPOC/appsettings.json diff --git a/Microsoft.FeatureManagement.sln b/Microsoft.FeatureManagement.sln index 5880e42d..8963b77b 100644 --- a/Microsoft.FeatureManagement.sln +++ b/Microsoft.FeatureManagement.sln @@ -27,8 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.FeatureManagement EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EvaluationDataToApplicationInsights", "examples\EvaluationDataToApplicationInsights\EvaluationDataToApplicationInsights.csproj", "{1502529E-47E9-4306-98C4-BF6CF7C7C275}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureBasedInjectionPOC", "examples\FeatureBasedInjectionPOC\FeatureBasedInjectionPOC.csproj", "{6655016D-900B-42D2-AA43-C33B0E161D6B}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,10 +73,6 @@ Global {1502529E-47E9-4306-98C4-BF6CF7C7C275}.Debug|Any CPU.Build.0 = Debug|Any CPU {1502529E-47E9-4306-98C4-BF6CF7C7C275}.Release|Any CPU.ActiveCfg = Release|Any CPU {1502529E-47E9-4306-98C4-BF6CF7C7C275}.Release|Any CPU.Build.0 = Release|Any CPU - {6655016D-900B-42D2-AA43-C33B0E161D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6655016D-900B-42D2-AA43-C33B0E161D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6655016D-900B-42D2-AA43-C33B0E161D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6655016D-900B-42D2-AA43-C33B0E161D6B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,7 +85,6 @@ Global {DACAB624-4611-42E8-844C-529F93A54980} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} {283D3EBB-4716-4F1D-BA51-A435F7E2AB82} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} {1502529E-47E9-4306-98C4-BF6CF7C7C275} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} - {6655016D-900B-42D2-AA43-C33B0E161D6B} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs b/examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs deleted file mode 100644 index ebd563e5..00000000 --- a/examples/FeatureBasedInjectionPOC/AlgorithmAlpha.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FeatureBasedInjectionPOC -{ - internal class AlgorithmAlpha : IAlgorithm - { - public string Name { get; set; } - - public AlgorithmAlpha() - { - Name = "Alpha"; - } - } -} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs b/examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs deleted file mode 100644 index 3f9b75bf..00000000 --- a/examples/FeatureBasedInjectionPOC/AlgorithmBeta.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FeatureBasedInjectionPOC -{ - internal class AlgorithmBeta : IAlgorithm - { - public string Name { get; set; } - - public AlgorithmBeta() - { - Name = "Beta"; - } - } -} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs b/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs deleted file mode 100644 index d2d60730..00000000 --- a/examples/FeatureBasedInjectionPOC/AlgorithmOmega.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.FeatureManagement; - -namespace FeatureBasedInjectionPOC -{ - [VariantServiceAlias("Omega")] - class AlgorithmOmega : IAlgorithm - { - public string Name { get; set; } - - public AlgorithmOmega(string name) - { - Name = name; - } - } -} diff --git a/examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs b/examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs deleted file mode 100644 index dc19ba5c..00000000 --- a/examples/FeatureBasedInjectionPOC/AlgorithmSigma.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FeatureBasedInjectionPOC -{ - internal class AlgorithmSigma : IAlgorithm - { - public string Name { get; set; } - - public AlgorithmSigma() - { - Name = "Sigma"; - } - } -} diff --git a/examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj b/examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj deleted file mode 100644 index 83e2d4f1..00000000 --- a/examples/FeatureBasedInjectionPOC/FeatureBasedInjectionPOC.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - Exe - net6.0 - enable - enable - - - - - - - - - - - - - - Always - - - - diff --git a/examples/FeatureBasedInjectionPOC/IAlgorithm.cs b/examples/FeatureBasedInjectionPOC/IAlgorithm.cs deleted file mode 100644 index a4cf4596..00000000 --- a/examples/FeatureBasedInjectionPOC/IAlgorithm.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FeatureBasedInjectionPOC -{ - public interface IAlgorithm - { - public string Name { get; } - } -} diff --git a/examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs b/examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs deleted file mode 100644 index acd78ca7..00000000 --- a/examples/FeatureBasedInjectionPOC/OnDemandTargetingContextAccessor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.FeatureManagement.FeatureFilters; - -namespace FeatureBasedInjectionPOC -{ - class OnDemandTargetingContextAccessor : ITargetingContextAccessor - { - public TargetingContext Current { get; set; } - - public ValueTask GetContextAsync() - { - return new ValueTask(Current); - } - } -} diff --git a/examples/FeatureBasedInjectionPOC/Program.cs b/examples/FeatureBasedInjectionPOC/Program.cs deleted file mode 100644 index bdea8711..00000000 --- a/examples/FeatureBasedInjectionPOC/Program.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.FeatureManagement; -using Microsoft.FeatureManagement.FeatureFilters; -using FeatureBasedInjectionPOC; - - -IConfiguration configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - -IServiceCollection services = new ServiceCollection(); - -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(sp => new AlgorithmOmega("Omega")); - -services.AddSingleton(configuration) - .AddFeatureManagement() - .AddFeatureFilter() - .AddVariantServiceProvider("MyFeature"); - -var targetingContextAccessor = new OnDemandTargetingContextAccessor(); - -services.AddSingleton(targetingContextAccessor); - -using ServiceProvider serviceProvider = services.BuildServiceProvider(); - -IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); - -IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); - -string[] userIds = { "Guest", "UserBeta", "UserSigma", "UserOmega" }; - -foreach (string userId in userIds) -{ - targetingContextAccessor.Current = new TargetingContext - { - UserId = userId - }; - - IAlgorithm algorithm = await featuredAlgorithm.GetAsync(CancellationToken.None); - - Variant variant = await featureManager.GetVariantAsync("MyFeature", CancellationToken.None); - - Console.WriteLine($"Get algorithm {algorithm?.Name ?? "Null"} because the feature variant is {variant?.Name ?? "Null"}"); -} diff --git a/examples/FeatureBasedInjectionPOC/appsettings.json b/examples/FeatureBasedInjectionPOC/appsettings.json deleted file mode 100644 index 2f398eea..00000000 --- a/examples/FeatureBasedInjectionPOC/appsettings.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "FeatureManagement": { - "MyFeature": { - "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" - ] - } - ] - } - } - } -} \ No newline at end of file From f48767c572a114f365d9d2ace82f9776f6d7511f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Jan 2024 13:14:15 +0800 Subject: [PATCH 17/25] add testcases & use method name GetServiceAsync --- .../IVariantServiceProvider.cs | 2 +- .../VariantServiceProvider.cs | 10 ++- .../FeatureManagement.cs | 67 +++++++++++++++++++ tests/Tests.FeatureManagement/Features.cs | 1 + .../VariantServices.cs | 40 +++++++++++ .../Tests.FeatureManagement/appsettings.json | 48 +++++++++++++ 6 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 tests/Tests.FeatureManagement/VariantServices.cs diff --git a/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs b/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs index 24638940..67d3cc0f 100644 --- a/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/IVariantServiceProvider.cs @@ -16,6 +16,6 @@ public interface IVariantServiceProvider where TService : class /// /// The cancellation token to cancel the operation. /// An implementation of TService. - ValueTask GetAsync(CancellationToken cancellationToken); + ValueTask GetServiceAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 193c22a8..8f9ccd5d 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -19,7 +19,13 @@ internal class VariantServiceProvider : IVariantServiceProvider + /// Creates a variant service provider. + /// + /// The provider of feature flag definitions. + /// Options controlling the behavior of the feature manager. + /// Thrown if is null. + /// Thrown if is null. public VariantServiceProvider(IEnumerable services, IVariantFeatureManager featureManager) { _services = services ?? throw new ArgumentNullException(nameof(services)); @@ -44,7 +50,7 @@ public string VariantFeatureName /// /// The cancellation token to cancel the operation. /// An implementation matched with the assigned variant. If there is no matched implementation, it will return null. - public async ValueTask GetAsync(CancellationToken cancellationToken) + public async ValueTask GetServiceAsync(CancellationToken cancellationToken) { Debug.Assert(_variantFeatureName != null); diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 3b130ae5..5ae7fa71 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1262,5 +1262,72 @@ 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(); + services.AddSingleton(); + services.AddSingleton(sp => new AlgorithmOmega("OMEGA")); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter() + .AddVariantServiceProvider(Features.VariantImplementationFeature); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + + IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); + + 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); + } } } \ No newline at end of file diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index d3d2b81f..c0819716 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -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"; } } diff --git a/tests/Tests.FeatureManagement/VariantServices.cs b/tests/Tests.FeatureManagement/VariantServices.cs new file mode 100644 index 00000000..942b110c --- /dev/null +++ b/tests/Tests.FeatureManagement/VariantServices.cs @@ -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; + } + } +} diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 4bfa426e..d6a3d798 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -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" + ] + } + ] + } } } } \ No newline at end of file From 1556c0d112ab4884e6c2ae876a9c4b67a90fd225 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 22 Jan 2024 13:18:17 +0800 Subject: [PATCH 18/25] update comments --- src/Microsoft.FeatureManagement/VariantServiceProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 8f9ccd5d..b6f97492 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -22,8 +22,8 @@ internal class VariantServiceProvider : IVariantServiceProvider /// Creates a variant service provider. /// - /// The provider of feature flag definitions. - /// Options controlling the behavior of the feature manager. + /// Implementation variants of TService. + /// Feature manager to get the assigned variant of the variant feature flag. /// Thrown if is null. /// Thrown if is null. public VariantServiceProvider(IEnumerable services, IVariantFeatureManager featureManager) From 7120abd97a0ee8ed44157d174198b0aeabc926d6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 29 Jan 2024 13:34:00 +0800 Subject: [PATCH 19/25] add variant service cache --- .../VariantServiceProvider.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index b6f97492..d9676130 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -18,6 +19,7 @@ internal class VariantServiceProvider : IVariantServiceProvider _services; private readonly IVariantFeatureManager _featureManager; private readonly string _variantFeatureName; + private readonly ConcurrentDictionary _variantServiceCache; /// /// Creates a variant service provider. @@ -30,6 +32,7 @@ public VariantServiceProvider(IEnumerable services, IVariantFeatureMan { _services = services ?? throw new ArgumentNullException(nameof(services)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); + _variantServiceCache = new ConcurrentDictionary(); } /// @@ -60,19 +63,20 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT if (variant != null) { - implementation = _services.FirstOrDefault(service => - IsMatchingVariant( - service.GetType(), - variant)); + implementation = _variantServiceCache.GetOrAdd( + variant.Name, + (_) => _services.FirstOrDefault( + service => IsMatchingVariantName( + service.GetType(), + variant.Name)) + ); } return implementation; } - private bool IsMatchingVariant(Type implementationType, Variant variant) + private bool IsMatchingVariantName(Type implementationType, string variantName) { - Debug.Assert(variant != null); - string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; if (implementationName == null) @@ -80,7 +84,7 @@ private bool IsMatchingVariant(Type implementationType, Variant variant) implementationName = implementationType.Name; } - return string.Equals(implementationName, variant.Name, StringComparison.OrdinalIgnoreCase); + return string.Equals(implementationName, variantName, StringComparison.OrdinalIgnoreCase); } } } From 1a4e0facd3596cb5b672eb704ac83fcd247ca795 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 31 Jan 2024 11:31:14 +0800 Subject: [PATCH 20/25] resolve comments --- .../FeatureManagementBuilderExtensions.cs | 8 ++++---- .../VariantServiceProvider.cs | 12 ++++++------ tests/Tests.FeatureManagement/FeatureManagement.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index c42e5ae6..5434cbab 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -44,9 +44,9 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement /// Adds a to the feature management system. /// /// The used to customize feature management functionality. - /// The variant feature flag used to assign variants. The will return different implementations of TService according to the assigned variant. + /// The feature flag that should be used to determine which variant of the service should be used. The will return different implementations of TService according to the assigned variant. /// A that can be used to customize feature management functionality. - public static IFeatureManagementBuilder AddVariantServiceProvider(this IFeatureManagementBuilder builder, string featureName) where TService : class + public static IFeatureManagementBuilder WithVariantService(this IFeatureManagementBuilder builder, string featureName) where TService : class { if (string.IsNullOrEmpty(featureName)) { @@ -59,7 +59,7 @@ public static IFeatureManagementBuilder AddVariantServiceProvider(this sp.GetRequiredService>(), sp.GetRequiredService()) { - VariantFeatureName = featureName, + FeatureName = featureName, }); } else @@ -68,7 +68,7 @@ public static IFeatureManagementBuilder AddVariantServiceProvider(this sp.GetRequiredService>(), sp.GetRequiredService()) { - VariantFeatureName = featureName, + FeatureName = featureName, }); } diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index d9676130..f4972ea8 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -18,7 +18,7 @@ internal class VariantServiceProvider : IVariantServiceProvider _services; private readonly IVariantFeatureManager _featureManager; - private readonly string _variantFeatureName; + private readonly string _featureName; private readonly ConcurrentDictionary _variantServiceCache; /// @@ -38,13 +38,13 @@ public VariantServiceProvider(IEnumerable services, IVariantFeatureMan /// /// The variant feature flag used to assign variants. /// - public string VariantFeatureName + public string FeatureName { - get => _variantFeatureName; + get => _featureName; init { - _variantFeatureName = value ?? throw new ArgumentNullException(nameof(value)); + _featureName = value ?? throw new ArgumentNullException(nameof(value)); } } @@ -55,9 +55,9 @@ public string VariantFeatureName /// An implementation matched with the assigned variant. If there is no matched implementation, it will return null. public async ValueTask GetServiceAsync(CancellationToken cancellationToken) { - Debug.Assert(_variantFeatureName != null); + Debug.Assert(_featureName != null); - Variant variant = await _featureManager.GetVariantAsync(_variantFeatureName, cancellationToken); + Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); TService implementation = null; diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 5ae7fa71..233b6430 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1279,7 +1279,7 @@ public async Task VariantBasedInjection() services.AddSingleton(configuration) .AddFeatureManagement() .AddFeatureFilter() - .AddVariantServiceProvider(Features.VariantImplementationFeature); + .WithVariantService(Features.VariantImplementationFeature); var targetingContextAccessor = new OnDemandTargetingContextAccessor(); From 926182a6aa1efa7b721683db9248f31ca457a099 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 2 Feb 2024 02:01:41 +0800 Subject: [PATCH 21/25] throw exception for duplicated registration --- .../FeatureManagementBuilderExtensions.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 5434cbab..82c85427 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -46,12 +46,19 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement /// The used to customize feature management functionality. /// The feature flag that should be used to determine which variant of the service should be used. The will return different implementations of TService according to the assigned variant. /// A that can be used to customize feature management functionality. + /// Thrown if feature name parameter is null. + /// Thrown if the variant service of the type has been added. public static IFeatureManagementBuilder WithVariantService(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))) + { + throw new InvalidOperationException($"Variant services of {typeof(TService)} has been added."); + } if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) { From 1065d7647c2dcb32b9c6502cd74d25d2ad113ef6 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 2 Feb 2024 02:07:58 +0800 Subject: [PATCH 22/25] add testcase --- tests/Tests.FeatureManagement/FeatureManagement.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 233b6430..1a61febb 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1328,6 +1328,16 @@ public async Task VariantBasedInjection() Assert.NotNull(algorithm); Assert.Equal("OMEGA", algorithm.Style); + + services = new ServiceCollection(); + + Assert.Throws(() => + { + services.AddFeatureManagement() + .WithVariantService("DummyFeature1") + .WithVariantService("DummyFeature2"); + } + ); } } } \ No newline at end of file From 37cda1b84ab7e2171629a5334dbbacb6720303d4 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 7 Feb 2024 17:29:27 +0800 Subject: [PATCH 23/25] remove unused package --- .../FeatureManagementBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 82c85427..5f7731d7 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Microsoft.FeatureManagement { From df47105a6292b59039f9fb6266ed9a95600c9b6c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 7 Feb 2024 17:31:51 +0800 Subject: [PATCH 24/25] update comment --- .../FeatureManagementBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 5f7731d7..7cc04d4c 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -46,7 +46,7 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement /// The feature flag that should be used to determine which variant of the service should be used. The will return different implementations of TService according to the assigned variant. /// A that can be used to customize feature management functionality. /// Thrown if feature name parameter is null. - /// Thrown if the variant service of the type has been added. + /// Thrown if a variant service of the type has already been added. public static IFeatureManagementBuilder WithVariantService(this IFeatureManagementBuilder builder, string featureName) where TService : class { if (string.IsNullOrEmpty(featureName)) From cbda0c6a831e6056292d27a6dc8834fcabfc04b4 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 9 Feb 2024 17:03:26 +0800 Subject: [PATCH 25/25] set feature name in constructor --- .../FeatureManagementBuilderExtensions.cs | 18 ++++++-------- .../VariantServiceProvider.cs | 24 ++++++------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 7cc04d4c..e958b628 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -56,26 +56,22 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IVariantServiceProvider))) { - throw new InvalidOperationException($"Variant services of {typeof(TService)} has been added."); + throw new InvalidOperationException($"A variant 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>(sp => new VariantServiceProvider( - sp.GetRequiredService>(), - sp.GetRequiredService()) - { - FeatureName = featureName, - }); + featureName, + sp.GetRequiredService(), + sp.GetRequiredService>())); } else { builder.Services.AddSingleton>(sp => new VariantServiceProvider( - sp.GetRequiredService>(), - sp.GetRequiredService()) - { - FeatureName = featureName, - }); + featureName, + sp.GetRequiredService(), + sp.GetRequiredService>())); } return builder; diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index f4972ea8..d4b3f514 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -24,30 +24,20 @@ internal class VariantServiceProvider : IVariantServiceProvider /// Creates a variant service provider. /// + /// The feature flag that should be used to determine which variant of the service should be used. + /// The feature manager to get the assigned variant of the feature flag. /// Implementation variants of TService. - /// Feature manager to get the assigned variant of the variant feature flag. - /// Thrown if is null. + /// Thrown if is null. /// Thrown if is null. - public VariantServiceProvider(IEnumerable services, IVariantFeatureManager featureManager) + /// Thrown if is null. + public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IEnumerable services) { - _services = services ?? throw new ArgumentNullException(nameof(services)); + _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); + _services = services ?? throw new ArgumentNullException(nameof(services)); _variantServiceCache = new ConcurrentDictionary(); } - /// - /// The variant feature flag used to assign variants. - /// - public string FeatureName - { - get => _featureName; - - init - { - _featureName = value ?? throw new ArgumentNullException(nameof(value)); - } - } - /// /// Gets implementation of TService according to the assigned variant from the feature flag. ///