Skip to content

Commit f47e188

Browse files
authored
Merge pull request #505 from microsoft/rossgrambo/snapshot-variant-fix
Resolves snapshot breaking change and adjusts feature tag helper as well
2 parents 7050fa0 + 3e07522 commit f47e188

File tree

7 files changed

+256
-123
lines changed

7 files changed

+256
-123
lines changed

src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ namespace Microsoft.FeatureManagement.Mvc.TagHelpers
1414
/// </summary>
1515
public class FeatureTagHelper : TagHelper
1616
{
17-
private readonly IVariantFeatureManager _featureManager;
17+
private readonly IFeatureManager _featureManager;
18+
private readonly IVariantFeatureManager _variantFeatureManager;
1819

1920
/// <summary>
2021
/// 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.
@@ -41,9 +42,12 @@ public class FeatureTagHelper : TagHelper
4142
/// Creates a feature tag helper.
4243
/// </summary>
4344
/// <param name="featureManager">The feature manager snapshot to use to evaluate feature state.</param>
44-
public FeatureTagHelper(IVariantFeatureManagerSnapshot featureManager)
45+
/// <param name="variantFeatureManager">The variant feature manager snapshot to use to evaluate feature state.</param>
46+
public FeatureTagHelper(IFeatureManagerSnapshot featureManager, IVariantFeatureManagerSnapshot variantFeatureManager)
4547
{
48+
// Takes both a feature manager and a variant feature manager for backwards compatibility.
4649
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
50+
_variantFeatureManager = variantFeatureManager ?? throw new ArgumentNullException(nameof(variantFeatureManager));
4751
}
4852

4953
/// <summary>
@@ -84,7 +88,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
8488
enabled = await variants.Any(
8589
async variant =>
8690
{
87-
Variant assignedVariant = await _featureManager.GetVariantAsync(features.First()).ConfigureAwait(false);
91+
Variant assignedVariant = await _variantFeatureManager.GetVariantAsync(features.First()).ConfigureAwait(false);
8892

8993
return variant == assignedVariant?.Name;
9094
});

src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,37 @@ namespace Microsoft.FeatureManagement
1616
/// </summary>
1717
class FeatureManagerSnapshot : IFeatureManagerSnapshot, IVariantFeatureManagerSnapshot
1818
{
19-
private readonly IVariantFeatureManager _featureManager;
19+
private readonly IFeatureManager _featureManager;
20+
private readonly IVariantFeatureManager _variantFeatureManager;
2021
private readonly ConcurrentDictionary<string, ValueTask<bool>> _flagCache = new ConcurrentDictionary<string, ValueTask<bool>>();
2122
private readonly ConcurrentDictionary<string, Variant> _variantCache = new ConcurrentDictionary<string, Variant>();
2223
private IEnumerable<string> _featureNames;
2324

24-
public FeatureManagerSnapshot(IVariantFeatureManager featureManager)
25+
// Takes both a feature manager and a variant feature manager for backwards compatibility.
26+
public FeatureManagerSnapshot(IFeatureManager featureManager, IVariantFeatureManager variantFeatureManager)
2527
{
2628
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
29+
_variantFeatureManager = variantFeatureManager ?? throw new ArgumentNullException(nameof(variantFeatureManager));
2730
}
2831

29-
public IAsyncEnumerable<string> GetFeatureNamesAsync()
32+
public async IAsyncEnumerable<string> GetFeatureNamesAsync()
3033
{
31-
return GetFeatureNamesAsync(CancellationToken.None);
34+
if (_featureNames == null)
35+
{
36+
var featureNames = new List<string>();
37+
38+
await foreach (string featureName in _featureManager.GetFeatureNamesAsync().ConfigureAwait(false))
39+
{
40+
featureNames.Add(featureName);
41+
}
42+
43+
_featureNames = featureNames;
44+
}
45+
46+
foreach (string featureName in _featureNames)
47+
{
48+
yield return featureName;
49+
}
3250
}
3351

3452
public async IAsyncEnumerable<string> GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
@@ -37,7 +55,7 @@ public async IAsyncEnumerable<string> GetFeatureNamesAsync([EnumeratorCancellati
3755
{
3856
var featureNames = new List<string>();
3957

40-
await foreach (string featureName in _featureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false))
58+
await foreach (string featureName in _variantFeatureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false))
4159
{
4260
featureNames.Add(featureName);
4361
}
@@ -55,28 +73,28 @@ public Task<bool> IsEnabledAsync(string feature)
5573
{
5674
return _flagCache.GetOrAdd(
5775
feature,
58-
(key) => _featureManager.IsEnabledAsync(key, CancellationToken.None)).AsTask();
76+
(key) => new ValueTask<bool>(_featureManager.IsEnabledAsync(key))).AsTask();
5977
}
6078

6179
public Task<bool> IsEnabledAsync<TContext>(string feature, TContext context)
6280
{
6381
return _flagCache.GetOrAdd(
6482
feature,
65-
(key) => _featureManager.IsEnabledAsync(key, context, CancellationToken.None)).AsTask();
83+
(key) => new ValueTask<bool>(_featureManager.IsEnabledAsync(key, context))).AsTask();
6684
}
6785

6886
public ValueTask<bool> IsEnabledAsync(string feature, CancellationToken cancellationToken)
6987
{
7088
return _flagCache.GetOrAdd(
7189
feature,
72-
(key) => _featureManager.IsEnabledAsync(key, cancellationToken));
90+
(key) => _variantFeatureManager.IsEnabledAsync(key, cancellationToken));
7391
}
7492

7593
public ValueTask<bool> IsEnabledAsync<TContext>(string feature, TContext context, CancellationToken cancellationToken)
7694
{
7795
return _flagCache.GetOrAdd(
7896
feature,
79-
(key) => _featureManager.IsEnabledAsync(key, context, cancellationToken));
97+
(key) => _variantFeatureManager.IsEnabledAsync(key, context, cancellationToken));
8098
}
8199

82100
public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToken cancellationToken)
@@ -90,7 +108,7 @@ public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToke
90108
return _variantCache[cacheKey];
91109
}
92110

93-
Variant variant = await _featureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false);
111+
Variant variant = await _variantFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false);
94112

95113
_variantCache[cacheKey] = variant;
96114

@@ -108,7 +126,7 @@ public async ValueTask<Variant> GetVariantAsync(string feature, ITargetingContex
108126
return _variantCache[cacheKey];
109127
}
110128

111-
Variant variant = await _featureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false);
129+
Variant variant = await _variantFeatureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false);
112130

113131
_variantCache[cacheKey] = variant;
114132

src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs

Lines changed: 38 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec
3434
"Scoped feature management has been registered.");
3535
}
3636

37-
services.AddLogging();
38-
39-
services.AddMemoryCache();
40-
41-
//
42-
// Add required services
43-
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();
37+
AddCommonFeatureManagementServices(services);
4438

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

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

61-
services.AddScoped<FeatureManagerSnapshot>();
62-
63-
services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
64-
65-
services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
66-
67-
var builder = new FeatureManagementBuilder(services);
68-
69-
//
70-
// Add built-in feature filters
71-
builder.AddFeatureFilter<PercentageFilter>();
72-
73-
builder.AddFeatureFilter<TimeWindowFilter>(sp =>
74-
new TimeWindowFilter()
75-
{
76-
Cache = sp.GetRequiredService<IMemoryCache>()
77-
});
78-
79-
builder.AddFeatureFilter<ContextualTargetingFilter>();
80-
81-
return builder;
55+
return GetFeatureManagementBuilder(services);
8256
}
8357

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

123-
services.AddLogging();
124-
125-
services.AddMemoryCache();
126-
127-
//
128-
// Add required services
129-
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();
97+
AddCommonFeatureManagementServices(services);
13098

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

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

147-
services.AddScoped<FeatureManagerSnapshot>();
148-
149-
services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
150-
151-
services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
152-
153-
var builder = new FeatureManagementBuilder(services);
154-
155-
//
156-
// Add built-in feature filters
157-
builder.AddFeatureFilter<PercentageFilter>();
158-
159-
builder.AddFeatureFilter<TimeWindowFilter>(sp =>
160-
new TimeWindowFilter()
161-
{
162-
Cache = sp.GetRequiredService<IMemoryCache>()
163-
});
164-
165-
builder.AddFeatureFilter<ContextualTargetingFilter>();
166-
167-
return builder;
115+
return GetFeatureManagementBuilder(services);
168116
}
169117

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

191139
return services.AddScopedFeatureManagement();
192140
}
141+
142+
private static void AddCommonFeatureManagementServices(IServiceCollection services)
143+
{
144+
services.AddLogging();
145+
146+
services.AddMemoryCache();
147+
148+
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();
149+
150+
services.AddScoped<FeatureManagerSnapshot>();
151+
152+
services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
153+
154+
services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
155+
}
156+
157+
private static IFeatureManagementBuilder GetFeatureManagementBuilder(IServiceCollection services)
158+
{
159+
var builder = new FeatureManagementBuilder(services);
160+
161+
//
162+
// Add built-in feature filters
163+
builder.AddFeatureFilter<PercentageFilter>();
164+
165+
builder.AddFeatureFilter<TimeWindowFilter>(sp =>
166+
new TimeWindowFilter()
167+
{
168+
Cache = sp.GetRequiredService<IMemoryCache>()
169+
});
170+
171+
builder.AddFeatureFilter<ContextualTargetingFilter>();
172+
173+
return builder;
174+
}
193175
}
194176
}

tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
using Microsoft.AspNetCore.Builder;
66
using Microsoft.AspNetCore.Hosting;
77
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.AspNetCore.Razor.TagHelpers;
89
using Microsoft.AspNetCore.TestHost;
910
using Microsoft.Extensions.Configuration;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.FeatureManagement;
13+
using Microsoft.FeatureManagement.Mvc.TagHelpers;
1214
using System.Collections.Generic;
1315
using System.Linq;
1416
using System.Net;
@@ -181,4 +183,71 @@ private static void DisableEndpointRouting(MvcOptions options)
181183
options.EnableEndpointRouting = false;
182184
}
183185
}
186+
187+
public class CustomImplementationsFeatureManagementTests
188+
{
189+
public class CustomIFeatureManager : IFeatureManager
190+
{
191+
public IAsyncEnumerable<string> GetFeatureNamesAsync()
192+
{
193+
return new string[1] { "Test" }.ToAsyncEnumerable();
194+
}
195+
196+
public async Task<bool> IsEnabledAsync(string feature)
197+
{
198+
return await Task.FromResult(feature == "Test");
199+
}
200+
201+
public async Task<bool> IsEnabledAsync<TContext>(string feature, TContext context)
202+
{
203+
return await Task.FromResult(feature == "Test");
204+
}
205+
}
206+
207+
[Fact]
208+
public async Task CustomIFeatureManagerAspNetCoreTest()
209+
{
210+
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
211+
212+
var services = new ServiceCollection();
213+
214+
services.AddSingleton(config)
215+
.AddSingleton<IFeatureManager, CustomIFeatureManager>()
216+
.AddFeatureManagement(); // Shouldn't override
217+
218+
ServiceProvider serviceProvider = services.BuildServiceProvider();
219+
220+
IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>();
221+
222+
Assert.True(await featureManager.IsEnabledAsync("Test"));
223+
Assert.False(await featureManager.IsEnabledAsync("NotTest"));
224+
225+
// FeatureTagHelper should use available IFeatureManager
226+
FeatureTagHelper featureTagHelper = new FeatureTagHelper(serviceProvider.GetRequiredService<IFeatureManagerSnapshot>(), serviceProvider.GetRequiredService<IVariantFeatureManagerSnapshot>());
227+
TagHelperOutput tagHelperOutput = new TagHelperOutput("TestTag", new TagHelperAttributeList(), (aBool, aHtmlEncoder) => { return null; });
228+
229+
// Test returns true, so it shouldn't be modified
230+
featureTagHelper.Name = "Test";
231+
Assert.False(tagHelperOutput.IsContentModified);
232+
await featureTagHelper.ProcessAsync(null, tagHelperOutput);
233+
Assert.False(tagHelperOutput.IsContentModified);
234+
235+
tagHelperOutput.Reinitialize("TestTag", TagMode.StartTagAndEndTag);
236+
237+
// NotTest returns false, so it should be modified
238+
featureTagHelper.Name = "NotTest";
239+
Assert.False(tagHelperOutput.IsContentModified);
240+
await featureTagHelper.ProcessAsync(null, tagHelperOutput);
241+
Assert.True(tagHelperOutput.IsContentModified);
242+
243+
tagHelperOutput.Reinitialize("TestTag", TagMode.StartTagAndEndTag);
244+
245+
// When variant is used, Test flag should no longer exist and return false
246+
featureTagHelper.Name = "Test";
247+
featureTagHelper.Variant = "Something";
248+
Assert.False(tagHelperOutput.IsContentModified);
249+
await featureTagHelper.ProcessAsync(null, tagHelperOutput);
250+
Assert.True(tagHelperOutput.IsContentModified);
251+
}
252+
}
184253
}

tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
<ItemGroup>
1717
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
18+
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
1819
<PackageReference Include="xunit" Version="2.9.0" />
1920
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
2021
</ItemGroup>

0 commit comments

Comments
 (0)