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,30 @@
# Tutorial 1: Hello World
The simplest possible workflow: initialize state from a start request, activate a single human task, and complete the workflow when the task is done.
## Concepts Introduced
- `IDeclarativeWorkflow<T>` — the contract every workflow implements
- `WorkflowSpec.For<T>()` — the builder entry point
- `.InitializeState()` — transforms the start request into workflow state
- `.StartWith(task)` — sets the first task to activate
- `WorkflowHumanTask.For<T>()` — defines a human task
- `.OnComplete(flow => flow.Complete())` — terminal step
## What Happens at Runtime
1. Client calls `StartWorkflowAsync` with `WorkflowName = "Greeting"` and payload `{ "customerName": "John" }`
2. State initializes to `{ "customerName": "John" }`
3. Task "Greet Customer" is created with status "Pending"
4. A user assigns the task to themselves, then completes it
5. `OnComplete` executes `.Complete()` — the workflow finishes
## Variants
- [C# Fluent DSL](csharp/)
- [Canonical JSON](json/)
## Next
[Tutorial 2: Service Tasks](../02-service-tasks/) — call external services before or after human tasks.

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Text.Json;
using WorkflowEngine.Abstractions;
using WorkflowEngine.Contracts;
namespace WorkflowEngine.Tutorials;
// Start request — defines the input contract for the workflow.
public sealed class GreetingRequest
{
public string CustomerName { get; set; } = string.Empty;
}
// Workflow definition — implements IDeclarativeWorkflow<TStartRequest>.
public sealed class GreetingWorkflow : IDeclarativeWorkflow<GreetingRequest>
{
// Identity: name + version uniquely identify the workflow definition.
public string WorkflowName => "Greeting";
public string WorkflowVersion => "1.0.0";
public string DisplayName => "Customer Greeting";
// Roles: which user roles can see and interact with this workflow's tasks.
public IReadOnlyCollection<string> WorkflowRoles => ["DBA", "UR_AGENT"];
// Spec: the workflow specification built via the fluent DSL.
public WorkflowSpec<GreetingRequest> Spec { get; } = WorkflowSpec
.For<GreetingRequest>()
// InitializeState: transform the start request into the workflow's mutable state.
// State is a Dictionary<string, JsonElement> — all values are JSON-serialized.
.InitializeState(request => new Dictionary<string, JsonElement>
{
["customerName"] = JsonSerializer.SerializeToElement(request.CustomerName),
})
// StartWith: register and activate this task as the first step.
.StartWith(greetTask)
.Build();
// Tasks: expose task descriptors for the registration catalog.
public IReadOnlyCollection<WorkflowTaskDescriptor> Tasks => Spec.TaskDescriptors;
// Task definition: defines name, type (UI component), route (navigation), and behavior.
private static readonly WorkflowHumanTaskDefinition<GreetingRequest> greetTask =
WorkflowHumanTask.For<GreetingRequest>(
taskName: "Greet Customer", // unique name within this workflow
taskType: "GreetCustomerTask", // UI component identifier
route: "customers/greet") // navigation route
.WithPayload(context => new Dictionary<string, JsonElement>
{
// Pass state values to the task's UI payload.
["customerName"] = context.StateValues
.GetRequired<string>("customerName").AsJsonElement(),
})
// OnComplete: what happens after the user completes this task.
.OnComplete(flow => flow.Complete()); // simply end the workflow
}

View File

@@ -0,0 +1,56 @@
{
"schemaVersion": "serdica.workflow.definition/v1",
"workflowName": "Greeting",
"workflowVersion": "1.0.0",
"displayName": "Customer Greeting",
"startRequest": {
"contractName": "GreetingRequest",
"allowAdditionalProperties": true
},
"workflowRoles": ["DBA", "UR_AGENT"],
"start": {
"initializeStateExpression": {
"$type": "object",
"properties": [
{
"name": "customerName",
"expression": { "$type": "path", "path": "start.customerName" }
}
]
},
"sequence": {
"steps": [
{
"$type": "activate-task",
"taskName": "Greet Customer"
}
]
}
},
"tasks": [
{
"taskName": "Greet Customer",
"taskType": "GreetCustomerTask",
"routeExpression": { "$type": "string", "value": "customers/greet" },
"taskRoles": [],
"payloadExpression": {
"$type": "object",
"properties": [
{
"name": "customerName",
"expression": { "$type": "path", "path": "state.customerName" }
}
]
},
"onCompleteSequence": {
"steps": [
{ "$type": "complete" }
]
}
}
]
}