Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5acf066
Add support for activity host registration across workflows
sfmskywalker Dec 26, 2025
e9f4a6c
Remove unused `using` directives across Workflow Management module
sfmskywalker Dec 26, 2025
f14866f
Add support for host method activity registration and description
sfmskywalker Dec 26, 2025
89d09d0
Refactor host method activity execution and cleanup.
sfmskywalker Dec 27, 2025
86a6cd4
Refactor `Bookmark` model to use mutable properties and update XML do…
sfmskywalker Dec 27, 2025
f94d3df
Refactor `BookmarkExecutionContextExtensions` to improve structure, a…
sfmskywalker Dec 27, 2025
e1ed963
Add extensibility for host method parameter binding with pluggable va…
sfmskywalker Dec 27, 2025
a24aae0
Refactor nullable usage and improve bookmark management logic
sfmskywalker Dec 27, 2025
f298f2b
Update src/modules/Elsa.Workflows.Management/Activities/CodeFirst/Hos…
sfmskywalker Dec 27, 2025
a58f1c5
Ensure `CallbackMethodName` is set and skip bookmarks with empty values
sfmskywalker Dec 27, 2025
61cbbb5
Merge remote-tracking branch 'origin/feat/activity-host' into feat/ac…
sfmskywalker Dec 27, 2025
967be8e
Update src/modules/Elsa.Workflows.Management/Features/WorkflowManagem…
sfmskywalker Dec 27, 2025
959b417
Update src/modules/Elsa.Workflows.Management/Contracts/IHostMethodAct…
sfmskywalker Dec 27, 2025
b9bde58
Update src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs
sfmskywalker Dec 27, 2025
cead8c2
Refactor `CodeFirst` namespace to `HostMethod` for improved clarity a…
sfmskywalker Dec 27, 2025
4b7e5e5
Merge remote-tracking branch 'origin/feat/activity-host' into feat/ac…
sfmskywalker Dec 27, 2025
d43f1be
Add `Penguin` activity host with sample activity methods and register…
sfmskywalker Dec 27, 2025
a9f727e
Add `TestHostMethod` activities and corresponding component tests. Re…
sfmskywalker Dec 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/apps/Elsa.Server.Web/ActivityHosts/Penguin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;

namespace Elsa.Server.Web.ActivityHosts;

/// <summary>
/// A sample activity host that represents a penguin.
/// Each of its public methods is an activity that can be executed.
/// Method arguments are interpreted as input values, except for ActivityExecutionContext and CancellationToken.
/// </summary>
/// <param name="logger"></param>
[UsedImplicitly]
public class Penguin(ILogger<Penguin> logger)
{
[Activity(Description = "Wag the penguin")]
public void Wag()
{
logger.LogInformation("The penguin is wagging!");
}

public void Jump()
{
logger.LogInformation("The penguin is jumping!");
}

public void Swim()
{
logger.LogInformation("The penguin is swimming!");
}

public void Eat(string food)
{
logger.LogInformation($"The penguin is eating {food}!");
}

public string Sleep(ActivityExecutionContext context)
{
logger.LogInformation("The penguin is sleeping!");
var bookmark = context.CreateBookmark(Wake);
return context.GenerateBookmarkTriggerToken(bookmark.Id);
}

private ValueTask Wake(ActivityExecutionContext context)
{
logger.LogInformation("The penguin woke up!");
return ValueTask.CompletedTask;
}
}
3 changes: 3 additions & 0 deletions src/apps/Elsa.Server.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using Elsa.Persistence.EFCore.Extensions;
using Elsa.Persistence.EFCore.Modules.Management;
using Elsa.Persistence.EFCore.Modules.Runtime;
using Elsa.Server.Web.Activities;
using Elsa.Server.Web.ActivityHosts;
using Elsa.Server.Web.Filters;
using Elsa.Tenants.AspNetCore;
using Elsa.Tenants.Extensions;
Expand Down Expand Up @@ -44,6 +46,7 @@
{
elsa
.AddActivitiesFrom<Program>()
.AddActivityHost<Penguin>()
.AddWorkflowsFrom<Program>()
.UseIdentity(identity =>
{
Expand Down
3 changes: 2 additions & 1 deletion src/apps/Elsa.Server.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Elsa": "Warning",
"Elsa.Workflows.ActivityRegistry": "Error"
"Elsa.Workflows.ActivityRegistry": "Error",
"Elsa.Server.Web.ActivityHosts": "Information"
}
},
"HostBuilder": {
Expand Down
109 changes: 57 additions & 52 deletions src/modules/Elsa.Http/Extensions/BookmarkExecutionContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,66 +14,71 @@ namespace Elsa.Extensions;
/// </summary>
public static class BookmarkExecutionContextExtensions
{
public static string GenerateBookmarkTriggerUrl(this ActivityExecutionContext context, string bookmarkId, TimeSpan lifetime) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId, lifetime);
public static string GenerateBookmarkTriggerUrl(this ActivityExecutionContext context, string bookmarkId, DateTimeOffset expiresAt) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId, expiresAt);
public static string GenerateBookmarkTriggerUrl(this ActivityExecutionContext context, string bookmarkId) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId);

/// <summary>
/// Generates a URL that can be used to resume a bookmarked workflow.
/// </summary>
/// <param name="context">The expression execution context.</param>
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
/// <param name="lifetime">The lifetime of the bookmark trigger token.</param>
/// <returns>A URL that can be used to resume a bookmarked workflow.</returns>
public static string GenerateBookmarkTriggerUrl(this ExpressionExecutionContext context, string bookmarkId, TimeSpan lifetime)
extension(ActivityExecutionContext context)
{
var token = context.GenerateBookmarkTriggerTokenInternal(bookmarkId, lifetime);
return context.GenerateBookmarkTriggerUrlInternal(token);
public string GenerateBookmarkTriggerUrl(string bookmarkId, TimeSpan lifetime) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId, lifetime);
public string GenerateBookmarkTriggerUrl(string bookmarkId, DateTimeOffset expiresAt) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId, expiresAt);
public string GenerateBookmarkTriggerUrl(string bookmarkId) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId);
public string GenerateBookmarkTriggerToken(string bookmarkId, TimeSpan? lifetime = null, DateTimeOffset? expiresAt = null) => context.ExpressionExecutionContext.GenerateBookmarkTriggerToken(bookmarkId, lifetime, expiresAt);
}

/// <summary>
/// Generates a URL that can be used to resume a bookmarked workflow.
/// </summary>
/// <param name="context">The expression execution context.</param>
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
/// <param name="expiresAt">The expiration date of the bookmark trigger token.</param>
/// <returns>A URL that can be used to resume a bookmarked workflow.</returns>
public static string GenerateBookmarkTriggerUrl(this ExpressionExecutionContext context, string bookmarkId, DateTimeOffset expiresAt)
extension(ExpressionExecutionContext context)
{
var token = context.GenerateBookmarkTriggerTokenInternal(bookmarkId, expiresAt: expiresAt);
return context.GenerateBookmarkTriggerUrlInternal(token);
}
/// <summary>
/// Generates a URL that can be used to resume a bookmarked workflow.
/// </summary>
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
/// <param name="lifetime">The lifetime of the bookmark trigger token.</param>
/// <returns>A URL that can be used to resume a bookmarked workflow.</returns>
public string GenerateBookmarkTriggerUrl(string bookmarkId, TimeSpan lifetime)
{
var token = context.GenerateBookmarkTriggerToken(bookmarkId, lifetime);
return context.GenerateBookmarkTriggerUrlInternal(token);
}

/// <summary>
/// Generates a URL that can be used to resume a bookmarked workflow.
/// </summary>
/// <param name="context">The expression execution context.</param>
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
/// <returns>A URL that can be used to trigger an event.</returns>
public static string GenerateBookmarkTriggerUrl(this ExpressionExecutionContext context, string bookmarkId)
{
var token = context.GenerateBookmarkTriggerTokenInternal(bookmarkId);
return context.GenerateBookmarkTriggerUrlInternal(token);
}
/// <summary>
/// Generates a URL that can be used to resume a bookmarked workflow.
/// </summary>
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
/// <param name="expiresAt">The expiration date of the bookmark trigger token.</param>
/// <returns>A URL that can be used to resume a bookmarked workflow.</returns>
public string GenerateBookmarkTriggerUrl(string bookmarkId, DateTimeOffset expiresAt)
{
var token = context.GenerateBookmarkTriggerToken(bookmarkId, expiresAt: expiresAt);
return context.GenerateBookmarkTriggerUrlInternal(token);
}

private static string GenerateBookmarkTriggerUrlInternal(this ExpressionExecutionContext context, string token)
{
var options = context.GetRequiredService<IOptions<ApiEndpointOptions>>().Value;
var url = $"{options.RoutePrefix}/bookmarks/resume?t={token}";
var absoluteUrlProvider = context.GetRequiredService<IAbsoluteUrlProvider>();
return absoluteUrlProvider.ToAbsoluteUrl(url).ToString();
}
/// <summary>
/// Generates a URL that can be used to resume a bookmarked workflow.
/// </summary>
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
/// <returns>A URL that can be used to trigger an event.</returns>
public string GenerateBookmarkTriggerUrl(string bookmarkId)
{
var token = context.GenerateBookmarkTriggerToken(bookmarkId);
return context.GenerateBookmarkTriggerUrlInternal(token);
}

private static string GenerateBookmarkTriggerTokenInternal(this ExpressionExecutionContext context, string bookmarkId, TimeSpan? lifetime = null, DateTimeOffset? expiresAt = null)
{
var workflowInstanceId = context.GetWorkflowExecutionContext().Id;
var payload = new BookmarkTokenPayload(bookmarkId, workflowInstanceId);
var tokenService = context.GetRequiredService<ITokenService>();
public string GenerateBookmarkTriggerToken(string bookmarkId, TimeSpan? lifetime = null, DateTimeOffset? expiresAt = null)
{
var workflowInstanceId = context.GetWorkflowExecutionContext().Id;
var payload = new BookmarkTokenPayload(bookmarkId, workflowInstanceId);
var tokenService = context.GetRequiredService<ITokenService>();

return lifetime != null
? tokenService.CreateToken(payload, lifetime.Value)
: expiresAt != null
? tokenService.CreateToken(payload, expiresAt.Value)
: tokenService.CreateToken(payload);
return lifetime != null
? tokenService.CreateToken(payload, lifetime.Value)
: expiresAt != null
? tokenService.CreateToken(payload, expiresAt.Value)
: tokenService.CreateToken(payload);
}

private string GenerateBookmarkTriggerUrlInternal(string token)
{
var options = context.GetRequiredService<IOptions<ApiEndpointOptions>>().Value;
var url = $"{options.RoutePrefix}/bookmarks/resume?t={token}";
var absoluteUrlProvider = context.GetRequiredService<IAbsoluteUrlProvider>();
return absoluteUrlProvider.ToAbsoluteUrl(url).ToString();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Elsa.Workflows.Attributes;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method)]
public class ActivityAttribute : Attribute
{
public ActivityAttribute()
Expand Down
86 changes: 61 additions & 25 deletions src/modules/Elsa.Workflows.Core/Models/Bookmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,71 @@ namespace Elsa.Workflows.Models;
/// <summary>
/// A bookmark represents a location in a workflow where the workflow can be resumed at a later time.
/// </summary>
/// <param name="Id">The ID of the bookmark.</param>
/// <param name="Name">The name of the bookmark.</param>
/// <param name="Hash">The hash of the bookmark.</param>
/// <param name="Payload">The data associated with the bookmark.</param>
/// <param name="ActivityId">The ID of the activity associated with the bookmark.</param>
/// <param name="ActivityNodeId">The ID of the activity node associated with the bookmark.</param>
/// <param name="ActivityInstanceId">The ID of the activity instance associated with the bookmark.</param>
/// <param name="CreatedAt">The date and time the bookmark was created.</param>
/// <param name="AutoBurn">Whether or not the bookmark should be automatically burned.</param>
/// <param name="CallbackMethodName">The name of the method on the activity class to invoke when the bookmark is resumed.</param>
/// <param name="AutoComplete">Whether or not the activity should be automatically completed when the bookmark is resumed.</param>
/// <param name="Metadata">Custom properties associated with the bookmark.</param>
public record Bookmark(
string Id,
string Name,
string Hash,
object? Payload,
string ActivityId,
string ActivityNodeId,
string? ActivityInstanceId,
DateTimeOffset CreatedAt,
bool AutoBurn = true,
string? CallbackMethodName = null,
bool AutoComplete = true,
IDictionary<string, string>? Metadata = null)
/// <param name="id">The ID of the bookmark.</param>
/// <param name="name">The name of the bookmark.</param>
/// <param name="hash">The hash of the bookmark.</param>
/// <param name="payload">The data associated with the bookmark.</param>
/// <param name="activityId">The ID of the activity associated with the bookmark.</param>
/// <param name="activityNodeId">The ID of the activity node associated with the bookmark.</param>
/// <param name="activityInstanceId">The ID of the activity instance associated with the bookmark.</param>
/// <param name="createdAt">The date and time the bookmark was created.</param>
/// <param name="autoBurn">Whether or not the bookmark should be automatically burned.</param>
/// <param name="callbackMethodName">The name of the method on the activity class to invoke when the bookmark is resumed.</param>
/// <param name="autoComplete">Whether or not the activity should be automatically completed when the bookmark is resumed.</param>
/// <param name="metadata">Custom properties associated with the bookmark.</param>
public class Bookmark(
string id,
string name,
string hash,
object? payload,
string activityId,
string activityNodeId,
string? activityInstanceId,
DateTimeOffset createdAt,
bool autoBurn = true,
string? callbackMethodName = null,
bool autoComplete = true,
IDictionary<string, string>? metadata = null)
{
/// <inheritdoc />
[JsonConstructor]
public Bookmark() : this("", "", "", null, "", "", "", default, false)
{
}

/// <summary>The ID of the bookmark.</summary>
public string Id { get; set; } = id;

/// <summary>The name of the bookmark.</summary>
public string Name { get; set; } = name;

/// <summary>The hash of the bookmark.</summary>
public string Hash { get; set; } = hash;

/// <summary>The data associated with the bookmark.</summary>
public object? Payload { get; set; } = payload;

/// <summary>The ID of the activity associated with the bookmark.</summary>
public string ActivityId { get; set; } = activityId;

/// <summary>The ID of the activity node associated with the bookmark.</summary>
public string ActivityNodeId { get; set; } = activityNodeId;

/// <summary>The ID of the activity instance associated with the bookmark.</summary>
public string? ActivityInstanceId { get; set; } = activityInstanceId;

/// <summary>The date and time the bookmark was created.</summary>
public DateTimeOffset CreatedAt { get; set; } = createdAt;

/// <summary>Whether the bookmark should be automatically burned.</summary>
public bool AutoBurn { get; set; } = autoBurn;

/// <summary>The name of the method on the activity class to invoke when the bookmark is resumed.</summary>
public string? CallbackMethodName { get; set; } = callbackMethodName;

/// <summary>Whether the activity should be automatically completed when the bookmark is resumed.</summary>
public bool AutoComplete { get; set; } = autoComplete;

/// <summary>Custom properties associated with the bookmark.</summary>
public IDictionary<string, string>? Metadata { get; set; } = metadata;
}
Loading
Loading