Workflow Engine can run without ASP.NET Core and Platform modules, but it is not a bundled database product. The host owns IPersistenceStore, transactions, schema, tenancy, and expression evaluation. Optional-port absence has concrete degradation.
Persistence and expression evaluation are hard build dependencies. Permission, query, notification, participant, and service-task ports are optional in construction but determine fail-closed writes, query cost, delivery, assignment, and automation. Production acceptance must distinguish “builds” from “operates safely.”
1. Packages
| Package | Purpose |
|---|---|
| Workflow.Abstractions | definitions, runtime models, and ports |
| Workflow.Engine | compiler, behavior, runtime, services |
| Workflow.DependencyInjection | IServiceCollection composition |
| Workflow.FusionCache | optional L1/L2 cache |
| Framework infrastructure adapters | EF Core/SqlSugar write stores and read adapters |
Do not reference Platform.Workflow.Application unless adopting BitzOrcas current-user, Mediator, authorization, and endpoint conventions too.
2. Builder
var engine = new WorkflowEngineBuilder() // Required: all workflow tables, transaction, and optimistic concurrency. .UsePersistence(persistenceStore) // Required: controlled expression evaluation. .UseExpressionEvaluator(new DefaultExpressionEvaluator()) // Strongly required for user interaction; otherwise writes fail closed. .UseFlowDataPermissionChecker(permissionChecker) .Build();
IWorkflowTransitionFlow transitions = engine.RuntimeTransition;ITaskService tasks = engine.Tasks;Missing persistence or expression evaluator throws. Noop cache, absent query store, notifications, or participant provider still builds but degrades capability.
3. DI
// Write and read stores must enforce identical tenant semantics.services.AddScoped<IPersistenceStore, TenantAwareWorkflowStore>();services.AddScoped<IWorkflowQueryStore, WorkflowReadStore>();services.AddScoped<IFlowDataPermissionChecker, WorkflowPermissionChecker>();services.AddScoped<IParticipantProvider, DirectoryParticipantProvider>();services.AddScoped<INotificationChannel, DurableWorkflowNotificationChannel>();services.AddScoped<IServiceTaskHandler, ApplicationServiceTaskHandler>();// Composition registers the graph; schema creation remains the host's job.services.AddWorkflowEngine();IWorkflowEngine is Scoped to avoid capturing a scoped DbContext. WorkflowEngineConfiguration switches are not currently enforced.
4. Persistence contract
An adapter must provide:
- one transaction across operation writes;
- expectedVersion on instance and task updates;
- tenant filtering on every read and write;
- complete definition, binding, runtime, task, activity, timer, history, and statistic semantics;
- cancellation and UTC DateTimeOffset;
- recognizable uniqueness and concurrency exceptions;
- stable query ordering.
Prefer official adapters and run parity tests instead of implementing only enough to compile.
5. QueryStore
Without IWorkflowQueryStore, task fallbacks can scan all data, done count can be full-memory, reports return empty, and statistics aggregation has no source. Production hosts need database assigned/candidate union and report range queries.
Advanced data-scope filtering still needs framework-level pushdown; the embedding guide cannot claim it is solved by injecting a QueryStore.
6. Participants and permission
IParticipantProvider expands directory rules; custom IParticipantResolver handles domain-specific assignment. Both must enforce tenant scope and return stable user IDs.
IFlowDataPermissionChecker maps View, Operate, and Manage to the host’s authorization. Actor and tenant must be trusted server context. Missing checker is intentionally fail-closed.
7. Service tasks
Map Implementation to an allowlist handler key; never execute arbitrary reflection or script.
// Treat implementation as an allowlisted key, never a reflected type name.return implementation switch{ "matter.reserve-case-number" => reserveCaseNumber.ExecuteAsync(context, cancellationToken), "conflict.register-waiver" => registerWaiver.ExecuteAsync(context, cancellationToken), // Unknown keys fail explicitly so operations can observe configuration drift. _ => throw new InvalidOperationException( $"Unsupported service task: {implementation}")};External calls require idempotency, timeout, retry, and compensation. Do not keep a database transaction open across the network.
8. Notifications and callback
INotificationChannel is optional; without one nothing is sent. A custom channel should mirror the platform adapter’s contract — persist per-recipient inbox plus Outbox facts and throw after counting any failure — because the engine dispatcher now rethrows channel exceptions and fails the transition; swallowing them mid-way would fake success. Direct Builder can still connect template and preference stores; AddWorkflowEngine currently does not forward those two services. IBusinessIntegrationCallback is not an Outbox. Prefer writing a local durable intent and processing it asynchronously.
9. Timers
The host must provide trusted tenant scope, atomic claim/lease, attempts and backoff, multi-instance exclusion, a notification channel, system permission for escalation/transfer, and crash recovery. The current official handler marks Fired before action and is not sufficient for a reliable SLA.
10. Cache
Noop is simplest and correct. Once caching is enabled, definition, binding, and todo-count invalidation become correctness. FusionCache is L1-only without Redis; multi-node products need L2/backplane and outage tests.
11. Health and observability
Readiness checks database/schema, query adapter, tenant context, and required ports. Timer and notification should have separate component health. Metrics cover command latency, conflicts, task age, timer lag, notification backlog, report scans, and cache. Do not label metrics with task, business, comment, or user identifiers.
12. Acceptance
- Workflow unit and integration tests pass;
- custom adapter parity covers transaction, concurrency, tenancy, and every table;
- missing QueryStore is prevented or surfaced;
- participant and permission ports reject cross-tenant access;
- service tasks are replay-safe;
- timer crash and retry recover;
- notification and business integration use durable intents;
- migration, backup, restore, archive, and cleanup runbooks work.
13. Commands
dotnet test tests/BitzOrcas.Workflow.Tests/BitzOrcas.Workflow.Tests.csprojdotnet test tests/BitzOrcas.Workflow.Integration.Tests/BitzOrcas.Workflow.Integration.Tests.csproj
rg -n "class .*PersistenceStore|IWorkflowQueryStore" src/Framework tests -g '*.cs' --glob '!**/bin/**' --glob '!**/obj/**'