Recovery orchestration for WorkflowForge persistence: resume workflows from last checkpoints, with configurable retry and hooks.
$ dotnet add package WorkflowForge.Extensions.Persistence.RecoveryRecovery orchestration extension for WorkflowForge to resume persisted workflows from the last checkpoint with configurable retry and backoff.
This extension has ZERO external dependencies. This means:
Lightweight architecture: Built entirely on WorkflowForge core and Persistence abstractions.
dotnet add package WorkflowForge.Extensions.Persistence.Recovery
Requires: .NET Standard 2.0 or later
using WorkflowForge;
using WorkflowForge.Extensions.Persistence.Abstractions;
using WorkflowForge.Extensions.Persistence.Recovery;
// Your provider must be shared (DB/cache) used also by the runtime persistence middleware
IWorkflowPersistenceProvider provider = new SQLitePersistenceProvider("workflows.db");
var coordinator = new RecoveryCoordinator(provider, new RecoveryPolicy
{
MaxAttempts = 3,
BaseDelay = TimeSpan.FromSeconds(1),
UseExponentialBackoff = true
});
await coordinator.ResumeAsync(
foundryFactory: () => WorkflowForge.CreateFoundry("OrderService"),
workflowFactory: BuildProcessOrderWorkflow,
foundryKey: stableFoundryKey,
workflowKey: stableWorkflowKey);
{
"WorkflowForge": {
"Extensions": {
"Recovery": {
"Enabled": true,
"MaxRetryAttempts": 3,
"BaseDelay": "00:00:01",
"UseExponentialBackoff": true,
"AttemptResume": true,
"LogRecoveryAttempts": true
}
}
}
}using WorkflowForge.Extensions.Persistence.Recovery.Options;
var options = new RecoveryMiddlewareOptions
{
Enabled = true,
MaxRetryAttempts = 3,
BaseDelay = TimeSpan.FromSeconds(1),
UseExponentialBackoff = true,
AttemptResume = true,
LogRecoveryAttempts = true
};
await smith.ForgeWithRecoveryAsync(
workflow,
foundry,
provider,
foundryKey,
workflowKey,
options);using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WorkflowForge.Extensions.Persistence.Recovery;
services.AddRecoveryConfiguration(configuration);
var options = serviceProvider.GetRequiredService<IOptions<RecoveryMiddlewareOptions>>().Value;See Configuration Guide for complete options.
// Resume from last checkpoint
await coordinator.ResumeAsync(
foundryFactory: () => WorkflowForge.CreateFoundry("OrderService"),
workflowFactory: BuildProcessOrderWorkflow,
foundryKey: stableFoundryKey,
workflowKey: stableWorkflowKey);public class MyCatalog : IRecoveryCatalog
{
private readonly IWorkflowPersistenceProvider _provider;
public async Task<IReadOnlyList<WorkflowExecutionSnapshot>> ListPendingAsync(
CancellationToken cancellationToken = default)
{
// Query your storage for pending workflows
return await _database.QueryAsync("SELECT * FROM Workflows WHERE Status = 'Pending'");
}
}
var catalog = new MyCatalog(provider);
int resumedCount = await coordinator.ResumeAllAsync(
foundryFactory: () => WorkflowForge.CreateFoundry("BatchRecovery"),
workflowFactory: BuildWorkflow,
catalog: catalog);Critical: Use stable, deterministic keys for foundry and workflow:
// Good: Stable keys
var foundryKey = Guid.Parse("FIXED-GUID-FOR-ORDER-SERVICE");
var workflowKey = Guid.Parse("FIXED-GUID-FOR-WORKFLOW-DEF");
// Bad: Random keys (won't find saved state)
var foundryKey = Guid.NewGuid(); // Different every time!Keep workflow operation order stable across versions:
// Version 1
workflow
.AddOperation("ValidateOrder")
.AddOperation("ChargePayment")
.Build();
// Version 2 - OK: Append new operations
workflow
.AddOperation("ValidateOrder")
.AddOperation("ChargePayment")
.AddOperation("SendNotification") // New operation
.Build();
// Version 2 - BAD: Reorder existing operations
workflow
.AddOperation("ChargePayment") // Order changed!
.AddOperation("ValidateOrder") // Recovery will break
.Build();Ensure necessary state is in foundry.Properties:
// Good: Store state in properties
foundry.SetProperty("OrderId", orderId);
foundry.SetProperty("CustomerId", customerId);
foundry.SetProperty("PaymentId", paymentId);
// Operations can access this state after recovery
var orderId = foundry.GetPropertyOrDefault<string>("OrderId");NextOperationIndextry
{
await coordinator.ResumeAsync(...);
}
catch (RecoveryException ex)
{
logger.LogError(ex, "Failed to resume workflow after {Attempts} attempts", ex.Attempts);
// Handle permanent failure
}