Skip to content

Stratiform-Group/workflowengine

Repository files navigation

WGC.WorkflowEngine

A lightweight, durable workflow engine for .NET 8. Define state-machine workflows with a fluent API, persist them with Entity Framework Core, and run them with built-in retry logic, timeout handling, and human override capabilities.

Features

  • Fluent Workflow Definition - Define states, transitions, and steps in clean C# code
  • Durable State Persistence - EF Core code-first with full migration support
  • Database Agnostic - SQL Server, PostgreSQL, MongoDB, and Redis providers included; extensible to any EF Core provider
  • Event-Driven Transitions - Trigger state changes from external events
  • Time-Based Transitions - Automatic timeouts that move workflows to a target state
  • Transition Guards - Conditional transitions with custom guard logic
  • Workflow Hooks - React to state changes, step completion, and workflow lifecycle events
  • Step Execution - Run custom logic on state entry/exit with dependency injection
  • Retry with Exponential Backoff - Failed steps are automatically retried
  • Concurrency Safe - Atomic step claiming prevents duplicate execution by parallel processors
  • Unit of Work - All state transitions are committed in a single atomic transaction
  • Transactional Outbox - Reliable outbound messaging pattern built in
  • Human Overrides - Skip steps, retry failed steps, force state transitions, suspend/resume workflows
  • Query Builder - Fluent query API for dashboards with filtering, sorting, and pagination
  • Health Checks - Monitor workflow engine health (stuck steps, failed steps, connectivity)
  • Full Audit Trail - Every action is logged for traceability
  • Dead Letter Queue - Failed messages are captured for manual review

Installation

# For SQL Server
dotnet add package WGC.WorkflowEngine.SqlServer

# For PostgreSQL
dotnet add package WGC.WorkflowEngine.PostgreSQL

# For MongoDB
dotnet add package WGC.WorkflowEngine.MongoDB

# For Redis
dotnet add package WGC.WorkflowEngine.Redis

Quick Start

1. Define a Workflow

using WGC.WorkflowEngine.Core.Builders;
using WGC.WorkflowEngine.Core.Interfaces;

public class OrderWorkflow : WorkflowDefinition
{
    public override string Name => "OrderProcessing";

    public override void Define(IWorkflowBuilder builder)
    {
        builder.InitialState("Pending");

        builder.State("Pending")
            .On("PaymentReceived").TransitionTo("Processing")
            .On("Cancelled").TransitionTo("Cancelled")
            .OnTimeout(TimeSpan.FromDays(3), "Expired")
            .OnEnter(step => step.Execute<SendConfirmationEmailStep>());

        builder.State("Processing")
            .On("Shipped").TransitionTo("Shipped")
            .OnEnter(step => step.Execute<NotifyWarehouseStep>());

        builder.State("Shipped")
            .On("Delivered").TransitionTo("Completed")
            .OnEnter(step => step.Execute<SendTrackingEmailStep>());

        builder.State("Completed").AsFinal();
        builder.State("Cancelled").AsFinal().AllowReopen("Pending");
        builder.State("Expired").AsFinal();
    }
}

2. Implement Step Handlers

using WGC.WorkflowEngine.Core.Interfaces;

public class SendConfirmationEmailStep : IStepHandler
{
    private readonly IEmailService _emailService;

    public SendConfirmationEmailStep(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task<StepResult> HandleAsync(StepExecutionContext context, CancellationToken ct)
    {
        var orderId = context.WorkflowInstance.CorrelationId;

        await _emailService.SendAsync(orderId, "Your order has been received!");

        return StepResult.Completed();
    }
}

3. Register Services

// Program.cs
builder.Services
    .AddWorkflowEngine(options =>
    {
        options.AddWorkflow<OrderWorkflow>();
        options.AddStepHandler<SendConfirmationEmailStep>();
        options.AddStepHandler<NotifyWarehouseStep>();
        options.AddStepHandler<SendTrackingEmailStep>();
    })
    .UseWorkflowSqlServer(connectionString);
    // or .UseWorkflowPostgreSql(connectionString);

4. Apply Migrations

dotnet ef migrations add InitWorkflow --context WorkflowDbContext
dotnet ef database update --context WorkflowDbContext

5. Use the Engine

public class OrderController : ControllerBase
{
    private readonly IWorkflowEngine _engine;

    public OrderController(IWorkflowEngine engine) => _engine = engine;

    [HttpPost]
    public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
    {
        // Start a new workflow
        var instance = await _engine.StartAsync(
            workflowName: "OrderProcessing",
            correlationId: request.OrderId,
            data: new { request.CustomerEmail, request.Items },
            triggeredBy: User.Identity?.Name);

        return Ok(new { WorkflowId = instance.Id });
    }

    [HttpPost("{id}/pay")]
    public async Task<IActionResult> ProcessPayment(Guid id)
    {
        // Trigger a state transition
        var instance = await _engine.TriggerEventAsync(id, "PaymentReceived");
        return Ok(new { State = instance.CurrentState });
    }
}

Transition Guards

Add conditions to transitions that must pass before a state change is allowed:

// Define a guard
public class HasQuoteGuard : ITransitionGuard
{
    public Task<GuardResult> EvaluateAsync(TransitionContext context, CancellationToken ct)
    {
        var hasQuote = context.WorkflowData.ContainsKey("QuoteId");
        return Task.FromResult(hasQuote
            ? GuardResult.Allow()
            : GuardResult.Deny("No quote has been created yet."));
    }
}

// Use in workflow definition
builder.State("Opportunity")
    .On("QuoteCreated")
        .WithGuard<HasQuoteGuard>()
        .TransitionTo("Quote");

// Register the guard
options.AddGuard<HasQuoteGuard>();

Workflow Hooks

React to workflow lifecycle events (send emails, trigger integrations, update external systems):

public class SalesNotificationHook : IWorkflowHook
{
    private readonly IEmailService _email;

    public SalesNotificationHook(IEmailService email) => _email = email;

    public async Task OnStateEnteredAsync(WorkflowInstance instance, string state, CancellationToken ct)
    {
        if (state == "Lead")
            await _email.SendAsync("sales@company.com", $"New lead: {instance.CorrelationId}");
    }

    public async Task OnWorkflowCompletedAsync(WorkflowInstance instance, CancellationToken ct)
    {
        await _email.SendAsync("manager@company.com", $"Workflow {instance.Id} completed.");
    }
}

// Register
options.AddHook<SalesNotificationHook>();

Available hook methods (all optional):

  • OnWorkflowStartedAsync - Workflow created
  • OnStateEnteredAsync - After transitioning to a new state
  • OnStateExitingAsync - Before leaving the current state
  • OnWorkflowCompletedAsync - Workflow reached a final state
  • OnStepCompletedAsync - A step finished successfully
  • OnStepFailedAsync - A step permanently failed (no more retries)

Query Builder (Dashboards)

Query workflow instances with a fluent API for dashboards and reporting:

// Inject IWorkflowQueryService
private readonly IWorkflowQueryService _queryService;

// Paginated, filtered query
var result = await _queryService.QueryAsync(q => q
    .ForWorkflow("SalesLifecycle")
    .InStatus(WorkflowStatus.Active)
    .InState("Quote")
    .CreatedAfter(DateTime.UtcNow.AddDays(-30))
    .OrderBy(WorkflowSortField.UpdatedAt, descending: true)
    .Page(1, 20));

// result.Items         -> List<WorkflowInstance>
// result.TotalCount    -> Total matching records
// result.TotalPages    -> Total pages
// result.HasNextPage   -> Pagination helper

// Dashboard summary (counts by status and state)
var summary = await _queryService.GetSummaryAsync("SalesLifecycle");
// summary.CountByStatus  -> { "Active": 42, "Completed": 100, ... }
// summary.CountByState   -> { "Contact": 10, "Lead": 15, "Quote": 8, ... }

Health Checks

Monitor the workflow engine in production:

// Program.cs
builder.Services.AddHealthChecks()
    .AddWorkflowEngineHealthCheck(options =>
    {
        options.StuckStepThreshold = TimeSpan.FromMinutes(30);
        options.MaxStuckStepsBeforeUnhealthy = 5;
        options.MaxFailedStepsBeforeDegraded = 10;
    });

Reports:

  • Healthy - Engine operating normally
  • Degraded - Stuck or permanently failed steps detected
  • Unhealthy - Database unreachable or too many stuck steps

Background Processing

The engine requires periodic background processing for scheduled steps, retries, and outbox messages. Integrate with your preferred scheduler:

// Example with a hosted service
public class WorkflowBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public WorkflowBackgroundService(IServiceProvider serviceProvider)
        => _serviceProvider = serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();

            var stepProcessor = scope.ServiceProvider.GetRequiredService<IStepProcessor>();
            await stepProcessor.ProcessPendingStepsAsync(ct: ct);
            await stepProcessor.ProcessScheduledStepsAsync(ct: ct);
            await stepProcessor.ProcessRetryStepsAsync(ct: ct);

            var outboxProcessor = scope.ServiceProvider.GetRequiredService<IOutboxProcessor>();
            await outboxProcessor.ProcessBatchAsync(ct: ct);

            await Task.Delay(TimeSpan.FromSeconds(10), ct);
        }
    }
}

Human Override Operations

All operations are logged in the audit trail:

// Force a state transition (bypass normal rules and guards)
await engine.ForceStateAsync(instanceId, "Processing", "Manager approved skip", "admin@company.com");

// Suspend a workflow
await engine.SuspendAsync(instanceId, "Waiting for customer response", "agent@company.com");

// Resume a suspended workflow
await engine.ResumeAsync(instanceId, "agent@company.com");

// Cancel a workflow
await engine.CancelAsync(instanceId, "Customer requested cancellation", "agent@company.com");

// Retry a failed step
await engine.RetryStepAsync(instanceId, stepId, "admin@company.com");

// Skip a step
await engine.SkipStepAsync(instanceId, stepId, "Not applicable", "admin@company.com");

Transactional Outbox

Send reliable messages from your step handlers:

public class NotifyWarehouseStep : IStepHandler
{
    private readonly IOutboxRepository _outbox;

    public NotifyWarehouseStep(IOutboxRepository outbox) => _outbox = outbox;

    public async Task<StepResult> HandleAsync(StepExecutionContext context, CancellationToken ct)
    {
        // Message is persisted in the same transaction as the step update
        await _outbox.CreateAsync(new OutboxMessage
        {
            WorkflowInstanceId = context.WorkflowInstance.Id,
            MessageType = "WarehouseNotification",
            Payload = JsonSerializer.Serialize(new { OrderId = context.WorkflowInstance.CorrelationId }),
            Destination = "warehouse-queue"
        }, ct);

        return StepResult.Completed();
    }
}

Embedded Dashboard

The dashboard provides a web UI for monitoring and managing workflows, accessible at a configurable route prefix (like Hangfire Dashboard).

Features

  • Overview - Status cards (active, completed, faulted, etc.), per-workflow summaries, recent transitions, stuck/failed step alerts
  • Instances - Filterable list with pagination, detail view with data, steps, event log timeline, and action buttons (force state, suspend, resume, cancel)
  • Definitions - View all registered workflows (code-first and database), read-only view for code-first, form-based editor for database definitions
  • Definition Editor - Create/edit workflow definitions stored in the database with states, transitions, guards, timeouts, and steps — all through a visual form

Setup

// Program.cs
builder.Services
    .AddWorkflowEngine(options =>
    {
        options.AddWorkflow<SalesWorkflow>(); // code-first still works
    })
    .UseWorkflowSqlServer(connectionString);

// Add the dashboard (configurable route prefix, optional auth policy)
builder.Services.AddWorkflowDashboard(options =>
{
    options.RoutePrefix = "/workflowengine";     // default
    options.AuthorizationPolicy = "AdminOnly";   // optional
    options.Title = "My Workflow Dashboard";      // optional
});

// Middleware
app.UseStaticFiles(); // required for dashboard CSS/JS
app.MapWorkflowDashboard(); // maps Razor Pages + API endpoints

Dashboard Pages

Page Route Description
Overview {prefix} Health status, counters, workflow summaries, recent transitions
Instances {prefix}/instances Filterable, paginated list of workflow instances
Instance Detail {prefix}/instances/{id} Full detail with data, steps, event log, actions
Definitions {prefix}/definitions List all workflows with source (Code/Database) badge
Definition View {prefix}/definitions/view/{name} Read-only view of states and transitions
Definition Editor {prefix}/definitions/editor/{id?} Form-based editor for database definitions

Database Definitions

Workflow definitions can be created via the dashboard form editor and stored in the database alongside code-first definitions. Both sources coexist — code-first definitions cannot be overwritten from the dashboard.

Dashboard API

All dashboard data is served via a REST API at {prefix}/api/:

Endpoint Method Description
/api/overview GET Dashboard overview data
/api/recent-transitions GET Recent event log entries
/api/instances GET Paginated, filtered instances
/api/instances/{id} GET Instance detail
/api/instances/{id}/force-state POST Force a state transition
/api/instances/{id}/suspend POST Suspend a workflow
/api/instances/{id}/resume POST Resume a suspended workflow
/api/instances/{id}/cancel POST Cancel a workflow
/api/definitions GET All definitions
/api/definitions/{id} GET Get definition by ID
/api/definitions/by-name/{name} GET Get definition config by name
/api/definitions POST Create a new database definition
/api/definitions/{id} PUT Update a database definition
/api/definitions/{id} DELETE Deactivate a database definition
/api/definitions/validate POST Validate a definition without saving

Project Structure

WGC.WorkflowEngine/
├── src/
│   ├── WGC.WorkflowEngine.Core/          # Core engine, entities, interfaces, builders
│   ├── WGC.WorkflowEngine.Persistence/   # EF Core DbContext, configurations, repositories
│   ├── WGC.WorkflowEngine.SqlServer/     # SQL Server provider
│   ├── WGC.WorkflowEngine.PostgreSQL/    # PostgreSQL provider
│   ├── WGC.WorkflowEngine.MongoDB/       # MongoDB provider
│   ├── WGC.WorkflowEngine.Redis/         # Redis provider
│   └── WGC.WorkflowEngine.Dashboard/     # Embedded web dashboard (Razor Class Library)
├── tests/
│   └── WGC.WorkflowEngine.Tests/         # Unit and integration tests
├── samples/
│   ├── Sample.Sql/                       # SQL Server sample app
│   ├── Sample.PostgreSQL/                # PostgreSQL sample app
│   ├── Sample.MongoDB/                   # MongoDB sample app
│   ├── Sample.Redis/                     # Redis sample app
│   ├── Sample.SqlFromNuget/              # Sample using NuGet packages
│   └── Sample.Shared/                    # Shared workflows, seeds, and step handlers
├── WGC.WorkflowEngine.sln
├── README.md
└── LICENSE

Database Tables

The engine creates the following tables (via EF Core migrations):

Table Description
WorkflowInstances Active and completed workflow instances
WorkflowSteps Individual steps with execution status
WorkflowEventLogs Full audit trail of every action
OutboxMessages Transactional outbox for reliable messaging
WorkflowDefinitions Database-stored workflow definitions (JSON)

Requirements

  • .NET 8.0 or later
  • One of: SQL Server 2016+, PostgreSQL 12+, MongoDB 5.0+, or Redis 6.0+

License

MIT License - see LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages