Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion Docs/API-Reference.md → Docs/API/API-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Complete API documentation for **ModularityKit.Mutators**.
- [Related Types](#related-types)
- [DI Extension Methods](#di-extension-methods)
- [AddMutators](#addmutators)
- [Governance API](#governance-api)
- [Execution Semantics](#execution-semantics)
- [Best Practices](#best-practices)
- [Complete Examples](#complete-examples)
Expand Down Expand Up @@ -246,6 +247,19 @@ services.AddMutators(MutationEngineOptions.Strict, addDefaultLoggingInterceptor:

---

## Governance API

Governance is documented separately so the core API reference can stay focused on direct mutation execution.

- [Governance package overview](../../src/Governance/README.md)
- [Governance API usage](API.md)
- [Redis provider API](Redis.md)

Governance-specific types such as `MutationRequestFactory`, `MutationRequestDecision`, and
`IGovernanceExecutionManager` live in the governance package, not the core mutation runtime.

---

## Execution Semantics

1. Call `Validate(state)`
Expand All @@ -272,4 +286,4 @@ services.AddMutators(MutationEngineOptions.Strict, addDefaultLoggingInterceptor:

---

> **Built with ❤️ for .NET developers**
> **Built with ❤️ for .NET developers**
158 changes: 158 additions & 0 deletions Docs/API/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Governance API

This page shows the practical entry points used by the governance examples.

## What this API is for

Use the governance API when execution is not immediate and you need one of these steps first:

- request creation
- approval workflow
- stale-version resolution
- governed execution
- request and decision history queries

The core package owns direct mutation execution. Governance wraps that execution with a request
model and stateful decision history.

## Request creation

Use `MutationRequestFactory` when you want to create a request from explicit strings and values:

```csharp
var request = MutationRequestFactory.PendingApproval(
stateId: "tenant-42:roles",
stateType: "IamRoleState",
mutationType: "GrantRoleMutation",
intent: new MutationIntent
{
OperationName = "GrantRole",
Category = "Security",
Description = "Grant elevated role to tenant operator"
},
context: MutationContext.User("requester-1", "Requester One", "Incident escalation"),
expectedStateVersion: "v10",
approvalRequirements:
[
MutationApprovalRequirement.SingleActorStep("security-lead"),
MutationApprovalRequirement.SingleActorStep("platform-owner")
]);
```

If you already have concrete CLR types, prefer the generic overloads such as:

- `MutationRequestFactory.Approved<TState, TMutation>()`
- `MutationRequestFactory.PendingApproval<TState, TMutation>()`

They remove repeated `stateType` and `mutationType` strings at the call site.

### Factory choices

- `Pending(...)` for a request that should enter the lifecycle without approval requirements
- `PendingApproval(...)` for a request that must collect one or more approval steps
- `Approved(...)` for a request that is terminally approved at creation time

Use the generic overloads when the CLR types already exist. Use the string-based overloads when the
request metadata comes from an external system or serialized payload.

## Decision history

Use `MutationRequestDecision` when you need to record a lifecycle, approval, or version-resolution decision and the category is already known:

```csharp
var decision = MutationRequestDecision.Lifecycle(
MutationRequestLifecycleDecisionType.Approved,
MutationContext.System("governance-runtime", "Approved by governance"));
```

The category-specific helpers are:

- `MutationRequestDecision.Lifecycle(...)`
- `MutationRequestDecision.Approval(...)`
- `MutationRequestDecision.VersionResolution(...)`

Use the low-level `MutationRequestDecision.Create(...)` only when the decision type is already dynamic and you do not know the category up front.

### Decision categories

- lifecycle decisions describe request state transitions such as submitted, pending, approved, rejected, executed, canceled, or expired
- approval decisions describe workflow-level approval actions such as requested, approved, rejected, or quorum satisfied
- version-resolution decisions describe how the runtime handled stale or matching versions before execution

The helper methods exist so call sites stay readable when the category is already known.

## Governed execution

Use `IGovernanceExecutionManager.ExecuteApproved(...)` when the runtime should resolve an approved request and execute the mutation:

```csharp
var execution = await executionManager.ExecuteApproved(
request.RequestId,
mutation,
currentState,
governanceContext: MutationContext.Service("governance-runtime", "Execute approved governance request"),
strategy: VersionedRequestResolutionStrategy.RejectStale);
```

If your state implements `IVersionedState`, use the overload that accepts `currentState` directly and reads `state.Version` for both version selectors.

### Typical execution flow

1. create or load a request
2. approve the request if needed
3. resolve the request against the current state version
4. execute the underlying mutation through the core engine
5. record the terminal request decision

That flow is what the `GovernedExecution` example demonstrates end to end.

## Common operations

### Create a pending approval request

```csharp
var request = MutationRequestFactory.PendingApproval(
stateId: "tenant-42:roles",
stateType: "IamRoleState",
mutationType: "GrantRoleMutation",
intent: new MutationIntent
{
OperationName = "GrantRole",
Category = "Security",
Description = "Grant elevated role to tenant operator"
},
context: MutationContext.User("requester-1", "Requester One", "Incident escalation"),
expectedStateVersion: "v10",
approvalRequirements:
[
MutationApprovalRequirement.SingleActorStep("security-lead"),
MutationApprovalRequirement.SingleActorStep("platform-owner")
]);
```

### Record an approval decision

```csharp
var decision = MutationRequestDecision.Approval(
MutationRequestApprovalDecisionType.Approved,
MutationContext.User("approver-1", "Approver One", "Approved after review"));
```

### Execute an already approved request

```csharp
var result = await executionManager.ExecuteApproved(
request.RequestId,
mutation,
currentState,
governanceContext: MutationContext.Service("governance-runtime", "Execute approved governance request"),
strategy: VersionedRequestResolutionStrategy.RejectStale);
```

## Where to look next

- `README.md` for the package overview
- `Examples/Governance/*/README.md` for runnable scenarios
- `src/Governance/Abstractions/Requests/Factory/MutationRequestFactory.cs` for request creation
- `src/Governance/Abstractions/Requests/Decisions/MutationRequestDecision.cs` for decision helpers
- `src/Governance/Runtime/Execution/Orchestration/GovernanceExecutionManager.cs` for governed execution
136 changes: 136 additions & 0 deletions Docs/API/Redis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Redis Governance Provider API

This page covers the Redis-backed provider package `ModularityKit.Mutator.Governance.Redis`.

It is the storage and query backend for governed requests. The package implements the public
request store and query store contracts while keeping Redis-specific persistence details internal.

## What this API is for

Use the Redis provider when you want the governance model from `ModularityKit.Mutator.Governance`
but need a persistent backing store instead of in-memory state.

Typical reasons:

- you want request persistence across process restarts
- you want Redis-backed query projections for approval queues and decision history
- you want the same governance API surface with a different storage backend
- you want to share governed request state across multiple runtime instances

## Primary APIs

### Dependency injection

- `RedisGovernanceServiceCollectionExtensions.AddRedisGovernanceStore(...)`

Registers the Redis-backed governance store and the query store against an existing
`IConnectionMultiplexer`.

```csharp
using Microsoft.Extensions.DependencyInjection;
using ModularityKit.Mutator.Governance.Redis;
using StackExchange.Redis;

var services = new ServiceCollection();
var multiplexer = await ConnectionMultiplexer.ConnectAsync("localhost:6379");

services.AddRedisGovernanceStore(
multiplexer,
options => options.KeyPrefix = "modularitykit:governance");
```

The extension method registers the provider-facing storage and query services against DI.

### Configuration details

`RedisMutationRequestStoreOptions.KeyPrefix` scopes all Redis keys created by the provider.

Use a dedicated prefix per environment or application if you need isolation between deployments.
The examples use `modularitykit:governance` as a readable default.

### Configuration

- `RedisMutationRequestStoreOptions`

Current option:

- `KeyPrefix` - Redis key prefix used for all request data, indexes, and query projections

### Storage facade

- `RedisMutationRequestStore`

Implements:

- `IMutationRequestStore`
- `IMutationRequestQueryStore`

Use it through DI rather than constructing it directly unless you are testing internals.

### What gets stored

The provider stores the data needed for:

- request documents
- decision and approval projections
- query indexes and queue membership
- optimistic concurrency state for request writes

The provider does not change the governance model. It only persists the same model through Redis.

### Keyspace and key helpers

- `RedisMutationRequestKeyspace`
- `RedisMutationRequestDocumentKeyFactory`

These types define the Redis naming and partitioning rules for request documents, indexes, and
query views.

## What the provider does

The Redis provider handles:

- request persistence
- query projection
- optimistic concurrency on request writes
- Redis index maintenance
- candidate selection for query execution

It does not change the governance model itself. It only replaces the in-memory storage backend with
Redis-specific persistence and read paths.

## Common usage

### Register the provider

```csharp
services.AddRedisGovernanceStore(
multiplexer,
options => options.KeyPrefix = "modularitykit:governance");
```

### Resolve the stores

```csharp
var requestStore = provider.GetRequiredService<IMutationRequestStore>();
var queryStore = provider.GetRequiredService<IMutationRequestQueryStore>();
```

### Use the normal governance APIs

Once registered, the rest of the application uses the same request and query contracts as the
in-memory provider.

## Usage pattern

1. Connect to Redis with `StackExchange.Redis`
2. Register the provider with `AddRedisGovernanceStore(...)`
3. Resolve `IMutationRequestStore` and `IMutationRequestQueryStore` from DI
4. Use the normal governance request and query APIs

## Related docs

- [`src/Redis/README.md`](../../src/Redis/README.md)
- [`Examples/Governance/RedisQueries/README.md`](../../Examples/Governance/RedisQueries/README.md)
- [`API-Reference.md`](API-Reference.md)
- [Governance API](API.md)
42 changes: 20 additions & 22 deletions Examples/Core/BillingQuotas/Mutations/DecreaseQuotaMutation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ namespace BillingQuotas.Mutations;
/// <summary>
/// Mutation that decreases the quota for specific user by specified amount.
/// </summary>
internal sealed record DecreaseQuotaMutation(
string UserId,
int Amount,
MutationContext Context
) : IMutation<QuotaState>
internal sealed class DecreaseQuotaMutation(
string userId,
int amount,
MutationContext context
) : MutationBase<QuotaState>(
CreateIntent(
operationName: "DecreaseQuota",
category: "Billing",
description: "Decrease user quota by given amount",
riskLevel: MutationRiskLevel.High),
context)
{
public MutationIntent Intent { get; } = new()
{
OperationName = "DecreaseQuota",
Category = "Billing",
RiskLevel = MutationRiskLevel.High,
Description = "Decrease user quota by given amount"
};
public string UserId { get; } = userId;

public int Amount { get; } = amount;

public ValidationResult Validate(QuotaState state)
public override ValidationResult Validate(QuotaState state)
{
var result = new ValidationResult();

Expand All @@ -40,19 +42,15 @@ public ValidationResult Validate(QuotaState state)
return result;
}

public MutationResult<QuotaState> Apply(QuotaState state)
public override MutationResult<QuotaState> Apply(QuotaState state)
{
var quotas = state.UserQuotas.ToDictionary(kv => kv.Key, kv => kv.Value);
quotas[UserId] = quotas.GetValueOrDefault(UserId) - Amount;

var newState = state with { UserQuotas = quotas };

var changes = ChangeSet.Single(
StateChange.Modified($"UserQuotas.{UserId}", null, quotas[UserId])
);

return MutationResult<QuotaState>.Success(newState, changes);
return Success(
newState,
StateChange.Modified($"UserQuotas.{UserId}", null, quotas[UserId]));
}

public MutationResult<QuotaState> Simulate(QuotaState state) => Apply(state);
}
}
Loading
Loading