diff --git a/src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs
new file mode 100644
index 000000000..c27200c88
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Extensions/PropertyBagExtensions.cs
@@ -0,0 +1,41 @@
+using System.Text.Json;
+using Elsa.Api.Client.Shared.Models;
+
+namespace Elsa.Api.Client.Extensions;
+
+///
+/// Provides extension methods for the PropertyBag class.
+///
+public static class PropertyBagExtensions
+{
+ ///
+ /// Tries to retrieve a value from the PropertyBag based on the provided key.
+ /// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function.
+ ///
+ /// The type of the value to retrieve.
+ /// The PropertyBag to retrieve the value from.
+ /// The key of the value to retrieve.
+ /// A function that returns the default value to be returned if the key does not exist in the PropertyBag.
+ /// The value associated with the key, or the default value if the key does not exist.
+ public static T TryGetValueOrDefault(this PropertyBag propertyBag, string key, Func defaultValue)
+ {
+ if (!propertyBag.TryGetValue(key, out var value))
+ return defaultValue();
+
+ var json = (string)value;
+ return JsonSerializer.Deserialize(json);
+ }
+
+ ///
+ /// Sets a value in the PropertyBag based on the provided key.
+ /// The value is serialized using JSON.
+ ///
+ /// The PropertyBag to set the value in.
+ /// The key to associate with the value.
+ /// The value to store in the PropertyBag.
+ public static void SetValue(this PropertyBag propertyBag, string key, object value)
+ {
+ var json = JsonSerializer.Serialize(value);
+ propertyBag[key] = json;
+ }
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs
index f3fe5d0eb..4d2ba3ef5 100644
--- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs
@@ -58,6 +58,11 @@ public class WorkflowDefinition : LinkedEntity
///
public IDictionary CustomProperties { get; set; } = new Dictionary();
+ ///
+ /// Stores custom information about the workflow. Can be used to store application-specific properties to associate with the workflow.
+ ///
+ public PropertyBag PropertyBag { get; set; } = new();
+
///
/// The name of the workflow provider that created this workflow, if any.
///
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs
index a617b54f3..76dba636d 100644
--- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs
@@ -55,6 +55,11 @@ public class WorkflowDefinitionModel : LinkedEntity
///
public IDictionary? CustomProperties { get; set; }
+ ///
+ /// Stores custom information about the workflow. Can be used to store application-specific properties to associate with the workflow.
+ ///
+ public PropertyBag PropertyBag { get; set; } = new();
+
///
/// Gets or sets whether the workflow definition is read-only.
///
diff --git a/src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs b/src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs
new file mode 100644
index 000000000..9abe27abc
--- /dev/null
+++ b/src/clients/Elsa.Api.Client/Shared/Models/PropertyBag.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace Elsa.Api.Client.Shared.Models;
+
+/// A dictionary of values that is skipped by polymorphic serialization.
+public class PropertyBag : Dictionary
+{
+ ///
+ [JsonConstructor]
+ public PropertyBag() : base(StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ ///
+ public PropertyBag(IDictionary dictionary) : this()
+ {
+ foreach (var kvp in dictionary)
+ Add(kvp.Key, kvp.Value);
+ }
+}
\ No newline at end of file
diff --git a/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs b/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs
index ea5c9225e..072f437fe 100644
--- a/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs
+++ b/src/clients/Elsa.Api.Client/Shared/Models/WrappedInput.cs
@@ -21,4 +21,5 @@ public class WrappedInput
/// Gets or sets the memory reference of this input.
///
public MemoryReference MemoryReference { get; set; } = default!;
-}
\ No newline at end of file
+}
+
diff --git a/src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs b/src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs
new file mode 100644
index 000000000..ad0359c21
--- /dev/null
+++ b/src/modules/Elsa.Common/Extensions/PropertyBagExtensions.cs
@@ -0,0 +1,67 @@
+using System.Text.Json;
+using Elsa.Common.Models;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+///
+/// Provides extension methods for the PropertyBag class.
+///
+public static class PropertyBagExtensions
+{
+ ///
+ /// Tries to retrieve a value from the PropertyBag based on the provided key.
+ /// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function.
+ ///
+ /// The type of the value to retrieve.
+ /// The PropertyBag to retrieve the value from.
+ /// The key of the value to retrieve.
+ /// A function that returns the default value to be returned if the key does not exist in the PropertyBag.
+ /// Optional JSON serializer options.
+ /// The value associated with the key, or the default value if the key does not exist.
+ public static T TryGetValueOrDefault(this PropertyBag propertyBag, string key, Func defaultValue, JsonSerializerOptions? options = null)
+ {
+ if (!propertyBag.TryGetValue(key, out var value))
+ return defaultValue();
+
+ var json = (string)value;
+ return JsonSerializer.Deserialize(json, options);
+ }
+
+ ///
+ /// Tries to retrieve a value from the PropertyBag based on the provided key.
+ /// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function.
+ ///
+ /// The type of the value to retrieve.
+ /// The PropertyBag to retrieve the value from.
+ /// The key of the value to retrieve.
+ /// The deserialized value.
+ /// Optional JSON serializer options.
+ /// True if the value exists, false otherwise.
+ public static bool TryGetValue(this PropertyBag propertyBag, string key, out T value, JsonSerializerOptions? options = null)
+ {
+ if (!propertyBag.TryGetValue(key, out var v))
+ {
+ value = default!;
+ return false;
+ }
+
+ var json = (string)v;
+ value = JsonSerializer.Deserialize(json, options);
+ return true;
+ }
+
+ ///
+ /// Sets a value in the PropertyBag based on the provided key.
+ /// The value is serialized using JSON.
+ ///
+ /// The PropertyBag to set the value in.
+ /// The key to associate with the value.
+ /// The value to store in the PropertyBag.
+ /// /// Optional JSON serializer options.
+ public static void SetValue(this PropertyBag propertyBag, string key, object value, JsonSerializerOptions? options = null)
+ {
+ var json = JsonSerializer.Serialize(value);
+ propertyBag[key] = json;
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Common/Models/PropertyBag.cs b/src/modules/Elsa.Common/Models/PropertyBag.cs
new file mode 100644
index 000000000..0f6209e81
--- /dev/null
+++ b/src/modules/Elsa.Common/Models/PropertyBag.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace Elsa.Common.Models;
+
+/// A dictionary of values that is skipped by polymorphic serialization.
+public class PropertyBag : Dictionary
+{
+ ///
+ [JsonConstructor]
+ public PropertyBag() : base(StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ ///
+ public PropertyBag(IDictionary dictionary) : this()
+ {
+ foreach (var kvp in dictionary)
+ Add(kvp.Key, kvp.Value);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Common/Models/VersionOptions.cs b/src/modules/Elsa.Common/Models/VersionOptions.cs
index f3c56f598..40ab3a0ad 100644
--- a/src/modules/Elsa.Common/Models/VersionOptions.cs
+++ b/src/modules/Elsa.Common/Models/VersionOptions.cs
@@ -114,4 +114,4 @@ public struct VersionOptions
/// Returns a simple string representation of this .
///
public override string ToString() => AllVersions ? "AllVersions" : IsDraft ? "Draft" : IsLatest ? "Latest" : IsPublished ? "Published" : IsLatestOrPublished ? "LatestOrPublished" : IsLatestAndPublished ? "LatestAndPublished" : Version.ToString();
-}
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs b/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs
index ebfb92beb..5d5f4e047 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsHostedService.cs
@@ -26,7 +26,6 @@ public class RunMigrationsHostedService : IHostedService where TDbCo
var dbContextFactory = scope.ServiceProvider.GetRequiredService>();
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.Database.MigrateAsync(cancellationToken);
- await dbContext.DisposeAsync();
}
///
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs
index 02cdccc5c..7f25567dd 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Configurations.cs
@@ -17,6 +17,7 @@ internal class Configurations : IEntityTypeConfiguration, IE
builder.Ignore(x => x.Outputs);
builder.Ignore(x => x.Outcomes);
builder.Ignore(x => x.CustomProperties);
+ builder.Ignore(x => x.PropertyBag);
builder.Ignore(x => x.Options);
builder.Property("Data");
builder.Property("UsableAsActivity");
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs
index 2c6886075..7421e5938 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs
@@ -152,7 +152,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
private ValueTask OnSaveAsync(ManagementElsaDbContext managementElsaDbContext, WorkflowDefinition entity, CancellationToken cancellationToken)
{
- var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties);
+ var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties, entity.PropertyBag);
var json = _payloadSerializer.Serialize(data);
managementElsaDbContext.Entry(entity).Property("Data").CurrentValue = json;
@@ -165,7 +165,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
if (entity == null)
return ValueTask.CompletedTask;
- var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties);
+ var data = new WorkflowDefinitionState(entity.Options, entity.Variables, entity.Inputs, entity.Outputs, entity.Outcomes, entity.CustomProperties, entity.PropertyBag);
var json = (string?)managementElsaDbContext.Entry(entity).Property("Data").CurrentValue;
if (!string.IsNullOrWhiteSpace(json))
@@ -177,6 +177,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
entity.Outputs = data.Outputs;
entity.Outcomes = data.Outcomes;
entity.CustomProperties = data.CustomProperties;
+ entity.PropertyBag = data.PropertyBag;
return ValueTask.CompletedTask;
}
@@ -225,7 +226,8 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
ICollection inputs,
ICollection outputs,
ICollection outcomes,
- IDictionary customProperties
+ IDictionary customProperties,
+ PropertyBag propertyBag
)
{
Options = options;
@@ -234,6 +236,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
Outputs = outputs;
Outcomes = outcomes;
CustomProperties = customProperties;
+ PropertyBag = propertyBag;
}
public WorkflowOptions Options { get; set; } = new();
@@ -241,6 +244,10 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
public ICollection Inputs { get; set; } = new List();
public ICollection Outputs { get; set; } = new List();
public ICollection Outcomes { get; set; } = new List();
+
+ [Obsolete("Use PropertyBag instead")]
public IDictionary CustomProperties { get; set; } = new Dictionary();
+
+ public PropertyBag PropertyBag { get; set; } = new();
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowDefinitionExtensions.cs b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowContextWorkflowDefinitionExtensions.cs
similarity index 69%
rename from src/modules/Elsa.WorkflowContexts/Extensions/WorkflowDefinitionExtensions.cs
rename to src/modules/Elsa.WorkflowContexts/Extensions/WorkflowContextWorkflowDefinitionExtensions.cs
index 1940aa46d..fc8279df0 100644
--- a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowDefinitionExtensions.cs
+++ b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowContextWorkflowDefinitionExtensions.cs
@@ -4,16 +4,16 @@ using Elsa.Workflows.Management.Entities;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
-///
/// Adds extension methods to .
-///
-public static class WorkflowDefinitionExtensions
+public static class WorkflowContextWorkflowDefinitionExtensions
{
///
/// Gets the workflow context provider types that are installed on the workflow definition.
///
/// The workflow definition to get the provider types from.
/// The workflow context provider types.
- public static IEnumerable GetWorkflowContextProviderTypes(this WorkflowDefinition workflowDefinition) =>
- workflowDefinition.CustomProperties.GetOrAdd(Constants.WorkflowContextProviderTypesKey, () => new List());
+ public static IEnumerable GetWorkflowContextProviderTypes(this WorkflowDefinition workflowDefinition)
+ {
+ return workflowDefinition.PropertyBag.GetOrAdd(Constants.WorkflowContextProviderTypesKey, () => new List());
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs b/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs
index ccb7d4ba2..7b1cda1bc 100644
--- a/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs
+++ b/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs
@@ -1,37 +1,38 @@
+using System.Text.Json;
+using Elsa.Expressions.Contracts;
using Elsa.Extensions;
using Elsa.WorkflowContexts.Contracts;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Pipelines.ActivityExecution;
-using Elsa.Workflows.Runtime.Middleware.Activities;
+using Elsa.Workflows.Serialization.Converters;
+using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.WorkflowContexts.Middleware;
-///
/// Middleware that loads and save workflow context into the currently executing workflow using installed workflow context providers.
-///
-public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMiddleware
+[UsedImplicitly]
+public class WorkflowContextActivityExecutionMiddleware(ActivityMiddlewareDelegate next, IServiceScopeFactory serviceScopeFactory, IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityExecutionMiddleware
{
- private readonly ActivityMiddlewareDelegate _next;
- private readonly IServiceScopeFactory _serviceScopeFactory;
-
- ///
- /// Constructor.
- ///
- public WorkflowContextActivityExecutionMiddleware(ActivityMiddlewareDelegate next, IServiceScopeFactory serviceScopeFactory)
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
{
- _next = next;
- _serviceScopeFactory = serviceScopeFactory;
- }
+ PropertyNameCaseInsensitive = true
+ }.WithConverters(new TypeJsonConverter(wellKnownTypeRegistry));
///
public async ValueTask InvokeAsync(ActivityExecutionContext context)
{
// Check if the workflow contains any workflow context providers.
- if (!context.WorkflowExecutionContext.Workflow.CustomProperties.TryGetValue>(Constants.WorkflowContextProviderTypesKey, out var providerTypes))
+ if (!context.WorkflowExecutionContext.Workflow.PropertyBag.TryGetValue>(Constants.WorkflowContextProviderTypesKey, out var providerTypes, _jsonSerializerOptions))
{
- await _next(context);
+ await next(context);
+ return;
+ }
+
+ if (providerTypes.Count == 0)
+ {
+ await next(context);
return;
}
@@ -41,12 +42,12 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd
// Is the activity configured to load the context?
foreach (var providerType in providerTypes)
{
- // Is the activity configured to load the context or is this a background execution?
+ // Is the activity configured to load the context, or is this a background execution?
var load = isBackgroundExecution || context.Activity.GetActivityWorkflowContextSettings(providerType).Load;
if (!load) continue;
// Load the context.
- using var scope = _serviceScopeFactory.CreateScope();
+ using var scope = serviceScopeFactory.CreateScope();
var provider = (IWorkflowContextProvider)ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, providerType);
var value = await provider.LoadAsync(context.WorkflowExecutionContext);
@@ -55,7 +56,7 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd
}
// Invoke the next middleware.
- await _next(context);
+ await next(context);
// Invoke each workflow context provider to persists the context.
foreach (var providerType in providerTypes)
@@ -65,7 +66,7 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd
if (!save) continue;
// Get the loaded value from the workflow execution context.
- using var scope = _serviceScopeFactory.CreateScope();
+ using var scope = serviceScopeFactory.CreateScope();
var value = context.WorkflowExecutionContext.GetWorkflowContext(providerType);
// Save the context.
diff --git a/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs b/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs
index da2a3351e..d065b5818 100644
--- a/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs
+++ b/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs
@@ -32,6 +32,7 @@ public class StaticWorkflowDefinitionLinker(
Outputs = workflowDefinitionModel.Outputs,
Outcomes = workflowDefinitionModel.Outcomes,
CustomProperties = workflowDefinitionModel.CustomProperties,
+ PropertyBag = workflowDefinitionModel.PropertyBag,
IsReadonly = workflowDefinitionModel.IsReadonly,
IsSystem = workflowDefinitionModel.IsSystem,
IsLatest = workflowDefinitionModel.IsLatest,
@@ -99,6 +100,7 @@ public class StaticWorkflowDefinitionLinker(
Outputs = item.Outputs,
Outcomes = item.Outcomes,
CustomProperties = item.CustomProperties,
+ PropertyBag = item.PropertyBag,
IsReadonly = item.IsReadonly,
IsSystem = item.IsSystem,
IsLatest = item.IsLatest,
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs b/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs
index 1116beb80..d366bd17d 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/Workflow.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using Elsa.Common.Models;
using Elsa.Expressions.Models;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Contracts;
@@ -30,6 +31,7 @@ public class Workflow : Composite