Introduction
Imagine an application that processes customer orders. After an order is placed, the application needs to verify payment, generate an invoice, update inventory, and send a confirmation email. Each operation may depend on the previous one, and the entire process can take several minutes.
Now consider what happens if the application restarts while processing the order.
If the workflow exists only in application memory, its progress can be lost. The customer may have already paid, but the application might restart before generating the invoice. When the application starts again, it may not know which operations were completed and which still need to run.
This problem is common in applications that handle payment processing, document generation, report creation, data imports, and background jobs.
A simple BackgroundService can execute long-running operations, but it does not automatically preserve their progress when the application stops. To handle restarts safely, the application needs to store workflow state outside its process memory.
In this article, we will build a basic persistent workflow in .NET using a database and a background worker. We will save each step's status, recover unfinished workflows after an application restart, and prevent completed operations from running again unnecessarily.
Why Long-Running Workflows Need Persistence
A normal background task runs inside the application process. It can use memory, dependency injection, and other application services while it is running.
However, application memory does not survive a process restart.
Consider the following implementation:
public class OrderProcessor
{
public async Task ProcessAsync(int orderId)
{
await VerifyPayment(orderId);
await GenerateInvoice(orderId);
await SendConfirmation(orderId);
}
}This code works when everything completes successfully. But suppose the application stops after payment verification and before invoice generation.
When the application starts again, there is no built-in mechanism to know that payment verification was already completed.
Running the entire method again may create duplicate operations. Skipping the order may leave it incomplete.
The solution is to save the workflow's progress in persistent storage.
Instead of storing only the order ID, the application should maintain information such as:
Current workflow status.
Completed steps.
Pending steps.
Failed operations.
Last execution time.
Retry information.
Error details.
With this information, the application can identify unfinished work after restarting.
Understanding Persistent Workflow Execution
A persistent workflow separates business progress from the lifetime of the application process.
The basic execution flow looks like this:
New Order
|
v
Create Workflow Record
|
v
Verify Payment
|
v
Save Step Status
|
v
Generate Invoice
|
v
Save Step Status
|
v
Send Confirmation
|
v
Mark Workflow CompletedIf the application stops during any step, the next application instance can load the workflow record and continue from the appropriate point.
For this example, we will use Entity Framework Core with a relational database.
The database will store workflow information, while a .NET BackgroundService will execute pending workflows.
Creating the Workflow Data Model
Start by creating an entity that represents a workflow.
public enum WorkflowStatus
{
Pending,
Running,
Completed,
Failed
}
public class OrderWorkflow
{
public Guid Id { get; set; }
public int OrderId { get; set; }
public WorkflowStatus Status { get; set; }
public bool PaymentVerified { get; set; }
public bool InvoiceGenerated { get; set; }
public bool ConfirmationSent { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? LastError { get; set; }
}Each Boolean property records whether an individual operation has completed.
For a small workflow, this structure is easy to understand. However, a larger application may have dozens of workflow steps. In that situation, storing every step as a separate property becomes difficult to maintain.
A separate workflow-step table is usually a better choice for complex processes.
For this example, we will keep the model simple so that the recovery process is easier to follow.
Next, create the database context:
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<OrderWorkflow> OrderWorkflows =>
Set<OrderWorkflow>();
}Register the database context in the application:
builder.Services.AddDbContext<ApplicationDbContext>(
options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")
)
);The database connection string should be stored in configuration or a secure secrets provider rather than hardcoded in the application.
Create and apply an EF Core migration to generate the workflow table.
dotnet ef migrations add CreateOrderWorkflow
dotnet ef database updateNow the application has a place to store workflow progress independently of its running process.
Creating a Workflow When an Order Is Received
When an order is created, save its workflow record before starting background processing.
public class OrderWorkflowService
{
private readonly ApplicationDbContext _dbContext;
public OrderWorkflowService(
ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Guid> CreateWorkflowAsync(
int orderId,
CancellationToken cancellationToken)
{
var workflow = new OrderWorkflow
{
Id = Guid.NewGuid(),
OrderId = orderId,
Status = WorkflowStatus.Pending,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_dbContext.OrderWorkflows.Add(workflow);
await _dbContext.SaveChangesAsync(
cancellationToken);
return workflow.Id;
}
}The method saves the workflow before returning its identifier.
This is important because the application should not depend on an in-memory task to remember that an order needs processing.
If the process stops immediately after the record is saved, the background worker can still find the pending workflow after the next startup.
Processing Workflows With BackgroundService
A BackgroundService provides a convenient way to execute background operations inside a .NET application.
Create a worker:
using Microsoft.EntityFrameworkCore;
public class OrderWorkflowWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderWorkflowWorker> _logger;
public OrderWorkflowWorker(
IServiceScopeFactory scopeFactory,
ILogger<OrderWorkflowWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope =
_scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider
.GetRequiredService<ApplicationDbContext>();
var workflow = await dbContext.OrderWorkflows
.Where(x =>
x.Status == WorkflowStatus.Pending)
.OrderBy(x => x.CreatedAt)
.FirstOrDefaultAsync(stoppingToken);
if (workflow is null)
{
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
continue;
}
await ProcessWorkflowAsync(
workflow.Id,
stoppingToken);
}
catch (OperationCanceledException)
when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"An error occurred while processing workflows.");
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}
private async Task ProcessWorkflowAsync(
Guid workflowId,
CancellationToken cancellationToken)
{
using var scope = _scopeFactory.CreateScope();
var processor = scope.ServiceProvider
.GetRequiredService<OrderWorkflowProcessor>();
await processor.ProcessAsync(
workflowId,
cancellationToken);
}
}The worker periodically checks the database for pending workflows.
Notice that the worker creates a dependency injection scope before resolving the database context. This is necessary because BackgroundService is registered as a singleton, while DbContext is normally scoped.
Join the conversation! Your thoughts help the community grow.