fix: register built-in activation strategy serialization aliases (#8049)
* fix: register built-in activation strategy serialization aliases 3.8 type-resolution hardening no longer falls back to Type.GetType, so Studio-selected and 3.7-era SQN activation strategy types failed to deserialize. Register the four built-in strategies with legacy names and advertise registry aliases from the descriptor endpoint. Fixes https://github.com/elsa-workflows/elsa-core/issues/8049 Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * test: DRY activation strategy serialization test setup Extract shared registry/JSON arrange helpers so activation alias tests share setup without collapsing their assertions. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * test: qualify Options.Create in serialization test helpers Avoid Elsa.Options colliding with Microsoft.Extensions.Options.Options. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * test: keep activation alias setup local and obvious Drop the shared Testing.Shared helper and extra Runtime wrapper. Those hid one-line registry construction. Leave same-class helpers only. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix: advertise resolvable activation strategy type names When the registry has no preferred alias, fall back to the simple assembly-qualified name so Studio stores an identifier the resolver can load. FullName is not a registered alias. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
parent
8191ae3055
commit
4f90cfbbdf
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns list of available <see cref="IWorkflowActivationStrategy" /> implementations.
|
||||
/// </summary>
|
||||
internal class List(IEnumerable<IWorkflowActivationStrategy> strategies) : ElsaEndpointWithoutRequest<ListResponse<WorkflowActivationStrategyDescriptor>>
|
||||
internal class List(IEnumerable<IWorkflowActivationStrategy> strategies, ISerializationTypeRegistry workflowJsonTypeRegistry) : ElsaEndpointWithoutRequest<ListResponse<WorkflowActivationStrategyDescriptor>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -21,15 +23,15 @@ internal class List(IEnumerable<IWorkflowActivationStrategy> strategies) : ElsaE
|
|||
|
||||
public override Task<ListResponse<WorkflowActivationStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var descriptors = strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
|
||||
var response =new ListResponse<WorkflowActivationStrategyDescriptor>(descriptors);
|
||||
var descriptors = strategies.Select(x => WorkflowActivationStrategyDescriptor.FromStrategy(x, workflowJsonTypeRegistry)).OrderBy(x => x.DisplayName).ToList();
|
||||
var response = new ListResponse<WorkflowActivationStrategyDescriptor>(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<DisplayNameAttribute>();
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -183,6 +183,7 @@ public class WorkflowsFeature : FeatureBase
|
|||
options.AddTypeAliasWithLegacyName<MemoryStorageDriver>(nameof(MemoryStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<FaultStrategy>(nameof(FaultStrategy));
|
||||
options.AddTypeAliasWithLegacyName<ContinueWithIncidentsStrategy>(nameof(ContinueWithIncidentsStrategy));
|
||||
options.AddTypeAliasWithLegacyName<AllowAlwaysStrategy>(nameof(AllowAlwaysStrategy));
|
||||
options.AddTypeAlias<Exception>(nameof(Exception));
|
||||
options.AddTypeAlias<ArgumentException>(nameof(ArgumentException));
|
||||
options.AddTypeAlias<ArgumentNullException>(nameof(ArgumentNullException));
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ public class WorkflowsFeature : IShellFeature
|
|||
options.AddTypeAliasWithLegacyName<MemoryStorageDriver>(nameof(MemoryStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<FaultStrategy>(nameof(FaultStrategy));
|
||||
options.AddTypeAliasWithLegacyName<ContinueWithIncidentsStrategy>(nameof(ContinueWithIncidentsStrategy));
|
||||
options.AddTypeAliasWithLegacyName<AllowAlwaysStrategy>(nameof(AllowAlwaysStrategy));
|
||||
options.AddTypeAlias<Exception>(nameof(Exception));
|
||||
options.AddTypeAlias<ArgumentException>(nameof(ArgumentException));
|
||||
options.AddTypeAlias<ArgumentNullException>(nameof(ArgumentNullException));
|
||||
|
|
|
|||
|
|
@ -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<EventStimulus>();
|
||||
options.AddTypeAlias<ExecuteWorkflowStimulus>();
|
||||
options.AddTypeAlias<RunTaskStimulus>();
|
||||
options.AddTypeAliasWithLegacyName<SingletonStrategy>(nameof(SingletonStrategy));
|
||||
options.AddTypeAliasWithLegacyName<CorrelatedSingletonStrategy>(nameof(CorrelatedSingletonStrategy));
|
||||
options.AddTypeAliasWithLegacyName<CorrelationStrategy>(nameof(CorrelationStrategy));
|
||||
|
||||
foreach (var workflowType in workflowTypes.Where(IsConcreteWorkflowType).Distinct())
|
||||
options.RegisterTypeAlias(workflowType, workflowType.GetSimpleAssemblyQualifiedName());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ISerializationTypeRegistry>();
|
||||
|
||||
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<IApiSerializer>();
|
||||
var strategyType = typeof(Elsa.Workflows.ActivationValidators.AllowAlwaysStrategy);
|
||||
|
||||
var byAlias = serializer.Deserialize<WorkflowOptions>("""{"activationStrategyType":"AllowAlwaysStrategy"}""");
|
||||
Assert.Equal(strategyType, byAlias.ActivationStrategyType);
|
||||
|
||||
var byLegacyName = serializer.Deserialize<WorkflowOptions>($$"""{"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<WorkflowOptions>(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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Type, string> 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<WorkflowOptions>($$"""{"activationStrategyType":{{JsonSerializer.Serialize(alias)}}}""", options);
|
||||
Assert.Equal(strategyType, byAlias!.ActivationStrategyType);
|
||||
|
||||
var byLegacyName = JsonSerializer.Deserialize<WorkflowOptions>($$"""{"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<WorkflowOptions>(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()) }
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue