Skip to content

Commit

Permalink
Resolves snapshot breaking change and adjusts feature tag helper as well
Browse files Browse the repository at this point in the history
  • Loading branch information
rossgrambo committed Sep 25, 2024
1 parent 7050fa0 commit 8be45cd
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ namespace Microsoft.FeatureManagement.Mvc.TagHelpers
/// </summary>
public class FeatureTagHelper : TagHelper
{
private readonly IVariantFeatureManager _featureManager;
private readonly IFeatureManager _featureManager;
private readonly IVariantFeatureManager _variantFeatureManager;

/// <summary>
/// A feature name, or comma separated list of feature names, for which the content should be rendered. By default, all specified features must be enabled to render the content, but this requirement can be controlled by the <see cref="Requirement"/> property.
Expand All @@ -38,12 +39,14 @@ public class FeatureTagHelper : TagHelper
public string Variant { get; set; }

/// <summary>
/// Creates a feature tag helper.
/// Creates a feature tag helper. Takes both a feature manager and a variant feature manager for backwards compatibility.
/// </summary>
/// <param name="featureManager">The feature manager snapshot to use to evaluate feature state.</param>
public FeatureTagHelper(IVariantFeatureManagerSnapshot featureManager)
/// <param name="variantFeatureManager">The variant feature manager snapshot to use to evaluate feature state.</param>
public FeatureTagHelper(IFeatureManagerSnapshot featureManager, IVariantFeatureManagerSnapshot variantFeatureManager)
{
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
_variantFeatureManager = variantFeatureManager ?? throw new ArgumentNullException(nameof(variantFeatureManager));
}

/// <summary>
Expand Down Expand Up @@ -84,7 +87,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
enabled = await variants.Any(
async variant =>
{
Variant assignedVariant = await _featureManager.GetVariantAsync(features.First()).ConfigureAwait(false);
Variant assignedVariant = await _variantFeatureManager.GetVariantAsync(features.First()).ConfigureAwait(false);
return variant == assignedVariant?.Name;
});
Expand Down
47 changes: 33 additions & 14 deletions src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,38 @@ namespace Microsoft.FeatureManagement
/// </summary>
class FeatureManagerSnapshot : IFeatureManagerSnapshot, IVariantFeatureManagerSnapshot
{
private readonly IVariantFeatureManager _featureManager;
private readonly ConcurrentDictionary<string, ValueTask<bool>> _flagCache = new ConcurrentDictionary<string, ValueTask<bool>>();
private readonly IFeatureManager _featureManager;
private readonly IVariantFeatureManager _variantFeatureManager;
private readonly ConcurrentDictionary<string, Task<bool>> _flagCache = new ConcurrentDictionary<string, Task<bool>>();
private readonly ConcurrentDictionary<string, ValueTask<bool>> _variantFlagCache = new ConcurrentDictionary<string, ValueTask<bool>>();
private readonly ConcurrentDictionary<string, Variant> _variantCache = new ConcurrentDictionary<string, Variant>();
private IEnumerable<string> _featureNames;

public FeatureManagerSnapshot(IVariantFeatureManager featureManager)
// Takes both a feature manager and a variant feature manager for backwards compatibility.
public FeatureManagerSnapshot(IFeatureManager featureManager, IVariantFeatureManager variantFeatureManager)
{
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
_variantFeatureManager = variantFeatureManager ?? throw new ArgumentNullException(nameof(variantFeatureManager));
}

public IAsyncEnumerable<string> GetFeatureNamesAsync()
public async IAsyncEnumerable<string> GetFeatureNamesAsync()
{
return GetFeatureNamesAsync(CancellationToken.None);
if (_featureNames == null)
{
var featureNames = new List<string>();

await foreach (string featureName in _featureManager.GetFeatureNamesAsync().ConfigureAwait(false))
{
featureNames.Add(featureName);
}

_featureNames = featureNames;
}

foreach (string featureName in _featureNames)
{
yield return featureName;
}
}

public async IAsyncEnumerable<string> GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
Expand All @@ -37,7 +56,7 @@ public async IAsyncEnumerable<string> GetFeatureNamesAsync([EnumeratorCancellati
{
var featureNames = new List<string>();

await foreach (string featureName in _featureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false))
await foreach (string featureName in _variantFeatureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false))
{
featureNames.Add(featureName);
}
Expand All @@ -55,28 +74,28 @@ public Task<bool> IsEnabledAsync(string feature)
{
return _flagCache.GetOrAdd(
feature,
(key) => _featureManager.IsEnabledAsync(key, CancellationToken.None)).AsTask();
(key) => _featureManager.IsEnabledAsync(key));
}

public Task<bool> IsEnabledAsync<TContext>(string feature, TContext context)
{
return _flagCache.GetOrAdd(
feature,
(key) => _featureManager.IsEnabledAsync(key, context, CancellationToken.None)).AsTask();
(key) => _featureManager.IsEnabledAsync(key, context));
}

public ValueTask<bool> IsEnabledAsync(string feature, CancellationToken cancellationToken)
{
return _flagCache.GetOrAdd(
return _variantFlagCache.GetOrAdd(
feature,
(key) => _featureManager.IsEnabledAsync(key, cancellationToken));
(key) => _variantFeatureManager.IsEnabledAsync(key, cancellationToken));
}

public ValueTask<bool> IsEnabledAsync<TContext>(string feature, TContext context, CancellationToken cancellationToken)
{
return _flagCache.GetOrAdd(
return _variantFlagCache.GetOrAdd(
feature,
(key) => _featureManager.IsEnabledAsync(key, context, cancellationToken));
(key) => _variantFeatureManager.IsEnabledAsync(key, context, cancellationToken));
}

public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToken cancellationToken)
Expand All @@ -90,7 +109,7 @@ public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToke
return _variantCache[cacheKey];
}

Variant variant = await _featureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false);
Variant variant = await _variantFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false);

_variantCache[cacheKey] = variant;

Expand All @@ -108,7 +127,7 @@ public async ValueTask<Variant> GetVariantAsync(string feature, ITargetingContex
return _variantCache[cacheKey];
}

Variant variant = await _featureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false);
Variant variant = await _variantFeatureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false);

_variantCache[cacheKey] = variant;

Expand Down
94 changes: 38 additions & 56 deletions src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec
"Scoped feature management has been registered.");
}

services.AddLogging();

services.AddMemoryCache();

//
// Add required services
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();
AddCommonFeatureManagementServices(services);

services.AddSingleton(sp => new FeatureManager(
sp.GetRequiredService<IFeatureDefinitionProvider>(),
Expand All @@ -58,27 +52,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec

services.TryAddSingleton<IVariantFeatureManager>(sp => sp.GetRequiredService<FeatureManager>());

services.AddScoped<FeatureManagerSnapshot>();

services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());

services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());

var builder = new FeatureManagementBuilder(services);

//
// Add built-in feature filters
builder.AddFeatureFilter<PercentageFilter>();

builder.AddFeatureFilter<TimeWindowFilter>(sp =>
new TimeWindowFilter()
{
Cache = sp.GetRequiredService<IMemoryCache>()
});

builder.AddFeatureFilter<ContextualTargetingFilter>();

return builder;
return GetFeatureManagementBuilder(services);
}

/// <summary>
Expand Down Expand Up @@ -120,13 +94,7 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService
"Singleton feature management has been registered.");
}

services.AddLogging();

services.AddMemoryCache();

//
// Add required services
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();
AddCommonFeatureManagementServices(services);

services.AddScoped(sp => new FeatureManager(
sp.GetRequiredService<IFeatureDefinitionProvider>(),
Expand All @@ -144,27 +112,7 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService

services.TryAddScoped<IVariantFeatureManager>(sp => sp.GetRequiredService<FeatureManager>());

services.AddScoped<FeatureManagerSnapshot>();

services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());

services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());

var builder = new FeatureManagementBuilder(services);

//
// Add built-in feature filters
builder.AddFeatureFilter<PercentageFilter>();

builder.AddFeatureFilter<TimeWindowFilter>(sp =>
new TimeWindowFilter()
{
Cache = sp.GetRequiredService<IMemoryCache>()
});

builder.AddFeatureFilter<ContextualTargetingFilter>();

return builder;
return GetFeatureManagementBuilder(services);
}

/// <summary>
Expand All @@ -190,5 +138,39 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService

return services.AddScopedFeatureManagement();
}

private static void AddCommonFeatureManagementServices(IServiceCollection services)
{
services.AddLogging();

services.AddMemoryCache();

services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();

services.AddScoped<FeatureManagerSnapshot>();

services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());

services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
}

private static IFeatureManagementBuilder GetFeatureManagementBuilder(IServiceCollection services)
{
var builder = new FeatureManagementBuilder(services);

//
// Add built-in feature filters
builder.AddFeatureFilter<PercentageFilter>();

builder.AddFeatureFilter<TimeWindowFilter>(sp =>
new TimeWindowFilter()
{
Cache = sp.GetRequiredService<IMemoryCache>()
});

builder.AddFeatureFilter<ContextualTargetingFilter>();

return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Mvc.TagHelpers;
using System.Collections.Generic;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -181,4 +183,71 @@ private static void DisableEndpointRouting(MvcOptions options)
options.EnableEndpointRouting = false;
}
}

public class CustomImplementationsFeatureManagementTests
{
public class CustomIFeatureManager : IFeatureManager
{
public IAsyncEnumerable<string> GetFeatureNamesAsync()
{
return new string[1] { "Test" }.ToAsyncEnumerable();
}

public async Task<bool> IsEnabledAsync(string feature)
{
return await Task.FromResult(feature == "Test");
}

public async Task<bool> IsEnabledAsync<TContext>(string feature, TContext context)
{
return await Task.FromResult(feature == "Test");
}
}

[Fact]
public async Task CustomIFeatureManagerAspNetCoreTest()
{
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();

var services = new ServiceCollection();

services.AddSingleton(config)
.AddSingleton<IFeatureManager, CustomIFeatureManager>()
.AddFeatureManagement(); // Shouldn't override

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

Assert.True(await featureManager.IsEnabledAsync("Test"));
Assert.False(await featureManager.IsEnabledAsync("NotTest"));

// FeatureTagHelper should use available IFeatureManager
FeatureTagHelper featureTagHelper = new FeatureTagHelper(serviceProvider.GetRequiredService<IFeatureManagerSnapshot>(), serviceProvider.GetRequiredService<IVariantFeatureManagerSnapshot>());
TagHelperOutput tagHelperOutput = new TagHelperOutput("TestTag", new TagHelperAttributeList(), (aBool, aHtmlEncoder) => { return null; });

// Test returns true, so it shouldn't be modified
featureTagHelper.Name = "Test";
Assert.False(tagHelperOutput.IsContentModified);
await featureTagHelper.ProcessAsync(null, tagHelperOutput);
Assert.False(tagHelperOutput.IsContentModified);

tagHelperOutput.Reinitialize("TestTag", TagMode.StartTagAndEndTag);

// NotTest returns false, so it should be modified
featureTagHelper.Name = "NotTest";
Assert.False(tagHelperOutput.IsContentModified);
await featureTagHelper.ProcessAsync(null, tagHelperOutput);
Assert.True(tagHelperOutput.IsContentModified);

tagHelperOutput.Reinitialize("TestTag", TagMode.StartTagAndEndTag);

// When variant is used, Test flag should no longer exist and return false
featureTagHelper.Name = "Test";
featureTagHelper.Variant = "Something";
Assert.False(tagHelperOutput.IsContentModified);
await featureTagHelper.ProcessAsync(null, tagHelperOutput);
Assert.True(tagHelperOutput.IsContentModified);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
Expand Down
Loading

0 comments on commit 8be45cd

Please sign in to comment.