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..2a29b1c5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -47,6 +47,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)) { @@ -58,19 +72,19 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu throw new InvalidOperationException($"A variant service of {typeof(TService).FullName} has already been added."); } + Func> variantSpFactory = sp => + { + var featureManager = sp.GetRequiredService(); + return new VariantServiceProvider(featureName, featureManager, sp, options); + }; + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) { - builder.Services.AddScoped>(sp => new VariantServiceProvider( - featureName, - sp.GetRequiredService(), - sp.GetRequiredService>())); + builder.Services.AddScoped(variantSpFactory); } else { - builder.Services.AddSingleton>(sp => new VariantServiceProvider( - featureName, - sp.GetRequiredService(), - sp.GetRequiredService>())); + builder.Services.AddSingleton(variantSpFactory); } 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/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index d4b3f514..e82b27ca 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,26 +17,30 @@ 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; + private readonly VariantServiceProviderOptions _options; + private readonly ConcurrentDictionary _variantServiceCache; /// /// 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. + /// 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. + /// 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, IEnumerable services) + /// Thrown if is 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)); - _services = services ?? throw new ArgumentNullException(nameof(services)); - _variantServiceCache = new ConcurrentDictionary(); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _variantServiceCache = new ConcurrentDictionary(); } /// @@ -47,34 +52,78 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT { Debug.Assert(_featureName != null); - Variant variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); + var variantService = await ResolveByVariantAsync(cancellationToken); + if (variantService != null) + { + return variantService; + } + + return await ResolveByStatusAsync(cancellationToken); + } + + private async Task ResolveByVariantAsync(CancellationToken cancellationToken) + { + var variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken); + + return variant != null ? _variantServiceCache.GetOrAdd(variant.Name, ResolveVariantService) : null; + } + + private async Task ResolveByStatusAsync(CancellationToken cancellationToken) + { + var isEnabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); + var statusKey = isEnabled ? _options.FallbackWhenEnabled : _options.FallbackWhenDisabled; + + return statusKey != null ? _variantServiceCache.GetOrAdd(statusKey, ResolveVariantService) : null; + } - TService implementation = null; + private TService ResolveVariantService(object variantKey) + { + // + // Prefer keyed resolution when supported. This enables lazy instantiation of variant implementations. + if (TryGetKeyedVariantService(variantKey, out var keyedVariantService)) + { + return keyedVariantService; + } + + if (variantKey is string variantName) + { + // + // Fall back to scanning non-keyed registrations and matching by VariantServiceAliasAttribute or type name. + return GetVariantServiceFallback(variantName); + } - if (variant != null) + return null; + } + + private bool TryGetKeyedVariantService(object variantName, out TService keyedService) + { + if (_serviceProvider is IKeyedServiceProvider keyedServiceProvider) { - implementation = _variantServiceCache.GetOrAdd( - variant.Name, - (_) => _services.FirstOrDefault( - service => IsMatchingVariantName( - service.GetType(), - variant.Name)) - ); + keyedService = keyedServiceProvider.GetKeyedService(variantName); + return keyedService != null; } - return implementation; + keyedService = null; + return false; + } + + private TService GetVariantServiceFallback(string variantName) + { + return _serviceProvider + .GetRequiredService>() + .FirstOrDefault(service => IsMatchingVariantName(service.GetType(), variantName)); } - private bool IsMatchingVariantName(Type implementationType, string variantName) + private static bool IsMatchingVariantName(Type implementationType, string name) { - string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; + var implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias; if (implementationName == null) { 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..f29d5510 --- /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 object FallbackWhenEnabled { get; set; } = true; + + /// + /// The alias used to resolve the variant service when the feature flag is disabled and no allocated variant matches. + /// + public object FallbackWhenDisabled { get; set; } = false; + } +} diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index a70e6a0d..c7c071ab 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 @@ -2234,6 +2234,415 @@ public async Task VariantBasedInjection() ); } + [Fact] + public async Task LazyVariantBasedInjectionScoped() + { + 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 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 VariantServiceProviderFallsBackToDefaultStatusAlias() + { + 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(true, (sp, _) => new AlgorithmOmega("Enabled")); + services.AddKeyedSingleton(false, (sp, _) => new AlgorithmOmega("Disabled")); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .WithVariantService(Features.OnTestFeature); + + IAlgorithm algorithm = await services.BuildServiceProvider() + .GetRequiredService>() + .GetServiceAsync(CancellationToken.None); + Assert.Equal("Enabled", algorithm.Style); + + services = new ServiceCollection(); + + services.AddKeyedSingleton(true, (sp, _) => new AlgorithmOmega("Enabled")); + services.AddKeyedSingleton(false, (sp, _) => new AlgorithmOmega("Disabled")); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .WithVariantService(Features.OffTestFeature); + + algorithm = await services.BuildServiceProvider() + .GetRequiredService>() + .GetServiceAsync(CancellationToken.None); + Assert.Equal("Disabled", algorithm.Style); + } + + [Fact] + public async Task VariantServiceProviderStatusEagerResolution() + { + 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.AddSingleton(); + services.AddSingleton(); + + var options = new VariantServiceProviderOptions + { + FallbackWhenEnabled = nameof(AlgorithmBeta), + FallbackWhenDisabled = nameof(AlgorithmSigma) + }; + + services.AddSingleton(configuration) + .AddFeatureManagement() + .WithVariantService(Features.OnTestFeature, options); + + IAlgorithm algorithm = await services.BuildServiceProvider() + .GetRequiredService>() + .GetServiceAsync(CancellationToken.None); + Assert.Equal("Beta", algorithm.Style); + + services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .WithVariantService(Features.OffTestFeature, options); + + algorithm = await services.BuildServiceProvider() + .GetRequiredService>() + .GetServiceAsync(CancellationToken.None); + Assert.Equal("Sigma", 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() {