diff --git a/Directory.Packages.props b/Directory.Packages.props
index 986818c65..ca4477da2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -30,10 +30,10 @@
-
+
-
+
diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
index 206866f3d..9d537dd43 100644
--- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
+++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
@@ -37,9 +37,6 @@
Always
-
- Always
-
diff --git a/src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa b/src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa
deleted file mode 100644
index 39439a8da..000000000
--- a/src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa
+++ /dev/null
@@ -1,16 +0,0 @@
-use Elsa.Activities.Console;
-
-workflow FlowchartTest {
- flowchart {
- var counter = 0;
-
- entry Start;
-
- Start: WriteLine("Starting flowchart");
- CheckCondition: WriteLine("Checking condition");
- EndNode: WriteLine("Done!");
-
- Start -> CheckCondition;
- CheckCondition.Done -> EndNode;
- }
-}
diff --git a/src/apps/Elsa.Server.Web/Workflows/multi-workflow-example.elsa b/src/apps/Elsa.Server.Web/Workflows/multi-workflow-example.elsa
deleted file mode 100644
index 05998558e..000000000
--- a/src/apps/Elsa.Server.Web/Workflows/multi-workflow-example.elsa
+++ /dev/null
@@ -1,29 +0,0 @@
-use Elsa.Activities.Console;
-
-workflow HelloWorldDsl(
- DisplayName: "Hello World DSL",
- Description: "Demonstrates a simple ElsaScript workflow with metadata.",
- DefinitionId: "hello-world-dsl",
- DefinitionVersionId: "hello-world-dsl-v1",
- Version: 1,
- UsableAsActivity: true
-)
-{
- use expressions js;
-
- WriteLine("Hello World from Elsa DSL!");
-}
-
-workflow HelloWorldAsActivity(
- DisplayName: "Hello World as Activity",
- Description: "Can be embedded as an activity in other workflows.",
- DefinitionId: "hello-world-activity",
- DefinitionVersionId: "hello-world-activity-v1",
- Version: 1,
- UsableAsActivity: true
-)
-{
- use expressions js;
-
- WriteLine(`Inner hello at ${Date.now()}`);
-}
diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Elsa.Persistence.EFCore.MySql.csproj b/src/modules/Elsa.Persistence.EFCore.MySql/Elsa.Persistence.EFCore.MySql.csproj
index a96f6299b..3ed3c1a69 100644
--- a/src/modules/Elsa.Persistence.EFCore.MySql/Elsa.Persistence.EFCore.MySql.csproj
+++ b/src/modules/Elsa.Persistence.EFCore.MySql/Elsa.Persistence.EFCore.MySql.csproj
@@ -1,6 +1,7 @@
-
+
+ net8.0;net9.0
Provides MySQL EF Core migrations for various modules.
@@ -8,12 +9,12 @@
-
-
+
+
-
+
\ No newline at end of file
diff --git a/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/BlobExtensions.cs b/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/BlobExtensions.cs
new file mode 100644
index 000000000..c0641b7ad
--- /dev/null
+++ b/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/BlobExtensions.cs
@@ -0,0 +1,8 @@
+using FluentStorage.Blobs;
+
+namespace Elsa.Extensions;
+
+public static class BlobExtensions
+{
+ public static string GetExtension(this Blob blob) => Path.GetExtension(blob.Name).TrimStart('.');
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/BlobWorkflowFormatHandlerExtensions.cs b/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/BlobWorkflowFormatHandlerExtensions.cs
new file mode 100644
index 000000000..e2d794dfa
--- /dev/null
+++ b/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/BlobWorkflowFormatHandlerExtensions.cs
@@ -0,0 +1,8 @@
+using Elsa.WorkflowProviders.BlobStorage.Contracts;
+
+namespace Elsa.Extensions;
+
+public static class BlobWorkflowFormatHandlerExtensions
+{
+ public static bool SupportsExtension(this IBlobWorkflowFormatHandler handler, string extension) => handler.SupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.WorkflowProviders.BlobStorage/Providers/BlobStorageWorkflowsProvider.cs b/src/modules/Elsa.WorkflowProviders.BlobStorage/Providers/BlobStorageWorkflowsProvider.cs
index bfc28b143..75de5fe9b 100644
--- a/src/modules/Elsa.WorkflowProviders.BlobStorage/Providers/BlobStorageWorkflowsProvider.cs
+++ b/src/modules/Elsa.WorkflowProviders.BlobStorage/Providers/BlobStorageWorkflowsProvider.cs
@@ -1,4 +1,5 @@
-using Elsa.WorkflowProviders.BlobStorage.Contracts;
+using Elsa.Extensions;
+using Elsa.WorkflowProviders.BlobStorage.Contracts;
using Elsa.Workflows.Runtime;
using FluentStorage.Blobs;
using JetBrains.Annotations;
@@ -71,11 +72,13 @@ public class BlobStorageWorkflowsProvider : IWorkflowsProvider
{
var blobStorage = _blobStorageProvider.GetBlobStorage();
var content = await blobStorage.ReadTextAsync(blob.FullPath, cancellationToken: cancellationToken);
-
var contentType = blob.Properties.TryGetValue("ContentType", out var ct) ? ct?.ToString() : null;
foreach (var handler in _handlers)
{
+ if (!handler.SupportsExtension(blob.GetExtension()))
+ continue;
+
if (!handler.CanHandle(blob, contentType))
continue;
diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs
index 28e017ca0..a07f8268e 100644
--- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs
+++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs
@@ -81,32 +81,53 @@ public class ActivityJsonConverter(
activityDescriptor = null;
activityTypeVersion = 0;
- // First try and find the activity by its workflow definition version id. This is a special case when working with the WorkflowDefinitionActivity.
- if (activityRoot.TryGetProperty("workflowDefinitionVersionId", out var workflowDefinitionVersionIdElement))
- {
- var workflowDefinitionVersionId = workflowDefinitionVersionIdElement.GetString();
- activityDescriptor = activityRegistry.Find(x =>
- x.CustomProperties.TryGetValue("WorkflowDefinitionVersionId", out var value) && (string?)value == workflowDefinitionVersionId);
- activityTypeVersion = activityDescriptor?.Version ?? 0;
- }
+ // First, we check whether the activity type name is a 'well-known' activity; not a workflow-as-activity
// If the activity type version is specified, use that to find the activity descriptor.
- if (activityDescriptor == null && activityRoot.TryGetProperty("version", out var activityVersionElement))
+ if (activityRoot.TryGetProperty("version", out var activityVersionElement))
{
activityTypeVersion = activityVersionElement.GetInt32();
activityDescriptor = activityRegistry.Find(activityTypeName, activityTypeVersion);
}
- // If the activity type version is not specified, use the latest version of the activity descriptor.
+ // If a version is not specified, or activity with specified version is not found: use the latest version of the activity descriptor.
if (activityDescriptor == null)
{
activityDescriptor = activityRegistry.Find(activityTypeName);
activityTypeVersion = activityDescriptor?.Version ?? 0;
}
+ // This is a special case when working with the WorkflowDefinitionActivity: workflowDefinitionVersionId should be used to find the workflow-as-activity.
+ if (activityRoot.TryGetProperty("workflowDefinitionVersionId", out var workflowDefinitionVersionIdElement))
+ {
+ var activityDescriptorOverride = FindActivityDescriptorByCustomProperty("WorkflowDefinitionVersionId", workflowDefinitionVersionIdElement);
+ if (activityDescriptorOverride is null)
+ return activityTypeName;
+
+ activityDescriptor = activityDescriptorOverride;
+ activityTypeVersion = activityDescriptor.Version;
+ }
+ // This is also a special case when working with the WorkflowDefinitionActivity: if no 'well-known' activity could be found, it might be a workflow-as-activity with a workflowDefinitionId
+ else if (activityDescriptor is null
+ && activityRoot.TryGetProperty("workflowDefinitionId", out var workflowDefinitionIdElement)
+ && workflowDefinitionIdElement.ValueKind == JsonValueKind.String)
+ {
+ activityDescriptor = FindActivityDescriptorByCustomProperty("WorkflowDefinitionId", workflowDefinitionIdElement);
+ activityTypeVersion = activityDescriptor?.Version ?? 0;
+ }
+
return activityTypeName;
}
+ private ActivityDescriptor? FindActivityDescriptorByCustomProperty(string customPropertyName, JsonElement valueElement)
+ {
+ if (valueElement.ValueKind != JsonValueKind.String)
+ return null;
+
+ var searchValue = valueElement.GetString();
+ return activityRegistry.Find(x => x.CustomProperties.TryGetValue(customPropertyName, out var value) && (string?)value == searchValue);
+ }
+
private JsonSerializerOptions GetClonedOptions(JsonSerializerOptions options)
{
var clonedOptions = new JsonSerializerOptions(options);
@@ -115,7 +136,7 @@ public class ActivityJsonConverter(
clonedOptions.Converters.Add(new ExpressionJsonConverterFactory(expressionDescriptorRegistry));
return clonedOptions;
}
-
+
private JsonSerializerOptions GetClonedWriterOptions(JsonSerializerOptions options)
{
var clonedOptions = GetClonedOptions(options);
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs
new file mode 100644
index 000000000..d8969c243
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs
@@ -0,0 +1,350 @@
+using System.Text.Json;
+using Elsa.Common.Serialization;
+using Elsa.Expressions.Services;
+using Elsa.Workflows.Activities;
+using Elsa.Workflows.Helpers;
+using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
+using Elsa.Workflows.Management.Services;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.Serialization.Configurators;
+using Elsa.Workflows.Serialization.Converters;
+using Elsa.Workflows.Serialization.Helpers;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+
+namespace Elsa.Workflows.Core.UnitTests.Serialization.Converters;
+
+public sealed class ActivityJsonConverterTests
+{
+ [Fact]
+ public void When_DeserializeKnownActivity_And_TypeNameSpecified_Then_FindsAndInstantiatesActivity()
+ {
+ // Arrange
+ var activityRegistry = CreateActivityRegistry(WriteLineActivityTypeName, WriteLineActivity);
+ var sut = CreateSut(activityRegistry);
+
+ // Act
+ var result = Execute(sut, WriteLineActivityJson_WithoutVersion);
+
+ // Assert
+ Assert.Same(WriteLineActivity, result);
+ }
+
+ [Fact]
+ public void When_DeserializeKnownActivity_And_TypeNameSpecified_And_VersionSpecified_Then_FindsAndInstantiatesActivity()
+ {
+ // Arrange
+ var activityRegistry = CreateActivityRegistry(WriteLineActivityTypeName, WriteLineActivity, version: 1);
+ var sut = CreateSut(activityRegistry);
+
+ // Act
+ var result = Execute(sut, WriteLineActivityJson_WithVersion);
+
+ // Assert
+ Assert.Same(WriteLineActivity, result);
+ }
+
+ [Fact]
+ public void When_DeserializeUnknownActivity_Then_ReturnsNotFoundActivity()
+ {
+ // Arrange
+ var activityRegistry = Substitute.For();
+ activityRegistry
+ .Find(NotFoundActivityTypeName)
+ .Returns(new ActivityDescriptor());
+
+ var sut = CreateSut(activityRegistry);
+
+ // Act
+ var result = Execute(sut, UnknownActivityJson);
+
+ // Assert
+ Assert.IsType(result);
+ var notFoundActivity = (NotFoundActivity)result;
+ Assert.Equal(UnknownActivityTypeName, notFoundActivity.MissingTypeName);
+ Assert.Equal(0, notFoundActivity.MissingTypeVersion);
+
+ var expectedJsonDoc = JsonDocument.Parse(UnknownActivityJson);
+ var actualJsonDoc = JsonDocument.Parse(notFoundActivity.OriginalActivityJson);
+ Assert.Equal(expectedJsonDoc.RootElement.ToString(), actualJsonDoc.RootElement.ToString());
+ Assert.True(notFoundActivity.Metadata.ContainsKey("displayText"));
+ Assert.True(notFoundActivity.Metadata.ContainsKey("description"));
+ }
+
+ [Fact]
+ public void When_DeserializeWorkflowAsActivity_And_WorkflowDefinitionIdSpecified_Then_FindsAndInstantiatesActivity()
+ {
+ // Arrange
+ var activityRegistry = CreateActivityRegistry_FindByCustomProperty(
+ WorkflowDefinitionIdCustomPropertyName,
+ WorkflowAsActivityDefinitionId
+ );
+
+ var sut = CreateSut(activityRegistry);
+
+ // Act
+ var result = Execute(sut, WorkflowAsActivityJson_WithDefinitionId);
+
+ // Assert
+ Assert.Same(WorkflowAsActivity, result);
+ }
+
+ [Fact]
+ public void When_DeserializeWorkflowAsActivity_And_WorkflowDefinitionVersionIdSpecified_Then_FindsAndInstantiatesActivity()
+ {
+ // Arrange
+ var activityRegistry = CreateActivityRegistry_FindByCustomProperty(
+ WorkflowDefinitionVersionIdCustomPropertyName,
+ WorkflowAsActivityDefinitionVersionId
+ );
+ var sut = CreateSut(activityRegistry);
+
+ // Act
+ var result = Execute(sut, WorkflowAsActivityJson_WithVersionId);
+
+ // Assert
+ Assert.Same(WorkflowAsActivity, result);
+ }
+
+ [Fact]
+ public void When_DeserializeWorkflowAsActivity_And_TypeNameSpecified_Then_FindsAndInstantiatesActivity()
+ {
+ // Arrange
+ var activityRegistry = CreateActivityRegistry(WorkflowAsActivityTypeName, WorkflowAsActivity);
+ var sut = CreateSut(activityRegistry);
+
+ // Act
+ var result = Execute(sut, WorkflowAsActivityJson_WithTypeNameOnly);
+
+ // Assert
+ Assert.Same(WorkflowAsActivity, result);
+ }
+
+ static IActivity? Execute(ActivityJsonConverter sut, string json) =>
+ JsonSerializer.Deserialize(json, GetSerializerOptions(sut));
+
+ static IActivityRegistry CreateActivityRegistry(string typeName, IActivity activity, int? version = null)
+ {
+ var descriptor = new ActivityDescriptor { Constructor = _ => activity };
+ var activityRegistry = Substitute.For();
+
+ if (version.HasValue)
+ activityRegistry.Find(typeName, version.Value).Returns(descriptor);
+ else
+ activityRegistry.Find(typeName).Returns(descriptor);
+
+ return activityRegistry;
+ }
+
+ static IActivityRegistry CreateActivityRegistry_FindByCustomProperty(string customPropertyName, string customPropertyValue)
+ {
+ var descriptor = new ActivityDescriptor
+ {
+ Constructor = _ => WorkflowAsActivity,
+ CustomProperties = { [customPropertyName] = customPropertyValue }
+ };
+
+ var activityRegistry = Substitute.For();
+
+ // Mock type name lookups to return null, ensuring the custom property lookup path is tested
+ activityRegistry.Find(Arg.Any()).Returns((ActivityDescriptor?)null);
+ activityRegistry.Find(Arg.Any(), Arg.Any()).Returns((ActivityDescriptor?)null);
+
+ activityRegistry
+ .Find(Arg.Any>())
+ .Returns(callInfo => callInfo.Arg>()(descriptor) ? descriptor : null);
+
+ return activityRegistry;
+ }
+
+ static JsonSerializerOptions GetSerializerOptions(ActivityJsonConverter sut) => new()
+ {
+ Converters =
+ {
+ sut,
+ new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault()),
+ new PolymorphicObjectConverterFactory()
+ },
+ TypeInfoResolver = new ModifiableJsonTypeInfoResolver(new CustomConstructorConfigurator().GetModifiers())
+ };
+
+ private const string UnknownActivityTypeName = "Some.Unknown.Activity";
+ private const string WorkflowAsActivityTypeName = "Test-SubWorkflow";
+ private const string WorkflowDefinitionIdCustomPropertyName = "WorkflowDefinitionId";
+ private const string WorkflowDefinitionVersionIdCustomPropertyName = "WorkflowDefinitionVersionId";
+ private const string WorkflowAsActivityDefinitionId = "bd94124913202141";
+ private const string WorkflowAsActivityDefinitionVersionId = "aaa1112fff2443";
+ private static readonly WriteLine WriteLineActivity = new("Hello world!");
+ private static readonly string WriteLineActivityTypeName = ActivityTypeNameHelper.GenerateTypeName();
+ private static readonly string NotFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName();
+
+ private const string WriteLineActivityJson_WithVersion =
+"""
+ {
+ "text": {
+ "typeName": "String",
+ "expression": {
+ "type": "Literal",
+ "value": "Hello world!"
+ }
+ },
+ "id": "b54b024aa7469287",
+ "nodeId": "Workflow1:51139b0cc83d3b07:b54b024aa7469287",
+ "name": "WriteLine1",
+ "type": "Elsa.WriteLine",
+ "version": 1,
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false
+ },
+ "metadata": {}
+ }
+ """;
+
+ private const string UnknownActivityJson =
+"""
+{
+ "text": {
+ "typeName": "String",
+ "expression": {
+ "type": "Literal",
+ "value": "Hello world!"
+ }
+ },
+ "id": "b54b024aa7469287",
+ "nodeId": "Workflow1:51139b0cc83d3b07:b54b024aa7469287",
+ "name": "Unknown1",
+ "type": "Some.Unknown.Activity",
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false
+ },
+ "metadata": {}
+}
+""";
+
+ private const string WriteLineActivityJson_WithoutVersion =
+"""
+{
+ "text": {
+ "typeName": "String",
+ "expression": {
+ "type": "Literal",
+ "value": "Hello world!"
+ }
+ },
+ "id": "b54b024aa7469287",
+ "nodeId": "Workflow1:51139b0cc83d3b07:b54b024aa7469287",
+ "name": "WriteLine1",
+ "type": "Elsa.WriteLine",
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false
+ },
+ "metadata": {}
+}
+""";
+
+ private const string WorkflowAsActivityJson_WithVersionId =
+"""
+{
+ "workflowDefinitionVersionId": "aaa1112fff2443",
+ "id": "5fc551ed366fe9fd",
+ "nodeId": "Workflow2:cad9d2186550a8d7:5fc551ed366fe9fd",
+ "name": "Test-SubWorkflow1",
+ "type": "Test-SubWorkflow",
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false,
+ "logPersistenceMode": {
+ "default": "Inherit",
+ "inputs": {},
+ "outputs": {}
+ },
+ "logPersistenceConfig": {
+ "default": null,
+ "internalState": null,
+ "inputs": {},
+ "outputs": {}
+ }
+ },
+ "metadata": {}
+}
+""";
+
+ private const string WorkflowAsActivityJson_WithDefinitionId =
+"""
+{
+ "workflowDefinitionId": "bd94124913202141",
+ "id": "5fc551ed366fe9fd",
+ "nodeId": "Workflow2:cad9d2186550a8d7:5fc551ed366fe9fd",
+ "name": "Test-SubWorkflow1",
+ "type": "Test-SubWorkflow",
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false,
+ "logPersistenceMode": {
+ "default": "Inherit",
+ "inputs": {},
+ "outputs": {}
+ },
+ "logPersistenceConfig": {
+ "default": null,
+ "internalState": null,
+ "inputs": {},
+ "outputs": {}
+ }
+ },
+ "metadata": {}
+}
+""";
+
+ private const string WorkflowAsActivityJson_WithTypeNameOnly =
+"""
+{
+ "id": "5fc551ed366fe9fd",
+ "nodeId": "Workflow2:cad9d2186550a8d7:5fc551ed366fe9fd",
+ "name": "Test-SubWorkflow1",
+ "type": "Test-SubWorkflow",
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false,
+ "logPersistenceMode": {
+ "default": "Inherit",
+ "inputs": {},
+ "outputs": {}
+ },
+ "logPersistenceConfig": {
+ "default": null,
+ "internalState": null,
+ "inputs": {},
+ "outputs": {}
+ }
+ },
+ "metadata": {}
+}
+""";
+
+ private static readonly IActivity WorkflowAsActivity = new WorkflowDefinitionActivity
+ {
+ WorkflowDefinitionId = WorkflowAsActivityDefinitionId,
+ WorkflowDefinitionVersionId = WorkflowAsActivityDefinitionVersionId
+ };
+
+ private static ActivityJsonConverter CreateSut(IActivityRegistry activityRegistry)
+ {
+ var expressionDescriptorRegistry = new ExpressionDescriptorRegistry([]);
+ return new(
+ activityRegistry,
+ expressionDescriptorRegistry,
+ new(
+ activityRegistry,
+ new(expressionDescriptorRegistry),
+ CreateLogger()
+ ),
+ Substitute.For()
+ );
+ }
+
+ private static ILogger CreateLogger() => LoggerFactory.Create(_ => { }).CreateLogger();
+}
\ No newline at end of file