Chd.Workflow
1.0.5
dotnet add package Chd.Workflow --version 1.0.5
NuGet\Install-Package Chd.Workflow -Version 1.0.5
<PackageReference Include="Chd.Workflow" Version="1.0.5" />
<PackageVersion Include="Chd.Workflow" Version="1.0.5" />
<PackageReference Include="Chd.Workflow" />
paket add Chd.Workflow --version 1.0.5
#r "nuget: Chd.Workflow, 1.0.5"
#:package Chd.Workflow@1.0.5
#addin nuget:?package=Chd.Workflow&version=1.0.5
#tool nuget:?package=Chd.Workflow&version=1.0.5
Chd.Workflow 🚀
Chd (Cleverly Handle Difficulty) library helps you cleverly handle difficulty, write code quickly, and keep your application stable. Chd.Workflow is a lightweight, tree-based workflow engine for .NET. Design workflows visually, execute backend actions on transitions, and build dynamic forms — all with minimal code.
📑 Table of Contents
- Features
- Why Chd.Workflow?
- Installation
- Quick Start
- Core Concepts
- Workflow Actions (Backend Methods)
- API Endpoints
- Form Types
- Condition Expressions
- Database Storage
- Custom Repository
- SQL Query Executor
- Frontend Integration
- Migration from Legacy Code
- Full Example
- Related Packages
- License
✨ Features
| Feature | Description |
|---|---|
| 🌳 Tree-based Structure | Hierarchical node design with parent-child relationships |
| 🔄 Transition Actions | Execute backend methods when transitioning between states |
| 📝 Dynamic Forms | Define form fields per node with 15+ field types |
| 🔐 Role-based Access | Control who can execute which transitions |
| 📊 Condition Engine | Route workflows based on data conditions |
| 🏢 Multi-tenant | Built-in tenant isolation |
| 🐘 PostgreSQL Support | Built-in EF Core repository with FluentMigrator |
| 🔷 SQL Server Support | Built-in EF Core repository with FluentMigrator |
| 💾 Pluggable Storage | InMemory default, or implement custom IWorkflowRepository |
| 🌐 REST API | Auto-registered minimal API endpoints |
| ⚡ Action Caching | Assembly scanned once at startup, actions cached |
| 🧩 Controller Method Actions | Register legacy controller/service methods as workflow actions via attribute |
| 🔒 Runtime SQL Isolation | optionsQuery executes on backend; runtime state returns resolved options without leaking raw SQL |
💡 Why Chd.Workflow?
Before (Traditional Approach)
// Scattered if-else statements across your codebase
public void ProcessRequest(Request request, string action)
{
if (request.Status == "Draft")
{
if (action == "submit")
{
if (request.Amount > 10000)
{
request.Status = "PendingGMApproval";
SendNotificationToGM();
}
else
{
request.Status = "PendingManagerApproval";
SendNotificationToManager();
}
SaveToDatabase();
}
}
else if (request.Status == "PendingManagerApproval")
{
if (action == "approve")
{
request.Status = "Approved";
UpdateBudget();
SendApprovalEmail();
// ... 50 more lines
}
else if (action == "reject")
{
// ... 30 more lines
}
}
// ... hundreds more lines of if-else
}
After (With Chd.Workflow)
// 1. Define workflow visually in Designer (or JSON)
// 2. Create simple action classes
[WorkflowAction("approve-request", DisplayName = "Approve Request")]
public class ApproveRequestAction : IWorkflowAction
{
private readonly IDbContext _db;
private readonly IEmailService _email;
public ApproveRequestAction(IDbContext db, IEmailService email)
{
_db = db;
_email = email;
}
public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
{
var request = await _db.Requests.FindAsync(context.FormData["requestId"]);
request.Status = "Approved";
request.ApprovedBy = context.UserId;
await _db.SaveChangesAsync();
await _email.SendApprovalEmail(request);
return WorkflowActionResult.Ok();
}
}
// 3. Bind action to transition in Designer UI - Done!
Benefits
| Aspect | Before | After |
|---|---|---|
| Workflow Changes | Modify code, deploy | Update in Designer, no deploy |
| New Approvals | Write new if-else | Add node + transition in UI |
| Debugging | Search through if-else | Check workflow instance history |
| Testing | Complex integration tests | Unit test each action |
| Onboarding | Read hundreds of lines | View visual workflow |
📦 Installation
dotnet add package Chd.Workflow
Supported Frameworks:
- .NET 8.0
- .NET 9.0
🚀 Quick Start
1. Register Services
// Program.cs
using Chd.Workflow.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Register workflow actions from your assembly (FIRST)
builder.Services.AddWorkflowActionsFromCallingAssembly();
// Add workflow services
builder.Services.AddWorkflow();
var app = builder.Build();
// Map workflow API endpoints
app.UseWorkflow();
app.Run();
2. Create a Workflow Action
using Chd.Workflow.Attributes;
using Chd.Workflow.Interfaces;
[WorkflowAction("approve-leave", DisplayName = "Approve Leave", Category = "HR")]
public class ApproveLeaveAction : IWorkflowAction
{
public Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
{
Console.WriteLine($"Leave approved by {context.UserId}");
return Task.FromResult(WorkflowActionResult.Ok(new Dictionary<string, object?>
{
["approvedAt"] = DateTime.UtcNow,
["approvedBy"] = context.UserId
}));
}
}
3. Use the API
# Get available actions (for Designer dropdown)
GET /api/workflow/actions
# Create workflow instance
POST /api/workflow/instances
{
"definitionId": "leave-request",
"data": { "employeeId": "123", "days": 5 }
}
# Execute transition
POST /api/workflow/instances/{id}/transition
{
"action": "approve",
"data": { "comment": "Approved!" }
}
# Get current state
GET /api/workflow/instances/{id}/state
📚 Core Concepts
Workflow Definition
A workflow definition is a template that describes the flow structure.
public class WorkflowDefinition
{
public string Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public int Version { get; set; }
public bool IsActive { get; set; }
public Node RootNode { get; set; } // Tree structure starts here
public string? TenantId { get; set; } // Multi-tenant support
}
Nodes
Nodes represent states/steps in your workflow.
public class Node
{
public string Id { get; set; }
public string Name { get; set; }
public string? Title { get; set; }
public NodeType Type { get; set; } // Start, Task, Decision, End
public List<Node> Children { get; set; } // Child nodes (tree structure)
public List<Field> Fields { get; set; } // Form fields for this node
public List<Transition> Transitions { get; set; }
public FormType FormType { get; set; } // Dynamic, External, Component, None
public string? FormUrl { get; set; } // For external forms
public string? DataQuery { get; set; } // SQL to load form data
}
Node Types:
| Type | Description |
|------|-------------|
| Start | Entry point of workflow |
| Task | User action required |
| Decision | Automatic routing based on conditions |
| End | Terminal state |
Transitions
Transitions define how to move from one node to another.
public class Transition
{
public string Id { get; set; }
public string Action { get; set; } // "approve", "reject", "submit"
public string? Label { get; set; } // Button label
public string TargetNodeId { get; set; } // Where to go
public string? Condition { get; set; } // When this transition is valid
public string? ActionHandler { get; set; } // Backend method to execute
public List<string> AllowedRoles { get; set; }
public bool RequiresComment { get; set; }
}
Fields (Dynamic Forms)
Define form fields that appear when a node is active.
public class Field
{
public string Name { get; set; }
public string? Label { get; set; }
public FieldType Type { get; set; } // Text, Number, Date, Dropdown, etc.
public bool Required { get; set; }
public string? Placeholder { get; set; }
public string? ValidationPattern { get; set; }
public string? OptionsQuery { get; set; } // SQL for dropdown options
public List<FieldOption>? Options { get; set; }
}
Field Types:
Text, TextArea, Number, Decimal, Date, DateTime, Time, Checkbox, Radio, Dropdown, MultiSelect, File, Image, RichText, Hidden
⚙️ Workflow Actions (Backend Methods)
The core feature of Chd.Workflow is binding backend methods to transitions.
Creating an Action
using Chd.Workflow.Attributes;
using Chd.Workflow.Interfaces;
[WorkflowAction("send-notification", DisplayName = "Send Notification", Category = "Communication")]
[WorkflowAuthorize(Roles = "Admin,Manager")] // Optional: Role restriction
public class SendNotificationAction : IWorkflowAction
{
private readonly INotificationService _notifications;
private readonly ILogger<SendNotificationAction> _logger;
// Constructor injection works!
public SendNotificationAction(
INotificationService notifications,
ILogger<SendNotificationAction> logger)
{
_notifications = notifications;
_logger = logger;
}
public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
{
try
{
// Access form data
var message = context.FormData["message"]?.ToString();
var recipientId = context.FormData["recipientId"]?.ToString();
// Access workflow context
_logger.LogInformation(
"Sending notification from node {Node} by user {User}",
context.CurrentNode.Title,
context.UserId);
await _notifications.SendAsync(recipientId, message);
// Return success with optional output data
return WorkflowActionResult.Ok(new Dictionary<string, object?>
{
["notificationSentAt"] = DateTime.UtcNow,
["notificationId"] = Guid.NewGuid().ToString()
});
}
catch (Exception ex)
{
// Return failure - transition will NOT happen
return WorkflowActionResult.Fail($"Failed to send notification: {ex.Message}");
}
}
}
Using Existing Controller/Service Methods
For legacy integrations, you can expose existing methods as workflow actions without implementing IWorkflowAction directly.
using Chd.Workflow.Attributes;
using Chd.Workflow.Interfaces;
public class LegacyBudgetController
{
[WorkflowControllerAction(
"legacy-budget-approve",
DisplayName = "Legacy Budget Approve",
Category = "Controller Actions",
InputType = typeof(BudgetApproveRequest))]
public async Task<WorkflowActionResult> ApproveFromLegacy(WorkflowActionContext context)
{
// Reuse existing business flow
return WorkflowActionResult.Ok();
}
}
Method signature rules:
- Must accept exactly one parameter:
WorkflowActionContext - Must return
WorkflowActionResultorTask<WorkflowActionResult>
WorkflowActionContext
The context provides access to all workflow data:
public class WorkflowActionContext
{
public WorkflowInstance Instance { get; } // Current instance
public WorkflowDefinition Definition { get; } // Workflow definition
public Node CurrentNode { get; } // Current node
public Node? TargetNode { get; } // Node we're transitioning to
public Transition? Transition { get; } // The transition being executed
public IDictionary<string, object?> FormData { get; } // Submitted form data
public string? UserId { get; } // Current user
public IReadOnlyList<string> UserRoles { get; }
public string? Comment { get; } // Optional comment
public IServiceProvider ServiceProvider { get; }
public CancellationToken CancellationToken { get; }
}
Registering Actions
// Program.cs
// Option 1: Register from calling assembly
builder.Services.AddWorkflowActionsFromCallingAssembly();
// Option 2: Register from specific assemblies
builder.Services.AddWorkflowActions(
typeof(ApproveLeaveAction).Assembly,
typeof(AnotherAction).Assembly
);
// Then add workflow services
builder.Services.AddWorkflow();
Binding Actions to Transitions
Actions are bound to transitions via the ActionHandler property:
{
"action": "approve",
"targetNodeId": "approved-node",
"actionHandler": "approve-leave"
}
Or via the visual Designer UI (dropdown selection).
Execution Flow
User clicks "Approve" button
↓
POST /api/workflow/instances/{id}/transition
↓
Find transition with action="approve"
↓
Check condition (if any)
↓
ActionHandler = "approve-leave"?
├── YES → Execute ApproveLeaveAction.ExecuteAsync()
│ ├── Success → Continue to target node
│ └── Fail → Return error, stay at current node
└── NO → Just transition to target node
↓
Update instance.CurrentNodeId
↓
Save to repository
↓
Return updated WorkflowState
🌐 API Endpoints
All endpoints are prefixed with /api/workflow by default.
Definitions
| Method | Endpoint | Description |
|---|---|---|
| GET | /definitions |
List all definitions |
| GET | /definitions/{id} |
Get definition by ID |
| POST | /definitions |
Create definition |
| PUT | /definitions/{id} |
Update definition |
| DELETE | /definitions/{id} |
Delete definition |
Instances
| Method | Endpoint | Description |
|---|---|---|
| GET | /instances/{id} |
Get instance by ID |
| GET | /instances/{id}/state |
Get current state with available actions and runtime-resolved field options |
| GET | /instances/{id}/field-options/{fieldName} |
Resolve dynamic options for a specific field |
| POST | /instances |
Create new instance |
| POST | /instances/{id}/transition |
Execute transition |
| POST | /instances/{id}/validate |
Validate form data |
| POST | /instances/{id}/cancel |
Cancel instance |
Actions
| Method | Endpoint | Description |
|---|---|---|
| GET | /actions |
List all registered actions |
| GET | /actions/grouped |
List actions grouped by category |
Custom Route Prefix
app.UseWorkflow("api/v2/workflow"); // Changes prefix
📋 Form Types
Each node can specify how its form should be rendered:
| Type | Description | Use Case |
|---|---|---|
Dynamic |
Auto-generated from Fields list |
Simple forms |
External |
Redirect to FormUrl |
Existing form pages |
Component |
Render named component | Custom React components |
None |
No form displayed | Auto-transitions, info nodes |
External Form Integration
When using External form type, workflow parameters are passed via URL:
/your-form?workflowInstanceId=xxx&workflowNodeId=yyy&workflowActions=approve,reject
Your form can then call the transition API when submitted.
🔀 Condition Expressions
Conditions control which transitions are available.
Supported Operators
| Operator | Example |
|---|---|
==, != |
status == 'pending' |
>, <, >=, <= |
amount > 10000 |
AND, OR |
amount > 10000 AND department == 'IT' |
NOT |
NOT isUrgent |
Supported Functions
| Function | Example |
|---|---|
contains() |
contains(description, 'urgent') |
startsWith() |
startsWith(code, 'PRJ') |
endsWith() |
endsWith(email, '@company.com') |
isEmpty() |
isEmpty(comment) |
isNotEmpty() |
isNotEmpty(approver) |
Example
{
"transitions": [
{
"action": "approve",
"targetNodeId": "gm-approval",
"condition": "amount > 10000 AND department == 'Finance'"
},
{
"action": "approve",
"targetNodeId": "manager-approval",
"condition": "amount <= 10000"
}
]
}
💾 Database Storage
Chd.Workflow supports multiple database providers out of the box:
| Provider | Extension Method | Description |
|---|---|---|
| InMemory | AddWorkflow() |
Default, for development |
| PostgreSQL | AddWorkflowWithPostgreSql() |
Production-ready with EF Core |
| SQL Server | AddWorkflowWithSqlServer() |
Production-ready with EF Core |
PostgreSQL Setup
// Program.cs
using Chd.Workflow.Extensions;
using Chd.Workflow.Migrations;
using FluentMigrator.Runner;
var builder = WebApplication.CreateBuilder(args);
// Register workflow actions FIRST
builder.Services.AddWorkflowActionsFromCallingAssembly();
// Add workflow with PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("PostgreSQL");
builder.Services.AddWorkflowWithPostgreSql(connectionString!);
// Add FluentMigrator for migrations
builder.Services.AddFluentMigratorCore()
.ConfigureRunner(rb => rb
.AddPostgres()
.WithGlobalConnectionString(connectionString)
.ScanIn(typeof(M20240419001_CreateWorkflowTables).Assembly).For.Migrations())
.AddLogging(lb => lb.AddFluentMigratorConsole());
var app = builder.Build();
// Run migrations on startup
using (var scope = app.Services.CreateScope())
{
var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp();
}
app.UseWorkflow();
app.Run();
SQL Server Setup
// Add workflow with SQL Server
var connectionString = builder.Configuration.GetConnectionString("SqlServer");
builder.Services.AddWorkflowWithSqlServer(connectionString!);
// Add FluentMigrator for SQL Server
builder.Services.AddFluentMigratorCore()
.ConfigureRunner(rb => rb
.AddSqlServer()
.WithGlobalConnectionString(connectionString)
.ScanIn(typeof(M20240419001_CreateWorkflowTables).Assembly).For.Migrations())
.AddLogging(lb => lb.AddFluentMigratorConsole());
Database Tables
FluentMigrator automatically creates these tables:
workflow_definitions | Column | Type | Description | |--------|------|-------------| | id | varchar(100) | Primary key | | name | varchar(200) | Workflow name | | description | varchar(1000) | Description | | version | int | Version number | | is_active | boolean | Active flag | | root_node_json | text/nvarchar(max) | Serialized node tree | | tenant_id | varchar(100) | Multi-tenant support | | created_at | datetime | Creation timestamp | | updated_at | datetime | Update timestamp | | created_by | varchar(100) | Creator user |
workflow_instances | Column | Type | Description | |--------|------|-------------| | id | varchar(100) | Primary key | | definition_id | varchar(100) | FK to definitions | | definition_version | int | Version at creation | | current_node_id | varchar(100) | Current state | | status | varchar(50) | Draft/Active/Completed/Cancelled | | data_json | text/nvarchar(max) | Instance data | | history_json | text/nvarchar(max) | Transition history | | tenant_id | varchar(100) | Multi-tenant support | | created_at / updated_at / completed_at | datetime | Timestamps | | created_by / updated_by | varchar(100) | User tracking |
Configuration Pattern
// appsettings.json
{
"DatabaseProvider": "PostgreSQL", // or "SqlServer" or "InMemory"
"ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=workflow;Username=postgres;Password=secret",
"SqlServer": "Server=localhost;Database=Workflow;Trusted_Connection=True;"
}
}
// Program.cs - Provider selection pattern
var dbProvider = builder.Configuration.GetValue<string>("DatabaseProvider") ?? "InMemory";
var connectionString = builder.Configuration.GetConnectionString(dbProvider);
switch (dbProvider.ToLower())
{
case "postgresql":
builder.Services.AddWorkflowWithPostgreSql(connectionString!);
// Add FluentMigrator for PostgreSQL...
break;
case "sqlserver":
builder.Services.AddWorkflowWithSqlServer(connectionString!);
// Add FluentMigrator for SQL Server...
break;
default:
builder.Services.AddWorkflow(); // InMemory
break;
}
🔧 Custom Repository
For custom database implementations, implement IWorkflowRepository:
public interface IWorkflowRepository
{
// Definitions
Task<WorkflowDefinition?> GetDefinitionAsync(string id, CancellationToken ct = default);
Task<List<WorkflowDefinition>> GetAllDefinitionsAsync(string? tenantId = null, CancellationToken ct = default);
Task<WorkflowDefinition> SaveDefinitionAsync(WorkflowDefinition definition, CancellationToken ct = default);
Task DeleteDefinitionAsync(string id, CancellationToken ct = default);
// Instances
Task<WorkflowInstance?> GetInstanceAsync(string id, CancellationToken ct = default);
Task<WorkflowInstance> SaveInstanceAsync(WorkflowInstance instance, CancellationToken ct = default);
}
Register Custom Repository
builder.Services.AddWorkflow<MyCustomRepository>();
🔍 SQL Query Executor
Load form data and dropdown options from database using SQL queries.
Setup
builder.Services.AddScoped<IWorkflowQueryExecutor, EfCoreQueryExecutor<MyDbContext>>();
Node Data Query
{
"id": "review-node",
"dataQuery": "SELECT EmployeeName, Department, ManagerEmail FROM Employees WHERE Id = @employeeId"
}
Field Options Query
{
"name": "department",
"type": "Dropdown",
"optionsQuery": "SELECT Id as value, Name as label FROM Departments WHERE IsActive = 1"
}
Security Features
- ✅ Parameterized queries (SQL injection protection)
- ✅ SELECT-only (INSERT/UPDATE/DELETE blocked)
- ✅ Keyword blacklist (DROP, EXEC, etc.)
- ✅ Query timeout (30 seconds default)
- ✅ Row limit (1000 rows default)
- ✅
JsonElementparameter normalization for DB providers - ✅ Runtime state sanitization (
optionsQueryremoved from client payload)
🎨 Frontend Integration
Chd.Workflow pairs with qp-workflow-react for visual workflow design and execution.
npm install qp-workflow-react
Visual Designer
import { TreeDesigner } from 'qp-workflow-react';
function DesignerPage() {
return (
<TreeDesigner
apiUrl="http://localhost:5035/api/workflow"
locale="en" // or "tr" for Turkish
onSaved={(definition) => console.log('Saved:', definition)}
/>
);
}
Workflow Runner
import { WorkflowRunner } from 'qp-workflow-react';
function WorkflowPage({ instanceId }) {
return (
<WorkflowRunner
apiUrl="http://localhost:5035/api/workflow"
instanceId={instanceId}
locale="en"
theme="dark"
onCompleted={() => console.log('Workflow completed!')}
onTransition={(action) => console.log('Transitioned:', action)}
/>
);
}
Features
| Feature | Description |
|---|---|
| 🌳 TreeDesigner | Visual tree-based workflow designer |
| 🏃 WorkflowRunner | Ready-to-use workflow execution component |
| 🌍 Localization | Built-in English and Turkish support |
| 🎨 Theme Support | Dark and light themes |
| 📋 Form Editor | Design dynamic forms with 15+ field types |
| ⚡ Action Binding | Select backend methods from dropdown |
See the npm package documentation for complete details.
🔄 Migration from Legacy Code
Step 1: Identify Workflow Logic
Find your if-else chains that represent state transitions.
Step 2: Extract Actions
Convert each action block into a IWorkflowAction class:
// Before
if (action == "approve")
{
request.Status = "Approved";
SendEmail();
UpdateBudget();
}
// After
[WorkflowAction("approve-request")]
public class ApproveRequestAction : IWorkflowAction
{
public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext ctx)
{
// Same logic, cleaner structure
var request = await GetRequest(ctx);
request.Status = "Approved";
await SendEmail();
await UpdateBudget();
return WorkflowActionResult.Ok();
}
}
Step 3: Design Workflow
Use TreeDesigner to visually create the workflow and bind actions.
Step 4: Parallel Running
Run both systems side by side during transition:
if (_featureFlags.UseWorkflow("leave-request"))
{
await _workflowEngine.TransitionAsync(instanceId, action, data);
}
else
{
// Legacy code
ProcessRequestLegacy(request, action);
}
Step 5: Remove Legacy Code
Once verified, remove the old if-else chains.
📖 Full Example
Workflow Definition (JSON)
{
"id": "leave-request",
"name": "Leave Request Workflow",
"isActive": true,
"rootNode": {
"id": "start",
"name": "Start",
"type": "Start",
"children": [
{
"id": "submit",
"name": "Submit Request",
"type": "Task",
"formType": "Dynamic",
"fields": [
{ "name": "days", "label": "Days", "type": "Number", "required": true },
{ "name": "reason", "label": "Reason", "type": "TextArea", "required": true }
],
"transitions": [
{
"action": "submit",
"label": "Submit",
"targetNodeId": "review",
"actionHandler": "send-notification"
}
],
"children": [
{
"id": "review",
"name": "Manager Review",
"type": "Task",
"transitions": [
{
"action": "approve",
"label": "Approve",
"targetNodeId": "approved",
"actionHandler": "approve-leave"
},
{
"action": "reject",
"label": "Reject",
"targetNodeId": "rejected",
"actionHandler": "reject-leave",
"requiresComment": true
}
],
"children": [
{ "id": "approved", "name": "Approved", "type": "End" },
{ "id": "rejected", "name": "Rejected", "type": "End" }
]
}
]
}
]
}
}
Actions
[WorkflowAction("approve-leave", DisplayName = "Approve Leave", Category = "HR")]
public class ApproveLeaveAction : IWorkflowAction
{
private readonly ILeaveService _leaveService;
private readonly IEmailService _emailService;
public ApproveLeaveAction(ILeaveService leaveService, IEmailService emailService)
{
_leaveService = leaveService;
_emailService = emailService;
}
public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
{
context.FormData.TryGetValue("leaveId", out var leaveIdObj);
var leaveId = leaveIdObj?.ToString();
if (string.IsNullOrEmpty(leaveId))
return WorkflowActionResult.Fail("Leave ID is required");
await _leaveService.ApproveAsync(leaveId, context.UserId!);
await _emailService.SendApprovalNotification(leaveId);
return WorkflowActionResult.Ok(new Dictionary<string, object?>
{
["approvedAt"] = DateTime.UtcNow,
["approvedBy"] = context.UserId
});
}
}
[WorkflowAction("reject-leave", DisplayName = "Reject Leave", Category = "HR")]
public class RejectLeaveAction : IWorkflowAction
{
public Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
{
if (string.IsNullOrEmpty(context.Comment))
return Task.FromResult(WorkflowActionResult.Fail("Rejection reason is required"));
// Rejection logic...
return Task.FromResult(WorkflowActionResult.Ok());
}
}
API Usage
# Create instance
curl -X POST http://localhost:5035/api/workflow/instances \
-H "Content-Type: application/json" \
-d '{"definitionId": "leave-request", "data": {"employeeId": "123"}}'
# Response: { "id": "instance-456", "currentNodeId": "start", ... }
# Submit form
curl -X POST http://localhost:5035/api/workflow/instances/instance-456/transition \
-H "Content-Type: application/json" \
-d '{"action": "submit", "data": {"days": 5, "reason": "Vacation"}}'
# Approve (manager)
curl -X POST http://localhost:5035/api/workflow/instances/instance-456/transition \
-H "Content-Type: application/json" \
-d '{"action": "approve", "userId": "manager-789"}'
📦 Related Packages
| Package | Platform | Description |
|---|---|---|
| Chd.Workflow | NuGet | .NET Workflow Engine (this package) |
| qp-workflow-react | npm | React components for workflow design & execution |
📄 License
MIT License - see LICENSE file.
🆕 Recent Updates
- Query-builder and guard support improved for richer multi-condition editing flows consumed by the designer.
- Minimal API JSON enum binding hardened to support string enum payloads consistently.
- Save/update flow compatibility improved for definition persistence scenarios.
- Added dynamic action input metadata exposure (
Inputs,ClrType, required inference) for form auto-generation. - Added
InputTypesupport on workflow actions and reflection-based input extraction. - Added
GET /instances/{id}/field-options/{fieldName}for SQL-backed runtime field options. - Added SQL query executor integration path for runtime dropdown/radio/multiselect options.
- Hardened SQL execution path:
id/nameandvalue/labelfallback mapping- blank-label fallback logic
JsonElementparameter normalization for DB provider compatibility
- Runtime payload security hardening:
- state now returns resolved runtime options
- raw
optionsQueryis sanitized from runtime client payloads
- Added controller/service method action support via
[WorkflowControllerAction]with signature validation and runtime adapter execution.
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Made with ❤️ for the .NET community
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- FluentMigrator (>= 5.2.0)
- FluentMigrator.Runner (>= 5.2.0)
- FluentMigrator.Runner.Postgres (>= 5.2.0)
- FluentMigrator.Runner.SqlServer (>= 5.2.0)
- Microsoft.EntityFrameworkCore (>= 8.0.4)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.4)
- Microsoft.EntityFrameworkCore.SqlServer (>= 8.0.4)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 8.0.4)
- System.Text.Json (>= 8.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.