diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index f8635a79..3ed15615 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -58,22 +58,102 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu throw new InvalidOperationException($"A variant service of {typeof(TService).FullName} has already been added."); } + // Capture the service descriptors before the service provider is built + // This allows us to create factories without instantiating services eagerly + List serviceDescriptors = builder.Services + .Where(descriptor => descriptor.ServiceType == typeof(TService)) + .ToList(); + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) { builder.Services.AddScoped>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp.GetRequiredService>())); + CreateServiceFactories(sp, serviceDescriptors))); } else { builder.Services.AddSingleton>(sp => new VariantServiceProvider( featureName, sp.GetRequiredService(), - sp.GetRequiredService>())); + CreateServiceFactories(sp, serviceDescriptors))); } return builder; } + + /// + /// Creates factory delegates for all registered implementations of TService. + /// This enables lazy instantiation - services are only created when actually needed. + /// + private static IEnumerable> CreateServiceFactories( + IServiceProvider serviceProvider, + List serviceDescriptors) where TService : class + { + var factories = new List>(); + + foreach (ServiceDescriptor descriptor in serviceDescriptors) + { + Type implementationType = GetImplementationType(descriptor); + + // Create a lazy factory for each service + // The factory creates a singleton instance that is only instantiated when first invoked + var lazyService = new Lazy(() => CreateServiceFromDescriptor(serviceProvider, descriptor)); + + // For factory-based registrations, implementationType will be null + // The VariantServiceFactory will lazily determine the type by invoking the factory + factories.Add(new VariantServiceFactory( + implementationType, + () => lazyService.Value)); + } + + return factories; + } + + /// + /// Creates a service instance from a service descriptor. + /// + private static TService CreateServiceFromDescriptor(IServiceProvider serviceProvider, ServiceDescriptor descriptor) where TService : class + { + if (descriptor.ImplementationInstance != null) + { + return descriptor.ImplementationInstance as TService; + } + else if (descriptor.ImplementationFactory != null) + { + return descriptor.ImplementationFactory(serviceProvider) as TService; + } + else if (descriptor.ImplementationType != null) + { + // Use ActivatorUtilities to create the instance with dependency injection + return ActivatorUtilities.CreateInstance(serviceProvider, descriptor.ImplementationType) as TService; + } + + return null; + } + + /// + /// Gets the implementation type from a service descriptor. + /// Returns null for factory-based registrations where the type cannot be determined without invocation. + /// + private static Type GetImplementationType(ServiceDescriptor descriptor) + { + if (descriptor.ImplementationType != null) + { + return descriptor.ImplementationType; + } + else if (descriptor.ImplementationInstance != null) + { + return descriptor.ImplementationInstance.GetType(); + } + else if (descriptor.ImplementationFactory != null) + { + // For factory-based registrations, we can't determine the type without invoking the factory + // Return null and let VariantServiceFactory lazily determine it + return null; + } + + return null; + } } } diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs index d4b3f514..c603cd2e 100644 --- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs +++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs @@ -11,12 +11,52 @@ namespace Microsoft.FeatureManagement { + /// + /// Holds factory metadata for lazy service instantiation. + /// + internal class VariantServiceFactory where TService : class + { + private readonly Lazy _implementationType; + + public Type ImplementationType => _implementationType.Value; + public Func Factory { get; } + + public VariantServiceFactory(Type implementationType, Func factory) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + Factory = factory; + + if (implementationType != null) + { + _implementationType = new Lazy(() => implementationType); + } + else + { + // For factory-based registrations where type is unknown upfront, + // we lazily determine it by invoking the factory once. + // Note: The factory will be invoked to get the type when checking for a match, + // which means the instance is created at that point. This is acceptable because + // it only happens when we're looking for a match, and the instance is cached. + // Alternative approaches would require breaking changes to the registration API. + _implementationType = new Lazy(() => + { + TService instance = factory(); + return instance?.GetType(); + }); + } + } + } + /// /// Used to get different implementations of TService depending on the assigned variant from a specific variant feature flag. /// internal class VariantServiceProvider : IVariantServiceProvider where TService : class { - private readonly IEnumerable _services; + private readonly IEnumerable> _serviceFactories; private readonly IVariantFeatureManager _featureManager; private readonly string _featureName; private readonly ConcurrentDictionary _variantServiceCache; @@ -26,15 +66,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. + /// Factory delegates for 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, IEnumerable> serviceFactories) { _featureName = featureName ?? throw new ArgumentNullException(nameof(featureName)); _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); - _services = services ?? throw new ArgumentNullException(nameof(services)); + _serviceFactories = serviceFactories ?? throw new ArgumentNullException(nameof(serviceFactories)); _variantServiceCache = new ConcurrentDictionary(); } @@ -55,10 +95,15 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT { implementation = _variantServiceCache.GetOrAdd( variant.Name, - (_) => _services.FirstOrDefault( - service => IsMatchingVariantName( - service.GetType(), - variant.Name)) + (_) => + { + // Find the matching factory by checking the implementation type + VariantServiceFactory matchingFactory = _serviceFactories.FirstOrDefault( + factory => IsMatchingVariantName(factory.ImplementationType, variant.Name)); + + // Only invoke the factory if a match is found (lazy instantiation) + return matchingFactory?.Factory(); + } ); } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 10636b84..38b80a01 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -1811,6 +1811,57 @@ public async Task VariantBasedInjection() ); } + [Fact] + public async Task VariantServiceProviderLazyInstantiation() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + // Add a singleton tracker to observe which services are instantiated + var tracker = new InstantiationTracker(); + services.AddSingleton(tracker); + + // Add variant implementations of IAlgorithm + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter() + .WithVariantService(Features.VariantImplementationFeature); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + + services.AddSingleton(targetingContextAccessor); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>(); + + // At this point, no services should have been instantiated yet + // This is what we want to achieve with lazy instantiation + Assert.Empty(tracker.InstantiatedServices); + + // Set user to resolve to "AlgorithmAlpha" variant + targetingContextAccessor.Current = new TargetingContext + { + UserId = "UserAlpha" + }; + + // Now request the service - only AlgorithmAlpha should be instantiated + IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None); + + // Verify that only the selected variant was instantiated + Assert.Single(tracker.InstantiatedServices); + Assert.Contains("Alpha", tracker.InstantiatedServices); + Assert.NotNull(algorithm); + Assert.Equal("Alpha", algorithm.Style); + } + [Fact] public async Task VariantFeatureFlagWithContextualFeatureFilter() { diff --git a/tests/Tests.FeatureManagement/VariantServices.cs b/tests/Tests.FeatureManagement/VariantServices.cs index 942b110c..fe3f7e93 100644 --- a/tests/Tests.FeatureManagement/VariantServices.cs +++ b/tests/Tests.FeatureManagement/VariantServices.cs @@ -1,4 +1,5 @@ using Microsoft.FeatureManagement; +using System.Collections.Generic; namespace Tests.FeatureManagement { @@ -37,4 +38,51 @@ public AlgorithmOmega(string style) Style = style; } } + + // Test service with tracking for lazy instantiation tests + class AlgorithmAlpha : IAlgorithm + { + public string Style { get; set; } + + public AlgorithmAlpha(InstantiationTracker tracker) + { + Style = "Alpha"; + tracker.RecordInstantiation("Alpha"); + } + } + + class AlgorithmGamma : IAlgorithm + { + public string Style { get; set; } + + public AlgorithmGamma(InstantiationTracker tracker) + { + Style = "Gamma"; + tracker.RecordInstantiation("Gamma"); + } + } + + class AlgorithmDelta : IAlgorithm + { + public string Style { get; set; } + + public AlgorithmDelta(InstantiationTracker tracker) + { + Style = "Delta"; + tracker.RecordInstantiation("Delta"); + } + } + + // Tracker to record which services are instantiated + class InstantiationTracker + { + private readonly List _instantiatedServices = new List(); + + public void RecordInstantiation(string serviceName) + { + _instantiatedServices.Add(serviceName); + } + + public IReadOnlyList InstantiatedServices => _instantiatedServices.AsReadOnly(); + } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 018ef5c4..a6f1ec61 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -469,7 +469,8 @@ "Users": [ "UserOmega", "UserSigma", - "UserBeta" + "UserBeta", + "UserAlpha" ] } } @@ -486,6 +487,9 @@ }, { "name": "Omega" + }, + { + "name": "AlgorithmAlpha" } ], "allocation": { @@ -507,6 +511,12 @@ "users": [ "UserSigma" ] + }, + { + "variant": "AlgorithmAlpha", + "users": [ + "UserAlpha" + ] } ] }