From 5a50a16334b334994549ddeda5ad37347b1ae264 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 27 May 2026 11:27:06 +0800 Subject: [PATCH 1/6] support keyed service --- examples/VariantServiceDemo/Program.cs | 7 +- .../FeatureManagementBuilderExtensions.cs | 4 +- .../VariantServiceProvider.cs | 35 ++-- .../FeatureManagementTest.cs | 151 ++++++++++++++++++ 4 files changed, 182 insertions(+), 15 deletions(-) diff --git a/examples/VariantServiceDemo/Program.cs b/examples/VariantServiceDemo/Program.cs index a866d02d..de43ffb9 100644 --- a/examples/VariantServiceDemo/Program.cs +++ b/examples/VariantServiceDemo/Program.cs @@ -19,10 +19,11 @@ builder.Services.AddApplicationInsightsTelemetry(); // -// Add variant implementations of ICalculator -builder.Services.AddSingleton(); +// Add variant implementations of ICalculator using keyed services so that only the +// implementation matching the assigned variant is instantiated on demand. +builder.Services.AddKeyedSingleton("DefaultCalculator"); -builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("RemoteCalculator"); // // Enter feature management diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index f8635a79..53dd19cf 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -63,14 +63,14 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu builder.Services.AddScoped>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp.GetRequiredService>())); + sp)); } else { builder.Services.AddSingleton>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp.GetRequiredService>())); + sp)); } return builder; diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index d4b3f514..8f740618 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -16,7 +17,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 +27,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. + /// The service provider used to resolve implementation variants of TService. If it implements , keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated. /// 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 serviceProvider) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); - _services = services ?? throw new ArgumentNullException(nameof(services)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _variantServiceCache = new ConcurrentDictionary(); } @@ -55,16 +56,30 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT { implementation = _variantServiceCache.GetOrAdd( variant.Name, - (_) => _services.FirstOrDefault( - service => IsMatchingVariantName( - service.GetType(), - variant.Name)) - ); + (variantName) => ResolveVariantService(variantName)); } return implementation; } + private TService ResolveVariantService(string variantName) + { + if (_serviceProvider is IKeyedServiceProvider) + { + TService keyedService = _serviceProvider.GetKeyedService(variantName); + + if (keyedService != null) + { + return keyedService; + } + } + + IEnumerable services = _serviceProvider.GetRequiredService>(); + + return services.FirstOrDefault( + service => IsMatchingVariantName(service.GetType(), variantName)); + } + private bool IsMatchingVariantName(Type implementationType, string variantName) { string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index a70e6a0d..bc3c8d25 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -2234,6 +2234,157 @@ public async Task VariantBasedInjection() ); } + [Fact] + public async Task VariantServiceProviderResolvesKeyedService() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + services.AddKeyedSingleton("AlgorithmBeta"); + services.AddKeyedSingleton("Sigma"); + services.AddKeyedSingleton("Omega", (sp, _) => new AlgorithmOmega("OMEGA")); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter() + .WithVariantService(Features.VariantImplementationFeature); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); + + targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" }; + IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.NotNull(algorithm); + Assert.Equal("Beta", algorithm.Style); + + targetingContextAccessor.Current = new TargetingContext { UserId = "UserSigma" }; + algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.NotNull(algorithm); + Assert.Equal("Sigma", algorithm.Style); + + targetingContextAccessor.Current = new TargetingContext { UserId = "UserOmega" }; + algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.NotNull(algorithm); + Assert.Equal("OMEGA", algorithm.Style); + } + + [Fact] + public async Task VariantServiceProviderKeyedServiceIsLazilyInstantiated() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + int betaInstantiationCount = 0; + int sigmaInstantiationCount = 0; + int omegaInstantiationCount = 0; + + services.AddKeyedSingleton("AlgorithmBeta", (sp, _) => + { + betaInstantiationCount++; + return new AlgorithmBeta(); + }); + services.AddKeyedSingleton("Sigma", (sp, _) => + { + sigmaInstantiationCount++; + return new AlgorithmSigma(); + }); + services.AddKeyedSingleton("Omega", (sp, _) => + { + omegaInstantiationCount++; + return new AlgorithmOmega("OMEGA"); + }); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter() + .WithVariantService(Features.VariantImplementationFeature); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); + + // + // No variant resolved yet - nothing should be instantiated. + Assert.Equal(0, betaInstantiationCount); + Assert.Equal(0, sigmaInstantiationCount); + Assert.Equal(0, omegaInstantiationCount); + + // + // Resolve the Beta variant. Only AlgorithmBeta should be instantiated. + targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" }; + IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.Equal("Beta", algorithm.Style); + Assert.Equal(1, betaInstantiationCount); + Assert.Equal(0, sigmaInstantiationCount); + Assert.Equal(0, omegaInstantiationCount); + + // + // Resolving Beta again should reuse the cached instance - no new instantiation. + algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.Equal("Beta", algorithm.Style); + Assert.Equal(1, betaInstantiationCount); + Assert.Equal(0, sigmaInstantiationCount); + Assert.Equal(0, omegaInstantiationCount); + + // + // Resolve the Sigma variant. Only AlgorithmSigma should be instantiated additionally. + targetingContextAccessor.Current = new TargetingContext { UserId = "UserSigma" }; + algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.Equal("Sigma", algorithm.Style); + Assert.Equal(1, betaInstantiationCount); + Assert.Equal(1, sigmaInstantiationCount); + Assert.Equal(0, omegaInstantiationCount); + } + + [Fact] + public async Task VariantServiceProviderPrefersKeyedOverNonKeyed() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + // + // Register both keyed and non-keyed implementations matching the same variant name. + // The keyed registration should take precedence. + services.AddSingleton(); + services.AddKeyedSingleton("AlgorithmBeta", (sp, _) => new AlgorithmOmega("KeyedBeta")); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter() + .WithVariantService(Features.VariantImplementationFeature); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); + + targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" }; + IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.NotNull(algorithm); + Assert.Equal("KeyedBeta", algorithm.Style); + } + [Fact] public async Task VariantFeatureFlagWithContextualFeatureFilter() { From 36ea9bf5a7879db5b1adc82b5084116850822e64 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Fri, 29 May 2026 17:59:58 +0800 Subject: [PATCH 2/6] support variant service provider options --- .../FeatureManagementBuilderExtensions.cs | 21 +++- .../Microsoft.FeatureManagement.csproj | 2 +- .../VariantServiceAliasAttribute.cs | 11 +++ .../VariantServiceProvider.cs | 77 +++++++++++---- .../VariantServiceProviderOptions.cs | 21 ++++ .../FeatureManagementTest.cs | 96 ++++++++++++++++++- 6 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 53dd19cf..7c242843 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 @@ -47,6 +46,20 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement /// Thrown if feature name parameter is null. /// Thrown if a variant service of the type has already been added. public static IFeatureManagementBuilder WithVariantService(this IFeatureManagementBuilder builder, string featureName) where TService : class + { + return WithVariantService(builder, featureName, new VariantServiceProviderOptions()); + } + + /// + /// Adds a to the feature management system. + /// + /// The used to customize feature management functionality. + /// The feature flag that should be used to determine which variant of the service should be used. + /// Options used to configure the variant service provider. + /// A that can be used to customize feature management functionality. + /// Thrown if feature name parameter is null. + /// Thrown if a variant service of the type has already been added. + public static IFeatureManagementBuilder WithVariantService(this IFeatureManagementBuilder builder, string featureName, VariantServiceProviderOptions options) where TService : class { if (string.IsNullOrEmpty(featureName)) { @@ -63,14 +76,16 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu builder.Services.AddScoped>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp)); + sp, + options)); } else { builder.Services.AddSingleton>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp)); + sp, + options)); } return builder; diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 170eafa8..c0a5eb89 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -16,7 +16,7 @@ true false ..\..\build\Microsoft.FeatureManagement.snk - 8.0 diff --git a/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs index beebb88b..ee0597ff 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs @@ -24,6 +24,17 @@ public VariantServiceAliasAttribute(string alias) Alias = alias; } + /// + /// Creates a variant service alias whose value matches the default + /// or . This overload is a convenience for toggling between + /// two service implementations based on the feature flag status with minimal code. + /// + /// If true, the alias is set to ; otherwise, . + public VariantServiceAliasAttribute(bool enabled) + { + Alias = enabled ? bool.TrueString : bool.FalseString; + } + /// /// The name that will be used to match variant name specified in the configuration. /// diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 8f740618..030f0316 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // using Microsoft.Extensions.DependencyInjection; @@ -20,6 +20,7 @@ internal class VariantServiceProvider : IVariantServiceProvider _variantServiceCache; /// @@ -27,15 +28,17 @@ 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. - /// The service provider used to resolve implementation variants of TService. If it implements , keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated. + /// The service provider used to resolve implementations of TService. + /// Options used to configure the variant service provider. /// Thrown if is null. /// Thrown if is null. /// Thrown if is null. - public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider) + public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider, VariantServiceProviderOptions options = null) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _options = options; _variantServiceCache = new ConcurrentDictionary(); } @@ -48,39 +51,73 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT { Debug.Assert(_featureName != null); + TService implementation = await ResolveByVariantAsync(cancellationToken); + + if (implementation != null) + { + return implementation; + } + + return await ResolveByStatusAsync(cancellationToken); + } + + private async ValueTask ResolveByVariantAsync(CancellationToken cancellationToken) + { Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); - TService implementation = null; + if (variant == null) + { + return null; + } + + return _variantServiceCache.GetOrAdd( + variant.Name, + (name) => ResolveVariantService(name)); + } - if (variant != null) + private async ValueTask ResolveByStatusAsync(CancellationToken cancellationToken) + { + if (_options == null) { - implementation = _variantServiceCache.GetOrAdd( - variant.Name, - (variantName) => ResolveVariantService(variantName)); + return null; } - return implementation; + bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); + + string alias = isEnabled ? _options.EnabledAlias : _options.DisabledAlias; + + if (string.IsNullOrEmpty(alias)) + { + return null; + } + + return _variantServiceCache.GetOrAdd( + alias, + (name) => ResolveVariantService(name)); } - private TService ResolveVariantService(string variantName) + private TService ResolveVariantService(string name) { - if (_serviceProvider is IKeyedServiceProvider) + // + // Prefer keyed resolution when supported. This enables lazy instantiation of variant implementations. + if (_serviceProvider is IKeyedServiceProvider keyedServiceProvider) { - TService keyedService = _serviceProvider.GetKeyedService(variantName); + TService keyedVariantService = keyedServiceProvider.GetKeyedService(name); - if (keyedService != null) + if (keyedVariantService != null) { - return keyedService; + return keyedVariantService; } } - IEnumerable services = _serviceProvider.GetRequiredService>(); - - return services.FirstOrDefault( - service => IsMatchingVariantName(service.GetType(), variantName)); + // + // Fall back to scanning non-keyed registrations and matching by VariantServiceAliasAttribute or type name. + return _serviceProvider + .GetRequiredService>() + .FirstOrDefault(service => IsMatchingVariantName(service.GetType(), name)); } - private bool IsMatchingVariantName(Type implementationType, string variantName) + private static bool IsMatchingVariantName(Type implementationType, string name) { string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; @@ -89,7 +126,7 @@ private bool IsMatchingVariantName(Type implementationType, string variantName) implementationName = implementationType.Name; } - return string.Equals(implementationName, variantName, StringComparison.OrdinalIgnoreCase); + return string.Equals(implementationName, name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs b/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs new file mode 100644 index 00000000..6691d9eb --- /dev/null +++ b/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + /// + /// Specifies the aliases used by a variant service provider to resolve an implementation based on the feature flag status when no allocated variant matches. + /// + public class VariantServiceProviderOptions + { + /// + /// The alias used to resolve the variant service when the feature flag is enabled and no allocated variant matches. + /// + public string EnabledAlias { get; set; } = bool.TrueString; + + /// + /// The alias used to resolve the variant service when the feature flag is disabled and no allocated variant matches. + /// + public string DisabledAlias { get; set; } = bool.FalseString; + } +} diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index bc3c8d25..cd2e7786 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 @@ -2385,6 +2385,98 @@ public async Task VariantServiceProviderPrefersKeyedOverNonKeyed() Assert.Equal("KeyedBeta", algorithm.Style); } + [Fact] + public async Task VariantServiceProviderFallsBackToStatusAlias() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + // + // OnTestFeature has no variants and is always enabled; OffTestFeature has none and is always disabled. + // The provider should fall back to the EnabledAlias / DisabledAlias respectively. + IServiceCollection services = new ServiceCollection(); + + services.AddKeyedSingleton("WhenEnabled", (sp, _) => new AlgorithmOmega("Enabled")); + services.AddKeyedSingleton("WhenDisabled", (sp, _) => new AlgorithmOmega("Disabled")); + + var options = new VariantServiceProviderOptions + { + EnabledAlias = "WhenEnabled", + DisabledAlias = "WhenDisabled" + }; + + services.AddSingleton(configuration) + .AddFeatureManagement() + .WithVariantService(Features.OnTestFeature, options); + + IAlgorithm algorithm = await services.BuildServiceProvider() + .GetRequiredService>() + .GetServiceAsync(CancellationToken.None); + Assert.Equal("Enabled", algorithm.Style); + + services = new ServiceCollection(); + + services.AddKeyedSingleton("WhenEnabled", (sp, _) => new AlgorithmOmega("Enabled")); + services.AddKeyedSingleton("WhenDisabled", (sp, _) => new AlgorithmOmega("Disabled")); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .WithVariantService(Features.OffTestFeature, options); + + algorithm = await services.BuildServiceProvider() + .GetRequiredService>() + .GetServiceAsync(CancellationToken.None); + Assert.Equal("Disabled", algorithm.Style); + } + + [Fact] + public async Task VariantServiceProviderPrefersAllocatedVariantOverStatusAlias() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + // + // Conflict scenario: the allocated variant name "AlgorithmBeta" is also configured as the EnabledAlias. + // The variant resolution path runs first, so the same key resolves to the registered service for both + // a targeted user (variant allocated) and a non-targeted user (status fallback). The variant takes precedence + // when both paths could match, and the cache slot is shared without contention. + services.AddKeyedSingleton("AlgorithmBeta"); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter() + .WithVariantService(Features.VariantImplementationFeature, new VariantServiceProviderOptions + { + EnabledAlias = "AlgorithmBeta", + DisabledAlias = "AlgorithmBeta" + }); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); + + // + // UserBeta is allocated the "AlgorithmBeta" variant; resolution succeeds via the variant path. + targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" }; + IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.Equal("Beta", algorithm.Style); + + // + // Guest is outside the targeting audience; no variant is allocated and the flag is disabled, + // so resolution falls back to DisabledAlias ("AlgorithmBeta") and resolves the same registration. + targetingContextAccessor.Current = new TargetingContext { UserId = "Guest" }; + algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + Assert.Equal("Beta", algorithm.Style); + } + [Fact] public async Task VariantFeatureFlagWithContextualFeatureFilter() { From ee7aa8957e0cc3a62e3b48542fdeb8147fd0fdc2 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Fri, 29 May 2026 18:38:05 +0800 Subject: [PATCH 3/6] update --- .../DefaultServiceAlias.cs | 21 +++++++++++++++++++ .../VariantServiceAliasAttribute.cs | 11 ---------- .../VariantServiceProvider.cs | 2 +- .../VariantServiceProviderOptions.cs | 4 ++-- .../FeatureManagementTest.cs | 14 ++++++------- 5 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/DefaultServiceAlias.cs diff --git a/src/Microsoft.FeatureManagement/DefaultServiceAlias.cs b/src/Microsoft.FeatureManagement/DefaultServiceAlias.cs new file mode 100644 index 00000000..bc15a130 --- /dev/null +++ b/src/Microsoft.FeatureManagement/DefaultServiceAlias.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + /// + /// Aliases recognized by the variant service provider when matching an implementation against the feature flag status. + /// + public static class DefaultServiceAlias + { + /// + /// The default alias used to resolve the variant service when the feature flag is enabled. + /// + public const string WhenEnabled = "Enabled"; + + /// + /// The default alias used to resolve the variant service when the feature flag is disabled. + /// + public const string WhenDisabled = "Disabled"; + } +} diff --git a/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs index ee0597ff..beebb88b 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceAliasAttribute.cs @@ -24,17 +24,6 @@ public VariantServiceAliasAttribute(string alias) Alias = alias; } - /// - /// Creates a variant service alias whose value matches the default - /// or . This overload is a convenience for toggling between - /// two service implementations based on the feature flag status with minimal code. - /// - /// If true, the alias is set to ; otherwise, . - public VariantServiceAliasAttribute(bool enabled) - { - Alias = enabled ? bool.TrueString : bool.FalseString; - } - /// /// The name that will be used to match variant name specified in the configuration. /// diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 030f0316..865bce22 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -84,7 +84,7 @@ private async ValueTask ResolveByStatusAsync(CancellationToken cancell bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); - string alias = isEnabled ? _options.EnabledAlias : _options.DisabledAlias; + string alias = isEnabled ? _options.FallbackWhenEnabled : _options.FallbackWhenDisabled; if (string.IsNullOrEmpty(alias)) { diff --git a/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs b/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs index 6691d9eb..eefa4e0b 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs @@ -11,11 +11,11 @@ public class VariantServiceProviderOptions /// /// The alias used to resolve the variant service when the feature flag is enabled and no allocated variant matches. /// - public string EnabledAlias { get; set; } = bool.TrueString; + public string FallbackWhenEnabled { get; set; } = DefaultServiceAlias.WhenEnabled; /// /// The alias used to resolve the variant service when the feature flag is disabled and no allocated variant matches. /// - public string DisabledAlias { get; set; } = bool.FalseString; + public string FallbackWhenDisabled { get; set; } = DefaultServiceAlias.WhenDisabled; } } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index cd2e7786..85533afc 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -2394,7 +2394,7 @@ public async Task VariantServiceProviderFallsBackToStatusAlias() // // OnTestFeature has no variants and is always enabled; OffTestFeature has none and is always disabled. - // The provider should fall back to the EnabledAlias / DisabledAlias respectively. + // The provider should fall back to the FallbackWhenEnabled / FallbackWhenDisabled respectively. IServiceCollection services = new ServiceCollection(); services.AddKeyedSingleton("WhenEnabled", (sp, _) => new AlgorithmOmega("Enabled")); @@ -2402,8 +2402,8 @@ public async Task VariantServiceProviderFallsBackToStatusAlias() var options = new VariantServiceProviderOptions { - EnabledAlias = "WhenEnabled", - DisabledAlias = "WhenDisabled" + FallbackWhenEnabled = "WhenEnabled", + FallbackWhenDisabled = "WhenDisabled" }; services.AddSingleton(configuration) @@ -2440,7 +2440,7 @@ public async Task VariantServiceProviderPrefersAllocatedVariantOverStatusAlias() IServiceCollection services = new ServiceCollection(); // - // Conflict scenario: the allocated variant name "AlgorithmBeta" is also configured as the EnabledAlias. + // Conflict scenario: the allocated variant name "AlgorithmBeta" is also configured as the FallbackWhenEnabled. // The variant resolution path runs first, so the same key resolves to the registered service for both // a targeted user (variant allocated) and a non-targeted user (status fallback). The variant takes precedence // when both paths could match, and the cache slot is shared without contention. @@ -2451,8 +2451,8 @@ public async Task VariantServiceProviderPrefersAllocatedVariantOverStatusAlias() .AddFeatureFilter() .WithVariantService(Features.VariantImplementationFeature, new VariantServiceProviderOptions { - EnabledAlias = "AlgorithmBeta", - DisabledAlias = "AlgorithmBeta" + FallbackWhenEnabled = "AlgorithmBeta", + FallbackWhenDisabled = "AlgorithmBeta" }); var targetingContextAccessor = new OnDemandTargetingContextAccessor(); @@ -2471,7 +2471,7 @@ public async Task VariantServiceProviderPrefersAllocatedVariantOverStatusAlias() // // Guest is outside the targeting audience; no variant is allocated and the flag is disabled, - // so resolution falls back to DisabledAlias ("AlgorithmBeta") and resolves the same registration. + // so resolution falls back to FallbackWhenDisabled ("AlgorithmBeta") and resolves the same registration. targetingContextAccessor.Current = new TargetingContext { UserId = "Guest" }; algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); Assert.Equal("Beta", algorithm.Style); From 3932886d7bb18eaed7dbd53d141360ce211daa54 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 1 Jun 2026 16:32:18 +0800 Subject: [PATCH 4/6] update --- .../VariantServiceProvider.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 865bce22..15cbef99 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -33,12 +33,13 @@ internal class VariantServiceProvider : IVariantServiceProviderThrown if is null. /// Thrown if is null. /// Thrown if is null. - public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider, VariantServiceProviderOptions options = null) + /// Thrown if is null. + public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider, VariantServiceProviderOptions options) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); _variantServiceCache = new ConcurrentDictionary(); } @@ -77,11 +78,6 @@ private async ValueTask ResolveByVariantAsync(CancellationToken cancel private async ValueTask ResolveByStatusAsync(CancellationToken cancellationToken) { - if (_options == null) - { - return null; - } - bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); string alias = isEnabled ? _options.FallbackWhenEnabled : _options.FallbackWhenDisabled; From 10771c8bb90b59c9cd0963c563e308caa52b8405 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 2 Jun 2026 14:12:18 +0800 Subject: [PATCH 5/6] revert variant service provider options --- .../DefaultServiceAlias.cs | 21 ----- .../FeatureManagementBuilderExtensions.cs | 21 +---- .../Microsoft.FeatureManagement.csproj | 2 +- .../VariantServiceProvider.cs | 73 ++++----------- .../VariantServiceProviderOptions.cs | 21 ----- .../FeatureManagementTest.cs | 92 ------------------- 6 files changed, 24 insertions(+), 206 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/DefaultServiceAlias.cs delete mode 100644 src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs diff --git a/src/Microsoft.FeatureManagement/DefaultServiceAlias.cs b/src/Microsoft.FeatureManagement/DefaultServiceAlias.cs deleted file mode 100644 index bc15a130..00000000 --- a/src/Microsoft.FeatureManagement/DefaultServiceAlias.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Microsoft.FeatureManagement -{ - /// - /// Aliases recognized by the variant service provider when matching an implementation against the feature flag status. - /// - public static class DefaultServiceAlias - { - /// - /// The default alias used to resolve the variant service when the feature flag is enabled. - /// - public const string WhenEnabled = "Enabled"; - - /// - /// The default alias used to resolve the variant service when the feature flag is disabled. - /// - public const string WhenDisabled = "Disabled"; - } -} diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 7c242843..53dd19cf 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement.FeatureFilters; using System; +using System.Collections.Generic; using System.Linq; namespace Microsoft.FeatureManagement @@ -46,20 +47,6 @@ public static IFeatureManagementBuilder WithTargeting(this IFeatureManagement /// Thrown if feature name parameter is null. /// Thrown if a variant service of the type has already been added. public static IFeatureManagementBuilder WithVariantService(this IFeatureManagementBuilder builder, string featureName) where TService : class - { - return WithVariantService(builder, featureName, new VariantServiceProviderOptions()); - } - - /// - /// Adds a to the feature management system. - /// - /// The used to customize feature management functionality. - /// The feature flag that should be used to determine which variant of the service should be used. - /// Options used to configure the variant service provider. - /// A that can be used to customize feature management functionality. - /// Thrown if feature name parameter is null. - /// Thrown if a variant service of the type has already been added. - public static IFeatureManagementBuilder WithVariantService(this IFeatureManagementBuilder builder, string featureName, VariantServiceProviderOptions options) where TService : class { if (string.IsNullOrEmpty(featureName)) { @@ -76,16 +63,14 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu builder.Services.AddScoped>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp, - options)); + sp)); } else { builder.Services.AddSingleton>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp, - options)); + sp)); } return builder; diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index c0a5eb89..170eafa8 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -16,7 +16,7 @@ true false ..\..\build\Microsoft.FeatureManagement.snk - 8.0 diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 15cbef99..8f740618 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // using Microsoft.Extensions.DependencyInjection; @@ -20,7 +20,6 @@ internal class VariantServiceProvider : IVariantServiceProvider _variantServiceCache; /// @@ -28,18 +27,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. - /// The service provider used to resolve implementations of TService. - /// Options used to configure the variant service provider. + /// The service provider used to resolve implementation variants of TService. If it implements , keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated. /// Thrown if is null. /// Thrown if is null. /// Thrown if is null. - /// Thrown if is null. - public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider, VariantServiceProviderOptions options) + public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _options = options ?? throw new ArgumentNullException(nameof(options)); _variantServiceCache = new ConcurrentDictionary(); } @@ -52,68 +48,39 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT { Debug.Assert(_featureName != null); - TService implementation = await ResolveByVariantAsync(cancellationToken); - - if (implementation != null) - { - return implementation; - } - - return await ResolveByStatusAsync(cancellationToken); - } - - private async ValueTask ResolveByVariantAsync(CancellationToken cancellationToken) - { Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); - if (variant == null) - { - return null; - } - - return _variantServiceCache.GetOrAdd( - variant.Name, - (name) => ResolveVariantService(name)); - } + TService implementation = null; - private async ValueTask ResolveByStatusAsync(CancellationToken cancellationToken) - { - bool isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); - - string alias = isEnabled ? _options.FallbackWhenEnabled : _options.FallbackWhenDisabled; - - if (string.IsNullOrEmpty(alias)) + if (variant != null) { - return null; + implementation = _variantServiceCache.GetOrAdd( + variant.Name, + (variantName) => ResolveVariantService(variantName)); } - return _variantServiceCache.GetOrAdd( - alias, - (name) => ResolveVariantService(name)); + return implementation; } - private TService ResolveVariantService(string name) + private TService ResolveVariantService(string variantName) { - // - // Prefer keyed resolution when supported. This enables lazy instantiation of variant implementations. - if (_serviceProvider is IKeyedServiceProvider keyedServiceProvider) + if (_serviceProvider is IKeyedServiceProvider) { - TService keyedVariantService = keyedServiceProvider.GetKeyedService(name); + TService keyedService = _serviceProvider.GetKeyedService(variantName); - if (keyedVariantService != null) + if (keyedService != null) { - return keyedVariantService; + return keyedService; } } - // - // Fall back to scanning non-keyed registrations and matching by VariantServiceAliasAttribute or type name. - return _serviceProvider - .GetRequiredService>() - .FirstOrDefault(service => IsMatchingVariantName(service.GetType(), name)); + IEnumerable services = _serviceProvider.GetRequiredService>(); + + return services.FirstOrDefault( + service => IsMatchingVariantName(service.GetType(), variantName)); } - private static bool IsMatchingVariantName(Type implementationType, string name) + private bool IsMatchingVariantName(Type implementationType, string variantName) { string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; @@ -122,7 +89,7 @@ private static bool IsMatchingVariantName(Type implementationType, string name) implementationName = implementationType.Name; } - return string.Equals(implementationName, name, StringComparison.OrdinalIgnoreCase); + return string.Equals(implementationName, variantName, StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs b/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs deleted file mode 100644 index eefa4e0b..00000000 --- a/src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Microsoft.FeatureManagement -{ - /// - /// Specifies the aliases used by a variant service provider to resolve an implementation based on the feature flag status when no allocated variant matches. - /// - public class VariantServiceProviderOptions - { - /// - /// The alias used to resolve the variant service when the feature flag is enabled and no allocated variant matches. - /// - public string FallbackWhenEnabled { get; set; } = DefaultServiceAlias.WhenEnabled; - - /// - /// The alias used to resolve the variant service when the feature flag is disabled and no allocated variant matches. - /// - public string FallbackWhenDisabled { get; set; } = DefaultServiceAlias.WhenDisabled; - } -} diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 85533afc..4219a518 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -2385,98 +2385,6 @@ public async Task VariantServiceProviderPrefersKeyedOverNonKeyed() Assert.Equal("KeyedBeta", algorithm.Style); } - [Fact] - public async Task VariantServiceProviderFallsBackToStatusAlias() - { - IConfiguration configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - - // - // OnTestFeature has no variants and is always enabled; OffTestFeature has none and is always disabled. - // The provider should fall back to the FallbackWhenEnabled / FallbackWhenDisabled respectively. - IServiceCollection services = new ServiceCollection(); - - services.AddKeyedSingleton("WhenEnabled", (sp, _) => new AlgorithmOmega("Enabled")); - services.AddKeyedSingleton("WhenDisabled", (sp, _) => new AlgorithmOmega("Disabled")); - - var options = new VariantServiceProviderOptions - { - FallbackWhenEnabled = "WhenEnabled", - FallbackWhenDisabled = "WhenDisabled" - }; - - services.AddSingleton(configuration) - .AddFeatureManagement() - .WithVariantService(Features.OnTestFeature, options); - - IAlgorithm algorithm = await services.BuildServiceProvider() - .GetRequiredService>() - .GetServiceAsync(CancellationToken.None); - Assert.Equal("Enabled", algorithm.Style); - - services = new ServiceCollection(); - - services.AddKeyedSingleton("WhenEnabled", (sp, _) => new AlgorithmOmega("Enabled")); - services.AddKeyedSingleton("WhenDisabled", (sp, _) => new AlgorithmOmega("Disabled")); - - services.AddSingleton(configuration) - .AddFeatureManagement() - .WithVariantService(Features.OffTestFeature, options); - - algorithm = await services.BuildServiceProvider() - .GetRequiredService>() - .GetServiceAsync(CancellationToken.None); - Assert.Equal("Disabled", algorithm.Style); - } - - [Fact] - public async Task VariantServiceProviderPrefersAllocatedVariantOverStatusAlias() - { - IConfiguration configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - - IServiceCollection services = new ServiceCollection(); - - // - // Conflict scenario: the allocated variant name "AlgorithmBeta" is also configured as the FallbackWhenEnabled. - // The variant resolution path runs first, so the same key resolves to the registered service for both - // a targeted user (variant allocated) and a non-targeted user (status fallback). The variant takes precedence - // when both paths could match, and the cache slot is shared without contention. - services.AddKeyedSingleton("AlgorithmBeta"); - - services.AddSingleton(configuration) - .AddFeatureManagement() - .AddFeatureFilter() - .WithVariantService(Features.VariantImplementationFeature, new VariantServiceProviderOptions - { - FallbackWhenEnabled = "AlgorithmBeta", - FallbackWhenDisabled = "AlgorithmBeta" - }); - - var targetingContextAccessor = new OnDemandTargetingContextAccessor(); - - services.AddSingleton(targetingContextAccessor); - - ServiceProvider serviceProvider = services.BuildServiceProvider(); - - IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); - - // - // UserBeta is allocated the "AlgorithmBeta" variant; resolution succeeds via the variant path. - targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" }; - IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); - Assert.Equal("Beta", algorithm.Style); - - // - // Guest is outside the targeting audience; no variant is allocated and the flag is disabled, - // so resolution falls back to FallbackWhenDisabled ("AlgorithmBeta") and resolves the same registration. - targetingContextAccessor.Current = new TargetingContext { UserId = "Guest" }; - algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); - Assert.Equal("Beta", algorithm.Style); - } - [Fact] public async Task VariantFeatureFlagWithContextualFeatureFilter() { From f15241a5ccbfba4de620f3cd73d260a644eda282 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 2 Jun 2026 14:30:08 +0800 Subject: [PATCH 6/6] add comment --- src/Microsoft.FeatureManagement/VariantServiceProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index 8f740618..16293fb1 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -64,6 +64,9 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT private TService ResolveVariantService(string variantName) { + // + // If the service provider supports keyed services, try to resolve the variant by its name as the key first. + // This allows lazy instantiation of the variant service. if (_serviceProvider is IKeyedServiceProvider) { TService keyedService = _serviceProvider.GetKeyedService(variantName); @@ -74,6 +77,8 @@ private TService ResolveVariantService(string variantName) } } + // + // Fall back to enumerating all non-keyed registrations of TService and matching by VariantServiceAliasAttribute or the implementation type name. IEnumerable services = _serviceProvider.GetRequiredService>(); return services.FirstOrDefault(