diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index af30c1b665..7cfca83b33 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -42,6 +42,7 @@
using Elsa.Tenants.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Api;
+using Elsa.Workflows.CommitStates.Strategies;
using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Compression;
@@ -215,6 +216,11 @@
{
workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing());
workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing());
+ workflows.UseCommitStrategies(strategies =>
+ {
+ strategies.AddStandardStrategies();
+ strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
+ });
})
.UseWorkflowManagement(management =>
{
diff --git a/src/apps/Elsa.Server.Web/SampleWorkflow.cs b/src/apps/Elsa.Server.Web/SampleWorkflow.cs
new file mode 100644
index 0000000000..4f031c47d0
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/SampleWorkflow.cs
@@ -0,0 +1,25 @@
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+using Elsa.Extensions;
+using Elsa.Workflows.CommitStates.Strategies;
+
+namespace Elsa.Server.Web;
+
+public class SampleWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ builder.WorkflowOptions.CommitStrategyName = "Every 10 seconds";
+ builder.Root = new Sequence
+ {
+ Activities =
+ {
+ new WriteLine("Commit before executing").SetCommitStrategy(nameof(ExecutingActivityStrategy)),
+ new WriteLine("Commit after executing").SetCommitStrategy(nameof(ExecutedActivityStrategy)),
+ new WriteLine("Commit before & after executing").SetCommitStrategy(nameof(CommitAlwaysActivityStrategy)),
+ new WriteLine("Commit only based on the workflow commit options").SetCommitStrategy(nameof(DefaultActivityStrategy)),
+ new WriteLine("Never commit the workflow when this activity is about to execute or has executed").SetCommitStrategy(nameof(CommitNeverActivityStrategy)),
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs
index 6ae48eab80..4990ea81a7 100644
--- a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs
+++ b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs
@@ -1,4 +1,5 @@
using System.Text.Json.Nodes;
+using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Extensions;
@@ -182,4 +183,14 @@ public static void SetDisplayText(this JsonObject activity, string? value)
/// Sets a value indicating whether the specified activity can trigger the workflow.
///
public static void SetRunAsynchronously(this JsonObject activity, bool value) => activity.SetProperty(JsonValue.Create(value), "customProperties", "runAsynchronously");
+
+ ///
+ /// Gets the commit state behavior for the specified activity.
+ ///
+ public static string? GetCommitStrategy(this JsonObject activity) => activity.TryGetProperty("customProperties", "commitStrategyName");
+
+ ///
+ /// Sets the commit state behavior for the specified activity.
+ ///
+ public static void SetCommitStrategy(this JsonObject activity, string? name) => activity.SetProperty(JsonValue.Create(name), "customProperties", "commitStrategyName");
}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs
index e8bb1afe3f..3e23b2e455 100644
--- a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs
+++ b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs
@@ -2,6 +2,7 @@
using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Contracts;
using Elsa.Api.Client.Resources.ActivityDescriptors.Contracts;
using Elsa.Api.Client.Resources.ActivityExecutions.Contracts;
+using Elsa.Api.Client.Resources.CommitStrategies.Contracts;
using Elsa.Api.Client.Resources.Features.Contracts;
using Elsa.Api.Client.Resources.Identity.Contracts;
using Elsa.Api.Client.Resources.IncidentStrategies.Contracts;
@@ -73,6 +74,7 @@ public static IServiceCollection AddDefaultApiClients(this IServiceCollection se
services.AddApi(builderOptions);
services.AddApi(builderOptions);
services.AddApi(builderOptions);
+ services.AddApi(builderOptions);
services.AddApi(builderOptions);
services.AddApi(builderOptions);
services.AddApi(builderOptions);
diff --git a/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs
index a1612f9c22..b65fcf394f 100644
--- a/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs
+++ b/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs
@@ -15,52 +15,43 @@ public static bool IsActivity(this JsonObject obj)
{
return obj.ContainsKey("type") && obj.ContainsKey("id") && obj.ContainsKey("version");
}
-
+
///
/// Serializes the specified value to a .
///
/// The value to serialize.
/// The to use.
/// A representing the specified value.
- public static JsonNode SerializeToNode(this object value, JsonSerializerOptions? options = default)
+ public static JsonNode SerializeToNode(this object value, JsonSerializerOptions? options = null)
{
- options ??= new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
-
+ options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
return JsonSerializer.SerializeToNode(value, options)!;
}
-
+
///
/// Serializes the specified value to a .
///
/// The value to serialize.
/// The to use.
/// A representing the specified value.
- public static JsonArray SerializeToArray(this object value, JsonSerializerOptions? options = default)
+ public static JsonArray SerializeToArray(this object value, JsonSerializerOptions? options = null)
{
- options ??= new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
-
+ options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
return JsonSerializer.SerializeToNode(value, options)!.AsArray();
}
-
+
///
/// Serializes the specified value to a .
///
/// The value to serialize.
/// The to use.
/// A representing the specified value.
- public static JsonArray SerializeToArray(this IEnumerable value, JsonSerializerOptions? options = default)
+ public static JsonArray SerializeToArray(this IEnumerable value, JsonSerializerOptions? options = null)
{
- options ??= new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
-
+ options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
return JsonSerializer.SerializeToNode(value, options)!.AsArray();
}
@@ -71,19 +62,23 @@ public static JsonArray SerializeToArray(this IEnumerable value, JsonSeria
/// The to use.
/// The type to deserialize to.
/// The deserialized value.
- public static T Deserialize(this JsonNode value, JsonSerializerOptions? options = default)
+ public static T Deserialize(this JsonNode value, JsonSerializerOptions? options = null)
{
- options ??= new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
-
+ options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
if (value is JsonObject jsonObject)
return JsonSerializer.Deserialize(jsonObject, options)!;
if (value is JsonArray jsonArray)
return JsonSerializer.Deserialize(jsonArray, options)!;
+ if (typeof(T).IsEnum || (Nullable.GetUnderlyingType(typeof(T))?.IsEnum ?? false))
+ {
+ if (value.GetValueKind() == JsonValueKind.Null)
+ return default!;
+ return (T)Enum.Parse(Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T), value.ToString());
+ }
+
if (value is JsonValue jsonValue)
return jsonValue.GetValue();
@@ -101,7 +96,7 @@ public static void SetProperty(this JsonObject model, JsonNode? value, params st
model = GetPropertyContainer(model, path);
model[path.Last()] = value?.SerializeToNode();
}
-
+
///
/// Sets the property value of the specified model.
///
@@ -113,7 +108,7 @@ public static void SetProperty(this JsonObject model, JsonArray? value, params s
model = GetPropertyContainer(model, path);
model[path.Last()] = value?.SerializeToNode();
}
-
+
///
/// Sets the property value of the specified model.
///
@@ -125,7 +120,7 @@ public static void SetProperty(this JsonObject model, IEnumerable valu
model = GetPropertyContainer(model, path);
model[path.Last()] = new JsonArray(value.Select(x => x.SerializeToNode()).ToArray());
}
-
+
///
/// Gets the property value of the specified model.
///
@@ -139,7 +134,7 @@ public static void SetProperty(this JsonObject model, IEnumerable valu
foreach (var prop in path.SkipLast(1))
{
if (currentModel[prop] is not JsonObject value)
- return default;
+ return null;
currentModel = value;
}
@@ -147,6 +142,25 @@ public static void SetProperty(this JsonObject model, IEnumerable valu
return currentModel[path.Last()];
}
+ ///
+ /// Gets the property value of the specified model.
+ ///
+ /// The model to get the property value from.
+ /// The path to the property.
+ /// The type to deserialize to.
+ /// The property value.
+ public static T? TryGetProperty(this JsonObject model, params string[] path)
+ {
+ try
+ {
+ return model.GetProperty(path);
+ }
+ catch (Exception e)
+ {
+ return default;
+ }
+ }
+
///
/// Gets the property value of the specified model.
///
@@ -173,7 +187,7 @@ public static void SetProperty(this JsonObject model, IEnumerable valu
var property = GetProperty(model, path);
return property != null ? property.Deserialize(options) : default;
}
-
+
///
/// Returns the property container of the specified model.
///
@@ -190,5 +204,4 @@ private static JsonObject GetPropertyContainer(this JsonObject model, params str
return model;
}
-
}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs
new file mode 100644
index 0000000000..cfee71fccc
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs
@@ -0,0 +1,25 @@
+using Elsa.Api.Client.Resources.CommitStrategies.Models;
+using Elsa.Api.Client.Shared.Models;
+using Refit;
+
+namespace Elsa.Api.Client.Resources.CommitStrategies.Contracts;
+
+///
+/// Represents a client for the commit strategies API.
+///
+public interface ICommitStrategiesApi
+{
+ ///
+ /// Lists workflow commit strategies.
+ ///
+ /// A list response containing activity commit strategy descriptors and their count.
+ [Get("/descriptors/commit-strategies/workflows")]
+ Task> ListWorkflowStrategiesAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Lists activity commit strategies.
+ ///
+ /// A list response containing activity commit strategy descriptors and their count.
+ [Get("/descriptors/commit-strategies/activities")]
+ Task> ListActivityStrategiesAsync(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs
new file mode 100644
index 0000000000..ad63d59310
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs
@@ -0,0 +1,7 @@
+namespace Elsa.Api.Client.Resources.CommitStrategies.Models;
+
+///
+/// Represents a descriptor for a commit strategy, containing information such as its technical name,
+/// display name, and description.
+///
+public record CommitStrategyDescriptor(string Name, string DisplayName, string Description);
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs
new file mode 100644
index 0000000000..8e14345c65
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs
@@ -0,0 +1,19 @@
+namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
+
+public class WorkflowCommitStateOptions
+{
+ ///
+ /// Commit workflow state before the workflow starts.
+ ///
+ public bool Starting { get; set; }
+
+ ///
+ /// Commit workflow state before an activity executes, unless the activity is configured to not commit state.
+ ///
+ public bool ActivityExecuting { get; set; }
+
+ ///
+ /// Commit workflow state after an activity executes, unless the activity is configured to not commit state.
+ ///
+ public bool ActivityExecuted { get; set; }
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs
index ec28ac197f..07b5c882df 100644
--- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs
@@ -29,4 +29,9 @@ public class WorkflowOptions
/// The type of IIncidentStrategy to use when a fault occurs in the workflow.
///
public string? IncidentStrategyType { get; set; }
+
+ ///
+ /// The options for committing workflow state.
+ ///
+ public string? CommitStrategyName { get; set; }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
index d5b8fdd6c6..3387e912aa 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
@@ -79,9 +79,13 @@ public async Task AddManyAsync(
Func? onSaving = default,
CancellationToken cancellationToken = default)
{
- await using var dbContext = await CreateDbContextAsync(cancellationToken);
var entityList = entities.ToList();
+ if (entityList.Count == 0)
+ return;
+
+ await using var dbContext = await CreateDbContextAsync(cancellationToken);
+
if (onSaving != null)
{
var savingTasks = entityList.Select(entity => onSaving(dbContext, entity, cancellationToken).AsTask()).ToList();
@@ -162,9 +166,13 @@ public async Task SaveManyAsync(
Func? onSaving = default,
CancellationToken cancellationToken = default)
{
- await using var dbContext = await CreateDbContextAsync(cancellationToken);
var entityList = entities.ToList();
+ if (entityList.Count == 0)
+ return;
+
+ await using var dbContext = await CreateDbContextAsync(cancellationToken);
+
if (onSaving != null)
{
var savingTasks = entityList.Select(entity => onSaving(dbContext, entity, cancellationToken).AsTask()).ToList();
diff --git a/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs b/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs
index 1c5ff28065..3ca25e6931 100644
--- a/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs
+++ b/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs
@@ -9,8 +9,9 @@ namespace Elsa.Expressions.Models;
public class ExpressionExecutionContext(
IServiceProvider serviceProvider,
MemoryRegister memory,
- ExpressionExecutionContext? parentContext = default,
- IDictionary