Prevent native property name collision

This change ensures that the WorkflowDefinitionActivity supports custom input definitions even if they use the same name as properties already present on the activity, such as WorkflowDefinitionId.
This commit is contained in:
Sipke Schoorstra 2023-10-23 12:28:03 +02:00
parent 2f2fb467a0
commit ec866ad381
3 changed files with 112 additions and 91 deletions

View file

@ -0,0 +1,21 @@
namespace Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
internal static class PropertyNameHelper
{
public static string GetSafePropertyName(Type type, string name)
{
var isReservedName = IsReservedName(type, name);
return isReservedName ? PrefixPropertyName(name) : name;
}
public static string GetUnsafePropertyName(Type type, string name)
{
var unsafeName = RemovePropertyNamePrefix(name);
var isReservedName = IsReservedName(type, unsafeName);
return isReservedName ? unsafeName : name;
}
private static string PrefixPropertyName(string name) => $"_{name}";
private static string RemovePropertyNamePrefix(string name) => name.TrimStart('_');
private static bool IsReservedName(Type type, string name) => type.GetProperty(name) != null;
}

View file

@ -48,71 +48,6 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
await context.ScheduleActivityAsync(Root, OnChildCompletedAsync);
}
private void CopyInputOutputToVariables(ActivityExecutionContext context)
{
foreach (var inputDescriptor in context.ActivityDescriptor.Inputs)
{
var input = SyntheticProperties.TryGetValue(inputDescriptor.Name, out var inputValue) ? (Input?)inputValue : default;
var evaluatedExpression = input != null ? context.Get(input.MemoryBlockReference()) : default;
// Create a local scope variable for each input property.
var variable = new Variable
{
Id = inputDescriptor.Name,
Name = inputDescriptor.Name,
StorageDriverType = inputDescriptor.StorageDriverType
};
context.ExpressionExecutionContext.Memory.Declare(variable);
variable.Set(context, evaluatedExpression);
}
foreach (var outputDescriptor in context.ActivityDescriptor.Outputs)
{
// Create a local scope variable for each output property.
var variable = new Variable
{
Id = outputDescriptor.Name,
Name = outputDescriptor.Name
};
context.ExpressionExecutionContext.Memory.Declare(variable);
}
}
private void DeclareInputOutputAsVariables(InitializationContext context)
{
var activityRegistry = context.ServiceProvider.GetRequiredService<IActivityRegistry>();
var activityDescriptor = activityRegistry.Find(Type, Version)!;
// Declare input variables.
foreach (var inputDescriptor in activityDescriptor.Inputs)
{
// Create a local scope variable for each input property.
var variable = new Variable(inputDescriptor.Name)
{
Id = inputDescriptor.Name,
Name = inputDescriptor.Name,
StorageDriverType = inputDescriptor.StorageDriverType
};
Variables.Declare(variable);
}
// Declare output variables.
foreach (var outputDescriptor in activityDescriptor.Outputs)
{
// Create a local scope variable for each output property.
var variable = new Variable(outputDescriptor.Name)
{
Id = outputDescriptor.Name,
Name = outputDescriptor.Name
};
Variables.Declare(variable);
}
}
private async ValueTask OnChildCompletedAsync(ActivityCompletedContext context)
{
var targetContext = context.TargetContext;
@ -150,10 +85,66 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
await targetContext.CompleteActivityAsync(completeCompositeSignal?.Value);
}
async ValueTask IInitializable.InitializeAsync(InitializationContext context)
private void CopyInputOutputToVariables(ActivityExecutionContext context)
{
var serviceProvider = context.GetRequiredService<IServiceProvider>();
DeclareInputAsVariables(serviceProvider, (descriptor, variable) =>
{
var inputName = descriptor.Name;
var input = SyntheticProperties.TryGetValue(inputName, out var inputValue) ? (Input?)inputValue : default;
var evaluatedExpression = input != null ? context.Get(input.MemoryBlockReference()) : default;
context.ExpressionExecutionContext.Memory.Declare(variable);
variable.Set(context, evaluatedExpression);
});
DeclareOutputAsVariables(serviceProvider, (descriptor, variable) => context.ExpressionExecutionContext.Memory.Declare(variable));
}
private void DeclareInputAsVariables(IServiceProvider serviceProvider, Action<InputDescriptor, Variable> configureVariable)
{
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
var activityDescriptor = activityRegistry.Find(Type, Version)!;
foreach (var inputDescriptor in activityDescriptor.Inputs)
{
var inputName = inputDescriptor.Name;
var unsafeInputName = PropertyNameHelper.GetUnsafePropertyName(typeof(WorkflowDefinitionActivity), inputName);
var variable = new Variable
{
Id = unsafeInputName,
Name = unsafeInputName,
StorageDriverType = inputDescriptor.StorageDriverType
};
configureVariable(inputDescriptor, variable);
}
}
private void DeclareOutputAsVariables(IServiceProvider serviceProvider, Action<OutputDescriptor, Variable> configureVariable)
{
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
var activityDescriptor = activityRegistry.Find(Type, Version)!;
foreach (var outputDescriptor in activityDescriptor.Outputs)
{
var outputName = outputDescriptor.Name;
var unsafeOutputName = PropertyNameHelper.GetUnsafePropertyName(typeof(WorkflowDefinitionActivity), outputName);
var variable = new Variable
{
Id = unsafeOutputName,
Name = unsafeOutputName
};
configureVariable(outputDescriptor, variable);
}
}
private async Task<WorkflowDefinition?> FindWorkflowDefinitionAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var serviceProvider = context.ServiceProvider;
var cancellationToken = context.CancellationToken;
var workflowDefinitionStore = serviceProvider.GetRequiredService<IWorkflowDefinitionStore>();
var filter = new WorkflowDefinitionFilter { DefinitionId = WorkflowDefinitionId };
@ -162,29 +153,32 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
else
filter.VersionOptions = VersionOptions.SpecificVersion(Version);
var workflowDefinition = await workflowDefinitionStore.FindAsync(filter, cancellationToken);
var workflowDefinition =
await workflowDefinitionStore.FindAsync(filter, cancellationToken)
?? (await workflowDefinitionStore.FindAsync(new WorkflowDefinitionFilter { DefinitionId = WorkflowDefinitionId, VersionOptions = VersionOptions.Published }, cancellationToken)
?? await workflowDefinitionStore.FindAsync(new WorkflowDefinitionFilter { DefinitionId = WorkflowDefinitionId, VersionOptions = VersionOptions.Latest }, cancellationToken));
return workflowDefinition;
}
async ValueTask IInitializable.InitializeAsync(InitializationContext context)
{
var serviceProvider = context.ServiceProvider;
var cancellationToken = context.CancellationToken;
var workflowDefinition = await FindWorkflowDefinitionAsync(serviceProvider, cancellationToken);
if (workflowDefinition == null)
{
// Find the latest published version.
workflowDefinition = await workflowDefinitionStore.FindAsync(new WorkflowDefinitionFilter { DefinitionId = WorkflowDefinitionId, VersionOptions = VersionOptions.Published }, cancellationToken);
if (workflowDefinition == null)
{
// Find the latest version.
workflowDefinition = await workflowDefinitionStore.FindAsync(new WorkflowDefinitionFilter { DefinitionId = WorkflowDefinitionId, VersionOptions = VersionOptions.Latest }, cancellationToken);
}
if (workflowDefinition == null)
throw new Exception($"Could not find workflow definition with ID {WorkflowDefinitionId}.");
}
throw new Exception($"Could not find workflow definition with ID {WorkflowDefinitionId}.");
// Construct the root activity stored in the activity definitions.
var materializer = serviceProvider.GetRequiredService<IWorkflowMaterializer>();
var root = await materializer.MaterializeAsync(workflowDefinition, cancellationToken);
DeclareInputOutputAsVariables(context);
// Declare input and output variables.
DeclareInputAsVariables(serviceProvider, (inputDescriptor, variable) => Variables.Declare(variable));
DeclareOutputAsVariables(serviceProvider, (outputDescriptor, variable) => Variables.Declare(variable));
// Set the root activity.
Root = root;
}
}

View file

@ -114,14 +114,16 @@ public class WorkflowDefinitionActivityProvider : IActivityProvider
var inputs = definition.Inputs.Select(inputDefinition =>
{
var nakedType = inputDefinition.Type;
var inputName = inputDefinition.Name;
var safeInputName = PropertyNameHelper.GetSafePropertyName(typeof(WorkflowDefinitionActivity), inputName);
return new InputDescriptor
{
Type = nakedType,
IsWrapped = true,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputDefinition.Name),
ValueSetter = (activity, value) => activity.SyntheticProperties[inputDefinition.Name] = value!,
Name = inputDefinition.Name,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(safeInputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[safeInputName] = value!,
Name = safeInputName,
DisplayName = inputDefinition.DisplayName,
Description = inputDefinition.Description,
Category = inputDefinition.Category,
@ -135,20 +137,24 @@ public class WorkflowDefinitionActivityProvider : IActivityProvider
yield return input;
}
private static IEnumerable<OutputDescriptor> DescribeOutputs(WorkflowDefinition definition) =>
definition.Outputs.Select(outputDefinition =>
private static IEnumerable<OutputDescriptor> DescribeOutputs(WorkflowDefinition definition)
{
return definition.Outputs.Select(outputDefinition =>
{
var nakedType = outputDefinition.Type;
var outputName = outputDefinition.Name;
var safeOutputName = PropertyNameHelper.GetSafePropertyName(typeof(WorkflowDefinitionActivity), outputName);
return new OutputDescriptor
{
Type = nakedType,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputDefinition.Name),
ValueSetter = (activity, value) => activity.SyntheticProperties[outputDefinition.Name] = value!,
Name = outputDefinition.Name,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(safeOutputName),
ValueSetter = (activity, value) => activity.SyntheticProperties[safeOutputName] = value!,
Name = safeOutputName,
DisplayName = outputDefinition.DisplayName,
Description = outputDefinition.Description,
IsSynthetic = true
};
});
}
}