Skip to content

Commit 394b419

Browse files
committed
docs(examples): demonstrate typed side effect usage
1 parent bf96299 commit 394b419

7 files changed

Lines changed: 90 additions & 9 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using ModularityKit.Mutator.Abstractions.Effects;
2+
3+
namespace WorkflowApprovals.Contracts;
4+
5+
[SideEffectDataContract("workflow.rejected", 1)]
6+
internal sealed record WorkflowRejectedSideEffectData
7+
{
8+
public required string Rejector { get; init; }
9+
10+
public required int StepCount { get; init; }
11+
12+
public required string State { get; init; }
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using ModularityKit.Mutator.Abstractions.Effects;
2+
3+
namespace WorkflowApprovals.Contracts;
4+
5+
[SideEffectDataContract("workflow.started", 1)]
6+
internal sealed record WorkflowStartedSideEffectData
7+
{
8+
public required string Initiator { get; init; }
9+
10+
public required int StepCount { get; init; }
11+
12+
public required string WorkflowId { get; init; }
13+
}

Examples/Core/WorkflowApprovals/Mutations/RejectWorkflowMutation.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ModularityKit.Mutator.Abstractions.Effects;
55
using ModularityKit.Mutator.Abstractions.Intent;
66
using ModularityKit.Mutator.Abstractions.Results;
7+
using WorkflowApprovals.Contracts;
78
using WorkflowApprovals.State;
89

910
namespace WorkflowApprovals.Mutations;
@@ -48,9 +49,9 @@ public override MutationResult<ApprovalWorkflowState> Apply(ApprovalWorkflowStat
4849
SideEffect.Critical(
4950
type: "WorkflowRejected",
5051
description: "Workflow rejection requires manual follow-up",
51-
data: new
52+
data: new WorkflowRejectedSideEffectData
5253
{
53-
Rejector,
54+
Rejector = Rejector,
5455
StepCount = steps.Count,
5556
State = "Rejected"
5657
})

Examples/Core/WorkflowApprovals/Mutations/StartApprovalMutation.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ModularityKit.Mutator.Abstractions.Effects;
55
using ModularityKit.Mutator.Abstractions.Intent;
66
using ModularityKit.Mutator.Abstractions.Results;
7+
using WorkflowApprovals.Contracts;
78
using WorkflowApprovals.State;
89

910
namespace WorkflowApprovals.Mutations;
@@ -54,9 +55,9 @@ public override MutationResult<ApprovalWorkflowState> Apply(ApprovalWorkflowStat
5455
SideEffect.Create(
5556
type: "WorkflowStarted",
5657
description: "Approval workflow started and ready for first review",
57-
data: new
58+
data: new WorkflowStartedSideEffectData
5859
{
59-
Initiator,
60+
Initiator = Initiator,
6061
StepCount = steps.Count,
6162
WorkflowId = newState.WorkflowId
6263
})

Examples/Core/WorkflowApprovals/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ The sample is intentionally sequential. It shows how stateful process can be adv
6767
- creates workflow steps from names
6868
- initializes a new workflow ID
6969
- emits change entry for the created step list
70-
- emits a `WorkflowStarted` side effect through `SideEffect.Create(...)`
70+
- emits a `WorkflowStarted` side effect through `SideEffect.Create(...)` with a typed contract payload
7171

7272
### Approve step
7373

@@ -85,7 +85,7 @@ The sample is intentionally sequential. It shows how stateful process can be adv
8585
- applies rejection to every step
8686
- records the actor who rejected the workflow
8787
- emits workflow level change
88-
- emits a critical `WorkflowRejected` side effect through `SideEffect.Critical(...)`
88+
- emits a critical `WorkflowRejected` side effect through `SideEffect.Critical(...)` with a typed contract payload
8989

9090
## Policies
9191

@@ -135,7 +135,8 @@ It shows:
135135

136136
- a standard side effect created with `SideEffect.Create(...)`
137137
- a critical side effect created with `SideEffect.Critical(...)`
138-
- how `Severity`, `RequiresAction`, and `Data` can be read from `MutationResult.SideEffects`
138+
- how typed payload contracts are registered through `SideEffectDataContractRegistry`
139+
- how `Severity`, `RequiresAction`, `DataContractType`, and typed `Data` can be read from `MutationResult.SideEffects`
139140

140141
## What to read first
141142

Examples/Core/WorkflowApprovals/Scenarios/SideEffectsScenario.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using ModularityKit.Mutator.Abstractions.Context;
22
using ModularityKit.Mutator.Abstractions.Effects;
33
using ModularityKit.Mutator.Abstractions.Engine;
4+
using System.Text.Json;
5+
using WorkflowApprovals.Contracts;
46
using WorkflowApprovals.Mutations;
57
using WorkflowApprovals.State;
68

@@ -12,6 +14,9 @@ internal static async Task Run(IMutationEngine engine)
1214
{
1315
Console.WriteLine("\n=== Side Effects Scenario ===");
1416

17+
SideEffectDataContractRegistry.Register<WorkflowStartedSideEffectData>();
18+
SideEffectDataContractRegistry.Register<WorkflowRejectedSideEffectData>();
19+
1520
var state = new ApprovalWorkflowState();
1621

1722
var startContext = MutationContext.System("Start side effect demo", correlationId: "workflow-side-effects");
@@ -51,9 +56,25 @@ private static void PrintSideEffects(string operation, IReadOnlyList<SideEffect>
5156
$" {effect.Type} | severity={effect.Severity} | requiresAction={effect.RequiresAction}");
5257
Console.WriteLine($" {effect.Description}");
5358

54-
if (effect.Data is not null)
59+
var roundtrip = JsonSerializer.Deserialize<SideEffect>(JsonSerializer.Serialize(effect));
60+
61+
if (roundtrip?.TryGetData<WorkflowStartedSideEffectData>(out var started) == true)
62+
{
63+
Console.WriteLine(
64+
$" contract={roundtrip.DataContractType}@v{roundtrip.DataContractVersion} | initiator={started!.Initiator} | workflowId={started.WorkflowId}");
65+
continue;
66+
}
67+
68+
if (roundtrip?.TryGetData<WorkflowRejectedSideEffectData>(out var rejected) == true)
69+
{
70+
Console.WriteLine(
71+
$" contract={roundtrip.DataContractType}@v{roundtrip.DataContractVersion} | rejector={rejected!.Rejector} | state={rejected.State}");
72+
continue;
73+
}
74+
75+
if (roundtrip?.Data is not null)
5576
{
56-
Console.WriteLine($" data={effect.Data}");
77+
Console.WriteLine($" data={roundtrip.Data}");
5778
}
5879
}
5980
}

src/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,37 @@ Core runtime concurrency is controlled by `MutationEngineOptions.MaxConcurrentMu
109109
- `ValidationResult`
110110
- `ChangeSet`
111111
- `StateChange`
112+
- `SideEffect`
113+
- `SideEffectDataContractAttribute`
114+
- `SideEffectDataContractRegistry`
115+
116+
### Typed side effects
117+
118+
Use typed side effect payloads when the emitted data is meant to survive serialization, audit, or downstream integration:
119+
120+
```csharp
121+
[SideEffectDataContract("workflow.started", 1)]
122+
public sealed record WorkflowStartedSideEffectData
123+
{
124+
public required string Initiator { get; init; }
125+
public required int StepCount { get; init; }
126+
public required string WorkflowId { get; init; }
127+
}
128+
129+
SideEffectDataContractRegistry.Register<WorkflowStartedSideEffectData>();
130+
131+
var effect = SideEffect.Create(
132+
type: "WorkflowStarted",
133+
description: "Approval workflow started",
134+
data: new WorkflowStartedSideEffectData
135+
{
136+
Initiator = "alice",
137+
StepCount = 2,
138+
WorkflowId = "wf-42"
139+
});
140+
```
141+
142+
The side effect keeps `DataContractType` and `DataContractVersion` alongside `Data`, so persistence and integration layers do not have to guess the payload shape.
112143

113144
### Context and intent
114145

0 commit comments

Comments
 (0)