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:
29
docs/workflow/tutorials/02-service-tasks/README.md
Normal file
29
docs/workflow/tutorials/02-service-tasks/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Tutorial 2: Service Tasks
|
||||
|
||||
Call external services (microservices, HTTP APIs, GraphQL, RabbitMQ) from within a workflow. Handle failures and timeouts gracefully.
|
||||
|
||||
## Concepts Introduced
|
||||
|
||||
- `.Call()` — invoke a transport with payload and optional response capture
|
||||
- Address types — `LegacyRabbit`, `Microservice`, `Http`, `Graphql`, `Rabbit`
|
||||
- `resultKey` — store the service response in workflow state
|
||||
- `whenFailure` / `whenTimeout` — recovery branches
|
||||
- `WorkflowHandledBranchAction.Complete` — shorthand for "complete on error"
|
||||
- `timeoutSeconds` — per-step timeout override (default: 1 hour)
|
||||
|
||||
## Key Points
|
||||
|
||||
- Each `Call` step executes synchronously within the workflow
|
||||
- The per-step timeout wraps the entire call including transport-level retries
|
||||
- Transport timeouts (30s default) control individual attempt duration
|
||||
- If no failure/timeout handler is defined, the error propagates and the signal pump retries
|
||||
|
||||
## Variants
|
||||
|
||||
- [C# Fluent DSL](csharp/)
|
||||
- [Canonical JSON](json/)
|
||||
|
||||
## Next
|
||||
|
||||
[Tutorial 3: Decisions](../03-decisions/) — branch workflow logic based on conditions.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
using WorkflowEngine.Abstractions;
|
||||
using WorkflowEngine.Contracts;
|
||||
|
||||
namespace WorkflowEngine.Tutorials;
|
||||
|
||||
public sealed class PolicyValidationRequest
|
||||
{
|
||||
public long PolicyId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PolicyValidationWorkflow : IDeclarativeWorkflow<PolicyValidationRequest>
|
||||
{
|
||||
public string WorkflowName => "PolicyValidation";
|
||||
public string WorkflowVersion => "1.0.0";
|
||||
public string DisplayName => "Policy Validation";
|
||||
public IReadOnlyCollection<string> WorkflowRoles => ["DBA"];
|
||||
|
||||
public WorkflowSpec<PolicyValidationRequest> Spec { get; } = WorkflowSpec
|
||||
.For<PolicyValidationRequest>()
|
||||
.InitializeState(WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("start.policyId"))))
|
||||
.StartWith(BuildFlow)
|
||||
.Build();
|
||||
|
||||
public IReadOnlyCollection<WorkflowTaskDescriptor> Tasks => Spec.TaskDescriptors;
|
||||
|
||||
private static void BuildFlow(WorkflowFlowBuilder<PolicyValidationRequest> flow)
|
||||
{
|
||||
flow
|
||||
// --- Example 1: Simple call with shorthand error handling ---
|
||||
.Call(
|
||||
"Validate Policy", // step name
|
||||
Address.LegacyRabbit("pas_policy_validate"), // transport address
|
||||
WorkflowExpr.Object( // payload (expression-based)
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("state.policyId"))),
|
||||
WorkflowHandledBranchAction.Complete, // on failure: complete workflow
|
||||
WorkflowHandledBranchAction.Complete) // on timeout: complete workflow
|
||||
|
||||
// --- Example 2: Call with typed response stored in state ---
|
||||
.Call<object>(
|
||||
"Load Policy Info",
|
||||
Address.LegacyRabbit("pas_get_policy_product_info"),
|
||||
WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("state.policyId"))),
|
||||
WorkflowHandledBranchAction.Complete,
|
||||
WorkflowHandledBranchAction.Complete,
|
||||
resultKey: "policyInfo") // store response as "policyInfo"
|
||||
|
||||
// Use the result to set state values
|
||||
.SetIfHasValue("productCode",
|
||||
WorkflowExpr.Func("upper", WorkflowExpr.Path("result.policyInfo.productCode")))
|
||||
.SetIfHasValue("lob",
|
||||
WorkflowExpr.Path("result.policyInfo.lob"))
|
||||
|
||||
// --- Example 3: Call with custom failure/timeout branches ---
|
||||
.Call(
|
||||
"Calculate Premium",
|
||||
Address.LegacyRabbit("pas_premium_calculate_for_object",
|
||||
SerdicaLegacyRabbitMode.MicroserviceConsumer),
|
||||
WorkflowExpr.Object(
|
||||
WorkflowExpr.Prop("policyId", WorkflowExpr.Path("state.policyId"))),
|
||||
whenFailure: fail => fail
|
||||
.Set("calculationFailed", WorkflowExpr.Bool(true))
|
||||
.Complete(),
|
||||
whenTimeout: timeout => timeout
|
||||
.Set("calculationTimedOut", WorkflowExpr.Bool(true))
|
||||
.Complete(),
|
||||
timeoutSeconds: 120) // per-step timeout: 2 minutes
|
||||
|
||||
// --- Example 4: HTTP transport ---
|
||||
// .Call("Notify External",
|
||||
// Address.Http("authority", "/api/v1/notifications", "POST"),
|
||||
// WorkflowExpr.Object(
|
||||
// WorkflowExpr.Prop("message", WorkflowExpr.String("Policy validated"))),
|
||||
// WorkflowHandledBranchAction.Complete,
|
||||
// WorkflowHandledBranchAction.Complete)
|
||||
|
||||
.Complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"schemaVersion": "serdica.workflow.definition/v1",
|
||||
"workflowName": "PolicyValidation",
|
||||
"workflowVersion": "1.0.0",
|
||||
"displayName": "Policy Validation",
|
||||
"workflowRoles": ["DBA"],
|
||||
|
||||
"start": {
|
||||
"initializeStateExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "start.policyId" } }
|
||||
]
|
||||
},
|
||||
"sequence": {
|
||||
"steps": [
|
||||
{
|
||||
"$type": "call-transport",
|
||||
"stepName": "Validate Policy",
|
||||
"invocation": {
|
||||
"address": { "$type": "legacy-rabbit", "command": "pas_policy_validate", "mode": "Envelope" },
|
||||
"payloadExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "state.policyId" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"whenFailure": { "steps": [{ "$type": "complete" }] },
|
||||
"whenTimeout": { "steps": [{ "$type": "complete" }] }
|
||||
},
|
||||
{
|
||||
"$type": "call-transport",
|
||||
"stepName": "Load Policy Info",
|
||||
"resultKey": "policyInfo",
|
||||
"invocation": {
|
||||
"address": { "$type": "legacy-rabbit", "command": "pas_get_policy_product_info", "mode": "Envelope" },
|
||||
"payloadExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "state.policyId" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"whenFailure": { "steps": [{ "$type": "complete" }] },
|
||||
"whenTimeout": { "steps": [{ "$type": "complete" }] }
|
||||
},
|
||||
{
|
||||
"$type": "set-state",
|
||||
"stateKey": "productCode",
|
||||
"valueExpression": {
|
||||
"$type": "function",
|
||||
"functionName": "upper",
|
||||
"arguments": [{ "$type": "path", "path": "result.policyInfo.productCode" }]
|
||||
},
|
||||
"onlyIfPresent": true
|
||||
},
|
||||
{
|
||||
"$type": "call-transport",
|
||||
"stepName": "Calculate Premium",
|
||||
"timeoutSeconds": 120,
|
||||
"invocation": {
|
||||
"address": { "$type": "legacy-rabbit", "command": "pas_premium_calculate_for_object", "mode": "MicroserviceConsumer" },
|
||||
"payloadExpression": {
|
||||
"$type": "object",
|
||||
"properties": [
|
||||
{ "name": "policyId", "expression": { "$type": "path", "path": "state.policyId" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"whenFailure": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "calculationFailed", "valueExpression": { "$type": "boolean", "value": true } },
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
},
|
||||
"whenTimeout": {
|
||||
"steps": [
|
||||
{ "$type": "set-state", "stateKey": "calculationTimedOut", "valueExpression": { "$type": "boolean", "value": true } },
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "$type": "complete" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"tasks": []
|
||||
}
|
||||
Reference in New Issue
Block a user