adding set var and main/sub workflow tests (#3855)

* Adding set var and main/sub workflow tests

* Deprecate SetWorkflowOutput and replace with SetOutput

---------

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
This commit is contained in:
herblinernexxbizz 2023-04-03 14:14:39 +02:00 committed by GitHub
parent 8390efc332
commit dd919a4000
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 269 additions and 84 deletions

View file

@ -1,4 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=abcdefghijklmnopqrstuvwxyz/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Deconstructor/@EntryIndexedValue">True</s:Boolean>

View file

@ -23,7 +23,7 @@ public static class ActivityDescriptorExtensions
public static IDictionary<string, Input?> GetWrappedInputProperties(this ActivityDescriptor activityDescriptor, IActivity activity)
{
var wrappedInputDescriptors = activityDescriptor.Inputs.Where(x => x.IsWrapped).ToList();
var inputLookup = wrappedInputDescriptors.ToDictionary(x => x.Name, x => (Input?)x.ValueAccessor(activity));
var inputLookup = wrappedInputDescriptors.ToDictionary(x => x.Name, x => (Input?)x.ValueGetter(activity));
return inputLookup;
}
@ -33,6 +33,6 @@ public static class ActivityDescriptorExtensions
public static Input? GetWrappedInputProperty(this ActivityDescriptor activityDescriptor, IActivity activity, string name)
{
var inputDescriptor = activityDescriptor.Inputs.FirstOrDefault(x => x.Name == name && x.IsWrapped);
return inputDescriptor?.ValueAccessor(activity) as Input;
return inputDescriptor?.ValueGetter(activity) as Input;
}
}

View file

@ -17,7 +17,7 @@ public class InputDescriptor : PropertyDescriptor
public InputDescriptor(
string name,
Type type,
Func<IActivity, object?> valueAccessor,
Func<IActivity, object?> valueGetter,
bool isWrapped,
string uiHint,
string displayName,
@ -35,7 +35,7 @@ public class InputDescriptor : PropertyDescriptor
{
Name = name;
Type = type;
ValueAccessor = valueAccessor;
ValueGetter = valueGetter;
IsWrapped = isWrapped;
UIHint = uiHint;
DisplayName = displayName;
@ -52,12 +52,6 @@ public class InputDescriptor : PropertyDescriptor
IsBrowsable = isBrowsable;
}
/// <summary>
/// Returns the value of the input property for the specified activity.
/// </summary>
[JsonIgnore]
public Func<IActivity, object?> ValueAccessor { get; set; } = default!;
/// <summary>
/// True if the property is wrapped with an <see cref="Input{T}"/> type, false otherwise.
/// </summary>

View file

@ -1,3 +1,6 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Models;
public class OutputDescriptor : PropertyDescriptor
@ -10,12 +13,14 @@ public class OutputDescriptor : PropertyDescriptor
string name,
string displayName,
Type type,
Func<IActivity, object?> valueGetter,
string? description = default,
bool? isBrowsable = default)
{
Name = name;
DisplayName = displayName;
Type = type;
ValueGetter = valueGetter;
Description = description;
IsBrowsable = isBrowsable;
}

View file

@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Models;
@ -11,34 +12,41 @@ public abstract class PropertyDescriptor
/// The name.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// The .NET type.
/// </summary>
[JsonPropertyName("typeName")]public Type Type { get; set; } = default!;
[JsonPropertyName("typeName")]
public Type Type { get; set; } = default!;
/// <summary>
/// The user friendly name of the input. Used by UI tools.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// The user friendly description of the input. Used by UI tools.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The order in which this input should be displayed by UI tools.
/// </summary>
public float Order { get; set; }
/// <summary>
/// True if this input should be displayed by UI tools, false otherwise.
/// </summary>
public bool? IsBrowsable { get; set; }
/// <summary>
/// True if this input property is synthetic, which means it does not exist physically on the activity's .NET type.
/// </summary>
public bool IsSynthetic { get; set; }
/// <summary>
/// Returns the value of the input property for the specified activity.
/// </summary>
[JsonIgnore]
public Func<IActivity, object?> ValueGetter { get; set; } = default!;
}

View file

@ -116,6 +116,7 @@ public class ActivityDescriber : IActivityDescriber
(outputAttribute?.Name ?? propertyInfo.Name).Pascalize(),
outputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title),
wrappedPropertyType,
propertyInfo.GetValue,
descriptionAttribute?.Description ?? outputAttribute?.Description,
outputAttribute?.IsBrowsable ?? true
);

View file

@ -0,0 +1,60 @@
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Management.Activities.SetOutput;
/// <summary>
/// Assigns a given value to a workflow definition's output.
/// </summary>
[Activity("Elsa", "Composition", "Assigns a given value to a the container's output.", Kind = ActivityKind.Action)]
[PublicAPI]
public class SetOutput : CodeActivity
{
/// <summary>
/// The name of the output to assign.
/// </summary>
[Input(
DisplayName = "Output",
Description = "The output to assign.",
UIHint = InputUIHints.OutputPicker
)]
public Input<string> OutputName { get; set; } = default!;
/// <summary>
/// The value to assign to the output.
/// </summary>
[Input(Description = "The value to assign.")]
public Input<object?> OutputValue { get; set; } = default!;
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var outputName = OutputName.Get(context);
var ancestorContext = context.GetAncestors().FirstOrDefault(x => x.ActivityDescriptor.Outputs.Any(y => y.Name == outputName));
if (ancestorContext != null)
SetAncestorActivityOutput(context, ancestorContext);
}
private void SetAncestorActivityOutput(ActivityExecutionContext context, ActivityExecutionContext ancestorContext)
{
var ancestorActivity = ancestorContext.Activity;
var ancestorActivityDescriptor = ancestorContext.ActivityDescriptor;
var outputName = OutputName.Get(context);
var ancestorOutputDescriptor = ancestorActivityDescriptor.Outputs.FirstOrDefault(x => x.Name == outputName);
if (ancestorOutputDescriptor == null)
return;
var outputValue = OutputValue.GetOrDefault(context);
var ancestorOutput = (Output)ancestorOutputDescriptor.ValueGetter(ancestorActivity)!;
ancestorContext.Set(ancestorOutput, outputValue);
// If the ancestor activity is the root workflow, we need to update the workflow execution context's output collection as well.
if (ancestorContext.ParentActivityExecutionContext == null)
context.WorkflowExecutionContext.Output[outputName] = outputValue!;
}
}

View file

@ -1,10 +1,8 @@
using Elsa.Common.Models;
using System.ComponentModel;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Extensions;
using JetBrains.Annotations;
namespace Elsa.Workflows.Management.Activities.SetWorkflowOutput;
@ -12,8 +10,10 @@ namespace Elsa.Workflows.Management.Activities.SetWorkflowOutput;
/// <summary>
/// Assigns a given value to a workflow definition's output.
/// </summary>
[Activity("Elsa", "Composition", "Assigns a given value to a workflow definition's output.", Kind = ActivityKind.Action)]
[Activity("Elsa", "Obsolete", "[OBSOLETE] Assigns a given value to a workflow definition's output.", Kind = ActivityKind.Action)]
[PublicAPI]
[Obsolete("Use the SetOutput activity instead.")]
[Browsable(false)]
public class SetWorkflowOutput : CodeActivity
{
/// <summary>
@ -34,49 +34,29 @@ public class SetWorkflowOutput : CodeActivity
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Get the first workflow definition activity in scope, so that we can get its output definitions.
var workflowDefinitionActivityContext = context.GetFirstWorkflowDefinitionActivityExecutionContext();
var outputName = OutputName.Get(context);
var ancestorContext = context.GetAncestors().FirstOrDefault(x => x.ActivityDescriptor.Outputs.Any(y => y.Name == outputName));
if (workflowDefinitionActivityContext != null)
// Set the output on the first workflow definition activity execution context in scope.
await SetWorkflowDefinitionActivityOutputAsync(context, workflowDefinitionActivityContext);
else
// If no workflow definition activity is in scope, set the output on the workflow execution context.
SetWorkflowDefinitionOutput(context);
if (ancestorContext != null)
SetAncestorActivityOutput(context, ancestorContext);
}
private void SetWorkflowDefinitionOutput(ActivityExecutionContext context)
private void SetAncestorActivityOutput(ActivityExecutionContext context, ActivityExecutionContext ancestorContext)
{
var ancestorActivity = ancestorContext.Activity;
var ancestorActivityDescriptor = ancestorContext.ActivityDescriptor;
var outputName = OutputName.Get(context);
var workflowActivityContext = context.GetAncestors().Last(x => x.Activity is Workflow);
var outputValue = OutputValue.Get(context);
workflowActivityContext.WorkflowExecutionContext.Output[outputName] = outputValue!;
}
var ancestorOutputDescriptor = ancestorActivityDescriptor.Outputs.FirstOrDefault(x => x.Name == outputName);
private async Task SetWorkflowDefinitionActivityOutputAsync(ActivityExecutionContext context, ActivityExecutionContext workflowDefinitionActivityContext)
{
var workflowDefinitionStore = context.GetRequiredService<IWorkflowDefinitionStore>();
var workflowDefinitionActivity = (WorkflowDefinitionActivity.WorkflowDefinitionActivity)workflowDefinitionActivityContext.Activity;
var definitionId = workflowDefinitionActivity.WorkflowDefinitionId;
var versionOptions = VersionOptions.SpecificVersion(workflowDefinitionActivity.Version);
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = versionOptions};
var workflowDefinition = await workflowDefinitionStore.FindAsync(filter, context.CancellationToken);
if (workflowDefinition == null)
return;
var outputName = OutputName.Get(context);
var outputDescriptor = workflowDefinition.Outputs.FirstOrDefault(x => x.Name == outputName);
if (outputDescriptor == null)
return;
var output = workflowDefinitionActivity.SyntheticProperties.TryGetValue(outputDescriptor.Name, out var outputProp) ? (Output)outputProp : default;
if (output == null)
if (ancestorOutputDescriptor == null)
return;
var outputValue = OutputValue.GetOrDefault(context);
context.Set(output, outputValue);
var ancestorOutput = (Output)ancestorOutputDescriptor.ValueGetter(ancestorActivity)!;
ancestorContext.Set(ancestorOutput, outputValue);
// If the ancestor activity is the root workflow, we need to update the workflow execution context's output collection as well.
if (ancestorContext.ParentActivityExecutionContext == null)
context.WorkflowExecutionContext.Output[outputName] = outputValue!;
}
}

View file

@ -106,7 +106,7 @@ public class WorkflowDefinitionActivityProvider : IActivityProvider
{
Type = nakedType,
IsWrapped = true,
ValueAccessor = activity => ((WorkflowDefinitionActivity)activity).SyntheticProperties.GetValueOrDefault(inputDefinition.Name),
ValueGetter = activity => ((WorkflowDefinitionActivity)activity).SyntheticProperties.GetValueOrDefault(inputDefinition.Name),
Name = inputDefinition.Name,
DisplayName = inputDefinition.DisplayName,
Description = inputDefinition.Description,
@ -129,6 +129,7 @@ public class WorkflowDefinitionActivityProvider : IActivityProvider
return new OutputDescriptor
{
Type = nakedType,
ValueGetter = activity => ((WorkflowDefinitionActivity)activity).SyntheticProperties.GetValueOrDefault(outputDefinition.Name),
Name = outputDefinition.Name,
DisplayName = outputDefinition.DisplayName,
Description = outputDefinition.Description,

View file

@ -1,26 +0,0 @@
using Elsa.Extensions;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
namespace Elsa.Workflows.Management.Extensions;
/// <summary>
/// Extends the <see cref="ActivityExecutionContext"/>.
/// </summary>
public static class ActivityExecutionContextExtensions
{
/// <summary>
/// Returns the first <see cref="WorkflowDefinitionActivity"/> in scope of the specified <see cref="ActivityExecutionContext"/>.
/// </summary>
public static WorkflowDefinitionActivity? GetFirstWorkflowDefinitionActivity(this ActivityExecutionContext context)
{
var workflowDefinitionActivityContext = context.GetFirstWorkflowDefinitionActivityExecutionContext();
return (WorkflowDefinitionActivity?)workflowDefinitionActivityContext?.Activity;
}
/// <summary>
/// Returns the first <see cref="WorkflowDefinitionActivity"/> in scope of the specified <see cref="ActivityExecutionContext"/>.
/// </summary>
public static ActivityExecutionContext? GetFirstWorkflowDefinitionActivityExecutionContext(this ActivityExecutionContext context) =>
context.GetAncestors().FirstOrDefault(x => x.Activity is WorkflowDefinitionActivity);
}

View file

@ -0,0 +1,40 @@
using System.Linq;
using System.Threading.Tasks;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core.Contracts;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.CompositesPassingData;
public class Tests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
public Tests(ITestOutputHelper testOutputHelper)
{
var services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.ConfigureElsa(elsa => elsa.UseJavaScript())
.Build();
_workflowBuilderFactory = services.GetRequiredService<IWorkflowBuilderFactory>();
_workflowRunner = services.GetRequiredService<IWorkflowRunner>();
}
[Fact(DisplayName = "The main workflow can capture the result of the composite activity")]
public async Task Test1()
{
var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync<AddTextMainWorkflow>();
// Start workflow.
await _workflowRunner.RunAsync(workflow);
// Verify expected output.
var line = _capturingTextWriter.Lines.ToList().Last();
Assert.Equal("hi there obi wan", line);
}
}

View file

@ -0,0 +1,64 @@
using Elsa.Extensions;
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Activities.SetOutput;
using Elsa.Workflows.Management.Activities.SetWorkflowOutput;
namespace Elsa.IntegrationTests.Scenarios.CompositesPassingData;
/// <summary>
/// A composite activity that adds text to a string
/// </summary>
public class AddTextSubWorkflow : Composite
{
public Input<string> A { get; set; } = default!;
public Output<string> B { get; set; } = default!;
public AddTextSubWorkflow()
{
var setOutput = new SetOutput()
{
OutputName = new Input<string>("B"),
OutputValue = new Input<object?>(context => "hi there " + A.Get(context))
};
Root = new Sequence
{
Activities =
{
setOutput
}
};
}
}
/// <summary>
/// A workflow that uses the <see cref="AddTextSubWorkflow"/> composite activity.
/// </summary>
public class AddTextMainWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var outVariable = new Variable<string>("out", "test");
var subWorkflow = new AddTextSubWorkflow()
{
A = new("obi wan"),
B = new (outVariable)
};
workflow.Variables.Add(outVariable);
workflow.Root = new Sequence
{
Activities =
{
subWorkflow,
new WriteLine(context => outVariable.Get(context))
}
};
}
}

View file

@ -0,0 +1,31 @@
using System.Linq;
using System.Threading.Tasks;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core.Contracts;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.SetGetVariables;
public class Tests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
public Tests(ITestOutputHelper testOutputHelper)
{
var services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
_workflowBuilderFactory = services.GetRequiredService<IWorkflowBuilderFactory>();
_workflowRunner = services.GetRequiredService<IWorkflowRunner>();
}
[Fact(DisplayName = "Subsequent activity does not get scheduled when previous activity created a bookmark")]
public async Task Test1()
{
await _workflowRunner.RunAsync<SetGetVariablesWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Line 5" }, lines);
}
}

View file

@ -0,0 +1,27 @@
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
namespace Elsa.IntegrationTests.Scenarios.SetGetVariables;
class SetGetVariablesWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var variable = new Variable<string>("myvar", "test");
workflow.Root = new Sequence
{
Variables = {
variable
},
Activities =
{
new SetVariable<string>(variable,"Line 5"),
new WriteLine(variable)
}
};
}
}