From dff2576fbe5aa4d866b1e1dac77a6f2e36c4b23a Mon Sep 17 00:00:00 2001 From: Max Brooks <45081361+MaxBrooks114@users.noreply.github.com> Date: Fri, 30 May 2025 13:51:32 -0400 Subject: [PATCH] Generic activity name fix (#6698) * Refactor ActivityRegistry to populate all activities in workflow editor page - Modify ListAll to return distinct activity descriptors. - Update RegisterAsync to call Add with correct parameters. - Refactor RefreshDescriptorsAsync for better collection usage. - Split Add method into two overloads for clarity. - Improve logging for replacing existing activity descriptors. * Add FuncExpressionValueConverter for JSON serialization Implemented FuncExpressionValueConverter to handle serialization and deserialization of Func> types, ensuring delegates are not serialized and cannot be rehydrated from JSON. Updated multiple serializers including ApiSerializer, BookmarkPayloadSerializer, JsonActivitySerializer, JsonPayloadSerializer, JsonWorkflowStateSerializer, and SafeSerializer to utilize the new converter in their JSON serialization options. * fix broken studio when when viewing suspended workflows * fix for tests * Refactor WorkflowStateExtractor for readability and safety Improved code formatting and consistency in the WorkflowStateExtractor class. Updated logic in several methods to enhance handling of input items, activity execution contexts, and completion callbacks. Replaced `First` with `FirstOrDefault` for safer item retrieval and added null checks to prevent exceptions. These changes improve overall code readability, maintainability, and safety within the workflow execution context. * Enhance ActivityDescriber with new functionality This commit introduces several improvements to the `ActivityDescriber` class, including: - A new `GetFriendlyActivityName` method for better naming of activity types. - Updates to `DescribeActivityAsync` to use the friendly name for `typeName` and `displayName`. - Refactoring of `flowPorts` initialization for improved readability. - Simplification of `GetInputProperties` and `GetOutputProperties` methods. - Streamlined creation of `OutputDescriptor` and `InputDescriptor` in their respective methods. - Addition of `DescribeInputPropertiesAsync` and `DescribeOutputPropertiesAsync` for asynchronous property descriptions. --------- Co-authored-by: Max Brooks Co-authored-by: Sipke Schoorstra --- .../Services/ActivityDescriber.cs | 57 +++++++++-------- .../Services/WorkflowStateExtractor.cs | 63 ++++++++++--------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index 9533f4652..919d45b38 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -22,11 +22,12 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve { var activityAttr = activityType.GetCustomAttribute(); var ns = activityAttr?.Namespace ?? ActivityTypeNameHelper.GenerateNamespace(activityType) ?? "Elsa"; - var typeName = activityAttr?.Type ?? activityType.Name; + var friendlyName = GetFriendlyActivityName(activityType); + var typeName = activityAttr?.Type ?? friendlyName; var typeVersion = activityAttr?.Version ?? 1; var fullTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType); var displayNameAttr = activityType.GetCustomAttribute(); - var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title); + var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? friendlyName.Humanize(LetterCasing.Title); var categoryAttr = activityType.GetCustomAttribute(); var category = categoryAttr?.Category ?? activityAttr?.Category ?? ActivityTypeNameHelper.GetCategoryFromNamespace(ns) ?? "Miscellaneous"; var descriptionAttr = activityType.GetCustomAttribute(); @@ -42,16 +43,19 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve Name = portAttr?.Name ?? prop.Name, DisplayName = portAttr?.DisplayName ?? portAttr?.Name ?? prop.Name, Type = PortType.Embedded, - IsBrowsable = portAttr != null && (portBrowsableAttr == null || portBrowsableAttr.Browsable) + IsBrowsable = portAttr != null && (portBrowsableAttr == null || portBrowsableAttr.Browsable), }; var flowNodeAttr = activityType.GetCustomAttribute(); - var flowPorts = flowNodeAttr?.Outcomes.Select(x => new Port - { - Type = PortType.Flow, - Name = x, - DisplayName = x - }).ToDictionary(x => x.Name) ?? new Dictionary(); + var flowPorts = + flowNodeAttr + ?.Outcomes.Select(x => new Port + { + Type = PortType.Flow, + Name = x, + DisplayName = x, + }) + .ToDictionary(x => x.Name) ?? new Dictionary(); var allPorts = embeddedPorts.Concat(flowPorts.Values); var inputProperties = GetInputProperties(activityType).ToList(); @@ -90,7 +94,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve var activity = activityFactory.Create(activityType, context); activity.Type = fullTypeName; return activity; - } + }, }; // If the activity has a default output, set its IsSerializable property to the value of the OutputAttribute.IsSerializable property. @@ -106,12 +110,10 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve } /// - public IEnumerable GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => - activityType.GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute() != null).DistinctBy(x => x.Name); + public IEnumerable GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => activityType.GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute() != null).DistinctBy(x => x.Name); /// - public IEnumerable GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => - activityType.GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList(); + public IEnumerable GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => activityType.GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList(); /// public Task DescribeOutputPropertyAsync(PropertyInfo propertyInfo, CancellationToken cancellationToken = default) @@ -121,18 +123,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve var typeArgs = propertyInfo.PropertyType.GenericTypeArguments; var wrappedPropertyType = typeArgs.Any() ? typeArgs[0] : typeof(object); - return Task.FromResult(new OutputDescriptor - ( - (outputAttribute?.Name ?? propertyInfo.Name).Pascalize(), - outputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title), - wrappedPropertyType, - propertyInfo.GetValue, - propertyInfo.SetValue, - propertyInfo, - descriptionAttribute?.Description ?? outputAttribute?.Description, - outputAttribute?.IsBrowsable ?? true, - outputAttribute?.IsSerializable - )); + return Task.FromResult(new OutputDescriptor((outputAttribute?.Name ?? propertyInfo.Name).Pascalize(), outputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title), wrappedPropertyType, propertyInfo.GetValue, propertyInfo.SetValue, propertyInfo, descriptionAttribute?.Description ?? outputAttribute?.Description, outputAttribute?.IsBrowsable ?? true, outputAttribute?.IsSerializable)); } /// @@ -150,8 +141,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve var uiSpecification = await propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken); - return new InputDescriptor - ( + return new InputDescriptor( inputAttribute?.Name ?? propertyInfo.Name, wrappedPropertyType, propertyInfo.GetValue, @@ -214,6 +204,15 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve return InputUIHints.SingleLine; } + private static string GetFriendlyActivityName(Type t) + { + if (!t.IsGenericType) + return t.Name; + var baseName = t.Name.Substring(0, t.Name.IndexOf('`')); + var argNames = string.Join(", ", t.GetGenericArguments().Select(a => a.Name)); + return $"{baseName}<{argNames}>"; + } + private async Task> DescribeInputPropertiesAsync(IEnumerable properties, CancellationToken cancellationToken = default) { return await Task.WhenAll(properties.Select(async x => await DescribeInputPropertyAsync(x, cancellationToken))); @@ -223,4 +222,4 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve { return await Task.WhenAll(properties.Select(async x => await DescribeOutputPropertyAsync(x, cancellationToken))); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index fb9c48f14..a2a545646 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -31,7 +31,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor IsSystem = workflowExecutionContext.Workflow.IsSystem, CreatedAt = workflowExecutionContext.CreatedAt, UpdatedAt = workflowExecutionContext.UpdatedAt, - FinishedAt = workflowExecutionContext.FinishedAt + FinishedAt = workflowExecutionContext.FinishedAt, }; ExtractProperties(state, workflowExecutionContext); @@ -64,12 +64,13 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor ApplyScheduledActivities(state, workflowExecutionContext); return workflowExecutionContext; } - + private void ApplyInput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { // Only add input from state if the input doesn't already exist on the workflow execution context. foreach (var inputItem in state.Input) - if (!workflowExecutionContext.Input.ContainsKey(inputItem.Key)) workflowExecutionContext.Input.Add(inputItem.Key, inputItem.Value); + if (!workflowExecutionContext.Input.ContainsKey(inputItem.Key)) + workflowExecutionContext.Input.Add(inputItem.Key, inputItem.Value); } private IDictionary GetPersistableInput(WorkflowExecutionContext workflowExecutionContext) @@ -102,24 +103,23 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor private static async Task ApplyActivityExecutionContextsAsync(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { - var activityExecutionContexts = (await Task.WhenAll( - state.ActivityExecutionContexts.Select(async item => await CreateActivityExecutionContextAsync(item)))) - .Where(x => x != null) - .Select(x => x!) - .ToList(); + var activityExecutionContexts = (await Task.WhenAll(state.ActivityExecutionContexts.Select(async item => await CreateActivityExecutionContextAsync(item)))).Where(x => x != null).Select(x => x!).ToList(); var lookup = activityExecutionContexts.ToDictionary(x => x.Id); // Reconstruct hierarchy. foreach (var contextState in state.ActivityExecutionContexts.Where(x => !string.IsNullOrWhiteSpace(x.ParentContextId))) { - var parentContext = lookup[contextState.ParentContextId!]; - var contextId = contextState.Id; - - if (lookup.TryGetValue(contextId, out var context)) + if (lookup.ContainsKey(contextState.ParentContextId)) { - context.ExpressionExecutionContext.ParentContext = parentContext.ExpressionExecutionContext; - context.ParentActivityExecutionContext = parentContext; + var parentContext = lookup[contextState.ParentContextId!]; + var contextId = contextState.Id; + + if (lookup.TryGetValue(contextId, out var context)) + { + context.ExpressionExecutionContext.ParentContext = parentContext.ExpressionExecutionContext; + context.ParentActivityExecutionContext = parentContext; + } } } @@ -144,10 +144,10 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor var activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity); activityExecutionContext.Id = activityExecutionContextState.Id; activityExecutionContext.Properties.Merge(properties); - - if(activityExecutionContextState.ActivityState != null) + + if (activityExecutionContextState.ActivityState != null) activityExecutionContext.ActivityState.Merge(activityExecutionContextState.ActivityState); - + activityExecutionContext.TransitionTo(activityExecutionContextState.Status); activityExecutionContext.IsExecuting = activityExecutionContextState.IsExecuting; activityExecutionContext.AggregateFaultCount = activityExecutionContextState.FaultCount; @@ -164,16 +164,19 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor { foreach (var completionCallbackEntry in state.CompletionCallbacks) { - var ownerActivityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == completionCallbackEntry.OwnerInstanceId); - var childNode = workflowExecutionContext.FindNodeById(completionCallbackEntry.ChildNodeId); + var ownerActivityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == completionCallbackEntry.OwnerInstanceId); + if (ownerActivityExecutionContext != null) + { + var childNode = workflowExecutionContext.FindNodeById(completionCallbackEntry.ChildNodeId); - if (childNode == null) - continue; + if (childNode == null) + continue; - var callbackName = completionCallbackEntry.MethodName; - var callbackDelegate = !string.IsNullOrEmpty(callbackName) ? ownerActivityExecutionContext.Activity.GetActivityCompletionCallback(callbackName) : default; - var tag = completionCallbackEntry.Tag; - workflowExecutionContext.AddCompletionCallback(ownerActivityExecutionContext, childNode, callbackDelegate, tag); + var callbackName = completionCallbackEntry.MethodName; + var callbackDelegate = !string.IsNullOrEmpty(callbackName) ? ownerActivityExecutionContext.Activity.GetActivityCompletionCallback(callbackName) : default; + var tag = completionCallbackEntry.Tag; + workflowExecutionContext.AddCompletionCallback(ownerActivityExecutionContext, childNode, callbackDelegate, tag); + } } } @@ -208,9 +211,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor throw new("Lost an owner context"); } - var completionCallbacks = workflowExecutionContext - .CompletionCallbacks - .Select(x => new CompletionCallbackState(x.Owner.Id, x.Child.NodeId, x.CompletionCallback?.Method.Name, x.Tag)); + var completionCallbacks = workflowExecutionContext.CompletionCallbacks.Select(x => new CompletionCallbackState(x.Owner.Id, x.Child.NodeId, x.CompletionCallback?.Method.Name, x.Tag)); state.CompletionCallbacks = completionCallbacks.ToList(); } @@ -243,7 +244,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor StartedAt = activityExecutionContext.StartedAt, CompletedAt = activityExecutionContext.CompletedAt, Tag = activityExecutionContext.Tag, - DynamicVariables = activityExecutionContext.DynamicVariables + DynamicVariables = activityExecutionContext.DynamicVariables, }; return activityExecutionContextState; } @@ -263,9 +264,9 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor Tag = x.Tag, Variables = x.Variables?.ToList(), ExistingActivityExecutionContextId = x.ExistingActivityExecutionContext?.Id, - Input = x.Input + Input = x.Input, }); state.ScheduledActivities = scheduledActivities.ToList(); } -} \ No newline at end of file +}