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:
parent
c83d42ed96
commit
dff2576fbe
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue