From dd919a4000c9dfbbfc48503d0ba39cb329fc1021 Mon Sep 17 00:00:00 2001 From: herblinernexxbizz <125579511+herblinernexxbizz@users.noreply.github.com> Date: Mon, 3 Apr 2023 14:14:39 +0200 Subject: [PATCH] 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 --- Elsa.sln.DotSettings | 2 +- .../ActivityDescriptorExtensions.cs | 4 +- .../Models/InputDescriptor.cs | 10 +-- .../Models/OutputDescriptor.cs | 5 ++ .../Models/PropertyDescriptor.cs | 22 +++++-- .../Services/ActivityDescriber.cs | 1 + .../Activities/SetOutput/SetOutput.cs | 60 +++++++++++++++++ .../SetWorkflowOutput/SetWorkflowOutput.cs | 58 ++++++----------- .../WorkflowDefinitionActivityProvider.cs | 3 +- .../ActivityExecutionContextExtensions.cs | 26 -------- .../Scenarios/CompositesPassingData/Tests.cs | 40 ++++++++++++ .../CompositesPassingData/Workflows.cs | 64 +++++++++++++++++++ .../Scenarios/SetGetVariables/Tests.cs | 31 +++++++++ .../Scenarios/SetGetVariables/Workflows.cs | 27 ++++++++ 14 files changed, 269 insertions(+), 84 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Management/Activities/SetOutput/SetOutput.cs delete mode 100644 src/modules/Elsa.Workflows.Management/Extensions/ActivityExecutionContextExtensions.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Tests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Workflows.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Tests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Workflows.cs diff --git a/Elsa.sln.DotSettings b/Elsa.sln.DotSettings index a00ee68f2..faa4b51d8 100644 --- a/Elsa.sln.DotSettings +++ b/Elsa.sln.DotSettings @@ -1,4 +1,4 @@ - + UI True True diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityDescriptorExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityDescriptorExtensions.cs index 39ede2f3f..952d9d316 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityDescriptorExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityDescriptorExtensions.cs @@ -23,7 +23,7 @@ public static class ActivityDescriptorExtensions public static IDictionary 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; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs index d34ccd1bd..887305229 100644 --- a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs @@ -17,7 +17,7 @@ public class InputDescriptor : PropertyDescriptor public InputDescriptor( string name, Type type, - Func valueAccessor, + Func 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; } - /// - /// Returns the value of the input property for the specified activity. - /// - [JsonIgnore] - public Func ValueAccessor { get; set; } = default!; - /// /// True if the property is wrapped with an type, false otherwise. /// diff --git a/src/modules/Elsa.Workflows.Core/Models/OutputDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/OutputDescriptor.cs index 7a30894a9..212d62446 100644 --- a/src/modules/Elsa.Workflows.Core/Models/OutputDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/OutputDescriptor.cs @@ -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 valueGetter, string? description = default, bool? isBrowsable = default) { Name = name; DisplayName = displayName; Type = type; + ValueGetter = valueGetter; Description = description; IsBrowsable = isBrowsable; } diff --git a/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs index 7b6155687..f0043dfee 100644 --- a/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs @@ -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. /// public string Name { get; set; } = default!; - + /// /// The .NET type. /// - [JsonPropertyName("typeName")]public Type Type { get; set; } = default!; - + [JsonPropertyName("typeName")] + public Type Type { get; set; } = default!; + /// /// The user friendly name of the input. Used by UI tools. /// public string? DisplayName { get; set; } - + /// /// The user friendly description of the input. Used by UI tools. /// public string? Description { get; set; } - + /// /// The order in which this input should be displayed by UI tools. /// public float Order { get; set; } - + /// /// True if this input should be displayed by UI tools, false otherwise. /// public bool? IsBrowsable { get; set; } - + /// /// True if this input property is synthetic, which means it does not exist physically on the activity's .NET type. /// public bool IsSynthetic { get; set; } + + /// + /// Returns the value of the input property for the specified activity. + /// + [JsonIgnore] + public Func ValueGetter { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index 56f1ec9d5..5a26c7846 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -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 ); diff --git a/src/modules/Elsa.Workflows.Management/Activities/SetOutput/SetOutput.cs b/src/modules/Elsa.Workflows.Management/Activities/SetOutput/SetOutput.cs new file mode 100644 index 000000000..09581220c --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Activities/SetOutput/SetOutput.cs @@ -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; + +/// +/// Assigns a given value to a workflow definition's output. +/// +[Activity("Elsa", "Composition", "Assigns a given value to a the container's output.", Kind = ActivityKind.Action)] +[PublicAPI] +public class SetOutput : CodeActivity +{ + /// + /// The name of the output to assign. + /// + [Input( + DisplayName = "Output", + Description = "The output to assign.", + UIHint = InputUIHints.OutputPicker + )] + public Input OutputName { get; set; } = default!; + + /// + /// The value to assign to the output. + /// + [Input(Description = "The value to assign.")] + public Input OutputValue { get; set; } = default!; + + /// + 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!; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Activities/SetWorkflowOutput/SetWorkflowOutput.cs b/src/modules/Elsa.Workflows.Management/Activities/SetWorkflowOutput/SetWorkflowOutput.cs index 6caa679eb..1e419c0f0 100644 --- a/src/modules/Elsa.Workflows.Management/Activities/SetWorkflowOutput/SetWorkflowOutput.cs +++ b/src/modules/Elsa.Workflows.Management/Activities/SetWorkflowOutput/SetWorkflowOutput.cs @@ -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; /// /// Assigns a given value to a workflow definition's output. /// -[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 { /// @@ -34,49 +34,29 @@ public class SetWorkflowOutput : CodeActivity /// 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(); - 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!; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityProvider.cs b/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityProvider.cs index 78dc7263d..d9be2907d 100644 --- a/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityProvider.cs +++ b/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityProvider.cs @@ -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, diff --git a/src/modules/Elsa.Workflows.Management/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Management/Extensions/ActivityExecutionContextExtensions.cs deleted file mode 100644 index af784d8b8..000000000 --- a/src/modules/Elsa.Workflows.Management/Extensions/ActivityExecutionContextExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Elsa.Extensions; -using Elsa.Workflows.Core.Models; -using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity; - -namespace Elsa.Workflows.Management.Extensions; - -/// -/// Extends the . -/// -public static class ActivityExecutionContextExtensions -{ - /// - /// Returns the first in scope of the specified . - /// - public static WorkflowDefinitionActivity? GetFirstWorkflowDefinitionActivity(this ActivityExecutionContext context) - { - var workflowDefinitionActivityContext = context.GetFirstWorkflowDefinitionActivityExecutionContext(); - return (WorkflowDefinitionActivity?)workflowDefinitionActivityContext?.Activity; - } - - /// - /// Returns the first in scope of the specified . - /// - public static ActivityExecutionContext? GetFirstWorkflowDefinitionActivityExecutionContext(this ActivityExecutionContext context) => - context.GetAncestors().FirstOrDefault(x => x.Activity is WorkflowDefinitionActivity); -} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Tests.cs new file mode 100644 index 000000000..5223974b9 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Tests.cs @@ -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(); + _workflowRunner = services.GetRequiredService(); + } + + [Fact(DisplayName = "The main workflow can capture the result of the composite activity")] + public async Task Test1() + { + var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync(); + + // Start workflow. + await _workflowRunner.RunAsync(workflow); + + // Verify expected output. + var line = _capturingTextWriter.Lines.ToList().Last(); + Assert.Equal("hi there obi wan", line); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Workflows.cs b/test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Workflows.cs new file mode 100644 index 000000000..8cc89e759 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/CompositesPassingData/Workflows.cs @@ -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; + +/// +/// A composite activity that adds text to a string +/// +public class AddTextSubWorkflow : Composite +{ + public Input A { get; set; } = default!; + public Output B { get; set; } = default!; + + public AddTextSubWorkflow() + { + var setOutput = new SetOutput() + { + OutputName = new Input("B"), + OutputValue = new Input(context => "hi there " + A.Get(context)) + }; + + Root = new Sequence + { + Activities = + { + setOutput + } + }; + + } +} + +/// +/// A workflow that uses the composite activity. +/// +public class AddTextMainWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder workflow) + { + var outVariable = new Variable("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)) + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Tests.cs new file mode 100644 index 000000000..86590d07b --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Tests.cs @@ -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(); + _workflowRunner = services.GetRequiredService(); + } + + [Fact(DisplayName = "Subsequent activity does not get scheduled when previous activity created a bookmark")] + public async Task Test1() + { + await _workflowRunner.RunAsync(); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Line 5" }, lines); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Workflows.cs b/test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Workflows.cs new file mode 100644 index 000000000..91c52d18f --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/SetGetVariables/Workflows.cs @@ -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("myvar", "test"); + + workflow.Root = new Sequence + { + Variables = { + variable + }, + + Activities = + { + new SetVariable(variable,"Line 5"), + new WriteLine(variable) + } + }; + } +} \ No newline at end of file