Add StellaOps.Workflow engine: 14 libraries, WebService, 8 test projects
Extract product-agnostic workflow engine from Ablera.Serdica.Workflow into standalone StellaOps.Workflow.* libraries targeting net10.0. Libraries (14): - Contracts, Abstractions (compiler, decompiler, expression runtime) - Engine (execution, signaling, scheduling, projections, hosted services) - ElkSharp (generic graph layout algorithm) - Renderer.ElkSharp, Renderer.ElkJs, Renderer.Msagl, Renderer.Svg - Signaling.Redis, Signaling.OracleAq - DataStore.MongoDB, DataStore.PostgreSQL, DataStore.Oracle WebService: ASP.NET Core Minimal API with 22 endpoints Tests (8 projects, 109 tests pass): - Engine.Tests (105 pass), WebService.Tests (4 E2E pass) - Renderer.Tests, DataStore.MongoDB/Oracle/PostgreSQL.Tests - Signaling.Redis.Tests, IntegrationTests.Shared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
22
docs/workflow/tutorials/06-advanced-patterns/README.md
Normal file
22
docs/workflow/tutorials/06-advanced-patterns/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Tutorial 6: Advanced Patterns
|
||||
|
||||
Fork (parallel branches), Repeat (retry loops), Timer (delays), and External Signal (wait for events).
|
||||
|
||||
## Patterns
|
||||
|
||||
| Pattern | Use When |
|
||||
|---------|----------|
|
||||
| **Fork** | Multiple independent operations that should run concurrently |
|
||||
| **Repeat** | Retry a service call with backoff, poll until condition met |
|
||||
| **Timer** | Delay between steps (backoff, scheduled processing) |
|
||||
| **External Signal** | Wait for an external event (document upload, approval from another system) |
|
||||
|
||||
## Variants
|
||||
|
||||
- [C# Fluent DSL](csharp/)
|
||||
- [Canonical JSON](json/)
|
||||
|
||||
## Next
|
||||
|
||||
[Tutorial 7: Shared Helpers](../07-shared-helpers/) — organizing reusable workflow components.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using WorkflowEngine.Abstractions;
|
||||
using WorkflowEngine.Contracts;
|
||||
|
||||
namespace WorkflowEngine.Tutorials;
|
||||
|
||||
public sealed class AdvancedPatternsWorkflow : IDeclarativeWorkflow<PolicyChangeWorkflowRequest>
|
||||
{
|
||||
public string WorkflowName => "AdvancedPatterns";
|
||||
public string WorkflowVersion => "1.0.0";
|
||||
public string DisplayName => "Advanced Patterns Example";
|
||||
public IReadOnlyCollection<string> WorkflowRoles => [];
|
||||
|
||||
public WorkflowSpec<PolicyChangeWorkflowRequest> Spec { get; } = WorkflowSpec
|
||||
.For<PolicyChangeWorkflowRequest>()
|
||||
.InitializeState(WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("start.policyId")),
|
||||
WorkflowExpr.Prop("retryAttempt", WorkflowExpr.Number(0)),
|
||||
WorkflowExpr.Prop("integrationFailed", WorkflowExpr.Bool(false))))
|
||||
.StartWith(BuildFlow)
|
||||
.Build();
|
||||
|
||||
public IReadOnlyCollection<WorkflowTaskDescriptor> Tasks => Spec.TaskDescriptors;
|
||||
|
||||
private static void BuildFlow(WorkflowFlowBuilder<PolicyChangeWorkflowRequest> flow)
|
||||
{
|
||||
flow
|
||||
// ═══════════════════════════════════════════════
|
||||
// FORK: parallel branches
|
||||
// ═══════════════════════════════════════════════
|
||||
// Both branches run concurrently. Workflow resumes after all complete.
|
||||
.Fork("Process Documents and Notify",
|
||||
documents => documents
|
||||
.Call("Generate PDF",
|
||||
Address.LegacyRabbit("pas_pdf_generate"),
|
||||
WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("state.policyId"))),
|
||||
WorkflowHandledBranchAction.Complete,
|
||||
WorkflowHandledBranchAction.Complete),
|
||||
notification => notification
|
||||
.Call("Send Email",
|
||||
Address.LegacyRabbit("notifications_send_email",
|
||||
SerdicaLegacyRabbitMode.MicroserviceConsumer),
|
||||
WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("to", WorkflowExpr.String("agent@company.com")),
|
||||
WorkflowExpr.Prop("subject", WorkflowExpr.String("Policy processed"))),
|
||||
WorkflowHandledBranchAction.Complete,
|
||||
WorkflowHandledBranchAction.Complete))
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// REPEAT: retry loop with backoff
|
||||
// ═══════════════════════════════════════════════
|
||||
// Retries up to 3 times while integrationFailed is true.
|
||||
.Repeat(
|
||||
"Retry Integration",
|
||||
WorkflowExpr.Number(3), // max iterations
|
||||
"retryAttempt", // counter state key
|
||||
WorkflowExpr.Or( // continue while:
|
||||
WorkflowExpr.Eq( // first attempt (counter == 0)
|
||||
WorkflowExpr.Path("state.retryAttempt"),
|
||||
WorkflowExpr.Number(0)),
|
||||
WorkflowExpr.Path("state.integrationFailed")), // or previous attempt failed
|
||||
body => body
|
||||
.Set("integrationFailed", WorkflowExpr.Bool(false))
|
||||
.Call("Transfer Policy",
|
||||
Address.Http("integration", "/api/transfer", "POST"),
|
||||
WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("state.policyId"))),
|
||||
whenFailure: fail => fail
|
||||
.Set("integrationFailed", WorkflowExpr.Bool(true))
|
||||
// TIMER: wait before retrying
|
||||
.Wait("Backoff", WorkflowExpr.String("00:05:00")),
|
||||
whenTimeout: timeout => timeout
|
||||
.Set("integrationFailed", WorkflowExpr.Bool(true))))
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// EXTERNAL SIGNAL: wait for event
|
||||
// ═══════════════════════════════════════════════
|
||||
// Workflow pauses until an external system raises the named signal.
|
||||
.WaitForSignal(
|
||||
"Wait for Document Upload",
|
||||
signalName: "documents-ready",
|
||||
resultKey: "uploadedDocs")
|
||||
|
||||
// Use the signal payload in subsequent steps.
|
||||
.Set("documentCount",
|
||||
WorkflowExpr.Func("length",
|
||||
WorkflowExpr.Path("result.uploadedDocs.fileIds")))
|
||||
.Complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"schemaVersion": "serdica.workflow.definition/v1",
|
||||
"workflowName": "AdvancedPatterns",
|
||||
"workflowVersion": "1.0.0",
|
||||
"displayName": "Advanced Patterns Example",
|
||||
"workflowRoles": [],
|
||||
"start": {
|
||||
"initializeStateExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "start.policyId" } },
|
||||
{ "name": "retryAttempt", "expression": { "$type": "number", "value": 0 } },
|
||||
{ "name": "integrationFailed", "expression": { "$type": "boolean", "value": false } }
|
||||
]
|
||||
},
|
||||
"sequence": {
|
||||
"steps": [
|
||||
{
|
||||
"$type": "fork",
|
||||
"stepName": "Process Documents and Notify",
|
||||
"branches": [
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"$type": "call-transport",
|
||||
"stepName": "Generate PDF",
|
||||
"invocation": {
|
||||
"address": { "$type": "legacy-rabbit", "command": "pas_pdf_generate", "mode": "Envelope" },
|
||||
"payloadExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "state.policyId" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"whenFailure": { "steps": [{ "$type": "complete" }] },
|
||||
"whenTimeout": { "steps": [{ "$type": "complete" }] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"$type": "call-transport",
|
||||
"stepName": "Send Email",
|
||||
"invocation": {
|
||||
"address": { "$type": "legacy-rabbit", "command": "notifications_send_email", "mode": "MicroserviceConsumer" },
|
||||
"payloadExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "to", "expression": { "$type": "string", "value": "agent@company.com" } },
|
||||
{ "name": "subject", "expression": { "$type": "string", "value": "Policy processed" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"whenFailure": { "steps": [{ "$type": "complete" }] },
|
||||
"whenTimeout": { "steps": [{ "$type": "complete" }] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$type": "repeat",
|
||||
"stepName": "Retry Integration",
|
||||
"maxIterationsExpression": { "$type": "number", "value": 3 },
|
||||
"iterationStateKey": "retryAttempt",
|
||||
"continueWhileExpression": {
|
||||
"$type": "binary", "operator": "or",
|
||||
"left": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "path", "path": "state.retryAttempt" },
|
||||
"right": { "$type": "number", "value": 0 }
|
||||
},
|
||||
"right": { "$type": "path", "path": "state.integrationFailed" }
|
||||
},
|
||||
"body": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "integrationFailed", "valueExpression": { "$type": "boolean", "value": false } },
|
||||
{
|
||||
"$type": "call-transport",
|
||||
"stepName": "Transfer Policy",
|
||||
"invocation": {
|
||||
"address": { "$type": "http", "target": "integration", "path": "/api/transfer", "method": "POST" },
|
||||
"payloadExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "state.policyId" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"whenFailure": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "integrationFailed", "valueExpression": { "$type": "boolean", "value": true } },
|
||||
{ "$type": "timer", "stepName": "Backoff", "delayExpression": { "$type": "string", "value": "00:05:00" } }
|
||||
]
|
||||
},
|
||||
"whenTimeout": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "integrationFailed", "valueExpression": { "$type": "boolean", "value": true } }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"$type": "external-signal",
|
||||
"stepName": "Wait for Document Upload",
|
||||
"signalNameExpression": { "$type": "string", "value": "documents-ready" },
|
||||
"resultKey": "uploadedDocs"
|
||||
},
|
||||
{
|
||||
"$type": "set-state",
|
||||
"stateKey": "documentCount",
|
||||
"valueExpression": {
|
||||
"$type": "function",
|
||||
"functionName": "length",
|
||||
"arguments": [{ "$type": "path", "path": "result.uploadedDocs.fileIds" }]
|
||||
}
|
||||
},
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"tasks": []
|
||||
}
|
||||
Reference in New Issue
Block a user