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()) } + }; +}