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:
28
docs/workflow/tutorials/03-decisions/README.md
Normal file
28
docs/workflow/tutorials/03-decisions/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Tutorial 3: Decisions
|
||||
|
||||
Branch workflow logic based on conditions — state values, payload answers, or complex expressions.
|
||||
|
||||
## Concepts Introduced
|
||||
|
||||
- `.WhenExpression()` — branch on any boolean expression
|
||||
- `.WhenStateFlag()` — shorthand for checking a boolean state value
|
||||
- `.WhenPayloadEquals()` — shorthand for checking a task completion payload value
|
||||
- Nested decisions — decisions inside decisions for complex routing
|
||||
|
||||
## Decision Types
|
||||
|
||||
| Method | Use When |
|
||||
|--------|----------|
|
||||
| `WhenExpression` | Complex conditions (comparisons, boolean logic, function calls) |
|
||||
| `WhenStateFlag` | Checking a boolean state key against true/false |
|
||||
| `WhenPayloadEquals` | Checking a task completion answer (inside OnComplete) |
|
||||
|
||||
## Variants
|
||||
|
||||
- [C# Fluent DSL](csharp/)
|
||||
- [Canonical JSON](json/)
|
||||
|
||||
## Next
|
||||
|
||||
[Tutorial 4: Human Tasks](../04-human-tasks/) — approve/reject patterns with OnComplete flows.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using WorkflowEngine.Abstractions;
|
||||
using WorkflowEngine.Contracts;
|
||||
|
||||
namespace WorkflowEngine.Tutorials;
|
||||
|
||||
public sealed class PolicyRoutingRequest
|
||||
{
|
||||
public long PolicyId { get; set; }
|
||||
public string AnnexType { get; set; } = string.Empty;
|
||||
public bool PolicyExistsOnIPAL { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PolicyRoutingWorkflow : IDeclarativeWorkflow<PolicyRoutingRequest>
|
||||
{
|
||||
public string WorkflowName => "PolicyRouting";
|
||||
public string WorkflowVersion => "1.0.0";
|
||||
public string DisplayName => "Policy Routing Example";
|
||||
public IReadOnlyCollection<string> WorkflowRoles => ["DBA"];
|
||||
|
||||
public WorkflowSpec<PolicyRoutingRequest> Spec { get; } = WorkflowSpec
|
||||
.For<PolicyRoutingRequest>()
|
||||
.InitializeState(WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("start.policyId")),
|
||||
WorkflowExpr.Prop("annexType", WorkflowExpr.Path("start.annexType")),
|
||||
WorkflowExpr.Prop("policyExistsOnIPAL",
|
||||
WorkflowExpr.Func("coalesce",
|
||||
WorkflowExpr.Path("start.policyExistsOnIPAL"),
|
||||
WorkflowExpr.Bool(true)))))
|
||||
.StartWith(BuildFlow)
|
||||
.Build();
|
||||
|
||||
public IReadOnlyCollection<WorkflowTaskDescriptor> Tasks => Spec.TaskDescriptors;
|
||||
|
||||
private static void BuildFlow(WorkflowFlowBuilder<PolicyRoutingRequest> flow)
|
||||
{
|
||||
// --- Example 1: State flag decision (boolean shorthand) ---
|
||||
flow.WhenStateFlag(
|
||||
"policyExistsOnIPAL", // state key to check
|
||||
true, // expected value
|
||||
"Policy exists on IPAL?", // decision name (appears in diagram)
|
||||
whenTrue: ipal => ipal
|
||||
|
||||
// --- Example 2: Expression decision ---
|
||||
.WhenExpression(
|
||||
"Annex Type?",
|
||||
WorkflowExpr.Eq(
|
||||
WorkflowExpr.Func("upper", WorkflowExpr.Path("state.annexType")),
|
||||
WorkflowExpr.String("BENEF")),
|
||||
benefit => benefit
|
||||
.Set("route", WorkflowExpr.String("BENEFIT_PROCESSING"))
|
||||
.Complete(),
|
||||
|
||||
// --- Example 3: Nested decision ---
|
||||
other => other.WhenExpression(
|
||||
"Is Equipment?",
|
||||
WorkflowExpr.Eq(
|
||||
WorkflowExpr.Func("upper", WorkflowExpr.Path("state.annexType")),
|
||||
WorkflowExpr.String("ADDEQ")),
|
||||
equipment => equipment
|
||||
.Set("route", WorkflowExpr.String("EQUIPMENT_PROCESSING"))
|
||||
.Complete(),
|
||||
cover => cover
|
||||
.Set("route", WorkflowExpr.String("COVER_CHANGE"))
|
||||
.Complete())),
|
||||
|
||||
whenElse: notIpal => notIpal
|
||||
.Set("route", WorkflowExpr.String("INSIS_PROCESSING"))
|
||||
.Complete());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"schemaVersion": "serdica.workflow.definition/v1",
|
||||
"workflowName": "PolicyRouting",
|
||||
"workflowVersion": "1.0.0",
|
||||
"displayName": "Policy Routing Example",
|
||||
"workflowRoles": ["DBA"],
|
||||
"start": {
|
||||
"initializeStateExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "start.policyId" } },
|
||||
{ "name": "annexType", "expression": { "$type": "path", "path": "start.annexType" } },
|
||||
{ "name": "policyExistsOnIPAL", "expression": {
|
||||
"$type": "function", "functionName": "coalesce",
|
||||
"arguments": [
|
||||
{ "$type": "path", "path": "start.policyExistsOnIPAL" },
|
||||
{ "$type": "boolean", "value": true }
|
||||
]
|
||||
}}
|
||||
]
|
||||
},
|
||||
"sequence": {
|
||||
"steps": [
|
||||
{
|
||||
"$type": "decision",
|
||||
"decisionName": "Policy exists on IPAL?",
|
||||
"conditionExpression": { "$type": "path", "path": "state.policyExistsOnIPAL" },
|
||||
"whenTrue": {
|
||||
"steps": [
|
||||
{
|
||||
"$type": "decision",
|
||||
"decisionName": "Annex Type?",
|
||||
"conditionExpression": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "function", "functionName": "upper", "arguments": [{ "$type": "path", "path": "state.annexType" }] },
|
||||
"right": { "$type": "string", "value": "BENEF" }
|
||||
},
|
||||
"whenTrue": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "route", "valueExpression": { "$type": "string", "value": "BENEFIT_PROCESSING" } },
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
},
|
||||
"whenElse": {
|
||||
"steps": [
|
||||
{
|
||||
"$type": "decision",
|
||||
"decisionName": "Is Equipment?",
|
||||
"conditionExpression": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "function", "functionName": "upper", "arguments": [{ "$type": "path", "path": "state.annexType" }] },
|
||||
"right": { "$type": "string", "value": "ADDEQ" }
|
||||
},
|
||||
"whenTrue": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "route", "valueExpression": { "$type": "string", "value": "EQUIPMENT_PROCESSING" } },
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
},
|
||||
"whenElse": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "route", "valueExpression": { "$type": "string", "value": "COVER_CHANGE" } },
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"whenElse": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "route", "valueExpression": { "$type": "string", "value": "INSIS_PROCESSING" } },
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"tasks": []
|
||||
}
|
||||
Reference in New Issue
Block a user