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..06016123 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 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,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() {