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:
master
2026-03-20 19:14:44 +02:00
parent e56f9a114a
commit f5b5f24d95
422 changed files with 85428 additions and 0 deletions

View File

@@ -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();
}
}