Adds durable, identity-neutral, workflow-bound human tasks, and reconciles the REST surface with the approved Studio contract. - Flat summary/detail DTOs, a global capability descriptor, and workflow context captured at activation. - Scope is part of the list authorization predicate; manager decisions require manage:user-tasks; a denied command answers 404 so it cannot prove a task exists. - Guest sessions are task-scoped, action-allowlisted, and revoked when the task closes. Invitations resolve by token hash through the repository, wait in a Data Protection encrypted outbox, and are rate limited per caller. - Masked form values are disclosed only through an audited reveal command. - Store-specific concurrency failures are translated into a single UserTaskRevisionConflictException, so a concurrent edit returns the documented revision-conflict result behind any provider instead of a 500. EF Core (SQLite, SQL Server, PostgreSQL, MySQL, Oracle) and VNext persistence, hosted due/reconciliation/delivery workers, docs, and 49 tests. Note: this branch also carries two commits inherited from its branch point that are not part of User Tasks and are squashed in here — the revert-version allocation change from #7917 (WorkflowDefinitionPublisher.RevertVersionAsync now allocates from the last version rather than the latest) and an NU1903 package pin. Merged deliberately rather than rebased out.
16 KiB
User Tasks Persistence Contract
Feature: 013-user-tasks
Status: Approved for implementation
Persistence precedent: Elsa.Secrets module and its EF Core provider packages
This contract defines the storage boundary and provider-parity requirements. It describes logical storage units rather than prescribing one physical database design, but every provider must preserve the same behavior and invariants.
Package and registration boundary
The module is split as follows:
Elsa.UserTasks
├── contracts, models, activity, manager, in-memory repository
├── claims resolver, access policy seam, projection/reconciliation
├── endpoints, permissions, notifications, invitation orchestration
└── no EF Core or Elsa.Identity dependency
Elsa.UserTasks.Persistence.EFCore
├── UserTasksElsaDbContext
├── entity configurations and repository
└── EFCoreUserTasksPersistenceFeature
Elsa.UserTasks.Persistence.EFCore.Sqlite
Elsa.UserTasks.Persistence.EFCore.SqlServer
Elsa.UserTasks.Persistence.EFCore.PostgreSql
Elsa.UserTasks.Persistence.EFCore.MySql
Elsa.UserTasks.Persistence.EFCore.Oracle
├── provider configuration and design-time factory
├── provider-specific migration assembly
└── provider shell feature
Elsa.UserTasks.Persistence.VNext
├── IPersistenceSchemaProvider descriptor
└── document-store adapter with equivalent semantics
The Core feature registers an in-memory repository for development/tests. EF Core and VNext packages replace the repository through feature composition, following the Elsa.Secrets pattern. Providers must not add a dependency on Elsa.Identity.
Logical storage units
All units are tenant-scoped. TenantId is either an explicit host tenant value or the configured default tenant; it is never taken from an untrusted arbitrary query string.
UserTasks
One row/document per User Task instance.
Required identity and workflow fields:
Id,TenantId.WorkflowDefinitionId,WorkflowInstanceId,ActivityInstanceId,BookmarkId.MaterializationKey, the normalized tenant/workflow-instance/activity-instance tuple.
Safe query and presentation fields:
Title,Summary,Reference,TaskType,Tags(bounded safe JSON or normalized tag representation).- Requester provider/type/ID and optional display snapshot.
Priority,DueAt,CreatedAt,UpdatedAt,AssignedAt,CompletedAt.Status,IsOverdue(computed or persisted consistently), and health summary.
Assignment fields:
- Assignee provider/type/ID and optional display snapshot.
MembershipResolutionMode.- Safe configuration flags such as exclusion-override enabled and timeout/cancellation enabled.
Protected and operational fields:
InstructionsJson/TaskDataJson, encrypted or provider-protected bounded JSON.- Requested and pinned form reference metadata, protected where provider-private.
- Terminal action key, protected normalized completion data, and completion actor reference.
Revision, an integer optimistic-concurrency token.CreatedFromBookmarkRevisionor equivalent workflow marker when needed for reconciliation.
Protected fields are never part of search indexes, list projections, events, notifications, or authorization scopes.
UserTaskCandidates
Materialized candidate relationships used for authorized list and claim queries:
Id,TenantId,TaskId.- Participant
Provider,Type, andParticipantId. - Optional display snapshot.
- Source (
DirectUser,DirectGroup,SnapshotMember, orInvitation) and source group reference where relevant. - Created/updated timestamps.
The canonical uniqueness key is tenant, task, provider, participant type, participant ID, and source identity. Duplicate bookmark notifications must not duplicate candidate rows.
For live groups, the group candidate is retained and evaluated against current actor claims. For snapshot groups, the expanded user rows are retained with the original group reference for audit. Exclusions are stored separately or with an unambiguous relationship marker and are checked before candidate eligibility.
UserTaskSnapshotMembers and UserTaskExclusions
These optional normalized units make snapshot membership and exclusion checks queryable without deserializing protected task data:
- Tenant/task identifiers.
- Provider, participant type, participant ID, and optional source group.
- Creation timestamp and safe source metadata.
An implementation may combine these with UserTaskCandidates when it preserves the same uniqueness and authorization semantics.
UserTaskEvents
Append-only audit timeline:
Id,TenantId,TaskId, and monotonically increasing taskRevision.EventType,OccurredAt, operation ID, actor provider/type/ID.- Safe reason and bounded safe metadata JSON. V1 has no generic completion comment.
Events never contain instructions, task data, forms, completion data, invitation secrets, guest session tokens, or provider-private payloads. Event writes occur in the same transaction as the state transition.
UserTaskOperations
Idempotency and asynchronous transition state:
Id,TenantId,TaskId, and clientOperationId.- Operation kind (
Claim,Release,Assign,ScheduleUpdate,Complete,Timeout,Cancel,RetryResolution, or invitation verification). - Expected revision, canonical request hash, state, attempts, and created/updated/completed timestamps.
- Safe outcome/status metadata and a protected completion payload when the operation is a completion request.
Unique key: (TenantId, TaskId, OperationId). A repeated operation with the same request hash returns the existing state. A different request hash is a conflict.
UserTaskInvitations
One-time guest invitation state:
Id,TenantId,TaskId, and optional sibling/replacement group ID.- Recipient reference or opaque destination metadata, never an Elsa.Identity foreign key.
- Cryptographically strong token hash and challenge/verifier provider name/configuration.
Status(Pending,Dispatched,Verified,Consumed,Revoked, orExpired).ExpiresAt,IssuedAt,VerifiedAt,ConsumedAt, andRevokedAt.
Only the token hash is retained in the aggregate. A raw token must not be recoverable from ordinary reads or logs.
UserTaskInvitationDeliveries
Encrypted transient delivery outbox:
Id,TenantId,TaskId,InvitationId, and dispatcher/provider name.- Encrypted token payload, delivery metadata, attempt count, state,
AvailableAt, expiry, and last error code. - Idempotency key and timestamps.
The encrypted payload is decrypted only for a dispatch attempt and returned once to the dispatcher boundary. Delivery failures are represented by safe error codes. Cleanup is independent of terminal task retention and must respect in-flight retries.
UserTaskGuestSessions
Task-scoped guest capability:
Id,TenantId,TaskId,InvitationId.- Hashed session token, guest participant reference, granted capability set, issued/expiry timestamps, and revocation timestamp.
The raw session token is returned only at issuance. The hash is unique and all queries are task- and tenant-scoped. A guest session cannot release, reassign, or access another task.
Required indexes and constraints
Names may vary by provider, but the following logical constraints are mandatory:
- Primary key for every unit on its opaque
Id. - Unique
(TenantId, MaterializationKey)onUserTasks. - Unique
(TenantId, BookmarkId)onUserTasks. - Indexes for tenant/status; tenant/assignee provider/type/ID; tenant/priority; tenant/due time; workflow definition; workflow instance; activity instance; created/completed time; and revision/health where used by reconciliation.
- Candidate lookup index on tenant/provider/type/participant ID/task status.
- Unique event ordering
(TaskId, Revision)or an equivalent monotonic event sequence. - Unique operation key
(TenantId, TaskId, OperationId)and an index for stale pending operations. - Unique invitation token hash and indexes for task/status/expiry.
- Unique guest session token hash and indexes for task/expiry/revocation.
Safe search may use normalized columns or a bounded safe search projection. It must cover title, summary, reference, tags, task type, workflow/correlation fields, and safe requester fields without inspecting protected JSON. Cursor pagination uses a stable (sortValue, Id) key; counts are optional.
Repository contracts and transaction behavior
public interface IUserTaskRepository
{
Task<UserTask?> GetAsync(string tenantId, string taskId, CancellationToken cancellationToken = default);
Task<UserTaskQueryResult> QueryAsync(UserTaskQuery query, CancellationToken cancellationToken = default);
Task<UserTask?> FindByMaterializationKeyAsync(string tenantId, string key, CancellationToken cancellationToken = default);
Task<UserTask?> FindByBookmarkIdAsync(string tenantId, string bookmarkId, CancellationToken cancellationToken = default);
Task SaveAsync(UserTask task, int expectedRevision, CancellationToken cancellationToken = default);
Task AddProjectionAsync(UserTaskProjection projection, CancellationToken cancellationToken = default);
}
The concrete repository may expose child-unit operations through a unit-of-work abstraction. It must support:
- Authorized, store-translatable list scopes that do not load protected payloads.
- Atomic compare-and-swap by task ID and expected revision.
- Atomic candidate/snapshot/exclusion updates.
- Atomic append of the lifecycle event and operation record with the state transition.
- Atomic insertion of a workflow-resumption outbox marker for asynchronous completion, timeout, and cancellation.
- At-least-once delivery with idempotent operation and bookmark keys.
If a database transaction is unavailable, the provider must provide equivalent compare-and-swap and durable ordering guarantees and document the recovery behavior. A mutation must never report success while dropping its event, operation, or required outbox marker.
Projection and reconciliation storage behavior
The workflow runtime is the source of truth for bookmark existence and workflow resumption. User Tasks is a durable projection with recovery responsibilities.
WorkflowBookmarksPersistedprojects committed materializations after the workflow transaction commits.- Projection is idempotent by
(TenantId, MaterializationKey)andBookmarkId. - A missing projection is recreated from the committed bookmark payload in bounded pages.
- A stale
Completing,TimingOut, orCancellingoperation is requeued or finalized from its durable operation marker. - A task whose bookmark no longer exists is finalized as
Completedonly when a matching pending operation marker proves the intended resumption; otherwise it is finalized asCancelled. - Ambiguous, cross-tenant, or malformed records are retained for manager diagnostics and reported as health issues.
- Reconciliation is safe to run on multiple nodes. A distributed lock or compare-and-swap lease prevents duplicate work, while duplicate delivery remains harmless.
EF Core provider contract
Elsa.UserTasks.Persistence.EFCore provides a dedicated UserTasksElsaDbContext : ElsaDbContextBase, entity configurations, repository, and EFCoreUserTasksPersistenceFeature depending on UserTasksFeature. Configuration must:
- Use the Elsa schema abstraction (
IElsaDbContextSchema) and provider-appropriate migration assembly. - Configure bounded string lengths, UTC
DateTimeOffsetvalues, string enum storage, JSON/protected payload columns, and the required keys/indexes. - Mark
Revisionas an optimistic concurrency token. An update with zero rows affected is a revision conflict, not a successful no-op. - Keep protected payload properties out of safe projections and indexes.
- Use one transaction for state, candidates, events, operations, and outbox markers whenever the provider supports it.
Provider packages must provide:
Elsa.UserTasks.Persistence.EFCore.Sqlitewith SQLite configuration, design-time context factory, migrations, and shell feature.Elsa.UserTasks.Persistence.EFCore.SqlServerwith SQL Server configuration, design-time context factory, migrations, and shell feature.Elsa.UserTasks.Persistence.EFCore.PostgreSqlwith PostgreSQL configuration, design-time context factory, migrations, and shell feature.Elsa.UserTasks.Persistence.EFCore.MySqlwith MySQL configuration, design-time context factory, migrations, and shell feature.Elsa.UserTasks.Persistence.EFCore.Oraclewith Oracle configuration, design-time context factory, migrations, and shell feature.
The provider differences are dialect and column-type details only. All providers must expose the same lifecycle transitions, revision conflicts, idempotency behavior, visibility scope, retention behavior, and projection/reconciliation semantics. SQLite stores UTC timestamps in its provider-compatible ISO-8601 representation; other providers use their native UTC-capable representation. JSON may use provider-native JSON types or text, but round-trip semantics and payload limits are identical.
Each provider migration must create its complete User Tasks schema, including child units, constraints, indexes, and any required schema marker. Migration tests must apply from an empty database and verify restart durability and indexed query behavior.
VNext provider parity
Elsa.UserTasks.Persistence.VNext registers an IPersistenceSchemaProvider describing the same logical storage units, fields, indexes, JSON payload limits, and uniqueness constraints under the Elsa.UserTasks schema namespace. Its document-store adapter uses compare-and-swap document versions for Revision and supports equivalent indexed queries for safe fields.
The VNext adapter must preserve:
- tenant/materialization/bookmark uniqueness;
- candidate, exclusion, and snapshot membership semantics;
- append-only events;
- operation idempotency and stale-operation scans;
- encrypted invitation delivery outbox and hashed invitation/session tokens;
- terminal-only retention purge; and
- the same protected-data and authorization boundaries.
No provider may make Elsa.Identity a prerequisite or silently weaken tenant isolation.
Retention and cleanup
- Terminal task and event retention is indefinite by default.
- An opt-in purge job deletes only terminal tasks and their associated terminal events after the configured age.
- Open tasks (
Unassigned,Available,Assigned, and transitional states) are never purged. - Expired/revoked invitations, guest sessions, and delivered invitation outbox entries may be purged under separate bounded policies.
- Purge is tenant-scoped, paged, observable, and safe to retry. It must not remove a task with a pending operation, unresolved health issue requiring recovery, or open workflow bookmark.
- Purge never writes raw payloads to logs or audit records.
Verification obligations
The provider test suite must cover:
- empty-database migration and restart/reload round trips;
- indexed tenant/status/assignment/due/workflow/cursor queries without protected payload loading;
- unique materialization and bookmark projection under duplicate delivery;
- concurrent claim, completion, timeout, cancellation, and revision conflicts;
- repeated and divergent operation IDs;
- event ordering and append-only behavior;
- invitation token hash, sibling revocation, delivery retry, guest-session expiry, and no raw-token persistence;
- reconciliation of missing projections, stale operations, and orphaned bookmarks; and
- terminal-only retention cleanup.