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
7 changes: 4 additions & 3 deletions examples/VariantServiceDemo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
builder.Services.AddApplicationInsightsTelemetry();

//
// Add variant implementations of ICalculator
builder.Services.AddSingleton<ICalculator, DefaultCalculator>();
// Add variant implementations of ICalculator using keyed services so that only the
// implementation matching the assigned variant is instantiated on demand.
builder.Services.AddKeyedSingleton<ICalculator, DefaultCalculator>("DefaultCalculator");

builder.Services.AddSingleton<ICalculator, RemoteCalculator>();
builder.Services.AddKeyedSingleton<ICalculator, RemoteCalculator>("RemoteCalculator");

//
// Enter feature management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ public static IFeatureManagementBuilder WithTargeting<T>(this IFeatureManagement
/// <exception cref="ArgumentNullException">Thrown if feature name parameter is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if a variant service of the type has already been added.</exception>
public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatureManagementBuilder builder, string featureName) where TService : class
{
return WithVariantService<TService>(builder, featureName, new VariantServiceProviderOptions());
}

/// <summary>
/// Adds a <see cref="VariantServiceProvider{TService}"/> to the feature management system.
/// </summary>
/// <param name="builder">The <see cref="IFeatureManagementBuilder"/> used to customize feature management functionality.</param>
/// <param name="featureName">The feature flag that should be used to determine which variant of the service should be used.</param>
/// <param name="options">Options used to configure the variant service provider.</param>
/// <returns>A <see cref="IFeatureManagementBuilder"/> that can be used to customize feature management functionality.</returns>
/// <exception cref="ArgumentNullException">Thrown if feature name parameter is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if a variant service of the type has already been added.</exception>
public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatureManagementBuilder builder, string featureName, VariantServiceProviderOptions options) where TService : class
{
if (string.IsNullOrEmpty(featureName))
{
Expand All @@ -58,19 +72,19 @@ public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatu
throw new InvalidOperationException($"A variant service of {typeof(TService).FullName} has already been added.");
}

Func<IServiceProvider, IVariantServiceProvider<TService>> variantSpFactory = sp =>
{
var featureManager = sp.GetRequiredService<IVariantFeatureManager>();
return new VariantServiceProvider<TService>(featureName, featureManager, sp, options);
};

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>>()));
builder.Services.AddScoped(variantSpFactory);
}
else
{
builder.Services.AddSingleton<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
builder.Services.AddSingleton(variantSpFactory);
}

return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<SignAssembly>true</SignAssembly>
<DelaySign>false</DelaySign>
<AssemblyOriginatorKeyFile>..\..\build\Microsoft.FeatureManagement.snk</AssemblyOriginatorKeyFile>
<!-- Microsoft.FeatureManagement uses the feature of async streams which is not supported in versions of C# earlier than 8.0.
<!-- Microsoft.FeatureManagement uses the feature of async streams which is not supported in versions of C# earlier than 8.0.
The library targets on netstandard 2.0. To ensure compatibility, the minimum language version requirement should be maintained. -->
<LangVersion>8.0</LangVersion>
</PropertyGroup>
Expand Down
91 changes: 70 additions & 21 deletions src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,26 +17,30 @@ 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;
private readonly VariantServiceProviderOptions _options;
private readonly ConcurrentDictionary<object, TService> _variantServiceCache;

/// <summary>
/// Creates a variant service provider.
/// </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">The service provider used to resolve implementation variants of TService. If it implements <see cref="IKeyedServiceProvider"/>, keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated.</param>
/// <param name="options">Options used to configure the variant service provider.</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>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="options"/> is null.</exception>
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<string, TService>();
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_options = options ?? throw new ArgumentNullException(nameof(options));
_variantServiceCache = new ConcurrentDictionary<object, TService>();
}

/// <summary>
Expand All @@ -47,34 +52,78 @@ public async ValueTask<TService> 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<TService> ResolveByVariantAsync(CancellationToken cancellationToken)
{
var variant = await _featureManager.GetVariantAsync(_featureName, cancellationToken);

return variant != null ? _variantServiceCache.GetOrAdd(variant.Name, ResolveVariantService) : null;
}

private async Task<TService> 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<TService>(variantName);
return keyedService != null;
}

return implementation;
keyedService = null;
return false;
}

private TService GetVariantServiceFallback(string variantName)
{
return _serviceProvider
.GetRequiredService<IEnumerable<TService>>()
.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);
}
}
}
21 changes: 21 additions & 0 deletions src/Microsoft.FeatureManagement/VariantServiceProviderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
namespace Microsoft.FeatureManagement
{
/// <summary>
/// Specifies the aliases used by a variant service provider to resolve an implementation based on the feature flag status when no allocated variant matches.
/// </summary>
public class VariantServiceProviderOptions
{
/// <summary>
/// The alias used to resolve the variant service when the feature flag is enabled and no allocated variant matches.
/// </summary>
public object FallbackWhenEnabled { get; set; } = true;

/// <summary>
/// The alias used to resolve the variant service when the feature flag is disabled and no allocated variant matches.
/// </summary>
public object FallbackWhenDisabled { get; set; } = false;
}
}
Loading