Introduce AmbientActivityExecutionContext to support direct access to composite activity input properties from child activities (#648)

This commit is contained in:
Sipke Schoorstra 2021-02-20 15:23:39 +01:00 committed by GitHub
parent 15f1411838
commit a29905bae2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 5 deletions

View file

@ -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<bool> CanExecuteAsync(ActivityExecutionContext context) => OnCanExecuteAsync(context);
public ValueTask<IActivityExecutionResult> ExecuteAsync(ActivityExecutionContext context) => OnExecuteAsync(context);
public ValueTask<IActivityExecutionResult> ResumeAsync(ActivityExecutionContext context) => OnResumeAsync(context);

View file

@ -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<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
if (!IsScheduled)

View file

@ -0,0 +1,30 @@
using System;
using System.Threading;
namespace Elsa.Services.Models
{
public static class AmbientActivityExecutionContext
{
private static readonly AsyncLocal<ActivityExecutionContext?> 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();
}
}
}

View file

@ -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);

View file

@ -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<int>(nameof(Start)), _ => 0, _ => -1, iterate =>
.For(() => Start, () => 0, () => -1, iterate =>
{
iterate.WriteLine(context => $"{context.ForScope().CurrentValue}...");
}, Operator.GreaterThan)