From a29905bae246e5f3954b8a7ea47aabc97e61c69d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 20 Feb 2021 15:23:39 +0100 Subject: [PATCH] Introduce AmbientActivityExecutionContext to support direct access to composite activity input properties from child activities (#648) --- .../Elsa.Abstractions/Services/Activity.cs | 2 +- .../Services/CompositeActivity.cs | 24 +++++++++++++++ .../Models/AmbientActivityExecutionContext.cs | 30 +++++++++++++++++++ src/core/Elsa.Core/Services/WorkflowRunner.cs | 3 ++ .../Activities/CountdownActivity.cs | 6 ++-- 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Services/Models/AmbientActivityExecutionContext.cs diff --git a/src/core/Elsa.Abstractions/Services/Activity.cs b/src/core/Elsa.Abstractions/Services/Activity.cs index 9a44376a0..67675e9c9 100644 --- a/src/core/Elsa.Abstractions/Services/Activity.cs +++ b/src/core/Elsa.Abstractions/Services/Activity.cs @@ -19,7 +19,7 @@ namespace Elsa.Services public bool PersistWorkflow { get; set; } public bool LoadWorkflowContext { get; set; } public bool SaveWorkflowContext { get; set; } - public JObject Data { get; set; } = new(); + public virtual JObject Data { get; set; } = new(); public ValueTask CanExecuteAsync(ActivityExecutionContext context) => OnCanExecuteAsync(context); public ValueTask ExecuteAsync(ActivityExecutionContext context) => OnExecuteAsync(context); public ValueTask ResumeAsync(ActivityExecutionContext context) => OnResumeAsync(context); diff --git a/src/core/Elsa.Abstractions/Services/CompositeActivity.cs b/src/core/Elsa.Abstractions/Services/CompositeActivity.cs index fd11a426a..6b7f167f0 100644 --- a/src/core/Elsa.Abstractions/Services/CompositeActivity.cs +++ b/src/core/Elsa.Abstractions/Services/CompositeActivity.cs @@ -4,6 +4,7 @@ using Elsa.ActivityResults; using Elsa.Builders; using Elsa.Models; using Elsa.Services.Models; +using Newtonsoft.Json.Linq; namespace Elsa.Services { @@ -21,6 +22,29 @@ namespace Elsa.Services set => SetState(value); } + public override JObject Data + { + get + { + // When executing a composite activity's workflow, child activities might try and reference the composite activity's "input" properties. + // Out of the box, that doesn't work, since "this" points to a new instance with an empty Data object. + // Instead, we need to capture the Data object of the composite activity (the parent of the currently executing child activity) using the ambient activity execution context. + if (AmbientActivityExecutionContext.Current == null) + return base.Data; + + var context = AmbientActivityExecutionContext.Current; + + // Check if the currently executing activity is something other than this composite activity. + if (context.ActivityBlueprint.Type == Type) + return base.Data; + + // A child activity is attempting to retrieve property data from its parent composite activity. + var parentId = context.ActivityBlueprint.Parent!.Id; + return context.WorkflowInstance.ActivityData.GetItem(parentId)!; + } + set => base.Data = value; + } + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { if (!IsScheduled) diff --git a/src/core/Elsa.Abstractions/Services/Models/AmbientActivityExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/AmbientActivityExecutionContext.cs new file mode 100644 index 000000000..c55e0d722 --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/Models/AmbientActivityExecutionContext.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; + +namespace Elsa.Services.Models +{ + public static class AmbientActivityExecutionContext + { + private static readonly AsyncLocal ActivityExecutionContext = new(); + + public static ActivityExecutionContext? Current + { + get => ActivityExecutionContext.Value; + private set => ActivityExecutionContext.Value = value; + } + + public static ActivityExecutionContextScope EnterScope(ActivityExecutionContext activityExecutionContext) + { + Current = activityExecutionContext; + return new ActivityExecutionContextScope(() => Current = null); + } + } + + public record ActivityExecutionContextScope(Action Reset) : IDisposable + { + public void Dispose() + { + Reset(); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index e26d854e8..477c157d9 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -287,6 +287,7 @@ namespace Elsa.Services resuming, cancellationToken); + using var executionScope = AmbientActivityExecutionContext.EnterScope(activityExecutionContext); var activity = await activityExecutionContext.ActivateActivityAsync(cancellationToken); var canExecute = await activity.ActivityType.CanExecuteAsync(activityExecutionContext); @@ -328,6 +329,8 @@ namespace Elsa.Services var activityExecutionContext = new ActivityExecutionContext(scope, workflowExecutionContext, activityBlueprint, scheduledActivity.Input, resuming, cancellationToken); var activity = await activityExecutionContext.ActivateActivityAsync(cancellationToken); + using var executionScope = AmbientActivityExecutionContext.EnterScope(activityExecutionContext); + if (resuming) await _mediator.Publish(new ActivityResuming(activityExecutionContext), cancellationToken); diff --git a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs index f971712bc..21f276d1a 100644 --- a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs +++ b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs @@ -12,7 +12,7 @@ namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Activities public class CountdownActivity : CompositeActivity { // Exposed properties on composite activities must store their values in the State property bag so that it gets persisted & available to child activities. - // Activities are scheduled one by one, which means that when the child activities are executed, the container activity (such as this one) will not be in memory - the default value will be captured when the property is used directly. + // Think of composite activities as workflows and their properties as inputs into the workflow. [ActivityProperty(Hint = "The start number.")] public int Start { @@ -23,9 +23,7 @@ namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Activities public override void Build(ICompositeActivityBuilder activity) { activity - // IMPORTANT: Notice that we need to get the "parent" state to get the `Start` value, since here we are in a different context than the CountDownActivity itself. - // Accessing `Start` directly would return the captured value at the time this `Build` method executed when creating workflow blueprints. - .For(context => context.GetContainerState(nameof(Start)), _ => 0, _ => -1, iterate => + .For(() => Start, () => 0, () => -1, iterate => { iterate.WriteLine(context => $"{context.ForScope().CurrentValue}..."); }, Operator.GreaterThan)