Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,102 @@ public static IFeatureManagementBuilder WithVariantService<TService>(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<ServiceDescriptor> serviceDescriptors = builder.Services
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is no guaranteed that the TService has been registered when WithVariantService is called.

.Where(descriptor => descriptor.ServiceType == typeof(TService))
.ToList();

if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped))
{
builder.Services.AddScoped<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
CreateServiceFactories<TService>(sp, serviceDescriptors)));
}
else
{
builder.Services.AddSingleton<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
CreateServiceFactories<TService>(sp, serviceDescriptors)));
}

return builder;
}

/// <summary>
/// Creates factory delegates for all registered implementations of TService.
/// This enables lazy instantiation - services are only created when actually needed.
/// </summary>
private static IEnumerable<VariantServiceFactory<TService>> CreateServiceFactories<TService>(
IServiceProvider serviceProvider,
List<ServiceDescriptor> serviceDescriptors) where TService : class
{
var factories = new List<VariantServiceFactory<TService>>();

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<TService>(() => CreateServiceFromDescriptor<TService>(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<TService>(
implementationType,
() => lazyService.Value));
}

return factories;
}

/// <summary>
/// Creates a service instance from a service descriptor.
/// </summary>
private static TService CreateServiceFromDescriptor<TService>(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;
}

/// <summary>
/// Gets the implementation type from a service descriptor.
/// Returns null for factory-based registrations where the type cannot be determined without invocation.
/// </summary>
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;
}
}
}
63 changes: 54 additions & 9 deletions src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,52 @@

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Holds factory metadata for lazy service instantiation.
/// </summary>
internal class VariantServiceFactory<TService> where TService : class
{
private readonly Lazy<Type> _implementationType;

public Type ImplementationType => _implementationType.Value;
public Func<TService> Factory { get; }

public VariantServiceFactory(Type implementationType, Func<TService> factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}

Factory = factory;

if (implementationType != null)
{
_implementationType = new Lazy<Type>(() => 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<Type>(() =>
{
TService instance = factory();
return instance?.GetType();
});
}
}
}

/// <summary>
/// Used to get different implementations of TService depending on the assigned variant from a specific variant feature flag.
/// </summary>
internal class VariantServiceProvider<TService> : IVariantServiceProvider<TService> where TService : class
{
private readonly IEnumerable<TService> _services;
private readonly IEnumerable<VariantServiceFactory<TService>> _serviceFactories;
private readonly IVariantFeatureManager _featureManager;
private readonly string _featureName;
private readonly ConcurrentDictionary<string, TService> _variantServiceCache;
Expand All @@ -26,15 +66,15 @@ internal class VariantServiceProvider<TService> : IVariantServiceProvider<TServi
/// </summary>
/// <param name="featureName">The feature flag that should be used to determine which variant of the service should be used.</param>
/// <param name="featureManager">The feature manager to get the assigned variant of the feature flag.</param>
/// <param name="services">Implementation variants of TService.</param>
/// <param name="serviceFactories">Factory delegates for implementation variants of TService.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureName"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureManager"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="services"/> is null.</exception>
public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IEnumerable<TService> services)
/// <exception cref="ArgumentNullException">Thrown if <paramref name="serviceFactories"/> is null.</exception>
public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IEnumerable<VariantServiceFactory<TService>> 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<string, TService>();
}

Expand All @@ -55,10 +95,15 @@ public async ValueTask<TService> 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<TService> matchingFactory = _serviceFactories.FirstOrDefault(
factory => IsMatchingVariantName(factory.ImplementationType, variant.Name));

// Only invoke the factory if a match is found (lazy instantiation)
return matchingFactory?.Factory();
}
);
}

Expand Down
51 changes: 51 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAlgorithm, AlgorithmAlpha>();
services.AddSingleton<IAlgorithm, AlgorithmGamma>();
services.AddSingleton<IAlgorithm, AlgorithmDelta>();

services.AddSingleton(configuration)
.AddFeatureManagement()
.AddFeatureFilter<TargetingFilter>()
.WithVariantService<IAlgorithm>(Features.VariantImplementationFeature);

var targetingContextAccessor = new OnDemandTargetingContextAccessor();

services.AddSingleton<ITargetingContextAccessor>(targetingContextAccessor);

ServiceProvider serviceProvider = services.BuildServiceProvider();

IVariantServiceProvider<IAlgorithm> featuredAlgorithm = serviceProvider.GetRequiredService<IVariantServiceProvider<IAlgorithm>>();

// 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()
{
Expand Down
48 changes: 48 additions & 0 deletions tests/Tests.FeatureManagement/VariantServices.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.FeatureManagement;
using System.Collections.Generic;

namespace Tests.FeatureManagement
{
Expand Down Expand Up @@ -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<string> _instantiatedServices = new List<string>();

public void RecordInstantiation(string serviceName)
{
_instantiatedServices.Add(serviceName);
}

public IReadOnlyList<string> InstantiatedServices => _instantiatedServices.AsReadOnly();
}
}
12 changes: 11 additions & 1 deletion tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,8 @@
"Users": [
"UserOmega",
"UserSigma",
"UserBeta"
"UserBeta",
"UserAlpha"
]
}
}
Expand All @@ -486,6 +487,9 @@
},
{
"name": "Omega"
},
{
"name": "AlgorithmAlpha"
}
],
"allocation": {
Expand All @@ -507,6 +511,12 @@
"users": [
"UserSigma"
]
},
{
"variant": "AlgorithmAlpha",
"users": [
"UserAlpha"
]
}
]
}
Expand Down