Introduce AmbientActivityExecutionContext to support direct access to composite activity input properties from child activities (#648)
This commit is contained in:
parent
15f1411838
commit
a29905bae2
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue