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<ExpressionExecutionContext,
ValueTask<object>> 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 <Max@compyl.com>
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
This commit is contained in:
Max Brooks 2025-05-30 13:51:32 -04:00 committed by GitHub
parent c83d42ed96
commit dff2576fbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 60 deletions

View file

@ -22,11 +22,12 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
{
var activityAttr = activityType.GetCustomAttribute<ActivityAttribute>();
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<DisplayNameAttribute>();
var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title);
var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? friendlyName.Humanize(LetterCasing.Title);
var categoryAttr = activityType.GetCustomAttribute<CategoryAttribute>();
var category = categoryAttr?.Category ?? activityAttr?.Category ?? ActivityTypeNameHelper.GetCategoryFromNamespace(ns) ?? "Miscellaneous";
var descriptionAttr = activityType.GetCustomAttribute<DescriptionAttribute>();
@ -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<FlowNodeAttribute>();
var flowPorts = flowNodeAttr?.Outcomes.Select(x => new Port
{
Type = PortType.Flow,
Name = x,
DisplayName = x
}).ToDictionary(x => x.Name) ?? new Dictionary<string, Port>();
var flowPorts =
flowNodeAttr
?.Outcomes.Select(x => new Port
{
Type = PortType.Flow,
Name = x,
DisplayName = x,
})
.ToDictionary(x => x.Name) ?? new Dictionary<string, Port>();
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
}
/// <inheritdoc />
public IEnumerable<PropertyInfo> GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) =>
activityType.GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).DistinctBy(x => x.Name);
public IEnumerable<PropertyInfo> GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => activityType.GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).DistinctBy(x => x.Name);
/// <inheritdoc />
public IEnumerable<PropertyInfo> GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) =>
activityType.GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList();
public IEnumerable<PropertyInfo> GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => activityType.GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList();
/// <inheritdoc />
public Task<OutputDescriptor> 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));
}
/// <inheritdoc />
@ -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<IEnumerable<InputDescriptor>> DescribeInputPropertiesAsync(IEnumerable<PropertyInfo> 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)));
}
}
}

View file

@ -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<string, object> 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();
}
}
}