diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index 22620706f..65f3d6063 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -3,7 +3,6 @@ using Elsa.Agents;
using Elsa.Alterations.Extensions;
using Elsa.Alterations.MassTransit.Extensions;
using Elsa.Common.DistributedLocks.Noop;
-using Elsa.Common.Multitenancy;
using Elsa.Dapper.Extensions;
using Elsa.Dapper.Services;
using Elsa.DropIns.Extensions;
@@ -29,8 +28,8 @@ using Elsa.Server.Web.Extensions;
using Elsa.Server.Web.Filters;
using Elsa.Tenants.AspNetCore;
using Elsa.Tenants.Extensions;
-using Elsa.Workflows;
using Elsa.Workflows.Api;
+using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.Management.Compression;
using Elsa.Workflows.Management.Stores;
using Elsa.Workflows.Runtime.Distributed.Extensions;
diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json
index fb46c79e8..a47727e5a 100644
--- a/src/apps/Elsa.Server.Web/appsettings.json
+++ b/src/apps/Elsa.Server.Web/appsettings.json
@@ -2,14 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Warning",
- "Elsa": "Warning",
- "MassTransit": "Warning",
- "Microsoft.Extensions.Http": "Warning",
- "Microsoft.Hosting.Lifetime": "Information",
- "Microsoft.EntityFrameworkCore": "Warning",
- "Microsoft.AspNetCore": "Warning",
- "Quartz": "Warning",
- "System.Net.Http": "Warning"
+ "Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
diff --git a/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj.DotSettings b/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj.DotSettings
index 8b01856ae..801d2b040 100644
--- a/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj.DotSettings
+++ b/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj.DotSettings
@@ -1,2 +1,4 @@
- True
\ No newline at end of file
+ True
+ True
+ True
\ 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 9f28a6070..4be3d8479 100644
--- a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs
+++ b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs
@@ -5,6 +5,7 @@ using Elsa.Api.Client.Resources.ActivityExecutions.Contracts;
using Elsa.Api.Client.Resources.Features.Contracts;
using Elsa.Api.Client.Resources.Identity.Contracts;
using Elsa.Api.Client.Resources.IncidentStrategies.Contracts;
+using Elsa.Api.Client.Resources.LogPersistenceStrategies;
using Elsa.Api.Client.Resources.Scripting.Contracts;
using Elsa.Api.Client.Resources.StorageDrivers.Contracts;
using Elsa.Api.Client.Resources.Tasks.Contracts;
@@ -65,6 +66,7 @@ public static class DependencyInjectionExtensions
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/Resources/LogPersistenceStrategies/Contracts/ILogPersistenceStrategiesApi.cs b/src/clients/Elsa.Api.Client/Resources/LogPersistenceStrategies/Contracts/ILogPersistenceStrategiesApi.cs
new file mode 100644
index 000000000..94f0fed61
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Resources/LogPersistenceStrategies/Contracts/ILogPersistenceStrategiesApi.cs
@@ -0,0 +1,19 @@
+using Elsa.Api.Client.Shared.Models;
+using JetBrains.Annotations;
+using Refit;
+
+namespace Elsa.Api.Client.Resources.LogPersistenceStrategies;
+
+///
+/// Represents a client API for managing log persistence strategies.
+///
+[UsedImplicitly]
+public interface ILogPersistenceStrategiesApi
+{
+ ///
+ /// Lists log persistence strategies.
+ ///
+ /// The cancellation token.
+ [Get("/descriptors/log-persistence-strategies")]
+ Task> ListAsync(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/LogPersistenceStrategies/Models/LogPersistenceStrategyDescriptor.cs b/src/clients/Elsa.Api.Client/Resources/LogPersistenceStrategies/Models/LogPersistenceStrategyDescriptor.cs
new file mode 100644
index 000000000..89cccfc46
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Resources/LogPersistenceStrategies/Models/LogPersistenceStrategyDescriptor.cs
@@ -0,0 +1,12 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Api.Client.Resources.LogPersistenceStrategies;
+
+///
+/// Represents a log persistence strategy.
+///
+/// The .NET type name of the log persistence strategy.
+/// The display name of the log persistence strategy.
+/// The description of the log persistence strategy.
+[UsedImplicitly]
+public record LogPersistenceStrategyDescriptor(string TypeName, string DisplayName, string? Description);
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/LogPersistenceStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/LogPersistenceStrategies/List/Endpoint.cs
new file mode 100644
index 000000000..0dd4992c0
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/LogPersistenceStrategies/List/Endpoint.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using Elsa.Abstractions;
+using Elsa.Extensions;
+using Elsa.Models;
+using Elsa.Workflows.LogPersistence;
+using Humanizer;
+
+namespace Elsa.Workflows.Api.Endpoints.LogPersistenceStrategies.List;
+
+///
+/// Returns list of available implementations.
+///
+internal class Endpoint(ILogPersistenceStrategyService logPersistenceStrategyService) : ElsaEndpointWithoutRequest>
+{
+ public override void Configure()
+ {
+ Get("/descriptors/log-persistence-strategies");
+ ConfigurePermissions("read:log-persistence-strategies");
+ }
+
+ public override Task> ExecuteAsync(CancellationToken cancellationToken)
+ {
+ var strategies = logPersistenceStrategyService.ListStrategies();
+ var descriptors = strategies.Select(LogPersistenceStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
+ var response =new ListResponse(descriptors);
+ return Task.FromResult(response);
+ }
+}
+
+internal record LogPersistenceStrategyDescriptor(string DisplayName, string Description, string TypeName)
+{
+ public static LogPersistenceStrategyDescriptor FromStrategy(ILogPersistenceStrategy strategy)
+ {
+ var type = strategy.GetType();
+ var displayNameAttribute = type.GetCustomAttribute();
+ var descriptionAttribute = type.GetCustomAttribute();
+ var displayAttribute = type.GetCustomAttribute();
+ var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize();
+ var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? "";
+
+ return new LogPersistenceStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName());
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs
index 5a96c696f..16ff509bd 100644
--- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs
+++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs
@@ -195,7 +195,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
///
/// Stores the evaluated inputs, serialized, for the current activity for historical purposes.
///
- public IDictionary ActivityState { get; set; } = new Dictionary();
+ public IDictionary ActivityState { get; set; } = new Dictionary();
///
/// Schedules the specified activity to be executed.
diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings
index a9f483ae3..57af41798 100644
--- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings
+++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings
@@ -12,5 +12,10 @@
True
True
True
+ True
+ True
+ True
+ True
+ True
True
True
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
index 4d8af941b..a73b24160 100644
--- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
@@ -9,6 +9,8 @@ using Elsa.Features.Services;
using Elsa.Workflows.ActivationValidators;
using Elsa.Workflows.Builders;
using Elsa.Workflows.IncidentStrategies;
+using Elsa.Workflows.LogPersistence;
+using Elsa.Workflows.LogPersistence.Strategies;
using Elsa.Workflows.Middleware.Activities;
using Elsa.Workflows.Middleware.Workflows;
using Elsa.Workflows.Pipelines.ActivityExecution;
@@ -194,9 +196,16 @@ public class WorkflowsFeature : FeatureBase
.AddScoped()
.AddScoped()
- // Logger state generators
+ // Logger state generators.
.AddSingleton(WorkflowLoggerStateGenerator)
.AddSingleton(ActivityLoggerStateGenerator)
+
+ // Log Persistence Strategies.
+ .AddScoped()
+ .AddScoped()
+ .AddScoped()
+ .AddScoped()
+ .AddScoped()
// Logging
.AddLogging();
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Contexts/LogPersistenceStrategyContext.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Contexts/LogPersistenceStrategyContext.cs
new file mode 100644
index 000000000..052278467
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Contexts/LogPersistenceStrategyContext.cs
@@ -0,0 +1,3 @@
+namespace Elsa.Workflows.LogPersistence;
+
+public record LogPersistenceStrategyContext(CancellationToken CancellationToken);
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Contracts/ILogPersistenceStrategy.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Contracts/ILogPersistenceStrategy.cs
new file mode 100644
index 000000000..63d0c1ef6
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Contracts/ILogPersistenceStrategy.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Workflows.LogPersistence;
+
+public interface ILogPersistenceStrategy
+{
+ Task GetPersistenceModeAsync(LogPersistenceStrategyContext context);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Contracts/ILogPersistenceStrategyService.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Contracts/ILogPersistenceStrategyService.cs
new file mode 100644
index 000000000..0606c5663
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Contracts/ILogPersistenceStrategyService.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Workflows.LogPersistence;
+
+public interface ILogPersistenceStrategyService
+{
+ IEnumerable ListStrategies();
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Enums/LogPersistenceMode.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Enums/LogPersistenceMode.cs
similarity index 89%
rename from src/modules/Elsa.Workflows.Core/Enums/LogPersistenceMode.cs
rename to src/modules/Elsa.Workflows.Core/LogPersistence/Enums/LogPersistenceMode.cs
index e2ef16466..57518f7ca 100644
--- a/src/modules/Elsa.Workflows.Core/Enums/LogPersistenceMode.cs
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Enums/LogPersistenceMode.cs
@@ -1,25 +1,25 @@
-using JetBrains.Annotations;
-
-namespace Elsa.Workflows;
-
-///
-/// Define the Log Persistence mode to store information
-///
-[UsedImplicitly]
-public enum LogPersistenceMode
-{
- ///
- /// Persist using the Parent mode
- ///
- Inherit,
-
- ///
- /// Include property to store
- ///
- Include,
-
- ///
- /// Exclude Property to store
- ///
- Exclude
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows.LogPersistence;
+
+///
+/// Define the Log Persistence mode to store information
+///
+[UsedImplicitly]
+public enum LogPersistenceMode
+{
+ ///
+ /// Persist using the Parent mode
+ ///
+ Inherit,
+
+ ///
+ /// Include property to store
+ ///
+ Include,
+
+ ///
+ /// Exclude Property to store
+ ///
+ Exclude
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Options/LogPersistenceOptions.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Options/LogPersistenceOptions.cs
new file mode 100644
index 000000000..fbc9be666
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Options/LogPersistenceOptions.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Workflows.LogPersistence;
+
+public class LogPersistenceOptions
+{
+ public LogPersistenceMode ConfiguredPersistenceMode { get; set; } = LogPersistenceMode.Inherit;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Services/DefaultLogPersistenceStrategyService.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Services/DefaultLogPersistenceStrategyService.cs
new file mode 100644
index 000000000..b7f6d4051
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Services/DefaultLogPersistenceStrategyService.cs
@@ -0,0 +1,12 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows.LogPersistence;
+
+[UsedImplicitly]
+public class DefaultLogPersistenceStrategyService(IEnumerable strategies) : ILogPersistenceStrategyService
+{
+ public IEnumerable ListStrategies()
+ {
+ return strategies;
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Configuration.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Configuration.cs
new file mode 100644
index 000000000..e73234cce
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Configuration.cs
@@ -0,0 +1,16 @@
+using JetBrains.Annotations;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.Workflows.LogPersistence.Strategies;
+
+///
+/// A log persistence strategy that uses the configured persistence mode.
+///
+[UsedImplicitly]
+public class Configuration(IOptions options) : ILogPersistenceStrategy
+{
+ public Task GetPersistenceModeAsync(LogPersistenceStrategyContext context)
+ {
+ return Task.FromResult(options.Value.ConfiguredPersistenceMode);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Exclude.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Exclude.cs
new file mode 100644
index 000000000..f8ace9acd
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Exclude.cs
@@ -0,0 +1,15 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows.LogPersistence.Strategies;
+
+///
+/// A log persistence strategy that returns .
+///
+[UsedImplicitly]
+public class Exclude : ILogPersistenceStrategy
+{
+ public Task GetPersistenceModeAsync(LogPersistenceStrategyContext context)
+ {
+ return Task.FromResult(LogPersistenceMode.Exclude);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Include.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Include.cs
new file mode 100644
index 000000000..7172d900c
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Include.cs
@@ -0,0 +1,15 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows.LogPersistence.Strategies;
+
+///
+/// A log persistence strategy that returns .
+///
+[UsedImplicitly]
+public class Include : ILogPersistenceStrategy
+{
+ public Task GetPersistenceModeAsync(LogPersistenceStrategyContext context)
+ {
+ return Task.FromResult(LogPersistenceMode.Include);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Inherit.cs b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Inherit.cs
new file mode 100644
index 000000000..f2d734a07
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/LogPersistence/Strategies/Inherit.cs
@@ -0,0 +1,15 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows.LogPersistence.Strategies;
+
+///
+/// A log persistence strategy that returns .
+///
+[UsedImplicitly]
+public class Inherit : ILogPersistenceStrategy
+{
+ public Task GetPersistenceModeAsync(LogPersistenceStrategyContext context)
+ {
+ return Task.FromResult(LogPersistenceMode.Inherit);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs
index 0f93919cf..d36463f52 100644
--- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs
+++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs
@@ -12,6 +12,7 @@ using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Features;
+using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
using Elsa.Workflows.Management.Compression;
using Elsa.Workflows.Management.Contracts;
diff --git a/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs b/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs
index e6f3c02ca..86b8d332b 100644
--- a/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs
+++ b/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs
@@ -1,3 +1,4 @@
+using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Options;
diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs
index 327dcc28a..df335a24d 100644
--- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs
@@ -109,7 +109,6 @@ public class BulkDispatchWorkflows : Activity
await foreach (var item in items)
{
- //context.DeferTask(async () => await DispatchChildWorkflowAsync(context, item));
await DispatchChildWorkflowAsync(context, item);
dispatchedInstancesCount++;
}
diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutonMapper.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutonMapper.cs
index c20c47648..c533c2b00 100644
--- a/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutonMapper.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutonMapper.cs
@@ -10,5 +10,5 @@ public interface IActivityExecutionMapper
///
/// Maps an activity execution context to an activity execution record.
///
- ActivityExecutionRecord Map(ActivityExecutionContext source);
+ Task MapAsync(ActivityExecutionContext source);
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/ILogRecordExtractor.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/ILogRecordExtractor.cs
index 2a4d0564a..a5a11d350 100644
--- a/src/modules/Elsa.Workflows.Runtime/Contracts/ILogRecordExtractor.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Contracts/ILogRecordExtractor.cs
@@ -6,5 +6,5 @@ namespace Elsa.Workflows.Runtime;
public interface ILogRecordExtractor where T: ILogRecord
{
/// Extracts execution logs from a workflow execution context.
- IEnumerable ExtractLogRecords(WorkflowExecutionContext context);
+ Task> ExtractLogRecordsAsync(WorkflowExecutionContext context);
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs b/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs
index 6c8b1de8c..429516fa8 100644
--- a/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs
@@ -42,7 +42,7 @@ public class ActivityExecutionRecord : Entity, ILogRecord
///
/// The state of the activity at the time this record is created or last updated.
///
- public IDictionary? ActivityState { get; set; }
+ public IDictionary? ActivityState { get; set; }
///
/// Any additional payload associated with the log record.
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs b/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs
index 85d1db7d5..139198419 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs
@@ -6,9 +6,10 @@ namespace Elsa.Workflows.Runtime;
public class ActivityExecutionRecordExtractor(IActivityExecutionMapper activityExecutionMapper) : ILogRecordExtractor
{
///
- public IEnumerable ExtractLogRecords(WorkflowExecutionContext context)
+ public async Task> ExtractLogRecordsAsync(WorkflowExecutionContext context)
{
var activityExecutionContexts = context.ActivityExecutionContexts;
- return activityExecutionContexts.Select(activityExecutionMapper.Map).ToList();
+ var tasks = activityExecutionContexts.Select(activityExecutionMapper.MapAsync).ToList();
+ return await Task.WhenAll(tasks);
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs
index f6ed24440..2fe92dce7 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs
@@ -1,5 +1,6 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
+using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.Management.Options;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Entities;
@@ -10,14 +11,24 @@ using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Runtime;
///
-public class DefaultActivityExecutionMapper(IOptions options) : IActivityExecutionMapper
+public class DefaultActivityExecutionMapper : IActivityExecutionMapper
{
- private const string LogPersistenceModeKey = "logPersistenceMode";
+ private readonly IOptions _options;
+ private readonly IDictionary _logPersistenceStrategies;
+
+ public DefaultActivityExecutionMapper(IOptions options, ILogPersistenceStrategyService logPersistenceStrategyService)
+ {
+ _options = options;
+ _logPersistenceStrategies = logPersistenceStrategyService.ListStrategies().ToDictionary(x => x.GetType().GetSimpleAssemblyQualifiedName(), x => x);
+ }
+
+ private const string LegacyLogPersistenceModeKey = "logPersistenceMode";
+ private const string LogPersistenceStrategyKey = "logPersistenceStrategy";
///
- public ActivityExecutionRecord Map(ActivityExecutionContext source)
+ public async Task MapAsync(ActivityExecutionContext source)
{
- /*
+ /* The following legacy JSON structure is expected to be found in the custom properties of the workflow and activity:
* {
* "logPersistenceMode": {
* "default": "default",
@@ -27,42 +38,38 @@ public class DefaultActivityExecutionMapper(IOptions options)
* }
*/
+ /* The following JSON structure is expected to be found in the custom properties of the workflow and activity:
+ * {
+ * "logPersistenceStrategy": {
+ * "default": "null",
+ * "inputs": { k : v },
+ * "outputs": { k: v }
+ * }
+ * }
+ */
+
+ var cancellationToken = source.WorkflowExecutionContext.CancellationToken;
var workflow = (Workflow?)source.GetAncestors().FirstOrDefault(x => x.Activity is Workflow)?.Activity ?? source.WorkflowExecutionContext.Workflow;
- var workflowPersistenceProperty = GetDefaultPersistenceMode(workflow.CustomProperties, () => options.Value.LogPersistenceMode);
- var activityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault>(LogPersistenceModeKey, () => new Dictionary());
- var activityPersistencePropertyDefault = GetDefaultPersistenceMode(source.Activity.CustomProperties, () => workflowPersistenceProperty);
+ var workflowPersistenceProperty = await GetDefaultPersistenceModeAsync(workflow.CustomProperties, () => _options.Value.LogPersistenceMode, cancellationToken);
+ var activityPersistencePropertyDefault = await GetDefaultPersistenceModeAsync(source.Activity.CustomProperties, () => workflowPersistenceProperty, cancellationToken);
+ var legacyActivityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault>(LegacyLogPersistenceModeKey, () => new Dictionary());
+ var activityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault>(LogPersistenceStrategyKey, () => new Dictionary());
+ var payload = GetPayload(source);
+ var outputs = GetOutputs(source);
- // Get any outcomes that were added to the activity execution context.
- var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : default;
- var payload = new Dictionary();
+ outputs = await StorePropertyUsingPersistenceMode(
+ outputs,
+ legacyActivityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary())!,
+ activityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary())!,
+ activityPersistencePropertyDefault,
+ cancellationToken);
- if (outcomes != null)
- payload.Add("Outcomes", outcomes);
-
- // Get any outputs that were added to the activity execution context.
- var activity = source.Activity;
- var expressionExecutionContext = source.ExpressionExecutionContext;
- var activityDescriptor = source.ActivityDescriptor;
- var outputDescriptors = activityDescriptor.Outputs;
-
- var outputs = outputDescriptors.ToDictionary(x => x.Name, x =>
- {
- if (x.IsSerializable == false)
- return "(not serializable)";
-
- var cachedValue = activity.GetOutput(expressionExecutionContext, x.Name);
-
- if (cachedValue != default)
- return cachedValue;
-
- if (x.ValueGetter(activity) is Output output && source.TryGet(output.MemoryBlockReference(), out var outputValue))
- return outputValue;
-
- return default;
- });
-
- outputs = StorePropertyUsingPersistenceMode(outputs, activityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary())!, activityPersistencePropertyDefault);
- var inputs = StorePropertyUsingPersistenceMode(source.ActivityState, activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!, activityPersistencePropertyDefault);
+ var inputs = await StorePropertyUsingPersistenceMode(
+ source.ActivityState,
+ legacyActivityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!,
+ activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!,
+ activityPersistencePropertyDefault,
+ cancellationToken);
return new ActivityExecutionRecord
{
@@ -85,32 +92,63 @@ public class DefaultActivityExecutionMapper(IOptions options)
};
}
- private static LogPersistenceMode GetDefaultPersistenceMode(IDictionary customProperties, Func defaultFactory)
+ private async Task GetDefaultPersistenceModeAsync(IDictionary customProperties, Func defaultFactory, CancellationToken cancellationToken)
{
- var properties = customProperties.GetValueOrDefault>(LogPersistenceModeKey, () => new Dictionary());
- var persistencePropertyDefault = properties!.GetValueOrDefault("default", defaultFactory);
+ var legacyProperties = customProperties.GetValueOrDefault>(LegacyLogPersistenceModeKey, () => new Dictionary());
+ var properties = customProperties.GetValueOrDefault>(LogPersistenceStrategyKey, () => new Dictionary());
+ var defaultPersistenceStrategyTypeName = properties!.GetValueOrDefault("default");
- if (persistencePropertyDefault == LogPersistenceMode.Inherit)
+ if (defaultPersistenceStrategyTypeName == null)
+ {
+ var legacyPersistencePropertyDefault = legacyProperties!.GetValueOrDefault("default", defaultFactory);
+
+ if (legacyPersistencePropertyDefault == LogPersistenceMode.Inherit)
+ return defaultFactory();
+ return legacyPersistencePropertyDefault;
+ }
+
+ var strategy = _logPersistenceStrategies.TryGetValue(defaultPersistenceStrategyTypeName, out var v) ? v : null;
+
+ if (strategy == null)
return defaultFactory();
- return persistencePropertyDefault;
+
+ var strategyContext = new LogPersistenceStrategyContext(cancellationToken);
+ return await strategy.GetPersistenceModeAsync(strategyContext);
}
- private static Dictionary StorePropertyUsingPersistenceMode(IDictionary state, IDictionary persistenceModeConfiguration, LogPersistenceMode defaultLogPersistenceMode)
+ private async Task> StorePropertyUsingPersistenceMode(
+ IDictionary state,
+ IDictionary persistenceModeConfiguration,
+ IDictionary persistenceStrategyConfiguration,
+ LogPersistenceMode defaultLogPersistenceMode,
+ CancellationToken cancellationToken)
{
var result = new Dictionary();
foreach (var value in state)
{
- var persistence = persistenceModeConfiguration.GetValueOrDefault(value.Key.Camelize(), () => defaultLogPersistenceMode);
- if (persistence.Equals(LogPersistenceMode.Include)
- || (persistence.Equals(LogPersistenceMode.Inherit) && defaultLogPersistenceMode is LogPersistenceMode.Include or LogPersistenceMode.Inherit))
+ var propKey = value.Key.Camelize();
+ var strategy = persistenceStrategyConfiguration.GetValueOrDefault(propKey) is string strategyTypeName ? _logPersistenceStrategies.TryGetValue(strategyTypeName, out var v) ? v : null : null;
+ var mode = defaultLogPersistenceMode;
+
+ if (strategy != null)
+ {
+ var strategyContext = new LogPersistenceStrategyContext(cancellationToken);
+ mode = await strategy.GetPersistenceModeAsync(strategyContext);
+ }
+ else
+ {
+ mode = persistenceModeConfiguration.GetValueOrDefault(propKey, () => defaultLogPersistenceMode);
+ }
+
+ if (mode == LogPersistenceMode.Include || mode == LogPersistenceMode.Inherit && defaultLogPersistenceMode is LogPersistenceMode.Include or LogPersistenceMode.Inherit)
result.Add(value.Key, value.Value);
}
return result;
}
- private ActivityStatus GetAggregateStatus(ActivityExecutionContext context)
+ private static ActivityStatus GetAggregateStatus(ActivityExecutionContext context)
{
// If any child activity is faulted, the aggregate status is faulted.
var descendantContexts = context.GetDescendants().ToList();
@@ -120,4 +158,42 @@ public class DefaultActivityExecutionMapper(IOptions options)
return context.Status;
}
+
+ private static IDictionary GetPayload(ActivityExecutionContext source)
+ {
+ var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : default;
+ var payload = new Dictionary();
+
+ if (outcomes != null)
+ payload.Add("Outcomes", outcomes);
+
+ return payload;
+ }
+
+ // Get the outputs of the activity execution context.
+ private static IDictionary GetOutputs(ActivityExecutionContext source)
+ {
+ var activity = source.Activity;
+ var expressionExecutionContext = source.ExpressionExecutionContext;
+ var activityDescriptor = source.ActivityDescriptor;
+ var outputDescriptors = activityDescriptor.Outputs;
+
+ var outputs = outputDescriptors.ToDictionary(x => x.Name, x =>
+ {
+ if (x.IsSerializable == false)
+ return "(not serializable)";
+
+ var cachedValue = activity.GetOutput(expressionExecutionContext, x.Name);
+
+ if (cachedValue != default)
+ return cachedValue;
+
+ if (x.ValueGetter(activity) is Output output && source.TryGet(output.MemoryBlockReference(), out var outputValue))
+ return outputValue;
+
+ return default;
+ });
+
+ return outputs;
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs
index 9699497b0..2c280a22c 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs
@@ -1,6 +1,7 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Notifications;
+using Open.Linq.AsyncExtensions;
namespace Elsa.Workflows.Runtime.Services;
@@ -13,7 +14,7 @@ public class StoreActivityExecutionLogSink(IActivityExecutionStore activityExecu
///
public async Task PersistExecutionLogsAsync(WorkflowExecutionContext context, CancellationToken cancellationToken = default)
{
- var records = extractor.ExtractLogRecords(context).ToList();
+ var records = await extractor.ExtractLogRecordsAsync(context).ToList();
await activityExecutionStore.SaveManyAsync(records, cancellationToken);
await notificationSender.SendAsync(new ActivityExecutionLogUpdated(context, records), cancellationToken);
}
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs
index 353ac4c0a..a91667858 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs
@@ -1,6 +1,7 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Notifications;
+using Open.Linq.AsyncExtensions;
namespace Elsa.Workflows.Runtime;
@@ -12,7 +13,7 @@ public class StoreWorkflowExecutionLogSink(IWorkflowExecutionLogStore store, ILo
///
public async Task PersistExecutionLogsAsync(WorkflowExecutionContext context, CancellationToken cancellationToken)
{
- var records = extractor.ExtractLogRecords(context).ToList();
+ var records = await extractor.ExtractLogRecordsAsync(context).ToList();
await store.AddManyAsync(records, context.CancellationToken);
await notificationSender.SendAsync(new WorkflowExecutionLogUpdated(context), context.CancellationToken);
}
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowExecutionLogRecordExtractor.cs b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowExecutionLogRecordExtractor.cs
index 4220111b5..081016122 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowExecutionLogRecordExtractor.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowExecutionLogRecordExtractor.cs
@@ -6,9 +6,9 @@ namespace Elsa.Workflows.Runtime;
public class WorkflowExecutionLogRecordExtractor(IIdentityGenerator identityGenerator) : ILogRecordExtractor
{
///
- public IEnumerable ExtractLogRecords(WorkflowExecutionContext context)
+ public Task> ExtractLogRecordsAsync(WorkflowExecutionContext context)
{
- return context.ExecutionLog.Select(x => new WorkflowExecutionLogRecord
+ var records = context.ExecutionLog.Select(x => new WorkflowExecutionLogRecord
{
Id = identityGenerator.GenerateId(),
ActivityInstanceId = x.ActivityInstanceId,
@@ -30,5 +30,7 @@ public class WorkflowExecutionLogRecordExtractor(IIdentityGenerator identityGene
Timestamp = x.Timestamp,
Sequence = x.Sequence
});
+
+ return Task.FromResult(records);
}
}
\ No newline at end of file
diff --git a/test/component/Elsa.AzureServiceBus.ComponentTests/Elsa.AzureServiceBus.ComponentTests.csproj b/test/component/Elsa.AzureServiceBus.ComponentTests/Elsa.AzureServiceBus.ComponentTests.csproj
index 96f81d332..89ed27dde 100644
--- a/test/component/Elsa.AzureServiceBus.ComponentTests/Elsa.AzureServiceBus.ComponentTests.csproj
+++ b/test/component/Elsa.AzureServiceBus.ComponentTests/Elsa.AzureServiceBus.ComponentTests.csproj
@@ -21,6 +21,7 @@
+
diff --git a/test/component/Elsa.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs b/test/component/Elsa.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs
index be10535a7..93c90530b 100644
--- a/test/component/Elsa.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs
+++ b/test/component/Elsa.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs
@@ -1,4 +1,5 @@
using System.Net.Http.Headers;
+using Elsa.Agents;
using Elsa.Alterations.Extensions;
using Elsa.AzureServiceBus.ComponentTests.Extensions;
using Elsa.EntityFrameworkCore.Extensions;
@@ -75,6 +76,8 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
http.UseCache();
});
elsa.UseAzureServiceBus();
+ elsa.UseAgents();
+ elsa.UseAgentPersistence(feature => feature.UseEntityFrameworkCore(ef => ef.UsePostgreSql(typeof(AgentsPostgreSqlProvidersExtensions).Assembly, dbConnectionString)));
};
}
diff --git a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj
index e8dd3145c..1f285015a 100644
--- a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj
+++ b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj
@@ -17,6 +17,7 @@
+
diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs
index c7d70f26c..dc868daae 100644
--- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs
+++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs
@@ -3,6 +3,7 @@ using System.Reflection;
using Elsa.Agents;
using Elsa.Alterations.Extensions;
using Elsa.Caching;
+using Elsa.EntityFrameworkCore;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
@@ -106,7 +107,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
http.UseCache();
});
elsa.UseAgents();
- elsa.UseAgentPersistence(feature => feature.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString)));
+ elsa.UseAgentPersistence(feature => feature.UseEntityFrameworkCore(ef => ef.UsePostgreSql(typeof(AgentsPostgreSqlProvidersExtensions).Assembly, dbConnectionString)));
};
}