diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs
index 3dcc82187..df85f5c79 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs
@@ -4,14 +4,16 @@ using System.Reflection;
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Models;
+using Elsa.Workflows;
using Humanizer;
+using Elsa.Common.Serialization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowActivationStrategies.List;
///
/// Returns list of available implementations.
///
-internal class List(IEnumerable strategies) : ElsaEndpointWithoutRequest>
+internal class List(IEnumerable strategies, ISerializationTypeRegistry workflowJsonTypeRegistry) : ElsaEndpointWithoutRequest>
{
public override void Configure()
{
@@ -21,15 +23,15 @@ internal class List(IEnumerable strategies) : ElsaE
public override Task> ExecuteAsync(CancellationToken cancellationToken)
{
- var descriptors = strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
- var response =new ListResponse(descriptors);
+ var descriptors = strategies.Select(x => WorkflowActivationStrategyDescriptor.FromStrategy(x, workflowJsonTypeRegistry)).OrderBy(x => x.DisplayName).ToList();
+ var response = new ListResponse(descriptors);
return Task.FromResult(response);
}
}
internal record WorkflowActivationStrategyDescriptor(string DisplayName, string Description, string TypeName)
{
- public static WorkflowActivationStrategyDescriptor FromStrategy(IWorkflowActivationStrategy strategy)
+ public static WorkflowActivationStrategyDescriptor FromStrategy(IWorkflowActivationStrategy strategy, ISerializationTypeRegistry workflowJsonTypeRegistry)
{
var type = strategy.GetType();
var displayNameAttribute = type.GetCustomAttribute();
@@ -38,6 +40,7 @@ internal record WorkflowActivationStrategyDescriptor(string DisplayName, string
var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize();
var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? "";
- return new WorkflowActivationStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName());
+ var typeName = workflowJsonTypeRegistry.TryGetAlias(type, out var alias) ? alias : type.GetSimpleAssemblyQualifiedName();
+ return new WorkflowActivationStrategyDescriptor(displayName, description, typeName);
}
}
\ 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 dbbd11a85..564f5ac67 100644
--- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
@@ -183,6 +183,7 @@ public class WorkflowsFeature : FeatureBase
options.AddTypeAliasWithLegacyName(nameof(MemoryStorageDriver));
options.AddTypeAliasWithLegacyName(nameof(FaultStrategy));
options.AddTypeAliasWithLegacyName(nameof(ContinueWithIncidentsStrategy));
+ options.AddTypeAliasWithLegacyName(nameof(AllowAlwaysStrategy));
options.AddTypeAlias(nameof(Exception));
options.AddTypeAlias(nameof(ArgumentException));
options.AddTypeAlias(nameof(ArgumentNullException));
diff --git a/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs
index dd2815811..3d585fa92 100644
--- a/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs
@@ -104,6 +104,7 @@ public class WorkflowsFeature : IShellFeature
options.AddTypeAliasWithLegacyName(nameof(MemoryStorageDriver));
options.AddTypeAliasWithLegacyName(nameof(FaultStrategy));
options.AddTypeAliasWithLegacyName(nameof(ContinueWithIncidentsStrategy));
+ options.AddTypeAliasWithLegacyName(nameof(AllowAlwaysStrategy));
options.AddTypeAlias(nameof(Exception));
options.AddTypeAlias(nameof(ArgumentException));
options.AddTypeAlias(nameof(ArgumentNullException));
diff --git a/src/modules/Elsa.Workflows.Runtime/WorkflowRuntimeTypeAliasRegistrar.cs b/src/modules/Elsa.Workflows.Runtime/WorkflowRuntimeTypeAliasRegistrar.cs
index 31a52542d..d764a778e 100644
--- a/src/modules/Elsa.Workflows.Runtime/WorkflowRuntimeTypeAliasRegistrar.cs
+++ b/src/modules/Elsa.Workflows.Runtime/WorkflowRuntimeTypeAliasRegistrar.cs
@@ -1,6 +1,7 @@
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Options;
+using Elsa.Workflows.Runtime.ActivationValidators;
using Elsa.Workflows.Runtime.Bookmarks;
using Elsa.Workflows.Runtime.Stimuli;
using Elsa.Common.Serialization;
@@ -24,6 +25,9 @@ internal static class WorkflowRuntimeTypeAliasRegistrar
options.AddTypeAlias();
options.AddTypeAlias();
options.AddTypeAlias();
+ options.AddTypeAliasWithLegacyName(nameof(SingletonStrategy));
+ options.AddTypeAliasWithLegacyName(nameof(CorrelatedSingletonStrategy));
+ options.AddTypeAliasWithLegacyName(nameof(CorrelationStrategy));
foreach (var workflowType in workflowTypes.Where(IsConcreteWorkflowType).Distinct())
options.RegisterTypeAlias(workflowType, workflowType.GetSimpleAssemblyQualifiedName());
diff --git a/test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/WorkflowActivationStrategies/ListTests.cs b/test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/WorkflowActivationStrategies/ListTests.cs
new file mode 100644
index 000000000..aefeae717
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/WorkflowActivationStrategies/ListTests.cs
@@ -0,0 +1,45 @@
+using Elsa.Extensions;
+using Elsa.Workflows.ActivationValidators;
+using Elsa.Workflows.Api.Endpoints.WorkflowActivationStrategies.List;
+using Elsa.Workflows.Options;
+using Elsa.Workflows.Services;
+using Elsa.Common.Serialization;
+
+namespace Elsa.Workflows.Api.UnitTests.Endpoints.WorkflowActivationStrategies;
+
+public class ListTests
+{
+ [Fact]
+ public async Task ExecuteAsync_ReturnsWorkflowJsonTypeIdentifier_ForActivationStrategyTypeName()
+ {
+ var options = new SerializationTypeOptions();
+ options.RegisterTypeAlias(typeof(AllowAlwaysStrategy), nameof(AllowAlwaysStrategy));
+ options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AllowAlwaysStrategy));
+ var registry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(options));
+ var endpoint = new List([new AllowAlwaysStrategy()], registry);
+
+ var response = await endpoint.ExecuteAsync(CancellationToken.None);
+
+ var descriptor = Assert.Single(response.Items);
+ Assert.Equal(nameof(AllowAlwaysStrategy), descriptor.TypeName);
+ Assert.True(registry.TryGetType(typeof(AllowAlwaysStrategy).GetSimpleAssemblyQualifiedName(), out var legacyType));
+ Assert.Equal(typeof(AllowAlwaysStrategy), legacyType);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WhenOnlyLegacyNameIsRegistered_AdvertisesResolvableIdentifier()
+ {
+ var options = new SerializationTypeOptions();
+ options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AllowAlwaysStrategy));
+ var registry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(options));
+ var endpoint = new List([new AllowAlwaysStrategy()], registry);
+
+ var response = await endpoint.ExecuteAsync(CancellationToken.None);
+
+ var descriptor = Assert.Single(response.Items);
+ var expectedTypeName = typeof(AllowAlwaysStrategy).GetSimpleAssemblyQualifiedName();
+ Assert.Equal(expectedTypeName, descriptor.TypeName);
+ Assert.True(registry.TryGetType(descriptor.TypeName, out var resolvedType));
+ Assert.Equal(typeof(AllowAlwaysStrategy), resolvedType);
+ }
+}
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs
index e392d6d09..a1aa92d9a 100644
--- a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs
@@ -5,6 +5,7 @@ using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.Workflows.Exceptions;
using Elsa.Workflows.Memory;
+using Elsa.Workflows.Models;
using Elsa.Workflows.Options;
using Elsa.Workflows.Serialization.Converters;
using Elsa.Workflows.Services;
@@ -153,11 +154,7 @@ public sealed class SerializationTypeResolverTests
[Fact]
public void When_ConfigureWorkflowsFeature_Then_RegistersCoreAliases()
{
- var services = new ServiceCollection();
- var module = services.CreateModule();
- module.UseWorkflows();
- module.Apply();
- using var serviceProvider = services.BuildServiceProvider();
+ using var serviceProvider = CreateWorkflowsServices();
var registry = serviceProvider.GetRequiredService();
var aliasRegistered = registry.TryGetAlias(typeof(NullReferenceException), out var alias);
@@ -177,6 +174,31 @@ public sealed class SerializationTypeResolverTests
Assert.Equal(nameof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy), incidentStrategyAlias);
Assert.True(registry.TryGetType(typeof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy).GetSimpleAssemblyQualifiedName(), out var legacyIncidentStrategyType));
Assert.Equal(typeof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy), legacyIncidentStrategyType);
+ Assert.True(registry.TryGetAlias(typeof(Elsa.Workflows.ActivationValidators.AllowAlwaysStrategy), out var activationStrategyAlias));
+ Assert.Equal(nameof(Elsa.Workflows.ActivationValidators.AllowAlwaysStrategy), activationStrategyAlias);
+ Assert.True(registry.TryGetType(typeof(Elsa.Workflows.ActivationValidators.AllowAlwaysStrategy).GetSimpleAssemblyQualifiedName(), out var legacyActivationStrategyType));
+ Assert.Equal(typeof(Elsa.Workflows.ActivationValidators.AllowAlwaysStrategy), legacyActivationStrategyType);
+ }
+
+ [Fact]
+ public void When_ConfigureWorkflowsFeature_Then_WorkflowOptionsRoundTripActivationStrategyType()
+ {
+ using var serviceProvider = CreateWorkflowsServices();
+ var serializer = serviceProvider.GetRequiredService();
+ var strategyType = typeof(Elsa.Workflows.ActivationValidators.AllowAlwaysStrategy);
+
+ var byAlias = serializer.Deserialize("""{"activationStrategyType":"AllowAlwaysStrategy"}""");
+ Assert.Equal(strategyType, byAlias.ActivationStrategyType);
+
+ var byLegacyName = serializer.Deserialize($$"""{"activationStrategyType":{{JsonString(strategyType.GetSimpleAssemblyQualifiedName())}}}""");
+ Assert.Equal(strategyType, byLegacyName.ActivationStrategyType);
+
+ var serialized = serializer.Serialize(new WorkflowOptions { ActivationStrategyType = strategyType });
+ Assert.Contains("AllowAlwaysStrategy", serialized);
+ Assert.DoesNotContain("UnregisteredClrType:", serialized);
+
+ var roundTrip = serializer.Deserialize(serialized);
+ Assert.Equal(strategyType, roundTrip.ActivationStrategyType);
}
[Fact]
@@ -338,6 +360,15 @@ public sealed class SerializationTypeResolverTests
Assert.Equal(typeof(LateRegisteredPayload), result);
}
+ private static ServiceProvider CreateWorkflowsServices()
+ {
+ var services = new ServiceCollection();
+ var module = services.CreateModule();
+ module.UseWorkflows();
+ module.Apply();
+ return services.BuildServiceProvider();
+ }
+
private static JsonSerializerOptions CreateOptions(ISerializationTypeRegistry workflowJsonTypeRegistry) => new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Features/WorkflowRuntimeFeatureTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Features/WorkflowRuntimeFeatureTests.cs
index ea633c152..110a5c7dc 100644
--- a/test/unit/Elsa.Workflows.Runtime.UnitTests/Features/WorkflowRuntimeFeatureTests.cs
+++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Features/WorkflowRuntimeFeatureTests.cs
@@ -1,17 +1,18 @@
using System.Reflection;
+using Elsa.Common.Serialization;
+using Elsa.Extensions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
-using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Features;
using Elsa.Workflows.Options;
+using Elsa.Workflows.Runtime.ActivationValidators;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Providers;
using NSubstitute;
using RuntimeFeature = Elsa.Workflows.Runtime.Features.WorkflowRuntimeFeature;
using ShellRuntimeFeature = Elsa.Workflows.Runtime.ShellFeatures.WorkflowRuntimeFeature;
-using Elsa.Common.Serialization;
namespace Elsa.Workflows.Runtime.UnitTests.Features;
@@ -143,6 +144,32 @@ public class WorkflowRuntimeFeatureTests
Assert.Contains(typeof(WorkflowsFeature), dependencyTypes);
}
+ [Fact]
+ public void RegisterWorkflowTypeAliases_RegistersBuiltInActivationStrategyAliases()
+ {
+ var options = new SerializationTypeOptions();
+
+ RegisterWorkflowTypeAliases(_feature, options);
+ var registry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(options));
+
+ AssertActivationStrategyAlias(options, registry, typeof(SingletonStrategy), nameof(SingletonStrategy));
+ AssertActivationStrategyAlias(options, registry, typeof(CorrelatedSingletonStrategy), nameof(CorrelatedSingletonStrategy));
+ AssertActivationStrategyAlias(options, registry, typeof(CorrelationStrategy), nameof(CorrelationStrategy));
+ }
+
+ [Fact]
+ public void ShellRegisterWorkflowTypeAliases_RegistersBuiltInActivationStrategyAliases()
+ {
+ var options = new SerializationTypeOptions();
+
+ RegisterWorkflowTypeAliases(_shellFeature, options);
+ var registry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(options));
+
+ AssertActivationStrategyAlias(options, registry, typeof(SingletonStrategy), nameof(SingletonStrategy));
+ AssertActivationStrategyAlias(options, registry, typeof(CorrelatedSingletonStrategy), nameof(CorrelatedSingletonStrategy));
+ AssertActivationStrategyAlias(options, registry, typeof(CorrelationStrategy), nameof(CorrelationStrategy));
+ }
+
[Fact]
public void RegisterWorkflowTypeAliases_RegistersOnlyTrackedWorkflowTypes()
{
@@ -211,4 +238,12 @@ public class WorkflowRuntimeFeatureTests
.GetMethod("RegisterWorkflowTypeAliases", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(feature, new object[] { options });
}
+
+ private static void AssertActivationStrategyAlias(SerializationTypeOptions options, ISerializationTypeRegistry registry, Type strategyType, string alias)
+ {
+ Assert.Equal(strategyType, options.AliasTypeDictionary[alias]);
+ Assert.Equal(alias, options.TypeAliasDictionary[strategyType]);
+ Assert.True(registry.TryGetType(strategyType.GetSimpleAssemblyQualifiedName(), out var legacyType));
+ Assert.Equal(strategyType, legacyType);
+ }
}
diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Serialization/ActivationStrategyTypeAliasTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Serialization/ActivationStrategyTypeAliasTests.cs
new file mode 100644
index 000000000..d0b7d6169
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Serialization/ActivationStrategyTypeAliasTests.cs
@@ -0,0 +1,68 @@
+using System.Text.Json;
+using Elsa.Common.Serialization;
+using Elsa.Extensions;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.Options;
+using Elsa.Workflows.Runtime.ActivationValidators;
+using Elsa.Workflows.Serialization.Converters;
+using Elsa.Workflows.Services;
+
+namespace Elsa.Workflows.Runtime.UnitTests.Serialization;
+
+public class ActivationStrategyTypeAliasTests
+{
+ public static TheoryData BuiltInActivationStrategies() => new()
+ {
+ { typeof(SingletonStrategy), nameof(SingletonStrategy) },
+ { typeof(CorrelatedSingletonStrategy), nameof(CorrelatedSingletonStrategy) },
+ { typeof(CorrelationStrategy), nameof(CorrelationStrategy) }
+ };
+
+ [Theory]
+ [MemberData(nameof(BuiltInActivationStrategies))]
+ public void When_RegisterRuntimeAliases_Then_ResolvesPreferredAndLegacyNames(Type strategyType, string alias)
+ {
+ var registry = CreateRegistry();
+
+ Assert.True(registry.TryGetAlias(strategyType, out var registeredAlias));
+ Assert.Equal(alias, registeredAlias);
+ Assert.True(registry.TryGetType(alias, out var byAlias));
+ Assert.Equal(strategyType, byAlias);
+ Assert.True(registry.TryGetType(strategyType.GetSimpleAssemblyQualifiedName(), out var byLegacyName));
+ Assert.Equal(strategyType, byLegacyName);
+ }
+
+ [Theory]
+ [MemberData(nameof(BuiltInActivationStrategies))]
+ public void When_DeserializeWorkflowOptions_Then_ResolvesActivationStrategyType(Type strategyType, string alias)
+ {
+ var options = CreateJsonOptions();
+
+ var byAlias = JsonSerializer.Deserialize($$"""{"activationStrategyType":{{JsonSerializer.Serialize(alias)}}}""", options);
+ Assert.Equal(strategyType, byAlias!.ActivationStrategyType);
+
+ var byLegacyName = JsonSerializer.Deserialize($$"""{"activationStrategyType":{{JsonSerializer.Serialize(strategyType.GetSimpleAssemblyQualifiedName())}}}""", options);
+ Assert.Equal(strategyType, byLegacyName!.ActivationStrategyType);
+
+ var serialized = JsonSerializer.Serialize(new WorkflowOptions { ActivationStrategyType = strategyType }, options);
+ Assert.Contains(alias, serialized);
+ Assert.DoesNotContain("UnregisteredClrType:", serialized);
+
+ var roundTrip = JsonSerializer.Deserialize(serialized, options);
+ Assert.Equal(strategyType, roundTrip!.ActivationStrategyType);
+ }
+
+ private static ISerializationTypeRegistry CreateRegistry()
+ {
+ var options = new SerializationTypeOptions();
+ WorkflowRuntimeTypeAliasRegistrar.Register(options, []);
+ return new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(options));
+ }
+
+ private static JsonSerializerOptions CreateJsonOptions() => new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ Converters = { new TypeJsonConverter(CreateRegistry()) }
+ };
+}