Skip to content
Open
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 @@ -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
Expand Down Expand Up @@ -58,20 +57,19 @@ public static IFeatureManagementBuilder WithVariantService<TService>(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<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
}
else
{
builder.Services.AddSingleton<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
}
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<TService>),
sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp),
variantSpLifetime));

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
</ItemGroup>
Expand Down
32 changes: 8 additions & 24 deletions src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -16,7 +16,7 @@ namespace Microsoft.FeatureManagement
/// </summary>
internal class VariantServiceProvider<TService> : IVariantServiceProvider<TService> where TService : class
{
private readonly IEnumerable<TService> _services;
private readonly IServiceProvider _serviceProvider;
private readonly IVariantFeatureManager _featureManager;
private readonly string _featureName;
private readonly ConcurrentDictionary<string, TService> _variantServiceCache;
Expand All @@ -26,15 +26,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="serviceProvider">Access to 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="serviceProvider"/> is null.</exception>
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<string, TService>();
}

Expand All @@ -55,26 +55,10 @@ public async ValueTask<TService> GetServiceAsync(CancellationToken cancellationT
{
implementation = _variantServiceCache.GetOrAdd(
variant.Name,
(_) => _services.FirstOrDefault(
service => IsMatchingVariantName(
service.GetType(),
variant.Name))
);
key => _serviceProvider.GetKeyedService<TService>(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);
}
}
}
87 changes: 82 additions & 5 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2166,9 +2166,9 @@ public async Task VariantBasedInjection()

IServiceCollection services = new ServiceCollection();

services.AddSingleton<IAlgorithm, AlgorithmBeta>();
services.AddSingleton<IAlgorithm, AlgorithmSigma>();
services.AddSingleton<IAlgorithm>(sp => new AlgorithmOmega("OMEGA"));
services.AddKeyedSingleton<IAlgorithm, AlgorithmBeta>(nameof(AlgorithmBeta));
services.AddKeyedSingleton<IAlgorithm, AlgorithmSigma>(nameof(AlgorithmSigma));
services.AddKeyedSingleton<IAlgorithm>("Omega", (sp, _) => new AlgorithmOmega("OMEGA"));

services.AddSingleton(configuration)
.AddFeatureManagement()
Expand Down Expand Up @@ -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<IAlgorithm, AlgorithmBeta>(nameof(AlgorithmBeta));
services.AddKeyedScoped<IAlgorithm, AlgorithmSigma>(nameof(AlgorithmSigma));
services.AddKeyedScoped<IAlgorithm>("Omega", (sp, _) => new AlgorithmOmega("OMEGA"));

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

var targetingContextAccessor = new OnDemandTargetingContextAccessor();

services.AddSingleton<ITargetingContextAccessor>(targetingContextAccessor);

var serviceProvider = services.BuildServiceProvider().CreateScope().ServiceProvider;

IVariantFeatureManager featureManager = serviceProvider.GetRequiredService<IVariantFeatureManager>();

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

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<InvalidOperationException>(() =>
{
services.AddFeatureManagement()
.WithVariantService<IAlgorithm>("DummyFeature1")
.WithVariantService<IAlgorithm>("DummyFeature2");
}
);
}

[Fact]
public async Task VariantFeatureFlagWithContextualFeatureFilter()
{
Expand Down