Skip to content

Commit 83fdca3

Browse files
committed
Add backend snapshot source providers
1 parent 3273efd commit 83fdca3

15 files changed

Lines changed: 391 additions & 27 deletions

ManagedCode.FeatureChecker.Tests/Access/FeatureAccessTests.cs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ public void FeatureCheckerFactory_ShouldIssueFreshCheckersFromFileProvider()
4343
}
4444
}
4545

46+
[Test]
47+
public void FeatureCheckerFactory_ShouldIssueFreshCheckersFromSnapshotSource()
48+
{
49+
var source = new MutableFeatureSnapshotSource(FeatureStatus.Disabled);
50+
var factory = new FeatureCheckerFactory(new FeatureSnapshotSourceProvider(source));
51+
52+
factory.Create().IsDisabled(FeatureNames.MarketplaceConnect).ShouldBeTrue();
53+
54+
source.Replace(FeatureStatus.Enabled);
55+
56+
factory.Create().IsEnabled(FeatureNames.MarketplaceConnect).ShouldBeTrue();
57+
}
58+
4659
[Test]
4760
public void FeatureCheckerFactory_ShouldCreateUserTenantAndSessionScopesForControllerCode()
4861
{
@@ -95,10 +108,15 @@ private static void SaveSnapshot(string path, FeatureStatus status)
95108
{
96109
FeatureSnapshotSerializer.Save(
97110
path,
98-
FeatureSnapshot.FromDefinitions(
99-
[
100-
FeatureDefinition.Create(FeatureNames.MarketplaceConnect, status)
101-
]));
111+
CreateSnapshot(status));
112+
}
113+
114+
private static FeatureSnapshot CreateSnapshot(FeatureStatus status)
115+
{
116+
return FeatureSnapshot.FromDefinitions(
117+
[
118+
FeatureDefinition.Create(FeatureNames.MarketplaceConnect, status)
119+
]);
102120
}
103121

104122
private static string CreateTempJsonPath()
@@ -155,4 +173,24 @@ private static class DefaultValues
155173
public const string TrueValue = "true";
156174
public const string FalseValue = "false";
157175
}
176+
177+
private sealed class MutableFeatureSnapshotSource : IFeatureSnapshotSource
178+
{
179+
private FeatureSnapshot _snapshot;
180+
181+
public MutableFeatureSnapshotSource(FeatureStatus status)
182+
{
183+
_snapshot = CreateSnapshot(status);
184+
}
185+
186+
public FeatureSnapshot GetSnapshot()
187+
{
188+
return _snapshot;
189+
}
190+
191+
public void Replace(FeatureStatus status)
192+
{
193+
_snapshot = CreateSnapshot(status);
194+
}
195+
}
158196
}

ManagedCode.FeatureChecker.Tests/DependencyInjection/FeatureCheckerDependencyInjectionTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,54 @@ public void ServiceCollection_ShouldRegisterFeatureCheckerFromConfiguration()
2929
checker.IsEnabled(FeatureNames.Checkout, context).ShouldBeTrue();
3030
}
3131

32+
[Test]
33+
public void ServiceCollection_ShouldRegisterFeatureCheckerFromSnapshotSource()
34+
{
35+
var source = new MutableFeatureSnapshotSource(FeatureStatus.Disabled);
36+
using var services = new ServiceCollection()
37+
.AddSingleton(source)
38+
.AddFeatureCheckerSnapshotSource<MutableFeatureSnapshotSource>()
39+
.BuildServiceProvider();
40+
41+
var factory = services.GetRequiredService<IFeatureCheckerFactory>();
42+
43+
factory.Create().IsDisabled(FeatureNames.Checkout).ShouldBeTrue();
44+
45+
source.Replace(FeatureStatus.Enabled);
46+
47+
factory.Create().IsEnabled(FeatureNames.Checkout).ShouldBeTrue();
48+
services.GetRequiredService<IFeatureEvaluator>().IsEnabled(FeatureNames.Checkout).ShouldBeTrue();
49+
}
50+
51+
[Test]
52+
public void ServiceCollection_ShouldPreferExplicitSnapshotSourceOverConfiguredOptions()
53+
{
54+
var configuration = new ConfigurationBuilder()
55+
.AddInMemoryCollection(new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
56+
{
57+
["Snapshot:Features:0:Key"] = FeatureNames.Checkout,
58+
["Snapshot:Features:0:Status"] = nameof(FeatureStatus.Disabled)
59+
})
60+
.Build();
61+
var source = new MutableFeatureSnapshotSource(FeatureStatus.Enabled);
62+
using var services = new ServiceCollection()
63+
.AddFeatureChecker(configuration)
64+
.AddFeatureCheckerSnapshotSource(source)
65+
.BuildServiceProvider();
66+
67+
services.GetRequiredService<IFeatureEvaluator>().IsEnabled(FeatureNames.Checkout).ShouldBeTrue();
68+
}
69+
70+
[Test]
71+
public void ServiceCollection_ShouldRegisterFeatureCheckerFromDefinitionProvider()
72+
{
73+
using var services = new ServiceCollection()
74+
.AddFeatureCheckerProvider<StaticFeatureDefinitionProvider>()
75+
.BuildServiceProvider();
76+
77+
services.GetRequiredService<IFeatureEvaluator>().IsEnabled(FeatureNames.Checkout).ShouldBeTrue();
78+
}
79+
3280
private static class FeatureNames
3381
{
3482
public const string Checkout = "checkout.new-flow";
@@ -43,4 +91,43 @@ private static class Values
4391
{
4492
public const string Enterprise = "enterprise";
4593
}
94+
95+
private sealed class MutableFeatureSnapshotSource : IFeatureSnapshotSource
96+
{
97+
private FeatureSnapshot _snapshot;
98+
99+
public MutableFeatureSnapshotSource(FeatureStatus status)
100+
{
101+
_snapshot = CreateSnapshot(status);
102+
}
103+
104+
public FeatureSnapshot GetSnapshot()
105+
{
106+
return _snapshot;
107+
}
108+
109+
public void Replace(FeatureStatus status)
110+
{
111+
_snapshot = CreateSnapshot(status);
112+
}
113+
114+
private static FeatureSnapshot CreateSnapshot(FeatureStatus status)
115+
{
116+
return FeatureSnapshot.FromDefinitions(
117+
[
118+
FeatureDefinition.Create(FeatureNames.Checkout, status)
119+
]);
120+
}
121+
}
122+
123+
private sealed class StaticFeatureDefinitionProvider : IFeatureDefinitionProvider
124+
{
125+
public IReadOnlyCollection<FeatureDefinition> GetFeatureDefinitions()
126+
{
127+
return
128+
[
129+
FeatureDefinition.Create(FeatureNames.Checkout, FeatureStatus.Enabled)
130+
];
131+
}
132+
}
46133
}

ManagedCode.FeatureChecker.Tests/Storage/FeatureStorageTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,52 @@ public void FeatureSnapshotSerializer_ShouldLoadAndSaveFilesForStorageAdapters()
5454
}
5555
}
5656

57+
[Test]
58+
public void FeatureFileProvider_ShouldLoadFullSnapshotWithSegments()
59+
{
60+
var snapshot = new FeatureSnapshot
61+
{
62+
Features =
63+
[
64+
new FeatureDefinition
65+
{
66+
Key = FeatureNames.MarketplaceConnect,
67+
Status = FeatureStatus.Disabled,
68+
Rules =
69+
[
70+
new FeatureTargetingRule
71+
{
72+
IncludeSegments = [SegmentNames.EnterpriseUsers],
73+
Status = FeatureStatus.Enabled
74+
}
75+
]
76+
}
77+
],
78+
Segments =
79+
[
80+
new FeatureSegment
81+
{
82+
Key = SegmentNames.EnterpriseUsers,
83+
IncludedKeys = [TargetingKeys.UserA]
84+
}
85+
]
86+
};
87+
var path = CreateTempJsonPath();
88+
89+
try
90+
{
91+
FeatureSnapshotSerializer.Save(path, snapshot);
92+
var checker = new FeatureCheckerEvaluator(new FeatureFileProvider(path));
93+
var context = FeatureEvaluationContextBuilder.Create().ForUser(TargetingKeys.UserA).Build();
94+
95+
checker.IsEnabled(FeatureNames.MarketplaceConnect, context).ShouldBeTrue();
96+
}
97+
finally
98+
{
99+
DeleteFile(path);
100+
}
101+
}
102+
57103
private static string CreateTempJsonPath()
58104
{
59105
return Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.json");
@@ -73,6 +119,16 @@ private static class FeatureNames
73119
public const string RetiredExport = "retired.export";
74120
}
75121

122+
private static class SegmentNames
123+
{
124+
public const string EnterpriseUsers = "enterprise-users";
125+
}
126+
127+
private static class TargetingKeys
128+
{
129+
public const string UserA = "user-a";
130+
}
131+
76132
private static class DefaultValues
77133
{
78134
public const string EnabledValue = "enabled-value";

ManagedCode.FeatureChecker/DependencyInjection/FeatureCheckerServiceCollectionExtensions.cs

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ManagedCode.FeatureChecker.Storage;
55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
78

89
namespace ManagedCode.FeatureChecker.DependencyInjection;
910

@@ -16,7 +17,7 @@ public static IServiceCollection AddFeatureChecker(this IServiceCollection servi
1617

1718
services.AddOptions<FeatureCheckerOptions>().Configure(configure);
1819

19-
return AddFeatureCheckerCore(services);
20+
return AddOptionsFeatureProvider(services).AddFeatureCheckerCore();
2021
}
2122

2223
public static IServiceCollection AddFeatureChecker(this IServiceCollection services, IConfiguration configuration)
@@ -26,16 +27,75 @@ public static IServiceCollection AddFeatureChecker(this IServiceCollection servi
2627

2728
services.Configure<FeatureCheckerOptions>(configuration);
2829

29-
return AddFeatureCheckerCore(services);
30+
return AddOptionsFeatureProvider(services).AddFeatureCheckerCore();
3031
}
3132

32-
private static IServiceCollection AddFeatureCheckerCore(IServiceCollection services)
33+
public static IServiceCollection AddFeatureCheckerProvider<TProvider>(this IServiceCollection services)
34+
where TProvider : class, IFeatureDefinitionProvider
3335
{
34-
services.AddSingleton<OptionsFeatureDefinitionProvider>();
35-
services.AddSingleton<IFeatureDefinitionProvider>(provider => provider.GetRequiredService<OptionsFeatureDefinitionProvider>());
36-
services.AddSingleton<IFeatureSegmentProvider>(provider => provider.GetRequiredService<OptionsFeatureDefinitionProvider>());
37-
services.AddSingleton<IFeatureCheckerFactory>(provider => new FeatureCheckerFactory(provider.GetRequiredService<IFeatureDefinitionProvider>()));
38-
services.AddTransient<IFeatureEvaluator>(provider => provider.GetRequiredService<IFeatureCheckerFactory>().Create());
36+
ArgumentNullException.ThrowIfNull(services);
37+
38+
services.TryAddSingleton<TProvider>();
39+
services.RemoveAll<IFeatureDefinitionProvider>();
40+
services.AddSingleton<IFeatureDefinitionProvider>(provider => provider.GetRequiredService<TProvider>());
41+
services.RemoveAll<IFeatureSegmentProvider>();
42+
43+
if (typeof(IFeatureSegmentProvider).IsAssignableFrom(typeof(TProvider)))
44+
{
45+
services.AddSingleton<IFeatureSegmentProvider>(provider => (IFeatureSegmentProvider)provider.GetRequiredService<TProvider>());
46+
}
47+
48+
return services.AddFeatureCheckerCore();
49+
}
50+
51+
public static IServiceCollection AddFeatureCheckerSnapshotSource<TSource>(this IServiceCollection services)
52+
where TSource : class, IFeatureSnapshotSource
53+
{
54+
ArgumentNullException.ThrowIfNull(services);
55+
56+
services.TryAddSingleton<TSource>();
57+
services.RemoveAll<IFeatureSnapshotSource>();
58+
services.AddSingleton<IFeatureSnapshotSource>(provider => provider.GetRequiredService<TSource>());
59+
60+
return AddSnapshotSourceProvider(services).AddFeatureCheckerCore();
61+
}
62+
63+
public static IServiceCollection AddFeatureCheckerSnapshotSource(this IServiceCollection services, IFeatureSnapshotSource source)
64+
{
65+
ArgumentNullException.ThrowIfNull(services);
66+
ArgumentNullException.ThrowIfNull(source);
67+
68+
services.RemoveAll<IFeatureSnapshotSource>();
69+
services.AddSingleton(source);
70+
71+
return AddSnapshotSourceProvider(services).AddFeatureCheckerCore();
72+
}
73+
74+
private static IServiceCollection AddOptionsFeatureProvider(IServiceCollection services)
75+
{
76+
services.TryAddSingleton<OptionsFeatureDefinitionProvider>();
77+
services.TryAddSingleton<IFeatureDefinitionProvider>(provider => provider.GetRequiredService<OptionsFeatureDefinitionProvider>());
78+
services.TryAddSingleton<IFeatureSegmentProvider>(provider => provider.GetRequiredService<OptionsFeatureDefinitionProvider>());
79+
80+
return services;
81+
}
82+
83+
private static IServiceCollection AddSnapshotSourceProvider(IServiceCollection services)
84+
{
85+
services.RemoveAll<FeatureSnapshotSourceProvider>();
86+
services.AddSingleton<FeatureSnapshotSourceProvider>();
87+
services.RemoveAll<IFeatureDefinitionProvider>();
88+
services.AddSingleton<IFeatureDefinitionProvider>(provider => provider.GetRequiredService<FeatureSnapshotSourceProvider>());
89+
services.RemoveAll<IFeatureSegmentProvider>();
90+
services.AddSingleton<IFeatureSegmentProvider>(provider => provider.GetRequiredService<FeatureSnapshotSourceProvider>());
91+
92+
return services;
93+
}
94+
95+
private static IServiceCollection AddFeatureCheckerCore(this IServiceCollection services)
96+
{
97+
services.TryAddSingleton<IFeatureCheckerFactory>(provider => new FeatureCheckerFactory(provider.GetRequiredService<IFeatureDefinitionProvider>()));
98+
services.TryAddTransient<IFeatureEvaluator>(provider => provider.GetRequiredService<IFeatureCheckerFactory>().Create());
3999

40100
return services;
41101
}

ManagedCode.FeatureChecker/Evaluation/FeatureChecker.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,20 @@ public FeatureChecker(IFeatureDefinitionProvider provider)
1919
{
2020
ArgumentNullException.ThrowIfNull(provider);
2121

22-
var segments = provider is IFeatureSegmentProvider segmentProvider
23-
? segmentProvider.GetFeatureSegments()
24-
: [];
22+
if (provider is IFeatureSnapshotSource snapshotSource)
23+
{
24+
var snapshot = snapshotSource.GetSnapshot();
25+
var snapshotMaps = CreateMaps(snapshot.Features, snapshot.Segments);
2526

26-
var maps = CreateMaps(provider.GetFeatureDefinitions(), segments);
27+
_features = snapshotMaps.Features;
28+
_engine = new FeatureEvaluationEngine(snapshotMaps.Features, snapshotMaps.Segments);
29+
30+
return;
31+
}
32+
33+
var maps = CreateMaps(
34+
provider.GetFeatureDefinitions(),
35+
provider is IFeatureSegmentProvider segmentProvider ? segmentProvider.GetFeatureSegments() : []);
2736
_features = maps.Features;
2837
_engine = new FeatureEvaluationEngine(maps.Features, maps.Segments);
2938
}

ManagedCode.FeatureChecker/Storage/FeatureFileProvider.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
using ManagedCode.FeatureChecker.Definitions;
2+
using ManagedCode.FeatureChecker.Segments;
23

34
namespace ManagedCode.FeatureChecker.Storage;
45

5-
public sealed class FeatureFileProvider : IFeatureDefinitionProvider
6+
public sealed class FeatureFileProvider : IFeatureDefinitionProvider, IFeatureSegmentProvider, IFeatureSnapshotSource
67
{
78
public FeatureFileProvider(string filePath)
89
{
@@ -15,6 +16,16 @@ public FeatureFileProvider(string filePath)
1516

1617
public IReadOnlyCollection<FeatureDefinition> GetFeatureDefinitions()
1718
{
18-
return FeatureSnapshotSerializer.Load(FilePath).Features;
19+
return GetSnapshot().Features;
20+
}
21+
22+
public IReadOnlyCollection<FeatureSegment> GetFeatureSegments()
23+
{
24+
return GetSnapshot().Segments;
25+
}
26+
27+
public FeatureSnapshot GetSnapshot()
28+
{
29+
return FeatureSnapshotSerializer.Load(FilePath);
1930
}
2031
}

0 commit comments

Comments
 (0)