Skip to content

Commit 663c4b1

Browse files
committed
feat(examples/feature-flags): add FeatureFlags sample with policies and batch scenarios
- Added FeatureFlags example project wired with Mutators engine - Implemented feature flag mutations: - EnableFeatureMutation - DisableFeatureMutation - Added domain policies: - BusinessHoursPolicy (high-risk changes only during business hours) - RequireTwoManApprovalPolicy for critical feature flags - Introduced execution scenarios: - EnableNewCheckoutScenario (single mutation) - DisableLegacyCheckoutScenario (approval-gated mutation) - BatchFeatureToggleScenario (mixed batch execution) - Demonstrated: - User and system MutationContext usage - Correlation IDs and metadata-based approvals - Policy blocking and decision reporting - Batch execution with final state aggregation - Mutation history logging and metrics/statistics output - Added immutable FeatureFlagsState domain model - Registered FeatureFlags project in solution structure
1 parent 2d8dfbb commit 663c4b1

14 files changed

Lines changed: 530 additions & 0 deletions

ModularityKit.Mutator.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Solution>
22
<Folder Name="/Polygon/">
33
<Project Path="Polygon/BillingQuotas/BillingQuotas.csproj" />
4+
<Project Path="Polygon/FeatureFlags/FeatureFlags.csproj" />
45
</Folder>
56
<Project Path="ModularityKit.Mutator/ModularityKit.Mutator.csproj" />
67
</Solution>

Polygon/BillingQuotas/BillingQuotas.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,12 @@
77
<Nullable>enable</Nullable>
88
</PropertyGroup>
99

10+
<ItemGroup>
11+
<ProjectReference Include="..\..\ModularityKit.Mutator\ModularityKit.Mutator.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
16+
</ItemGroup>
17+
1018
</Project>

Polygon/BillingQuotas/Mutations/ResetQuotaMutation.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ModularityKit.Mutator.Abstractions.Engine;
55
using ModularityKit.Mutator.Abstractions.Intent;
66
using ModularityKit.Mutator.Abstractions.Results;
7+
using ValidationResult = ModularityKit.Mutator.Abstractions.Results.ValidationResult;
78

89
namespace BillingQuotas.Mutations;
910

Polygon/BillingQuotas/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.DependencyInjection;
33
using ModularityKit.Mutator.Abstractions;
44
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Runtime;
56

67
namespace BillingQuotas;
78

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\ModularityKit.Mutator\ModularityKit.Mutator.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using FeatureFlags.State;
2+
using ModularityKit.Mutator.Abstractions.Changes;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.Intent;
6+
using ModularityKit.Mutator.Abstractions.Results;
7+
8+
namespace FeatureFlags.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that disables a feature flag in the current <see cref="FeatureFlagsState"/>.
12+
/// </summary>
13+
internal sealed record DisableFeatureMutation(string FeatureName, MutationContext Context) : IMutation<FeatureFlagsState>
14+
{
15+
public MutationIntent Intent { get; } = new()
16+
{
17+
OperationName = "DisableFeature",
18+
Category = "Configuration",
19+
RiskLevel = MutationRiskLevel.High,
20+
Description = "Disables a feature flag."
21+
};
22+
23+
public MutationResult<FeatureFlagsState> Apply(FeatureFlagsState state)
24+
{
25+
var newFlags = new Dictionary<string, bool>(state.Flags);
26+
if (newFlags.ContainsKey(FeatureName))
27+
newFlags[FeatureName] = false;
28+
29+
var newState = state with { Flags = newFlags };
30+
var changes = ChangeSet.Single(StateChange.Modified($"Flags.{FeatureName}", true, false));
31+
return MutationResult<FeatureFlagsState>.Success(newState, changes);
32+
}
33+
34+
public ValidationResult Validate(FeatureFlagsState state)
35+
{
36+
var result = new ValidationResult();
37+
38+
if (string.IsNullOrEmpty(FeatureName))
39+
result.AddError("FeatureName", "Feature name cannot be empty");
40+
if (!state.Flags.ContainsKey(FeatureName))
41+
result.AddError("FeatureName", $"Feature '{FeatureName}' does not exist");
42+
return result;
43+
}
44+
45+
public MutationResult<FeatureFlagsState> Simulate(FeatureFlagsState state) => Apply(state);
46+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using FeatureFlags.State;
2+
using ModularityKit.Mutator.Abstractions.Changes;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.Intent;
6+
using ModularityKit.Mutator.Abstractions.Results;
7+
8+
namespace FeatureFlags.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that enables feature flag in the current <see cref="FeatureFlagsState"/>.
12+
/// </summary>
13+
internal sealed record EnableFeatureMutation(string FeatureName, MutationContext Context) : IMutation<FeatureFlagsState>
14+
{
15+
public MutationIntent Intent { get; } = new()
16+
{
17+
OperationName = "EnableFeature",
18+
Category = "Security",
19+
Tags = new HashSet<string> { "auth" },
20+
RiskLevel = MutationRiskLevel.High,
21+
Description = "Enables a feature flag."
22+
};
23+
24+
public MutationResult<FeatureFlagsState> Apply(FeatureFlagsState state)
25+
{
26+
if (state.Flags.TryGetValue(FeatureName, out var oldValue) && oldValue)
27+
return MutationResult<FeatureFlagsState>.Success(state, ChangeSet.Empty);
28+
29+
var newFlags = new Dictionary<string, bool>(state.Flags)
30+
{
31+
[FeatureName] = true
32+
};
33+
var newState = state with { Flags = newFlags };
34+
var changes = ChangeSet.Single(
35+
StateChange.Modified($"Flags.{FeatureName}", oldValue, true)
36+
);
37+
return MutationResult<FeatureFlagsState>.Success(newState, changes);
38+
}
39+
40+
public ValidationResult Validate(FeatureFlagsState state)
41+
{
42+
var result = new ValidationResult();
43+
if (string.IsNullOrEmpty(FeatureName))
44+
{
45+
result.AddError("FeatureName", "Feature name cannot be empty");
46+
}
47+
else if (!state.Flags.ContainsKey(FeatureName))
48+
{
49+
result.AddError("FeatureName", $"Feature '{FeatureName}' does not exist");
50+
}
51+
return result;
52+
}
53+
54+
public MutationResult<FeatureFlagsState> Simulate(FeatureFlagsState state) => Apply(state);
55+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using FeatureFlags.State;
2+
using ModularityKit.Mutator.Abstractions.Engine;
3+
using ModularityKit.Mutator.Abstractions.Intent;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
6+
namespace FeatureFlags.Policies;
7+
8+
/// <summary>
9+
/// Policy that restricts high risk mutations to business hours (9:00-17:00 UTC).
10+
/// </summary>
11+
public class BusinessHoursPolicy : IMutationPolicy<FeatureFlagsState>
12+
{
13+
public string Name => "BusinessHoursPolicy";
14+
public int Priority => 100;
15+
public string Description => "High-risk mutations are only allowed during business hours (9:00-17:00 UTC).";
16+
17+
public PolicyDecision Evaluate(IMutation<FeatureFlagsState> mutation, FeatureFlagsState state)
18+
{
19+
if (mutation.Intent.RiskLevel < MutationRiskLevel.High) return PolicyDecision.Allow(Name);
20+
var hour = DateTime.UtcNow.Hour;
21+
22+
if (hour is < 9 or >= 17)
23+
{
24+
return PolicyDecision.Deny(
25+
"High-risk changes are only allowed during business hours (9-17 UTC).",
26+
Name);
27+
}
28+
return PolicyDecision.Allow(Name);
29+
}
30+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using FeatureFlags.Mutations;
2+
using FeatureFlags.State;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
6+
namespace FeatureFlags.Policies;
7+
8+
/// <summary>
9+
/// Policy that requires two man approval for disabling critical feature flags.
10+
/// </summary>
11+
public sealed class RequireTwoManApprovalPolicy : IMutationPolicy<FeatureFlagsState>
12+
{
13+
public string Name => nameof(RequireTwoManApprovalPolicy);
14+
public int Priority => 100;
15+
public string Description => "Description";
16+
17+
private readonly HashSet<string> _criticalFlags = ["NewCheckout", "BetaFeatures"];
18+
19+
public PolicyDecision Evaluate(IMutation<FeatureFlagsState> mutation, FeatureFlagsState state)
20+
{
21+
if (mutation is not DisableFeatureMutation disable || !_criticalFlags.Contains(disable.FeatureName))
22+
return PolicyDecision.Allow(Name);
23+
24+
if (!mutation.Context.Metadata.TryGetValue("approvedBy", out var approvedObj))
25+
return PolicyDecision.Deny("Critical feature requires two-man approval (none provided)", Name);
26+
27+
var approvedBy = (approvedObj switch
28+
{
29+
string s => s.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)),
30+
IEnumerable<string> arr => arr,
31+
_ => (IEnumerable<string>)[]
32+
})
33+
.Where(x => !string.Equals(x, mutation.Context.ActorId, StringComparison.OrdinalIgnoreCase))
34+
.Distinct(StringComparer.OrdinalIgnoreCase)
35+
.ToArray();
36+
37+
return approvedBy.Length switch
38+
{
39+
< 2 => PolicyDecision.Deny("Critical feature requires at least two distinct approvers (excluding the actor)", Name),
40+
_ => PolicyDecision.Allow(Name)
41+
};
42+
}
43+
}

Polygon/FeatureFlags/Program.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using FeatureFlags.Policies;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using ModularityKit.Mutator.Abstractions;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Runtime;
6+
using ModularityKit.Mutator.Runtime.Loggers;
7+
8+
namespace FeatureFlags;
9+
10+
internal static class Program
11+
{
12+
private static async Task Main()
13+
{
14+
var services = new ServiceCollection();
15+
services.AddMutators(MutationEngineOptions.Strict, addDefaultLoggingInterceptor: true);
16+
17+
var provider = services.BuildServiceProvider();
18+
var engine = provider.GetRequiredService<IMutationEngine>();
19+
20+
//engine.RegisterPolicy(new BusinessHoursPolicy());
21+
engine.RegisterPolicy(new RequireTwoManApprovalPolicy());
22+
23+
Console.WriteLine("=== ModularityKit.Mutators - Complete Example ===\n");
24+
25+
await Scenarios.EnableNewCheckoutScenario.Run(engine);
26+
await Scenarios.DisableLegacyCheckoutScenario.Run(engine);
27+
await Scenarios.BatchFeatureToggleScenario.Run(engine);
28+
29+
var history = await engine.GetHistoryAsync(stateId: "EnableNewCheckout");
30+
MutationHistoryLogger.LogHistory(history);
31+
32+
Console.WriteLine("\n METRICS & STATISTICS");
33+
34+
var stats = await engine.GetStatisticsAsync();
35+
36+
Console.WriteLine($"\n Mutation Statistics:");
37+
Console.WriteLine($" Total executed: {stats.TotalExecuted}");
38+
39+
Console.WriteLine($"\n Performance Metrics:");
40+
Console.WriteLine($" Average execution time: {stats.AverageExecutionTime.TotalMilliseconds:F2} ms");
41+
Console.WriteLine($" Median execution time: {stats.MedianExecutionTime.TotalMilliseconds:F2} ms");
42+
Console.WriteLine($" P95 execution time: {stats.P95ExecutionTime.TotalMilliseconds:F2} ms");
43+
}
44+
}

0 commit comments

Comments
 (0)