- gRPC auth service for token validation - Value comparator system (string, numeric, boolean, datetime, collection) - Condition evaluator with strategy chain - Form definition and data improvements - Workflow instance/task endpoints updated - Seed data and EF design-time factory - Test coverage for comparators and handlers
4.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Cross-repo rules — see
/Users/wen/project/rag/CLAUDE.mdfor full workspace conventions. Key shared rules:
- File-scoped namespaces, primary constructor DI,
recordfor DTOs/Commands/Queries- Entity:
BaseEntity + IAuditable/ISoftDelete/IFullAuditmarker interfaces- EF Config: snake_case table,
ValueGeneratedNever(),IEntityTypeConfiguration<T>- Endpoint:
FastEndpoint<TReq, TRes>+Permissions("resource:action")- Middleware:
Cors → GlobalException → ApiResponse → Auth → AuthZ → FastEndpoints- Response:
{ code: 0, data, message: "ok" }(auto-wrapped)- JWT shared key:
RagJwtSecretKey2026MustBeAtLeast32CharsLong!- Other repos:
rag-backend(5211),im-system(5212),work-flow,file-system(8080 Go),rag-frontend(5666 Vue)
Build & Run
dotnet build
cd src/Workflow.Api && dotnet run
cd src/Workflow.Api && dotnet run -- --seed
cd src/Workflow.Infrastructure && dotnet ef migrations add <Name> --startup-project ../Workflow.Api
dotnet test # xUnit + FluentAssertions + Moq
Architecture
.NET 10, four projects: Api → Application → Infrastructure → Domain. Same ABP+FastEndpoints+MediatR stack as rag-backend, but with workflow-specific engine patterns.
Key Difference: Domain-Driven State Machines
The core domain logic lives in stateless state machines, not in entities. Entities are anemic data holders.
Three state machines in Domain/StateMachine/:
InstanceStateMachine— Running ↔ Suspended → Completed/TerminatedTaskStateMachine— Pending → Approved/Rejected/Transferred/DelegatedTokenStateMachine— Active → Consumed/Terminated
Pattern: tuple switch with context objects:
return (currentState, operation) switch
{
(InstanceStatus.Running, InstanceOperation.Suspend) => InstanceStatus.Suspended,
(TaskStatus.Pending, TaskOperation.Approve) => TaskStatus.Approved,
_ => throw new InvalidStateTransitionException(...)
};
ProcessEngine — Token Propagation
Application/Engine/ProcessEngine.cs — routes tokens through 7 node types:
| Node Type | Behavior |
|---|---|
| Start | Consumes token, creates new token on first outgoing edge |
| End | Consumes token, checks if all instance tokens consumed → Complete |
| Approval | Creates WorkflowTask, token stays Active until human action |
| Cc | Creates notification tasks, immediately propagates token (fire-and-forget) |
| Condition | Evaluates edge conditions (first-match-wins), creates one new token |
| Parallel | Fork: one token per outgoing edge. Join: waits for all incoming tokens |
| SubProcess | Creates child instance, parent token waits |
Condition Evaluation — Strategy Chain
ConditionEvaluator
→ IValueComparatorRegistry (chain-of-responsibility)
→ NumericComparator (==, !=, >, <, >=, <=)
→ DateTimeComparator (==, !=, >, <, >=, <=)
→ BooleanComparator (==, !=)
→ CollectionComparator (in)
→ StringComparator (==, !=, contains, startsWith, endsWith, isEmpty)
Two condition syntaxes: simple text ("amount > 5000", regex-parsed) and JSON tree ({"and": [...], "or": [...]}).
Node Action Hooks
public interface INodeAction { Task ExecuteAsync(NodeActionContext context); }
Config specifies "onEnter": "send-notification". Engine resolves from DI. Hook failures are swallowed — never block token propagation.
Testing
tests/Workflow.Tests/
Condition/ — 6 test files (one per comparator + evaluator + registry)
Engine/ — ProcessEngineTests (~775 lines, #region-grouped)
Form/ — Form definition + data tests with fixture
Handlers/ — CQRS handler tests with InMemory DB
StateMachine/ — One test file per state machine (pure unit tests)
Stack: xUnit + FluentAssertions + Moq + EF Core InMemory.
Handler test pattern:
await using var db = new WorkflowDbContext(new DbContextOptionsBuilder<WorkflowDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
var handler = new CreateXxxCommandHandler(db);
var result = await handler.Handle(command, CancellationToken.None);
result.Should().NotBeNull();
Code Patterns
Same conventions as rag-backend (see workspace CLAUDE.md), plus:
- Table prefix:
wf_(e.g.,wf_workflow_definitions,wf_workflow_tasks) - Request DTOs co-located in endpoint files (not separate DTO files)
- Endpoints use
AllowAnonymous()on all routes (auth via gRPC to rag-backend, not local) - Entity audit fields are explicit (no base audit class):
Guid CreatedBy,DateTime CreatedAt AuditInterceptorregistered viaOnConfiguringoverride, not DIAddInterceptors