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:
36
docs/workflow/tutorials/08-expressions/README.md
Normal file
36
docs/workflow/tutorials/08-expressions/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Tutorial 8: Expressions
|
||||
|
||||
The expression system enables declarative logic that compiles to portable canonical JSON. All expressions are evaluable at runtime without recompilation.
|
||||
|
||||
## Path Navigation
|
||||
|
||||
| Prefix | Source | Example |
|
||||
|--------|--------|---------|
|
||||
| `start.*` | Start request fields | `start.policyId` |
|
||||
| `state.*` | Mutable workflow state | `state.customerName` |
|
||||
| `payload.*` | Task completion payload | `payload.answer` |
|
||||
| `result.*` | Step result (by resultKey) | `result.productInfo.lob` |
|
||||
|
||||
## Built-in Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `coalesce` | First non-null | `coalesce(state.id, start.id, 0)` |
|
||||
| `concat` | String join | `concat("Policy #", state.policyNo)` |
|
||||
| `add` | Sum | `add(state.attempt, 1)` |
|
||||
| `if` | Conditional | `if(state.isVip, "VIP", "Standard")` |
|
||||
| `isNullOrWhiteSpace` | Null/empty check | `isNullOrWhiteSpace(state.name)` |
|
||||
| `length` | String/array length | `length(state.items)` |
|
||||
| `upper` | Uppercase | `upper(state.annexType)` |
|
||||
| `first` | First array element | `first(state.objects)` |
|
||||
| `mergeObjects` | Deep merge | `mergeObjects(state, payload)` |
|
||||
|
||||
## Variants
|
||||
|
||||
- [C# Expression Builder](csharp/)
|
||||
- [JSON Expression Format](json/)
|
||||
|
||||
## Next
|
||||
|
||||
[Tutorial 9: Testing](../09-testing/) — unit test setup with recording transports.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
using WorkflowEngine.Abstractions;
|
||||
|
||||
namespace WorkflowEngine.Tutorials;
|
||||
|
||||
/// <summary>
|
||||
/// Expression builder examples showing all expression types.
|
||||
/// These examples are not a runnable workflow — they demonstrate the WorkflowExpr API.
|
||||
/// </summary>
|
||||
internal static class ExpressionExamples
|
||||
{
|
||||
// ═══════════════════════════════════════════════
|
||||
// LITERALS
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
static readonly WorkflowExpressionDefinition nullExpr = WorkflowExpr.Null();
|
||||
static readonly WorkflowExpressionDefinition stringExpr = WorkflowExpr.String("hello");
|
||||
static readonly WorkflowExpressionDefinition numberExpr = WorkflowExpr.Number(42);
|
||||
static readonly WorkflowExpressionDefinition boolExpr = WorkflowExpr.Bool(true);
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// PATH NAVIGATION
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
// Read from start request
|
||||
static readonly WorkflowExpressionDefinition fromStart = WorkflowExpr.Path("start.policyId");
|
||||
|
||||
// Read from workflow state
|
||||
static readonly WorkflowExpressionDefinition fromState = WorkflowExpr.Path("state.customerName");
|
||||
|
||||
// Read from task completion payload
|
||||
static readonly WorkflowExpressionDefinition fromPayload = WorkflowExpr.Path("payload.answer");
|
||||
|
||||
// Read from step result (requires resultKey on the Call step)
|
||||
static readonly WorkflowExpressionDefinition fromResult = WorkflowExpr.Path("result.productInfo.lob");
|
||||
|
||||
// Nested path navigation
|
||||
static readonly WorkflowExpressionDefinition nested = WorkflowExpr.Path("state.entityData.address.city");
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// OBJECT & ARRAY CONSTRUCTION
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
static readonly WorkflowExpressionDefinition obj = WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("state.policyId")),
|
||||
WorkflowExpr.Prop("status", WorkflowExpr.String("ACTIVE")),
|
||||
WorkflowExpr.Prop("tags", WorkflowExpr.Array(
|
||||
WorkflowExpr.String("motor"),
|
||||
WorkflowExpr.String("casco"))));
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// COMPARISONS
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
static readonly WorkflowExpressionDefinition eq = WorkflowExpr.Eq(
|
||||
WorkflowExpr.Path("state.status"), WorkflowExpr.String("APPROVED"));
|
||||
static readonly WorkflowExpressionDefinition ne = WorkflowExpr.Ne(
|
||||
WorkflowExpr.Path("state.status"), WorkflowExpr.String("REJECTED"));
|
||||
static readonly WorkflowExpressionDefinition gt = WorkflowExpr.Gt(
|
||||
WorkflowExpr.Path("state.premium"), WorkflowExpr.Number(1000));
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// BOOLEAN LOGIC
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
static readonly WorkflowExpressionDefinition not = WorkflowExpr.Not(
|
||||
WorkflowExpr.Path("state.isRejected"));
|
||||
static readonly WorkflowExpressionDefinition and = WorkflowExpr.And(
|
||||
WorkflowExpr.Path("state.isValid"),
|
||||
WorkflowExpr.Not(WorkflowExpr.Path("state.isRejected")));
|
||||
static readonly WorkflowExpressionDefinition or = WorkflowExpr.Or(
|
||||
WorkflowExpr.Eq(WorkflowExpr.Path("state.status"), WorkflowExpr.String("APPROVED")),
|
||||
WorkflowExpr.Eq(WorkflowExpr.Path("state.status"), WorkflowExpr.String("OVERRIDE")));
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// FUNCTION CALLS
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
// Coalesce: first non-null value
|
||||
static readonly WorkflowExpressionDefinition coalesce = WorkflowExpr.Func("coalesce",
|
||||
WorkflowExpr.Path("state.customerId"),
|
||||
WorkflowExpr.Path("start.customerId"),
|
||||
WorkflowExpr.Number(0));
|
||||
|
||||
// String concatenation
|
||||
static readonly WorkflowExpressionDefinition concat = WorkflowExpr.Func("concat",
|
||||
WorkflowExpr.String("Policy #"),
|
||||
WorkflowExpr.Path("state.policyNo"));
|
||||
|
||||
// Arithmetic
|
||||
static readonly WorkflowExpressionDefinition increment = WorkflowExpr.Func("add",
|
||||
WorkflowExpr.Func("coalesce",
|
||||
WorkflowExpr.Path("state.attempt"), WorkflowExpr.Number(0)),
|
||||
WorkflowExpr.Number(1));
|
||||
|
||||
// Conditional value
|
||||
static readonly WorkflowExpressionDefinition conditional = WorkflowExpr.Func("if",
|
||||
WorkflowExpr.Path("state.isVip"),
|
||||
WorkflowExpr.String("VIP"),
|
||||
WorkflowExpr.String("Standard"));
|
||||
|
||||
// Uppercase
|
||||
static readonly WorkflowExpressionDefinition upper = WorkflowExpr.Func("upper",
|
||||
WorkflowExpr.Path("state.annexType"));
|
||||
|
||||
// Null check
|
||||
static readonly WorkflowExpressionDefinition nullCheck = WorkflowExpr.Func("isNullOrWhiteSpace",
|
||||
WorkflowExpr.Path("state.integrationId"));
|
||||
|
||||
// Array length
|
||||
static readonly WorkflowExpressionDefinition length = WorkflowExpr.Func("length",
|
||||
WorkflowExpr.Path("state.documents"));
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// COMBINING EXPRESSIONS (real-world patterns)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
// "Use integration customer ID if present, otherwise use lookup ID"
|
||||
static readonly WorkflowExpressionDefinition resolveCustomerId = WorkflowExpr.Func("if",
|
||||
WorkflowExpr.Not(
|
||||
WorkflowExpr.Func("isNullOrWhiteSpace",
|
||||
WorkflowExpr.Path("state.integrationCustomerId"))),
|
||||
WorkflowExpr.Path("state.integrationCustomerId"),
|
||||
WorkflowExpr.Path("state.lookupCustomerId"));
|
||||
|
||||
// "Should we retry? (first attempt or previous failed, and not timed out)"
|
||||
static readonly WorkflowExpressionDefinition shouldRetry = WorkflowExpr.Or(
|
||||
WorkflowExpr.Eq(WorkflowExpr.Path("state.retryAttempt"), WorkflowExpr.Number(0)),
|
||||
WorkflowExpr.And(
|
||||
WorkflowExpr.Not(WorkflowExpr.Path("state.timedOut")),
|
||||
WorkflowExpr.Path("state.integrationFailed")));
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"_comment": "Expression examples in canonical JSON format. Each key shows a different expression pattern.",
|
||||
|
||||
"literals": {
|
||||
"null": { "$type": "null" },
|
||||
"string": { "$type": "string", "value": "hello" },
|
||||
"number": { "$type": "number", "value": 42 },
|
||||
"boolean": { "$type": "boolean", "value": true }
|
||||
},
|
||||
|
||||
"paths": {
|
||||
"fromStartRequest": { "$type": "path", "path": "start.policyId" },
|
||||
"fromState": { "$type": "path", "path": "state.customerName" },
|
||||
"fromPayload": { "$type": "path", "path": "payload.answer" },
|
||||
"fromResult": { "$type": "path", "path": "result.productInfo.lob" },
|
||||
"nestedPath": { "$type": "path", "path": "state.entityData.address.city" }
|
||||
},
|
||||
|
||||
"objectConstruction": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "state.policyId" } },
|
||||
{ "name": "status", "expression": { "$type": "string", "value": "ACTIVE" } },
|
||||
{ "name": "tags", "expression": {
|
||||
"$type": "array",
|
||||
"items": [
|
||||
{ "$type": "string", "value": "motor" },
|
||||
{ "$type": "string", "value": "casco" }
|
||||
]
|
||||
}}
|
||||
]
|
||||
},
|
||||
|
||||
"comparisons": {
|
||||
"equals": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "path", "path": "state.status" },
|
||||
"right": { "$type": "string", "value": "APPROVED" }
|
||||
},
|
||||
"notEquals": {
|
||||
"$type": "binary", "operator": "ne",
|
||||
"left": { "$type": "path", "path": "state.status" },
|
||||
"right": { "$type": "string", "value": "REJECTED" }
|
||||
},
|
||||
"greaterThan": {
|
||||
"$type": "binary", "operator": "gt",
|
||||
"left": { "$type": "path", "path": "state.premium" },
|
||||
"right": { "$type": "number", "value": 1000 }
|
||||
}
|
||||
},
|
||||
|
||||
"booleanLogic": {
|
||||
"not": {
|
||||
"$type": "unary", "operator": "not",
|
||||
"operand": { "$type": "path", "path": "state.isRejected" }
|
||||
},
|
||||
"and": {
|
||||
"$type": "binary", "operator": "and",
|
||||
"left": { "$type": "path", "path": "state.isValid" },
|
||||
"right": {
|
||||
"$type": "unary", "operator": "not",
|
||||
"operand": { "$type": "path", "path": "state.isRejected" }
|
||||
}
|
||||
},
|
||||
"or": {
|
||||
"$type": "binary", "operator": "or",
|
||||
"left": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "path", "path": "state.status" },
|
||||
"right": { "$type": "string", "value": "APPROVED" }
|
||||
},
|
||||
"right": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "path", "path": "state.status" },
|
||||
"right": { "$type": "string", "value": "OVERRIDE" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"functions": {
|
||||
"coalesce": {
|
||||
"$type": "function", "functionName": "coalesce",
|
||||
"arguments": [
|
||||
{ "$type": "path", "path": "state.customerId" },
|
||||
{ "$type": "path", "path": "start.customerId" },
|
||||
{ "$type": "number", "value": 0 }
|
||||
]
|
||||
},
|
||||
"concat": {
|
||||
"$type": "function", "functionName": "concat",
|
||||
"arguments": [
|
||||
{ "$type": "string", "value": "Policy #" },
|
||||
{ "$type": "path", "path": "state.policyNo" }
|
||||
]
|
||||
},
|
||||
"increment": {
|
||||
"$type": "function", "functionName": "add",
|
||||
"arguments": [
|
||||
{
|
||||
"$type": "function", "functionName": "coalesce",
|
||||
"arguments": [
|
||||
{ "$type": "path", "path": "state.attempt" },
|
||||
{ "$type": "number", "value": 0 }
|
||||
]
|
||||
},
|
||||
{ "$type": "number", "value": 1 }
|
||||
]
|
||||
},
|
||||
"conditional": {
|
||||
"$type": "function", "functionName": "if",
|
||||
"arguments": [
|
||||
{ "$type": "path", "path": "state.isVip" },
|
||||
{ "$type": "string", "value": "VIP" },
|
||||
{ "$type": "string", "value": "Standard" }
|
||||
]
|
||||
},
|
||||
"uppercase": {
|
||||
"$type": "function", "functionName": "upper",
|
||||
"arguments": [{ "$type": "path", "path": "state.annexType" }]
|
||||
},
|
||||
"nullCheck": {
|
||||
"$type": "function", "functionName": "isNullOrWhiteSpace",
|
||||
"arguments": [{ "$type": "path", "path": "state.integrationId" }]
|
||||
},
|
||||
"arrayLength": {
|
||||
"$type": "function", "functionName": "length",
|
||||
"arguments": [{ "$type": "path", "path": "state.documents" }]
|
||||
}
|
||||
},
|
||||
|
||||
"realWorldPatterns": {
|
||||
"resolveCustomerId_comment": "Use integration customer ID if present, otherwise use lookup ID",
|
||||
"resolveCustomerId": {
|
||||
"$type": "function", "functionName": "if",
|
||||
"arguments": [
|
||||
{
|
||||
"$type": "unary", "operator": "not",
|
||||
"operand": {
|
||||
"$type": "function", "functionName": "isNullOrWhiteSpace",
|
||||
"arguments": [{ "$type": "path", "path": "state.integrationCustomerId" }]
|
||||
}
|
||||
},
|
||||
{ "$type": "path", "path": "state.integrationCustomerId" },
|
||||
{ "$type": "path", "path": "state.lookupCustomerId" }
|
||||
]
|
||||
},
|
||||
|
||||
"shouldRetry_comment": "First attempt or previous failed and not timed out",
|
||||
"shouldRetry": {
|
||||
"$type": "binary", "operator": "or",
|
||||
"left": {
|
||||
"$type": "binary", "operator": "eq",
|
||||
"left": { "$type": "path", "path": "state.retryAttempt" },
|
||||
"right": { "$type": "number", "value": 0 }
|
||||
},
|
||||
"right": {
|
||||
"$type": "binary", "operator": "and",
|
||||
"left": {
|
||||
"$type": "unary", "operator": "not",
|
||||
"operand": { "$type": "path", "path": "state.timedOut" }
|
||||
},
|
||||
"right": { "$type": "path", "path": "state.integrationFailed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user