@@ -524,12 +524,12 @@ public async Task MergesFeatureFlagsFromDifferentConfigurationSources()
524524 * Feature1: true
525525 * Feature2: true
526526 * FeatureA: true
527- *
527+ *
528528 * appsettings2.json
529529 * Feature1: true
530530 * Feature2: false
531531 * FeatureB: true
532- *
532+ *
533533 * appsettings3.json
534534 * Feature1: false
535535 * Feature2: false
@@ -2234,6 +2234,157 @@ public async Task VariantBasedInjection()
22342234 ) ;
22352235 }
22362236
2237+ [ Fact ]
2238+ public async Task VariantServiceProviderResolvesKeyedService ( )
2239+ {
2240+ IConfiguration configuration = new ConfigurationBuilder ( )
2241+ . AddJsonFile ( "appsettings.json" )
2242+ . Build ( ) ;
2243+
2244+ IServiceCollection services = new ServiceCollection ( ) ;
2245+
2246+ services . AddKeyedSingleton < IAlgorithm , AlgorithmBeta > ( "AlgorithmBeta" ) ;
2247+ services . AddKeyedSingleton < IAlgorithm , AlgorithmSigma > ( "Sigma" ) ;
2248+ services . AddKeyedSingleton < IAlgorithm > ( "Omega" , ( sp , _ ) => new AlgorithmOmega ( "OMEGA" ) ) ;
2249+
2250+ services . AddSingleton ( configuration )
2251+ . AddFeatureManagement ( )
2252+ . AddFeatureFilter < TargetingFilter > ( )
2253+ . WithVariantService < IAlgorithm > ( Features . VariantImplementationFeature ) ;
2254+
2255+ var targetingContextAccessor = new OnDemandTargetingContextAccessor ( ) ;
2256+
2257+ services . AddSingleton < ITargetingContextAccessor > ( targetingContextAccessor ) ;
2258+
2259+ ServiceProvider serviceProvider = services . BuildServiceProvider ( ) ;
2260+
2261+ IVariantServiceProvider < IAlgorithm > featuredAlgorithm = serviceProvider . GetRequiredService < IVariantServiceProvider < IAlgorithm > > ( ) ;
2262+
2263+ targetingContextAccessor . Current = new TargetingContext { UserId = "UserBeta" } ;
2264+ IAlgorithm algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2265+ Assert . NotNull ( algorithm ) ;
2266+ Assert . Equal ( "Beta" , algorithm . Style ) ;
2267+
2268+ targetingContextAccessor . Current = new TargetingContext { UserId = "UserSigma" } ;
2269+ algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2270+ Assert . NotNull ( algorithm ) ;
2271+ Assert . Equal ( "Sigma" , algorithm . Style ) ;
2272+
2273+ targetingContextAccessor . Current = new TargetingContext { UserId = "UserOmega" } ;
2274+ algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2275+ Assert . NotNull ( algorithm ) ;
2276+ Assert . Equal ( "OMEGA" , algorithm . Style ) ;
2277+ }
2278+
2279+ [ Fact ]
2280+ public async Task VariantServiceProviderKeyedServiceIsLazilyInstantiated ( )
2281+ {
2282+ IConfiguration configuration = new ConfigurationBuilder ( )
2283+ . AddJsonFile ( "appsettings.json" )
2284+ . Build ( ) ;
2285+
2286+ IServiceCollection services = new ServiceCollection ( ) ;
2287+
2288+ int betaInstantiationCount = 0 ;
2289+ int sigmaInstantiationCount = 0 ;
2290+ int omegaInstantiationCount = 0 ;
2291+
2292+ services . AddKeyedSingleton < IAlgorithm > ( "AlgorithmBeta" , ( sp , _ ) =>
2293+ {
2294+ betaInstantiationCount ++ ;
2295+ return new AlgorithmBeta ( ) ;
2296+ } ) ;
2297+ services . AddKeyedSingleton < IAlgorithm > ( "Sigma" , ( sp , _ ) =>
2298+ {
2299+ sigmaInstantiationCount ++ ;
2300+ return new AlgorithmSigma ( ) ;
2301+ } ) ;
2302+ services . AddKeyedSingleton < IAlgorithm > ( "Omega" , ( sp , _ ) =>
2303+ {
2304+ omegaInstantiationCount ++ ;
2305+ return new AlgorithmOmega ( "OMEGA" ) ;
2306+ } ) ;
2307+
2308+ services . AddSingleton ( configuration )
2309+ . AddFeatureManagement ( )
2310+ . AddFeatureFilter < TargetingFilter > ( )
2311+ . WithVariantService < IAlgorithm > ( Features . VariantImplementationFeature ) ;
2312+
2313+ var targetingContextAccessor = new OnDemandTargetingContextAccessor ( ) ;
2314+
2315+ services . AddSingleton < ITargetingContextAccessor > ( targetingContextAccessor ) ;
2316+
2317+ ServiceProvider serviceProvider = services . BuildServiceProvider ( ) ;
2318+
2319+ IVariantServiceProvider < IAlgorithm > featuredAlgorithm = serviceProvider . GetRequiredService < IVariantServiceProvider < IAlgorithm > > ( ) ;
2320+
2321+ //
2322+ // No variant resolved yet - nothing should be instantiated.
2323+ Assert . Equal ( 0 , betaInstantiationCount ) ;
2324+ Assert . Equal ( 0 , sigmaInstantiationCount ) ;
2325+ Assert . Equal ( 0 , omegaInstantiationCount ) ;
2326+
2327+ //
2328+ // Resolve the Beta variant. Only AlgorithmBeta should be instantiated.
2329+ targetingContextAccessor . Current = new TargetingContext { UserId = "UserBeta" } ;
2330+ IAlgorithm algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2331+ Assert . Equal ( "Beta" , algorithm . Style ) ;
2332+ Assert . Equal ( 1 , betaInstantiationCount ) ;
2333+ Assert . Equal ( 0 , sigmaInstantiationCount ) ;
2334+ Assert . Equal ( 0 , omegaInstantiationCount ) ;
2335+
2336+ //
2337+ // Resolving Beta again should reuse the cached instance - no new instantiation.
2338+ algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2339+ Assert . Equal ( "Beta" , algorithm . Style ) ;
2340+ Assert . Equal ( 1 , betaInstantiationCount ) ;
2341+ Assert . Equal ( 0 , sigmaInstantiationCount ) ;
2342+ Assert . Equal ( 0 , omegaInstantiationCount ) ;
2343+
2344+ //
2345+ // Resolve the Sigma variant. Only AlgorithmSigma should be instantiated additionally.
2346+ targetingContextAccessor . Current = new TargetingContext { UserId = "UserSigma" } ;
2347+ algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2348+ Assert . Equal ( "Sigma" , algorithm . Style ) ;
2349+ Assert . Equal ( 1 , betaInstantiationCount ) ;
2350+ Assert . Equal ( 1 , sigmaInstantiationCount ) ;
2351+ Assert . Equal ( 0 , omegaInstantiationCount ) ;
2352+ }
2353+
2354+ [ Fact ]
2355+ public async Task VariantServiceProviderPrefersKeyedOverNonKeyed ( )
2356+ {
2357+ IConfiguration configuration = new ConfigurationBuilder ( )
2358+ . AddJsonFile ( "appsettings.json" )
2359+ . Build ( ) ;
2360+
2361+ IServiceCollection services = new ServiceCollection ( ) ;
2362+
2363+ //
2364+ // Register both keyed and non-keyed implementations matching the same variant name.
2365+ // The keyed registration should take precedence.
2366+ services . AddSingleton < IAlgorithm , AlgorithmBeta > ( ) ;
2367+ services . AddKeyedSingleton < IAlgorithm > ( "AlgorithmBeta" , ( sp , _ ) => new AlgorithmOmega ( "KeyedBeta" ) ) ;
2368+
2369+ services . AddSingleton ( configuration )
2370+ . AddFeatureManagement ( )
2371+ . AddFeatureFilter < TargetingFilter > ( )
2372+ . WithVariantService < IAlgorithm > ( Features . VariantImplementationFeature ) ;
2373+
2374+ var targetingContextAccessor = new OnDemandTargetingContextAccessor ( ) ;
2375+
2376+ services . AddSingleton < ITargetingContextAccessor > ( targetingContextAccessor ) ;
2377+
2378+ ServiceProvider serviceProvider = services . BuildServiceProvider ( ) ;
2379+
2380+ IVariantServiceProvider < IAlgorithm > featuredAlgorithm = serviceProvider . GetRequiredService < IVariantServiceProvider < IAlgorithm > > ( ) ;
2381+
2382+ targetingContextAccessor . Current = new TargetingContext { UserId = "UserBeta" } ;
2383+ IAlgorithm algorithm = await featuredAlgorithm . GetServiceAsync ( CancellationToken . None ) ;
2384+ Assert . NotNull ( algorithm ) ;
2385+ Assert . Equal ( "KeyedBeta" , algorithm . Style ) ;
2386+ }
2387+
22372388 [ Fact ]
22382389 public async Task VariantFeatureFlagWithContextualFeatureFilter ( )
22392390 {
0 commit comments