V2 to V3 Migration Guide
Complete migration guide from Elsa Workflows V2 to V3, covering breaking changes, custom activities, workflows, and concepts.
Overview
Elsa Workflows 3 is a complete rewrite of the library with significant architectural improvements for scalability, performance, and extensibility. This guide helps you migrate from Elsa V2 to V3.
Important: There is no automated migration path from V2 to V3. The internal representation of workflow definitions, activities, and properties has changed substantially. Manual recreation of workflows in V3 is required.
What's Changed
Complete rewrite with new execution model
New workflow JSON schema and structure
Updated custom activity implementation
Different NuGet package structure
New database schema
Improved background activity scheduler
Enhanced blocking activities and triggers
Migration Strategy
Given the scope of changes, consider these approaches:
Parallel Operation: Run V2 and V3 systems side-by-side, allowing V2 workflows to complete while starting new workflows in V3
Incremental Migration: Migrate workflows in phases, starting with simpler workflows
Fresh Start: For smaller implementations, recreate workflows from scratch in V3
Migration Checklist
Use this checklist to track your migration progress:
Preparation
Package Migration
Custom Activities
Workflow Definitions
Configuration
Testing
Deployment
Breaking Changes
NuGet Packages
V2 Package Structure
<PackageReference Include="Elsa.Core" Version="2.x.x" />
<PackageReference Include="Elsa.Server.Api" Version="2.x.x" />
<PackageReference Include="Elsa.Designer.Components.Web" Version="2.x.x" />
<PackageReference Include="Elsa.Persistence.EntityFramework.SqlServer" Version="2.x.x" />V3 Package Structure
<!-- Main package includes core components -->
<PackageReference Include="Elsa" Version="3.x.x" />
<!-- Or use individual packages -->
<PackageReference Include="Elsa.Workflows.Core" Version="3.x.x" />
<PackageReference Include="Elsa.Workflows.Management" Version="3.x.x" />
<PackageReference Include="Elsa.Workflows.Runtime" Version="3.x.x" />
<PackageReference Include="Elsa.EntityFrameworkCore.SqlServer" Version="3.x.x" />Key Changes:
Consolidated packages: The
Elsameta-package includesElsa.Api.Common,Elsa.Mediator,Elsa.Workflows.Core,Elsa.Workflows.Management, andElsa.Workflows.RuntimePersistence packages renamed:
Elsa.Persistence.*→Elsa.EntityFrameworkCore.*.NET 8+ required (no longer supports .NET Standard 2.0)
Namespace Changes
Common Namespace Mappings
Elsa.Activities
Elsa.Workflows.Activities
Elsa.Services
Elsa.Workflows.Core / Elsa.Workflows.Runtime
Elsa.Models
Elsa.Workflows.Models
Elsa.Attributes
Elsa.Workflows.Attributes
Example Migration
V2:
using Elsa;
using Elsa.Activities;
using Elsa.Services;
using Elsa.Attributes;V3:
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Attributes;
using Elsa.Extensions;Startup Configuration
V2 Configuration
public void ConfigureServices(IServiceCollection services)
{
services
.AddElsa(elsa => elsa
.UseEntityFrameworkPersistence(ef => ef.UseSqlServer(connectionString))
.AddConsoleActivities()
.AddHttpActivities()
.AddQuartzTemporalActivities()
.AddActivity<MyCustomActivity>()
);
}
public void Configure(IApplicationBuilder app)
{
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}V3 Configuration
var builder = WebApplication.CreateBuilder(args);
// Add Elsa services
builder.Services.AddElsa(elsa =>
{
elsa
.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore(ef =>
ef.UseSqlServer(connectionString));
})
.UseWorkflowRuntime(runtime =>
{
runtime.UseEntityFrameworkCore(ef =>
ef.UseSqlServer(connectionString));
})
.UseIdentity(identity =>
{
identity.UseEntityFrameworkCore(ef =>
ef.UseSqlServer(connectionString));
})
.UseDefaultAuthentication()
.UseHttp()
.AddActivitiesFrom<Program>();
});
var app = builder.Build();
// Use Elsa middleware
app.UseWorkflowsApi();
app.UseWorkflows();
app.Run();Key Changes:
Separate management and runtime configuration
Explicit middleware registration
More granular control over features
Custom Activities Migration
Activity Implementation Changes
V2 Custom Activity
using Elsa;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Services;
using Elsa.Services.Models;
[Activity(
Category = "MyCategory",
Description = "Prints a message to the console",
Outcomes = new[] { OutcomeNames.Done }
)]
public class PrintMessage : Activity
{
[ActivityInput(
Label = "Message",
Hint = "The message to print",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string Message { get; set; } = default!;
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
Console.WriteLine(Message);
return Done();
}
}V3 Custom Activity
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
[Activity("MyCompany", "MyCategory", "Prints a message to the console")]
public class PrintMessage : CodeActivity
{
[Input(Description = "The message to print.")]
public Input<string> Message { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{
var message = Message.Get(context);
Console.WriteLine(message);
}
}Key Differences:
Base Class:
V2:
Activitybase classV3:
ActivityorCodeActivity(CodeActivity auto-completes)
Execute Method:
V2:
OnExecuteorOnExecuteAsyncreturningIActivityExecutionResultV3:
ExecuteAsyncorExecute(for CodeActivity)
Completion:
V2: Return
Done(),Outcome(), etc.V3: Call
await context.CompleteActivityAsync()(or automatic with CodeActivity)
Attributes:
V2:
[Activity]with separate Category parameterV3:
[Activity]with namespace, category, and description
Input Properties:
V2: Simple types with
[ActivityInput]V3: Wrapped in
Input<T>with[Input]
Getting Input Values:
V2: Direct property access
V3:
Message.Get(context)
Activity with Outputs
V2 Activity with Output
[Activity(Category = "Custom", Description = "Generates a random number")]
public class GenerateRandomNumber : Activity
{
[ActivityOutput]
public decimal Result { get; set; }
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
Result = Random.Shared.Next(1, 100);
return Done();
}
}V3 Activity with Output
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
[Activity("MyCompany", "Custom", "Generates a random number")]
public class GenerateRandomNumber : CodeActivity
{
[Output(Description = "The generated random number.")]
public Output<decimal> Result { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{
var randomNumber = Random.Shared.Next(1, 100);
Result.Set(context, randomNumber);
}
}Key Changes:
Outputs wrapped in
Output<T>Use
Result.Set(context, value)instead of direct assignment
Async Activities
V2 Async Activity
public class CallApiActivity : Activity
{
[ActivityInput]
public string Url { get; set; } = default!;
[ActivityOutput]
public string Response { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(
ActivityExecutionContext context)
{
using var client = new HttpClient();
Response = await client.GetStringAsync(Url);
return Done();
}
}V3 Async Activity
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Models;
[Activity("MyCompany", "Http", "Calls an HTTP API")]
public class CallApiActivity : Activity
{
[Input(Description = "The URL to call")]
public Input<string> Url { get; set; } = default!;
[Output(Description = "The API response")]
public Output<string> Response { get; set; } = default!;
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var url = Url.Get(context);
using var client = context.GetRequiredService<IHttpClientFactory>().CreateClient();
var response = await client.GetStringAsync(url);
Response.Set(context, response);
await context.CompleteActivityAsync();
}
}Key Changes:
Method name:
OnExecuteAsync→ExecuteAsyncMust explicitly call
await context.CompleteActivityAsync()Use service location via
context.GetRequiredService<T>()instead of constructor injection
Blocking Activities
V2 Blocking Activity
public class WaitForEvent : Activity
{
[ActivityInput]
public string EventName { get; set; } = default!;
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
return Suspend();
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
return Done();
}
}V3 Blocking Activity
using Elsa.Workflows;
using Elsa.Workflows.Models;
[Activity("MyCompany", "Events", "Waits for an event to occur")]
public class WaitForEvent : Activity
{
[Input(Description = "The name of the event to wait for")]
public Input<string> EventName { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{
var eventName = EventName.Get(context);
context.CreateBookmark(eventName);
}
}Key Changes:
V2: Return
Suspend()to blockV3: Call
context.CreateBookmark(payload)to blockV3: No separate
OnResumemethod; execution continues after bookmark is resumed
Trigger Activities
V2 Trigger Activity
[Trigger(
Category = "Custom",
Description = "Triggers workflow when event occurs"
)]
public class MyEventTrigger : Activity
{
[ActivityInput]
public string EventName { get; set; } = default!;
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
return Suspend();
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
return Done();
}
}V3 Trigger Activity
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Models;
[Activity("MyCompany", "Events", "Triggers workflow when event occurs")]
public class MyEventTrigger : Trigger
{
[Input(Description = "The name of the event")]
public Input<string> EventName { get; set; } = default!;
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// If this trigger started the workflow, complete immediately
if (context.IsTriggerOfWorkflow())
{
await context.CompleteActivityAsync();
return;
}
// Otherwise, create a bookmark to wait
var eventName = EventName.Get(context);
context.CreateBookmark(eventName);
}
protected override object GetTriggerPayload(TriggerIndexingContext context)
{
var eventName = EventName.Get(context.ExpressionExecutionContext);
return eventName;
}
}Key Changes:
V3: Inherit from
Triggerbase classV3: Check
context.IsTriggerOfWorkflow()to handle trigger vs resumptionV3: Implement
GetTriggerPayloadto return the bookmark payload for indexing
Activity Registration
V2 Registration
services.AddElsa(elsa => elsa
.AddActivity<PrintMessage>()
.AddActivity<GenerateRandomNumber>()
.AddActivity<CallApiActivity>()
);
// Or register all from assembly
services.AddElsa(elsa => elsa
.AddActivitiesFrom<Startup>()
);V3 Registration
builder.Services.AddElsa(elsa => elsa
.AddActivity<PrintMessage>()
.AddActivity<GenerateRandomNumber>()
.AddActivity<CallApiActivity>()
);
// Or register all from assembly
builder.Services.AddElsa(elsa => elsa
.AddActivitiesFrom<Program>()
);Note: Registration pattern is similar, but uses the new builder pattern in V3.
Workflow JSON Migration
V2 Workflow JSON Structure
{
"id": "workflow-1",
"version": 1,
"name": "Hello World Workflow",
"description": "A simple workflow",
"isPublished": true,
"activities": [
{
"activityId": "activity-1",
"type": "WriteLine",
"displayName": "Write Line",
"properties": [
{
"name": "Text",
"syntax": "Literal",
"expressions": {
"Literal": "Hello World!"
}
}
]
},
{
"activityId": "activity-2",
"type": "Delay",
"properties": [
{
"name": "Duration",
"syntax": "Literal",
"expressions": {
"Literal": "00:00:01"
}
}
]
},
{
"activityId": "activity-3",
"type": "WriteLine",
"properties": [
{
"name": "Text",
"syntax": "Literal",
"expressions": {
"Literal": "Goodbye!"
}
}
]
}
],
"connections": [
{
"sourceActivityId": "activity-1",
"targetActivityId": "activity-2",
"outcome": "Done"
},
{
"sourceActivityId": "activity-2",
"targetActivityId": "activity-3",
"outcome": "Done"
}
]
}V3 Workflow JSON Structure
{
"id": "HelloWorld-v1",
"definitionId": "HelloWorld",
"name": "Hello World Workflow",
"description": "A simple workflow",
"version": 1,
"isLatest": true,
"isPublished": true,
"root": {
"id": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"activities": [
{
"id": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"name": "WriteLine1",
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello World!"
}
}
},
{
"id": "Delay1",
"type": "Elsa.Delay",
"version": 1,
"name": "Delay1",
"duration": {
"typeName": "TimeSpan",
"expression": {
"type": "Literal",
"value": "00:00:01"
}
}
},
{
"id": "WriteLine2",
"type": "Elsa.WriteLine",
"version": 1,
"name": "WriteLine2",
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Goodbye!"
}
}
}
],
"connections": [
{
"source": {
"activity": "WriteLine1",
"port": "Done"
},
"target": {
"activity": "Delay1",
"port": "In"
}
},
{
"source": {
"activity": "Delay1",
"port": "Done"
},
"target": {
"activity": "WriteLine2",
"port": "In"
}
}
]
}
}Key JSON Schema Changes
Root Container
Activities listed directly
Activities wrapped in root object (Flowchart, Sequence)
Activity Types
Simple names: "WriteLine"
Fully qualified: "Elsa.WriteLine"
Properties
Array of property objects
Direct properties on activity with expression wrappers
Property Structure
properties[].name with expressions object
Direct property with typeName and expression
Connections
sourceActivityId / targetActivityId
Nested source/target objects with activity and port
Metadata
Basic id, version, isPublished
Additional definitionId, isLatest
Migration Steps for JSON
Add Root Container: Wrap all activities in a
rootobject{ "root": { "type": "Elsa.Flowchart", "activities": [ /* your activities */ ], "connections": [ /* your connections */ ] } }Update Activity Type Names: Add
Elsa.prefix to all activity typesWriteLine→Elsa.WriteLineDelay→Elsa.DelayHttpEndpoint→Elsa.HttpEndpointSendHttpRequest→Elsa.Http.SendHttpRequest
Convert Property Structure: Transform property arrays to direct properties
V2:
"properties": [ { "name": "Text", "syntax": "Literal", "expressions": { "Literal": "Hello" } } ]V3:
"text": { "typeName": "String", "expression": { "type": "Literal", "value": "Hello" } }Update Connection Structure: Change to nested source/target format
V2:
{ "sourceActivityId": "activity-1", "targetActivityId": "activity-2", "outcome": "Done" }V3:
{ "source": { "activity": "activity-1", "port": "Done" }, "target": { "activity": "activity-2", "port": "In" } }Add Required Metadata: Include new V3 fields
{ "definitionId": "unique-workflow-id", "isLatest": true }
Expression Type Mapping
Literal
Literal
JavaScript
JavaScript
Liquid
Liquid
Json
Object
Programmatic Workflows
V2 Programmatic Workflow
using Elsa.Activities.Console;
using Elsa.Builders;
public class HelloWorldWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder builder)
{
builder
.StartWith<WriteLine>(x => x.WithText("Hello World!"))
.Then<WriteLine>(x => x.WithText("Goodbye!"));
}
}
// Registration
services.AddElsa(elsa => elsa
.AddWorkflow<HelloWorldWorkflow>()
);V3 Programmatic Workflow
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
public class HelloWorldWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
{
new WriteLine("Hello World!"),
new WriteLine("Goodbye!")
}
};
}
}
// Registration
builder.Services.AddElsa(elsa => elsa
.AddWorkflow<HelloWorldWorkflow>()
);Key Changes:
V2: Implement
IWorkflowand use fluent API withStartWith/ThenV3: Inherit from
WorkflowBaseand build activity tree directlyV3: More explicit activity composition with
builder.RootV3: Activities instantiated directly instead of using extension methods
Workflow with Variables
V2 Workflow with Variables
public class VariableWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder builder)
{
var counter = builder.WithVariable<int>("Counter");
builder
.StartWith<SetVariable>(x => x
.WithVariableName("Counter")
.WithValue(0))
.Then<WriteLine>(x => x
.WithText(context => $"Counter: {counter.Get(context)}"));
}
}V3 Workflow with Variables
public class VariableWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
var counter = builder.WithVariable<int>("Counter", 0);
builder.Root = new Sequence
{
Activities =
{
new SetVariable
{
Variable = counter,
Value = new(10)
},
new WriteLine
{
Text = new(context => $"Counter: {counter.Get(context)}")
}
}
};
}
}Key Changes:
Variable declaration syntax similar but initialization improved
Setting variables uses direct property assignment instead of fluent methods
Accessing variables uses same
counter.Get(context)pattern
Database and Persistence
Schema Changes
The database schema has changed significantly between V2 and V3:
Table names and structures are different
Workflow instance data serialization format changed
No automated migration scripts available
Recommended Approach:
Parallel Databases: Use separate databases for V2 and V3
Let V2 Complete: Allow existing V2 workflows to finish naturally
Fresh Start in V3: Create new workflow instances in V3
V2 Persistence Configuration
services.AddElsa(elsa => elsa
.UseEntityFrameworkPersistence(ef => ef
.UseSqlServer(connectionString)
)
);V3 Persistence Configuration
builder.Services.AddElsa(elsa =>
{
elsa
.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore(ef =>
ef.UseSqlServer(connectionString));
})
.UseWorkflowRuntime(runtime =>
{
runtime.UseEntityFrameworkCore(ef =>
ef.UseSqlServer(connectionString));
});
});Key Changes:
Separate persistence configuration for management and runtime
Different method names:
UseEntityFrameworkPersistence→UseEntityFrameworkCoreMore explicit control over what gets persisted
Supported Providers
SQL Server
Elsa.Persistence.EntityFramework.SqlServer
Elsa.EntityFrameworkCore.SqlServer
PostgreSQL
Elsa.Persistence.EntityFramework.PostgreSql
Elsa.EntityFrameworkCore.PostgreSql
MySQL
Elsa.Persistence.EntityFramework.MySql
Elsa.EntityFrameworkCore.MySql
SQLite
Elsa.Persistence.EntityFramework.Sqlite
Elsa.EntityFrameworkCore.Sqlite
MongoDB
Elsa.Persistence.MongoDb
Elsa.MongoDb
Background Job Scheduler
V2 Job Scheduling
In V2, background activities required external job scheduler like Hangfire:
services.AddElsa(elsa => elsa
.UseQuartzTemporalActivities()
.AddActivity<LongRunningTask>()
);
// Configure Hangfire
services.AddHangfire(config => config
.UseSqlServerStorage(connectionString));V3 Job Scheduling
V3 includes a built-in background activity scheduler using .NET Channels:
// Background scheduling is included by default
builder.Services.AddElsa(elsa =>
{
elsa
.UseWorkflowRuntime(runtime =>
{
// Configure background activity scheduler options if needed
runtime.ConfigureBackgroundActivityScheduler(options =>
{
options.MaxConcurrentActivities = 10;
});
});
});
// Mark activities for background execution
[Activity("MyCompany", "Tasks", "Long running task", Kind = ActivityKind.Job)]
public class LongRunningTask : CodeActivity
{
protected override void Execute(ActivityExecutionContext context)
{
// Long-running work
}
}Key Changes:
Built-in scheduler eliminates need for Hangfire in basic scenarios
Use
ActivityKind.Jobfor background activitiesHangfire still supported for advanced scenarios (failover, distributed execution)
Common Migration Pitfalls
1. Direct JSON Import
Problem: Attempting to import V2 workflow JSON directly into V3.
Solution: V2 and V3 use incompatible JSON schemas. You must:
Export V2 workflows as JSON
Transform the JSON structure to V3 format
Update all activity type names to fully qualified names
Test thoroughly before importing
2. Assuming API Compatibility
Problem: Expecting V2 APIs to work in V3 with minor changes.
Solution: V3 is a complete rewrite. Expect to rewrite:
Custom activities completely
Workflow definitions
Integration points
Extension implementations
3. Database Migration
Problem: Trying to migrate the database schema from V2 to V3.
Solution:
Use separate databases for V2 and V3
Run systems in parallel during transition
Let V2 workflows complete naturally
Start new workflows in V3
4. Constructor Injection in Activities
Problem: Using constructor injection in custom activities.
V2 Pattern (worked but discouraged):
public class MyActivity : Activity
{
private readonly IMyService _service;
public MyActivity(IMyService service)
{
_service = service;
}
}V3 Solution (service location):
public class MyActivity : CodeActivity
{
protected override void Execute(ActivityExecutionContext context)
{
var service = context.GetRequiredService<IMyService>();
// Use service
}
}Reason: Service location makes activity instantiation easier in workflow definitions.
5. Forgetting Activity Completion
Problem: Not calling CompleteActivityAsync in V3 activities.
Incorrect:
public class MyActivity : Activity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Console.WriteLine("Done");
// Missing completion!
}
}Correct:
public class MyActivity : Activity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Console.WriteLine("Done");
await context.CompleteActivityAsync(); // Required!
}
}Or use CodeActivity:
public class MyActivity : CodeActivity
{
protected override void Execute(ActivityExecutionContext context)
{
Console.WriteLine("Done");
// Auto-completes!
}
}6. Input/Output Property Access
Problem: Accessing Input<T> and Output<T> properties directly.
Incorrect:
public class MyActivity : CodeActivity
{
public Input<string> Message { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{
Console.WriteLine(Message); // Wrong!
}
}Correct:
public class MyActivity : CodeActivity
{
public Input<string> Message { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{
var message = Message.Get(context); // Correct!
Console.WriteLine(message);
}
}7. Bookmark Resumption
Problem: Using V2 bookmark/resumption patterns in V3.
V2 Pattern:
// Creating bookmark
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
return Suspend();
}
// Resuming
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
return Done();
}V3 Pattern:
// Creating bookmark
protected override void Execute(ActivityExecutionContext context)
{
context.CreateBookmark("MyBookmark");
}
// No separate resume method - execution continues after bookmark8. Missing Root Container in JSON
Problem: V2-style JSON without root container fails to parse in V3.
Solution: Always wrap activities in a root container:
{
"root": {
"type": "Elsa.Flowchart",
"activities": [ /* activities here */ ]
}
}9. Incorrect Package References
Problem: Mixing V2 and V3 packages.
Solution: Ensure all Elsa packages are V3:
<!-- All packages should be version 3.x -->
<PackageReference Include="Elsa" Version="3.5.1" />
<PackageReference Include="Elsa.EntityFrameworkCore.SqlServer" Version="3.5.1" />10. Trigger Activity Implementation
Problem: Not checking IsTriggerOfWorkflow() in trigger activities.
Incorrect:
public class MyTrigger : Trigger
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
context.CreateBookmark("MyTrigger"); // Always blocks!
}
}Correct:
public class MyTrigger : Trigger
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
if (context.IsTriggerOfWorkflow())
{
await context.CompleteActivityAsync();
return;
}
context.CreateBookmark("MyTrigger");
}
protected override object GetTriggerPayload(TriggerIndexingContext context)
{
return "MyTrigger";
}
}Testing and Validation
Testing Strategy
Unit Test Custom Activities
[Fact] public async Task PrintMessage_Should_Write_To_Console() { // Arrange var services = new ServiceCollection() .AddElsa() .BuildServiceProvider(); var activityExecutor = services.GetRequiredService<IActivityExecutor>(); var activity = new PrintMessage { Message = new Input<string>("Hello") }; // Act var result = await activityExecutor.ExecuteAsync(activity); // Assert Assert.True(result.IsCompleted); }Integration Test Workflows
[Fact] public async Task Workflow_Should_Execute_Successfully() { // Arrange var services = new ServiceCollection() .AddElsa(elsa => elsa.AddWorkflow<HelloWorldWorkflow>()) .BuildServiceProvider(); var workflowRunner = services.GetRequiredService<IWorkflowRunner>(); // Act var result = await workflowRunner.RunAsync<HelloWorldWorkflow>(); // Assert Assert.Equal(WorkflowStatus.Finished, result.Status); }Test JSON Workflows
[Fact] public async Task Should_Load_And_Execute_JSON_Workflow() { var json = File.ReadAllText("workflow.json"); var services = new ServiceCollection() .AddElsa() .BuildServiceProvider(); var serializer = services.GetRequiredService<IActivitySerializer>(); var workflowDefinitionModel = serializer.Deserialize<WorkflowDefinitionModel>(json); var workflowDefinitionMapper = services.GetRequiredService<WorkflowDefinitionMapper>(); var workflow = workflowDefinitionMapper.Map(workflowDefinitionModel); var runner = services.GetRequiredService<IWorkflowRunner>(); var result = await runner.RunAsync(workflow); Assert.Equal(WorkflowStatus.Finished, result.Status); }
Validation Checklist
Migration Timeline Example
Phase 1: Preparation (Week 1-2)
Set up V3 development environment
Inventory all V2 workflows and custom activities
Review V3 documentation
Create proof-of-concept migrations
Phase 2: Custom Activities (Week 3-4)
Rewrite all custom activities for V3
Unit test each activity
Register activities with V3 engine
Phase 3: Workflow Migration (Week 5-8)
Convert workflow definitions to V3
Test each workflow individually
Migrate programmatic workflows
Update integration points
Phase 4: Infrastructure (Week 9-10)
Set up V3 database schema
Configure persistence providers
Deploy V3 application to staging
Configure monitoring and logging
Phase 5: Parallel Operation (Week 11-12)
Run V2 and V3 side-by-side
Monitor both systems
Route new workflows to V3
Allow V2 workflows to complete
Phase 6: Cutover (Week 13)
Verify all V2 workflows completed
Decommission V2 system
Full production deployment of V3
Monitor and optimize
Resources
Documentation
GitHub Resources
Community
Summary
Migrating from Elsa V2 to V3 requires significant effort due to the complete rewrite:
Key Takeaways:
✅ No automated migration path exists
✅ Custom activities must be rewritten
✅ Workflow JSON must be transformed
✅ Database schemas are incompatible
✅ Plan for parallel operation during transition
✅ V3 offers significant improvements in scalability and performance
✅ Built-in background scheduler eliminates need for Hangfire in basic scenarios
✅ More explicit and type-safe API
Migration Approach:
Start with a comprehensive inventory of V2 assets
Rewrite custom activities using V3 patterns
Transform workflow definitions to V3 format
Run V2 and V3 in parallel during transition
Thoroughly test all migrated components
Monitor carefully during cutover
While migration requires significant effort, V3's improvements in architecture, performance, and extensibility make it worthwhile for long-term success.
Last updated