From 71d711500439d3e0671216c5dc0829dddaf26550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 14 May 2026 15:30:20 +0300 Subject: [PATCH 1/2] feat(VariantBasedInjection): Keyed DI PoC VariantServiceProvider uses sp to get keyed services BREAKING CHANGE: behavioural: keyed services registration needed unless we override descriptors for backward comp --- .../FeatureManagementBuilderExtensions.cs | 28 +++--- .../Microsoft.FeatureManagement.csproj | 1 + .../VariantServiceProvider.cs | 32 ++----- .../FeatureManagementTest.cs | 87 +++++++++++++++++-- 4 files changed, 104 insertions(+), 44 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index f8635a79..b2e23b0c 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement.FeatureFilters; using System; -using System.Collections.Generic; using System.Linq; namespace Microsoft.FeatureManagement @@ -58,20 +57,19 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu 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( - featureName, - sp.GetRequiredService(), - sp.GetRequiredService>())); - } - else - { - builder.Services.AddSingleton>(sp => new VariantServiceProvider( - featureName, - sp.GetRequiredService(), - sp.GetRequiredService>())); - } + var variantSpLifetime = builder.Services + .Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && + descriptor.Lifetime == ServiceLifetime.Scoped) + ? ServiceLifetime.Scoped + : ServiceLifetime.Singleton; + builder.Services.Add( + ServiceDescriptor.Describe( + typeof(IVariantServiceProvider), + sp => new VariantServiceProvider( + featureName, + sp.GetRequiredService(), + sp), + variantSpLifetime)); return builder; } diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 170eafa8..9eb0e8ff 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index d4b3f514..6e261b9b 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // + +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,7 +16,7 @@ namespace Microsoft.FeatureManagement /// internal class VariantServiceProvider : IVariantServiceProvider where TService : class { - private readonly IEnumerable _services; + private readonly IServiceProvider _serviceProvider; private readonly IVariantFeatureManager _featureManager; private readonly string _featureName; private readonly ConcurrentDictionary _variantServiceCache; @@ -26,15 +26,15 @@ internal class VariantServiceProvider : IVariantServiceProvider /// 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. + /// Access to Implementation variants of TService. /// Thrown if is null. /// Thrown if is null. - /// Thrown if is null. - public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IEnumerable services) + /// Thrown if is null. + public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider keyedServiceProvider) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); - _services = services ?? throw new ArgumentNullException(nameof(services)); + _serviceProvider = keyedServiceProvider ?? throw new ArgumentNullException(nameof(keyedServiceProvider)); _variantServiceCache = new ConcurrentDictionary(); } @@ -55,26 +55,10 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT { implementation = _variantServiceCache.GetOrAdd( variant.Name, - (_) => _services.FirstOrDefault( - service => IsMatchingVariantName( - service.GetType(), - variant.Name)) - ); + key => _serviceProvider.GetKeyedService(key)); } return implementation; } - - private bool IsMatchingVariantName(Type implementationType, string variantName) - { - string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; - - if (implementationName == null) - { - implementationName = implementationType.Name; - } - - return string.Equals(implementationName, variantName, StringComparison.OrdinalIgnoreCase); - } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index a70e6a0d..89094925 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -524,12 +524,12 @@ public async Task MergesFeatureFlagsFromDifferentConfigurationSources() * Feature1: true * Feature2: true * FeatureA: true - * + * * appsettings2.json * Feature1: true * Feature2: false * FeatureB: true - * + * * appsettings3.json * Feature1: false * Feature2: false @@ -2166,9 +2166,9 @@ public async Task VariantBasedInjection() IServiceCollection services = new ServiceCollection(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => new AlgorithmOmega("OMEGA")); + services.AddKeyedSingleton(nameof(AlgorithmBeta)); + services.AddKeyedSingleton(nameof(AlgorithmSigma)); + services.AddKeyedSingleton("Omega", (sp, _) => new AlgorithmOmega("OMEGA")); services.AddSingleton(configuration) .AddFeatureManagement() @@ -2234,6 +2234,83 @@ public async Task VariantBasedInjection() ); } + [Fact] + public async Task VariantBasedInjectionScoped() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + services.AddKeyedScoped(nameof(AlgorithmBeta)); + services.AddKeyedScoped(nameof(AlgorithmSigma)); + services.AddKeyedScoped("Omega", (sp, _) => new AlgorithmOmega("OMEGA")); + + services.AddSingleton(configuration) + .AddScopedFeatureManagement() + .AddFeatureFilter() + .WithVariantService(Features.VariantImplementationFeature); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + var serviceProvider = services.BuildServiceProvider().CreateScope().ServiceProvider; + + 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); + + services = new ServiceCollection(); + + Assert.Throws(() => + { + services.AddFeatureManagement() + .WithVariantService("DummyFeature1") + .WithVariantService("DummyFeature2"); + } + ); + } + [Fact] public async Task VariantFeatureFlagWithContextualFeatureFilter() { From 57303b407d79dd828c7bd5fd2fc2029ff0e41c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 14 May 2026 18:00:39 +0300 Subject: [PATCH 2/2] refactor(VariantServiceProvider): ctor param name rename keyedServiceProvider to serviceProvider for clarity --- src/Microsoft.FeatureManagement/VariantServiceProvider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 6e261b9b..06016123 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -26,15 +26,15 @@ internal class VariantServiceProvider : IVariantServiceProvider /// 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. - /// Access to Implementation variants of TService. + /// Access to Implementation variants of TService. /// Thrown if is null. /// Thrown if is null. - /// Thrown if is null. - public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider keyedServiceProvider) + /// Thrown if is null. + public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); - _serviceProvider = keyedServiceProvider ?? throw new ArgumentNullException(nameof(keyedServiceProvider)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _variantServiceCache = new ConcurrentDictionary(); }