Skip to content
bitzorcas
中EN

Guide

Tasks, Candidates, and Todo Queries

Assigned/candidate union, data scope, query fallback, paging cost, detail authorization, idempotent read marker, candidate refresh, and cache.

Last updated

A WorkflowTask is human work attached to an Execution. Eligibility comes from direct assignee or materialized candidate rows. Business-object authorization remains the business module’s responsibility; todo visibility never bypasses that boundary.

1. Task creation

UserTask

ParticipantResolverRegistry

users / role / position / org

WorkflowTask per resolved user

WorkflowTaskCandidate rows

Todo query

Default resolution retains explicit users and expands directory rules through IParticipantProvider. A custom resolver runs first and explicit users are merged.

2. Todo query

Platform QueryWorkflowTodo takes actor and tenant from CurrentUser, clamps PageSize to 1..200, and always sets ApplyDataScope=true.

Application query
var query = new TodoQuery
{
// User and tenant come from CurrentUser, never client substitution.
UserId = currentUserId,
TenantId = currentTenantId,
BusinessType = "MatterIntake",
OfficeId = officeId,
Keyword = keyword,
ApplyDataScope = true,
Page = page <= 0 ? 1 : page,
// Clamp at the boundary even though the fallback can still full-scan.
PageSize = pageSize is <= 0 or > 200 ? 20 : pageSize
};
TodoPage pageResult =
await taskService.QueryTodoAsync(query, cancellationToken);

Without advanced filters, IWorkflowQueryStore performs assigned/candidate union, count, and paging in the database. ApplyDataScope, Keyword, OfficeId, or FlowStates triggers the compatibility path: both task sets are loaded with int.MaxValue, then instances, candidates, permissions, filters, and paging run in memory.

3. Data permission

For each task, TaskService invokes IFlowDataPermissionChecker(View). The platform implementation parses userId as long, creates workflow/task/{taskId}, and requires an explicit allow. Parse failure denies.

FlowDataScopeConfig from DSL is not passed to this checker. Business detail endpoints must authorize the business record separately.

4. Todo count

GetTodoCount uses IWorkflowCache and QueryStore when present. Its contract intentionally omits data scope, allowing over-inclusion so badges do not omit work. A count larger than the visible page can be expected.

Cache keys include tenant, user, and business type. Handoff and candidate changes must invalidate every affected user.

5. Done query

QueryStore provides database paging when available. The fallback loads a page and then loads all done tasks with int.MaxValue to calculate TotalCount. Production standalone hosts need a QueryStore.

Done means the user’s task is complete, not that the process is terminal.

6. Current task-detail defect

GetTaskAsync calls permission with an empty user ID. Platform FlowDataPermissionChecker cannot parse it and denies, so a normally composed detail read returns null.

Boundary to fix
bool allowed = await permissionChecker.HasPermissionAsync(
string.Empty, // No caller parameter is available.
taskId,
task.TenantId,
FlowDataScopeCheck.View,
cancellationToken);

Add trusted userId to the port or inject a trusted current-user abstraction, then add IDOR tests.

7. Mark read

MarkRead permits assignee or candidate. Repeated calls return success without advancing Version. The first update uses task.Version and maps DBConcurrencyException to Task.ConcurrencyConflict, then invalidates assignee and candidate caches.

Idempotent read marker
// A repeat call succeeds without advancing Version again.
Result result = await taskService.MarkReadAsync(
taskId,
currentUserId,
cancellationToken);

Provider-specific concurrency exception parity still needs testing.

8. Candidate refresh

RefreshCandidates loads task, instance, and participant rule, deletes old candidates, then expands new rows. Delete and rebuild are not explicitly transactional. Rule resolution reads versions[0] instead of the instance’s DefinitionId, so it can apply the wrong version.

RefreshCandidatesByRole runs tasks sequentially, ignores per-task Result, and returns success. It has no checkpoint, failure list, or rate control.

9. Return-model limits

TodoItem includes task, instance, business, definition, node, state, assignee, priority, and due fields. DefinitionName currently receives DefinitionKey rather than the actual definition display name.

10. Performance target

Compile DataScope, keyword, office, and flow-state filters into the QueryStore provider so tenant filter, assigned/candidate union, authorization scope, stable sort, count, and page happen together. Never page before filtering and never fetch all rows.

Benchmark 10K, 100K, and 1M pending tasks, role churn, hotspot users, and tenant concurrency. Record P50/P95, rows scanned, allocations, and cache hit rate.

11. Operational acceptance

  • Confirm a direct assignee and a role candidate see the same task only once.
  • Verify every query is tenant-scoped before candidate union and paging.
  • Compare badge count with the visible list and document the intentional difference.
  • Record database rows scanned for both the fast path and every advanced filter.
  • Rotate a role while queries are active and reconcile candidate refresh failures.
  • Transfer work and prove old and new assignee cache keys are both invalidated.
  • Exercise detail reads with owner, candidate, unrelated user, and cross-tenant user.
  • Race MarkRead, Complete, Transfer, and candidate refresh under both ORM adapters.
  • Reject deployment when the tenant’s expected task volume exceeds the measured budget.

12. Source checks

Terminal window
rg -n "HasAdvancedFilters|int.MaxValue|GetTaskAsync|string.Empty" src/Framework/BitzOrcas.Workflow/BitzOrcas.Workflow.Engine/Services/TaskService.cs
rg -n "RefreshCandidates|ResolveParticipantRule|versions\[0\]" src/Framework/BitzOrcas.Workflow -g '*.cs'

Next: history and analytics

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%