Skip to content

Commit 2d8dfbb

Browse files
committed
feat(examples/billing): add BillingQuotas sample using Mutators engine
Added BillingQuotas project as a full reference example - Implemented quota mutations: - IncreaseQuotaMutation - DecreaseQuotaMutation - ResetQuotaMutation - Added domain policies: - MaxQuotaPolicy (upper quota limit) - PreventNegativeQuotaPolicy (no negative quotas) - Introduced example scenarios: - EmergencyIncreaseScenario (batch execution + policy blocking) - MonthlyResetScenario (system-level batch reset) - Wired Mutators engine with DI and default logging - Demonstrated metrics & statistics aggregation usage - Added immutable QuotaState domain model
1 parent 7f5021a commit 2d8dfbb

9 files changed

Lines changed: 425 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using BillingQuotas.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 BillingQuotas.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that decreases the quota for specific user by specified amount.
12+
/// </summary>
13+
internal sealed record DecreaseQuotaMutation(
14+
string UserId,
15+
int Amount,
16+
MutationContext Context
17+
) : IMutation<QuotaState>
18+
{
19+
public MutationIntent Intent { get; } = new()
20+
{
21+
OperationName = "DecreaseQuota",
22+
Category = "Billing",
23+
RiskLevel = MutationRiskLevel.High,
24+
Description = "Decrease user quota by given amount"
25+
};
26+
27+
public ValidationResult Validate(QuotaState state)
28+
{
29+
var result = new ValidationResult();
30+
31+
if (string.IsNullOrEmpty(UserId))
32+
result.AddError("UserId", "UserId cannot be empty");
33+
34+
if (Amount <= 0)
35+
result.AddError("Amount", "Amount must be positive");
36+
37+
if (state.UserQuotas.GetValueOrDefault(UserId) < Amount)
38+
result.AddError("Amount", "Cannot decrease below zero");
39+
40+
return result;
41+
}
42+
43+
public MutationResult<QuotaState> Apply(QuotaState state)
44+
{
45+
var quotas = state.UserQuotas.ToDictionary(kv => kv.Key, kv => kv.Value);
46+
quotas[UserId] = quotas.GetValueOrDefault(UserId) - Amount;
47+
48+
var newState = state with { UserQuotas = quotas };
49+
50+
var changes = ChangeSet.Single(
51+
StateChange.Modified($"UserQuotas.{UserId}", null, quotas[UserId])
52+
);
53+
54+
return MutationResult<QuotaState>.Success(newState, changes);
55+
}
56+
57+
public MutationResult<QuotaState> Simulate(QuotaState state) => Apply(state);
58+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using BillingQuotas.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 BillingQuotas.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that increases the quota for specific user by given amount.
12+
/// </summary>
13+
internal sealed record IncreaseQuotaMutation(
14+
string UserId,
15+
int Amount,
16+
MutationContext Context
17+
) : IMutation<QuotaState>
18+
{
19+
public MutationIntent Intent { get; } = new()
20+
{
21+
OperationName = "IncreaseQuota",
22+
Category = "Billing",
23+
RiskLevel = MutationRiskLevel.Medium,
24+
Description = "Increase user quota by given amount"
25+
};
26+
27+
public ValidationResult Validate(QuotaState state)
28+
{
29+
var result = new ValidationResult();
30+
31+
if (string.IsNullOrEmpty(UserId))
32+
result.AddError("UserId", "UserId cannot be empty");
33+
34+
if (Amount <= 0)
35+
result.AddError("Amount", "Amount must be positive");
36+
37+
return result;
38+
}
39+
40+
public MutationResult<QuotaState> Apply(QuotaState state)
41+
{
42+
var quotas = state.UserQuotas.ToDictionary(kv => kv.Key, kv => kv.Value);
43+
quotas[UserId] = quotas.GetValueOrDefault(UserId) + Amount;
44+
45+
var newState = state with { UserQuotas = quotas };
46+
47+
var changes = ChangeSet.Single(
48+
StateChange.Modified($"UserQuotas.{UserId}", null, quotas[UserId])
49+
);
50+
51+
return MutationResult<QuotaState>.Success(newState, changes);
52+
}
53+
54+
public MutationResult<QuotaState> Simulate(QuotaState state) => Apply(state);
55+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using BillingQuotas.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 BillingQuotas.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that resets user's quota to zero.
12+
/// </summary>
13+
internal sealed record ResetQuotaMutation(
14+
string UserId,
15+
MutationContext Context
16+
) : IMutation<QuotaState>
17+
{
18+
public MutationIntent Intent { get; } = new()
19+
{
20+
OperationName = "ResetQuota",
21+
Category = "Billing",
22+
RiskLevel = MutationRiskLevel.High,
23+
Description = "Reset user quota to zero"
24+
};
25+
26+
public ValidationResult Validate(QuotaState state)
27+
{
28+
var result = new ValidationResult();
29+
30+
if (string.IsNullOrEmpty(UserId))
31+
result.AddError("UserId", "UserId cannot be empty");
32+
33+
return result;
34+
}
35+
36+
public MutationResult<QuotaState> Apply(QuotaState state)
37+
{
38+
var quotas = state.UserQuotas.ToDictionary(kv => kv.Key, kv => kv.Value);
39+
quotas[UserId] = 0;
40+
41+
var newState = state with { UserQuotas = quotas };
42+
43+
var changes = ChangeSet.Single(
44+
StateChange.Modified($"UserQuotas.{UserId}", null, 0)
45+
);
46+
47+
return MutationResult<QuotaState>.Success(newState, changes);
48+
}
49+
50+
public MutationResult<QuotaState> Simulate(QuotaState state) => Apply(state);
51+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using BillingQuotas.Mutations;
2+
using BillingQuotas.State;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
6+
namespace BillingQuotas.Policies;
7+
8+
/// <summary>
9+
/// Policy ensuring that user's quota cannot exceed specified maximum.
10+
/// </summary>
11+
public sealed class MaxQuotaPolicy(int maxQuota = 100) : IMutationPolicy<QuotaState>
12+
{
13+
public string Name => "MaxQuotaPolicy";
14+
public int Priority => 100;
15+
public string Description => $"Ensures that user quota does not exceed {maxQuota}.";
16+
17+
public PolicyDecision Evaluate(IMutation<QuotaState> mutation, QuotaState state)
18+
{
19+
if (mutation is not IncreaseQuotaMutation inc) return PolicyDecision.Allow();
20+
21+
var current = state.UserQuotas.GetValueOrDefault(inc.UserId);
22+
if (current + inc.Amount > maxQuota)
23+
return PolicyDecision.Deny(
24+
$"Quota cannot exceed {maxQuota} (current: {current}, increase: {inc.Amount})");
25+
26+
return PolicyDecision.Allow();
27+
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using BillingQuotas.Mutations;
2+
using BillingQuotas.State;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
6+
namespace BillingQuotas.Policies;
7+
8+
/// <summary>
9+
/// Policy ensuring that user's quota never drops below zero.
10+
/// Blocks any <see cref="DecreaseQuotaMutation"/> that would result in negative quota.
11+
/// </summary>
12+
public sealed class PreventNegativeQuotaPolicy : IMutationPolicy<QuotaState>
13+
{
14+
public string Name => "PreventNegativeQuotaPolicy";
15+
public int Priority => 100;
16+
public string Description => "Prevents user quota from going below zero.";
17+
18+
public PolicyDecision Evaluate(IMutation<QuotaState> mutation, QuotaState state)
19+
{
20+
if (mutation is not DecreaseQuotaMutation dec) return PolicyDecision.Allow();
21+
22+
var current = state.UserQuotas.GetValueOrDefault(dec.UserId);
23+
if (current - dec.Amount < 0)
24+
return PolicyDecision.Deny(
25+
$"Quota cannot go below zero (current: {current}, decrease: {dec.Amount})");
26+
27+
return PolicyDecision.Allow();
28+
}
29+
}

Polygon/BillingQuotas/Program.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using BillingQuotas.Policies;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using ModularityKit.Mutator.Abstractions;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
6+
namespace BillingQuotas;
7+
8+
internal static class Program
9+
{
10+
private static async Task Main()
11+
{
12+
var services = new ServiceCollection();
13+
services.AddMutators(MutationEngineOptions.Strict, addDefaultLoggingInterceptor: true);
14+
15+
var provider = services.BuildServiceProvider();
16+
var engine = provider.GetRequiredService<IMutationEngine>();
17+
18+
engine.RegisterPolicy(new MaxQuotaPolicy());
19+
engine.RegisterPolicy(new PreventNegativeQuotaPolicy());
20+
21+
Console.WriteLine("=== ModularityKit.Mutators - Complete Example ===\n");
22+
23+
await Scenarios.EmergencyIncreaseScenario.Run(engine);
24+
await Scenarios.MonthlyResetScenario.Run(engine);
25+
Console.WriteLine("\n METRICS & STATISTICS");
26+
27+
var stats = await engine.GetStatisticsAsync();
28+
29+
Console.WriteLine($"\n Mutation Statistics:");
30+
Console.WriteLine($" Total executed: {stats.TotalExecuted}");
31+
32+
Console.WriteLine($"\n Performance Metrics:");
33+
Console.WriteLine($" Average execution time: {stats.AverageExecutionTime.TotalMilliseconds:F2} ms");
34+
Console.WriteLine($" Median execution time: {stats.MedianExecutionTime.TotalMilliseconds:F2} ms");
35+
Console.WriteLine($" P95 execution time: {stats.P95ExecutionTime.TotalMilliseconds:F2} ms");
36+
}
37+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using BillingQuotas.Mutations;
2+
using BillingQuotas.Policies;
3+
using BillingQuotas.State;
4+
using ModularityKit.Mutator.Abstractions.Context;
5+
using ModularityKit.Mutator.Abstractions.Engine;
6+
7+
namespace BillingQuotas.Scenarios;
8+
9+
/// <summary>
10+
/// EmergencyIncreaseScenario
11+
///
12+
/// Demonstrates an emergency increase of user quotas, simulating high priority situation where system administrators
13+
/// need to temporarily raise quotas beyond normal limits.
14+
///
15+
/// This scenario exercises following capabilities of Mutators framework:
16+
///
17+
/// - Batch execution of multiple <see cref="IMutationEngine.ExecuteBatchAsync{TState}"/> instances via <see cref="IMutationEngine"/>
18+
/// - User level <see cref="MutationContext"/> usage for audit and traceability
19+
/// - Policy evaluation and enforcement for high-risk operations
20+
/// - Reporting of successes and policy-denied mutations
21+
///
22+
/// Key Steps:
23+
/// 1. Initialize <see cref="IncreaseQuotaMutation"/> with example user quotas.
24+
/// 2. Create <see cref="IMutationEngine.ExecuteBatchAsync{TState}"/> representing a system administrator initiating emergency action.
25+
/// 3. Generate <see cref="IMutationEngine"/> instances for each affected user.
26+
/// 4. Execute all mutations in batch using <see cref="MaxQuotaPolicy"/>.
27+
/// 5. Iterate over the batch results, printing success messages or policy denied reasons.
28+
///
29+
/// Example Use Case:
30+
/// - Emergency quota increase in SaaS billing or subscription systems.
31+
/// - Temporary overrides for high priority customers or system critical operations.
32+
///
33+
/// Notes:
34+
/// - Mutations blocked by policies are reported individually but do not prevent execution of other mutations in batch.
35+
/// - This scenario assumes that policies such as <see cref="IMutation{TState}"/> or <see cref="MutationContext"/>
36+
/// may apply, and demonstrates how violations are surfaced to the operator.
37+
/// </summary>
38+
internal static class EmergencyIncreaseScenario
39+
{
40+
internal static async Task Run(IMutationEngine engine)
41+
{
42+
Console.WriteLine("\n=== Emergency Increase Scenario ===");
43+
44+
var state = new QuotaState
45+
{
46+
UserQuotas = new Dictionary<string, int>
47+
{
48+
["alice"] = 50,
49+
["bob"] = 95
50+
}
51+
};
52+
53+
var ctx = MutationContext.User(
54+
userId: "admin",
55+
userName: "System Admin",
56+
reason: "Emergency quota increase");
57+
58+
var mutations = new IMutation<QuotaState>[]
59+
{
60+
new IncreaseQuotaMutation("alice", 15, ctx),
61+
new IncreaseQuotaMutation("bob", 10, ctx)
62+
};
63+
64+
var result = await engine.ExecuteBatchAsync(mutations, state);
65+
66+
foreach (var res in result.Results)
67+
{
68+
if (res.IsSuccess)
69+
Console.WriteLine($"✓ {res.NewState!.UserQuotas.Keys.First()} quota updated");
70+
else
71+
{
72+
Console.WriteLine("✗ Mutation blocked:");
73+
foreach (var decision in res.PolicyDecisions)
74+
Console.WriteLine($" Policy: {decision.PolicyName}{decision.Reason}");
75+
}
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)