Skip to content

Commit

Permalink
Merge pull request #6343 from elsa-workflows/feature/4835
Browse files Browse the repository at this point in the history
Controlled Commit States Feature Implementation
  • Loading branch information
sfmskywalker authored Feb 6, 2025
2 parents 8a309e6 + f45fb4a commit aa09708
Show file tree
Hide file tree
Showing 79 changed files with 1,166 additions and 185 deletions.
6 changes: 6 additions & 0 deletions src/apps/Elsa.Server.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 =>
{
Expand Down
25 changes: 25 additions & 0 deletions src/apps/Elsa.Server.Web/SampleWorkflow.cs
Original file line number Diff line number Diff line change
@@ -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)),
}
};
}
}
11 changes: 11 additions & 0 deletions src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
/// </summary>
public static void SetRunAsynchronously(this JsonObject activity, bool value) => activity.SetProperty(JsonValue.Create(value), "customProperties", "runAsynchronously");

/// <summary>
/// Gets the commit state behavior for the specified activity.
/// </summary>
public static string? GetCommitStrategy(this JsonObject activity) => activity.TryGetProperty<string?>("customProperties", "commitStrategyName");

/// <summary>
/// Sets the commit state behavior for the specified activity.
/// </summary>
public static void SetCommitStrategy(this JsonObject activity, string? name) => activity.SetProperty(JsonValue.Create(name), "customProperties", "commitStrategyName");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +74,7 @@ public static IServiceCollection AddDefaultApiClients(this IServiceCollection se
services.AddApi<IWorkflowActivationStrategiesApi>(builderOptions);
services.AddApi<IIncidentStrategiesApi>(builderOptions);
services.AddApi<ILogPersistenceStrategiesApi>(builderOptions);
services.AddApi<ICommitStrategiesApi>(builderOptions);
services.AddApi<ILoginApi>(builderOptions);
services.AddApi<IFeaturesApi>(builderOptions);
services.AddApi<IJavaScriptApi>(builderOptions);
Expand Down
79 changes: 46 additions & 33 deletions src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,43 @@ public static bool IsActivity(this JsonObject obj)
{
return obj.ContainsKey("type") && obj.ContainsKey("id") && obj.ContainsKey("version");
}

/// <summary>
/// Serializes the specified value to a <see cref="JsonObject"/>.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <returns>A <see cref="JsonObject"/> representing the specified value.</returns>
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)!;
}

/// <summary>
/// Serializes the specified value to a <see cref="JsonArray"/>.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <returns>A <see cref="JsonObject"/> representing the specified value.</returns>
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();
}

/// <summary>
/// Serializes the specified value to a <see cref="JsonArray"/>.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <returns>A <see cref="JsonObject"/> representing the specified value.</returns>
public static JsonArray SerializeToArray<T>(this IEnumerable<T> value, JsonSerializerOptions? options = default)
public static JsonArray SerializeToArray<T>(this IEnumerable<T> value, JsonSerializerOptions? options = null)
{
options ??= new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

return JsonSerializer.SerializeToNode(value, options)!.AsArray();
}

Expand All @@ -71,19 +62,23 @@ public static JsonArray SerializeToArray<T>(this IEnumerable<T> value, JsonSeria
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The deserialized value.</returns>
public static T Deserialize<T>(this JsonNode value, JsonSerializerOptions? options = default)
public static T Deserialize<T>(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<T>(jsonObject, options)!;

if (value is JsonArray jsonArray)
return JsonSerializer.Deserialize<T>(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<T>();

Expand All @@ -101,7 +96,7 @@ public static void SetProperty(this JsonObject model, JsonNode? value, params st
model = GetPropertyContainer(model, path);
model[path.Last()] = value?.SerializeToNode();
}

/// <summary>
/// Sets the property value of the specified model.
/// </summary>
Expand All @@ -113,7 +108,7 @@ public static void SetProperty(this JsonObject model, JsonArray? value, params s
model = GetPropertyContainer(model, path);
model[path.Last()] = value?.SerializeToNode();
}

/// <summary>
/// Sets the property value of the specified model.
/// </summary>
Expand All @@ -125,7 +120,7 @@ public static void SetProperty(this JsonObject model, IEnumerable<JsonNode> valu
model = GetPropertyContainer(model, path);
model[path.Last()] = new JsonArray(value.Select(x => x.SerializeToNode()).ToArray());
}

/// <summary>
/// Gets the property value of the specified model.
/// </summary>
Expand All @@ -139,14 +134,33 @@ public static void SetProperty(this JsonObject model, IEnumerable<JsonNode> valu
foreach (var prop in path.SkipLast(1))
{
if (currentModel[prop] is not JsonObject value)
return default;
return null;

currentModel = value;
}

return currentModel[path.Last()];
}

/// <summary>
/// Gets the property value of the specified model.
/// </summary>
/// <param name="model">The model to get the property value from.</param>
/// <param name="path">The path to the property.</param>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The property value.</returns>
public static T? TryGetProperty<T>(this JsonObject model, params string[] path)
{
try
{
return model.GetProperty<T>(path);
}
catch (Exception e)
{
return default;
}
}

/// <summary>
/// Gets the property value of the specified model.
/// </summary>
Expand All @@ -173,7 +187,7 @@ public static void SetProperty(this JsonObject model, IEnumerable<JsonNode> valu
var property = GetProperty(model, path);
return property != null ? property.Deserialize<T>(options) : default;
}

/// <summary>
/// Returns the property container of the specified model.
/// </summary>
Expand All @@ -190,5 +204,4 @@ private static JsonObject GetPropertyContainer(this JsonObject model, params str

return model;
}

}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a client for the commit strategies API.
/// </summary>
public interface ICommitStrategiesApi
{
/// <summary>
/// Lists workflow commit strategies.
/// </summary>
/// <returns>A list response containing activity commit strategy descriptors and their count.</returns>
[Get("/descriptors/commit-strategies/workflows")]
Task<ListResponse<CommitStrategyDescriptor>> ListWorkflowStrategiesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Lists activity commit strategies.
/// </summary>
/// <returns>A list response containing activity commit strategy descriptors and their count.</returns>
[Get("/descriptors/commit-strategies/activities")]
Task<ListResponse<CommitStrategyDescriptor>> ListActivityStrategiesAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Elsa.Api.Client.Resources.CommitStrategies.Models;

/// <summary>
/// Represents a descriptor for a commit strategy, containing information such as its technical name,
/// display name, and description.
/// </summary>
public record CommitStrategyDescriptor(string Name, string DisplayName, string Description);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;

public class WorkflowCommitStateOptions
{
/// <summary>
/// Commit workflow state before the workflow starts.
/// </summary>
public bool Starting { get; set; }

/// <summary>
/// Commit workflow state before an activity executes, unless the activity is configured to not commit state.
/// </summary>
public bool ActivityExecuting { get; set; }

/// <summary>
/// Commit workflow state after an activity executes, unless the activity is configured to not commit state.
/// </summary>
public bool ActivityExecuted { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public class WorkflowOptions
/// The type of <c>IIncidentStrategy</c> to use when a fault occurs in the workflow.
/// </summary>
public string? IncidentStrategyType { get; set; }

/// <summary>
/// The options for committing workflow state.
/// </summary>
public string? CommitStrategyName { get; set; }
}
12 changes: 10 additions & 2 deletions src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,13 @@ public async Task AddManyAsync(
Func<TDbContext, TEntity, CancellationToken, ValueTask>? 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();
Expand Down Expand Up @@ -162,9 +166,13 @@ public async Task SaveManyAsync(
Func<TDbContext, TEntity, CancellationToken, ValueTask>? 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();
Expand Down
Loading

0 comments on commit aa09708

Please sign in to comment.