Skip to content

Commit 1222929

Browse files
committed
Docs: add Redis governance documentation and fix descriptions
1 parent 7c75dc8 commit 1222929

11 files changed

Lines changed: 411 additions & 2 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using RedisQueries.Scenarios;
2+
3+
await GovernanceRedisQueriesScenario.Run();
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Governance RedisQueries
2+
3+
This example shows how to use `ModularityKit.Mutator.Governance.Redis` as the backing store for governed request writes and query-oriented reads.
4+
5+
It focuses on real Redis-backed request persistence, approval queue reads, and decision/history queries through the same provider.
6+
7+
## What it demonstrates
8+
9+
- connecting to Redis with `ConnectionMultiplexer`
10+
- registering the Redis governance provider through `AddRedisGovernanceStore(...)`
11+
- creating governed requests through `IMutationRequestStore`
12+
- reading pending request queues through `IMutationRequestQueryStore`
13+
- projecting approval views and decision views from Redis-backed data
14+
- isolating sample data with a dedicated Redis key prefix
15+
16+
## Prerequisite
17+
18+
You need a running Redis instance.
19+
20+
Default connection:
21+
22+
```bash
23+
localhost:6379
24+
```
25+
26+
Override it with a full connection string:
27+
28+
```bash
29+
export MODULARITYKIT_REDIS="your-host:6379"
30+
```
31+
32+
Or with separate settings:
33+
34+
```bash
35+
export MODULARITYKIT_REDIS_HOST="localhost"
36+
export MODULARITYKIT_REDIS_PORT="6379"
37+
export MODULARITYKIT_REDIS_PASSWORD=""
38+
```
39+
40+
## Start Redis locally
41+
42+
From this folder:
43+
44+
```bash
45+
docker compose up -d
46+
```
47+
48+
This starts a local Redis instance using [`docker-compose.yml`](docker-compose.yml).
49+
50+
Optional overrides:
51+
52+
```bash
53+
export MODULARITYKIT_REDIS_PORT="6380"
54+
export MODULARITYKIT_REDIS_PASSWORD="secret"
55+
docker compose up -d
56+
```
57+
58+
Stop it with:
59+
60+
```bash
61+
docker compose down
62+
```
63+
64+
## Key files
65+
66+
- [`Program.cs`](Program.cs)
67+
- [`Scenarios/GovernanceRedisQueriesScenario.cs`](Scenarios/GovernanceRedisQueriesScenario.cs)
68+
- [`src/Redis/DependencyInjection/RedisGovernanceServiceCollectionExtensions.cs`](../../../src/Redis/DependencyInjection/RedisGovernanceServiceCollectionExtensions.cs)
69+
- [`src/Redis/Storage/RedisMutationRequestStore.cs`](../../../src/Redis/Storage/RedisMutationRequestStore.cs)
70+
71+
## Run
72+
73+
```bash
74+
dotnet run --project Examples/Governance/RedisQueries/RedisQueries.csproj -c Release
75+
```
76+
77+
## Expected output
78+
79+
The sample prints:
80+
81+
- the Redis connection and key prefix used by the run
82+
- pending approval queue entries
83+
- approval views filtered by approver
84+
- recent decision views for execution outcomes
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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="..\..\..\src\ModularityKit.Mutator.Governance.csproj" />
12+
<ProjectReference Include="..\..\..\src\Redis\ModularityKit.Mutator.Governance.Redis.csproj" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModularityKit.Mutator.Abstractions.Context;
3+
using ModularityKit.Mutator.Abstractions.Intent;
4+
using ModularityKit.Mutator.Abstractions.Policies;
5+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
6+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Contracts;
7+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
8+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
9+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Factory;
10+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
11+
using ModularityKit.Mutator.Governance.Abstractions.Storage;
12+
using ModularityKit.Mutator.Governance.Redis;
13+
using StackExchange.Redis;
14+
15+
namespace RedisQueries.Scenarios;
16+
17+
internal static class GovernanceRedisQueriesScenario
18+
{
19+
public static async Task Run()
20+
{
21+
var redisConnectionString = BuildRedisConnectionString();
22+
var keyPrefix = $"modularitykit:examples:governance:redis:{Guid.NewGuid():N}";
23+
24+
try
25+
{
26+
await using var multiplexer = await ConnectionMultiplexer.ConnectAsync(redisConnectionString);
27+
var services = new ServiceCollection();
28+
29+
services.AddRedisGovernanceStore(
30+
multiplexer,
31+
options => options.KeyPrefix = keyPrefix);
32+
33+
await using var provider = services.BuildServiceProvider();
34+
var requestStore = provider.GetRequiredService<IMutationRequestStore>();
35+
var queryStore = provider.GetRequiredService<IMutationRequestQueryStore>();
36+
37+
await Seed(requestStore);
38+
39+
Console.WriteLine($"Redis: {redisConnectionString}");
40+
Console.WriteLine($"KeyPrefix: {keyPrefix}");
41+
42+
PrintSection("Pending Approval Queue");
43+
PrintRequests(await queryStore.GetPendingApprovalQueueAsync());
44+
45+
PrintSection("Pending Approvals For security-lead");
46+
PrintApprovals(await queryStore.GetPendingApprovalsAsync(new MutationApprovalQuery
47+
{
48+
ApproverIds = new HashSet<string> { "security-lead" }
49+
}));
50+
51+
PrintSection("Recent Execution Outcomes");
52+
PrintDecisions(await queryStore.GetRecentDecisionsAsync(
53+
MutationRequestDecisionQuery.RecentExecutionOutcomes(),
54+
take: 5));
55+
}
56+
catch (RedisConnectionException exception)
57+
{
58+
Console.Error.WriteLine($"Could not connect to Redis at '{redisConnectionString}'.");
59+
Console.Error.WriteLine(exception.Message);
60+
Console.Error.WriteLine("Start Redis locally or set MODULARITYKIT_REDIS to a reachable endpoint.");
61+
}
62+
}
63+
64+
private static string BuildRedisConnectionString()
65+
{
66+
var explicitConnectionString = Environment.GetEnvironmentVariable("MODULARITYKIT_REDIS");
67+
if (!string.IsNullOrWhiteSpace(explicitConnectionString))
68+
return explicitConnectionString;
69+
70+
var host = Environment.GetEnvironmentVariable("MODULARITYKIT_REDIS_HOST") ?? "localhost";
71+
var port = Environment.GetEnvironmentVariable("MODULARITYKIT_REDIS_PORT") ?? "6379";
72+
var password = Environment.GetEnvironmentVariable("MODULARITYKIT_REDIS_PASSWORD");
73+
74+
return string.IsNullOrWhiteSpace(password)
75+
? $"{host}:{port}"
76+
: $"{host}:{port},password={password}";
77+
}
78+
79+
private static async Task Seed(IMutationRequestStore requestStore)
80+
{
81+
await requestStore.Create(CreatePendingApprovalRequest(
82+
requestId: "redis-req-security-1",
83+
stateId: "tenant-42:roles",
84+
category: "Security",
85+
approverId: "security-lead",
86+
createdAt: new DateTimeOffset(2026, 6, 25, 8, 0, 0, TimeSpan.Zero)));
87+
88+
await requestStore.Create(CreatePendingApprovalRequest(
89+
requestId: "redis-req-billing-1",
90+
stateId: "tenant-42:quota",
91+
category: "Billing",
92+
approverId: "billing-owner",
93+
createdAt: new DateTimeOffset(2026, 6, 25, 9, 0, 0, TimeSpan.Zero)));
94+
95+
await requestStore.Create(CreateExecutedRequest(
96+
requestId: "redis-req-executed-1",
97+
stateId: "tenant-42:quota",
98+
category: "Billing",
99+
executedAt: new DateTimeOffset(2026, 6, 25, 12, 0, 0, TimeSpan.Zero)));
100+
}
101+
102+
private static MutationRequest CreatePendingApprovalRequest(
103+
string requestId,
104+
string stateId,
105+
string category,
106+
string approverId,
107+
DateTimeOffset createdAt)
108+
=> MutationRequestFactory.PendingApproval(
109+
stateId: stateId,
110+
stateType: "ExampleState",
111+
mutationType: "ExampleMutation",
112+
intent: new MutationIntent
113+
{
114+
OperationName = "ExampleOperation",
115+
Category = category,
116+
Description = $"Redis-backed governed request for {category.ToLowerInvariant()} flow"
117+
},
118+
context: MutationContext.User("requester", "Requester", "Need governed change"),
119+
requirements:
120+
[
121+
PolicyRequirement.Approval(approverId, $"Approval required from {approverId}")
122+
])
123+
with
124+
{
125+
RequestId = requestId,
126+
CreatedAt = createdAt,
127+
UpdatedAt = createdAt
128+
};
129+
130+
private static MutationRequest CreateExecutedRequest(
131+
string requestId,
132+
string stateId,
133+
string category,
134+
DateTimeOffset executedAt)
135+
=> MutationRequestFactory.Approved(
136+
stateId: stateId,
137+
stateType: "ExampleState",
138+
mutationType: "ExampleMutation",
139+
intent: new MutationIntent
140+
{
141+
OperationName = "ExampleOperation",
142+
Category = category,
143+
Description = "Executed governed request"
144+
},
145+
context: MutationContext.Service("governance-runtime", "Execute approved request"))
146+
with
147+
{
148+
RequestId = requestId,
149+
Status = MutationRequestStatus.Executed,
150+
CreatedAt = executedAt.AddMinutes(-15),
151+
UpdatedAt = executedAt,
152+
ExecutedAt = executedAt,
153+
Decisions =
154+
[
155+
MutationRequestDecision.Create(
156+
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Submitted),
157+
MutationContext.Service("governance-runtime", "Submitted"))
158+
with
159+
{
160+
Timestamp = executedAt.AddMinutes(-15)
161+
},
162+
MutationRequestDecision.Create(
163+
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Approved),
164+
MutationContext.Service("governance-runtime", "Approved"))
165+
with
166+
{
167+
Timestamp = executedAt.AddMinutes(-5)
168+
},
169+
MutationRequestDecision.Create(
170+
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Executed),
171+
MutationContext.Service("governance-runtime", "Executed"))
172+
with
173+
{
174+
Timestamp = executedAt
175+
}
176+
]
177+
};
178+
179+
private static void PrintSection(string title)
180+
{
181+
Console.WriteLine();
182+
Console.WriteLine($"=== {title} ===");
183+
}
184+
185+
private static void PrintRequests(IReadOnlyList<MutationRequest> requests)
186+
{
187+
foreach (var request in requests)
188+
{
189+
Console.WriteLine(
190+
$"- {request.RequestId} | {request.StateId} | {request.Intent.Category} | {request.Status} | pending: {request.PendingReason?.ToString() ?? "-"}");
191+
}
192+
193+
if (requests.Count == 0)
194+
Console.WriteLine("- none");
195+
}
196+
197+
private static void PrintApprovals(IReadOnlyList<MutationApprovalView> approvals)
198+
{
199+
foreach (var approval in approvals)
200+
{
201+
Console.WriteLine(
202+
$"- {approval.Request.RequestId} | {approval.Request.Intent.Category} | approver: {approval.Approval.ApproverId} | status: {approval.Approval.Status}");
203+
}
204+
205+
if (approvals.Count == 0)
206+
Console.WriteLine("- none");
207+
}
208+
209+
private static void PrintDecisions(IReadOnlyList<MutationRequestDecisionView> decisions)
210+
{
211+
foreach (var decision in decisions)
212+
{
213+
Console.WriteLine(
214+
$"- {decision.Request.RequestId} | {decision.Decision.Type.Category}:{decision.Decision.Type.Code} | {decision.Decision.Timestamp:O}");
215+
}
216+
217+
if (decisions.Count == 0)
218+
Console.WriteLine("- none");
219+
}
220+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
redis:
3+
image: redis:7-alpine
4+
ports:
5+
- "${MODULARITYKIT_REDIS_PORT:-6379}:6379"
6+
environment:
7+
REDIS_PASSWORD: "${MODULARITYKIT_REDIS_PASSWORD:-}"
8+
command:
9+
- sh
10+
- -c
11+
- >
12+
if [ -n "$$REDIS_PASSWORD" ]; then
13+
exec redis-server --save "" --appendonly no --requirepass "$$REDIS_PASSWORD";
14+
else
15+
exec redis-server --save "" --appendonly no;
16+
fi

Examples/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The projects are intentionally small and focused. Each one demonstrates a differ
2828
| `ApprovalWorkflow` | request-level approvals, multi-step sign-off, and governed approval actions | [`Examples/Governance/ApprovalWorkflow/README.md`](Governance/ApprovalWorkflow/README.md) |
2929
| `VersionedResolution` | stale request handling and expected state version semantics | [`Examples/Governance/VersionedResolution/README.md`](Governance/VersionedResolution/README.md) |
3030
| `Queries` | request queries, pending approvals, and decision-oriented governance read views | [`Examples/Governance/Queries/README.md`](Governance/Queries/README.md) |
31+
| `RedisQueries` | Redis-backed governance request storage and query-oriented reads | [`Examples/Governance/RedisQueries/README.md`](Governance/RedisQueries/README.md) |
3132

3233
## How to use these examples
3334

@@ -61,6 +62,7 @@ dotnet build Examples/Governance/DecisionTaxonomy/DecisionTaxonomy.csproj -c Rel
6162
dotnet build Examples/Governance/ApprovalWorkflow/ApprovalWorkflow.csproj -c Release
6263
dotnet build Examples/Governance/VersionedResolution/VersionedResolution.csproj -c Release
6364
dotnet build Examples/Governance/Queries/Queries.csproj -c Release
65+
dotnet build Examples/Governance/RedisQueries/RedisQueries.csproj -c Release
6466
```
6567

6668
## Run
@@ -80,6 +82,7 @@ dotnet run --project Examples/Governance/DecisionTaxonomy/DecisionTaxonomy.cspro
8082
dotnet run --project Examples/Governance/ApprovalWorkflow/ApprovalWorkflow.csproj
8183
dotnet run --project Examples/Governance/VersionedResolution/VersionedResolution.csproj
8284
dotnet run --project Examples/Governance/Queries/Queries.csproj
85+
dotnet run --project Examples/Governance/RedisQueries/RedisQueries.csproj
8386
```
8487

8588
If you want to run one sample repeatedly while changing code, stay in its folder:
@@ -173,6 +176,12 @@ Shows the governance read side as a first-class API for listing requests, approv
173176

174177
See [`Governance/Queries/README.md`](Governance/Queries/README.md).
175178

179+
### RedisQueries
180+
181+
Shows the same governance read side backed by `ModularityKit.Mutator.Governance.Redis` instead of the in-memory store.
182+
183+
See [`Governance/RedisQueries/README.md`](Governance/RedisQueries/README.md).
184+
176185
## Notes
177186

178187
- The examples are separate console applications, not libraries.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2-
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rejector/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rejector/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unioned/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2-
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAnnotations_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Frain_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb153369fa8993154b386b8c0b4a24f62395f169bf190b19b193ec650dc224fb_003FAnnotations_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
2+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAnnotations_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Frain_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb153369fa8993154b386b8c0b4a24f62395f169bf190b19b193ec650dc224fb_003FAnnotations_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
3+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARedisKey_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d5ee58b95bf6126b8ea2cf82244b865cd3445e16cd3962846a3da884cfe_003FRedisKey_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

0 commit comments

Comments
 (0)