Add ability to control when to evaluate input

This commit is contained in:
Sipke Schoorstra 2022-04-24 22:26:33 +02:00
parent b947177ece
commit dbd682eacb
10 changed files with 123 additions and 96 deletions

View file

@ -26,7 +26,7 @@ public class Fork : Activity
[Outbound]
public ICollection<IActivity> Branches { get; set; } = new List<IActivity>();
protected override void Execute(ActivityExecutionContext context) => context.PostActivities(Branches.Reverse(), CompleteChildAsync);
protected override void Execute(ActivityExecutionContext context) => context.ScheduleActivities(Branches.Reverse(), CompleteChildAsync);
private async ValueTask CompleteChildAsync(ActivityExecutionContext context, ActivityExecutionContext childContext)
{

View file

@ -43,7 +43,7 @@ public class While : Activity
{
}
[Input] public Input<bool> Condition { get; set; } = new(false);
[Input(AutoEvaluate = false)] public Input<bool> Condition { get; set; } = new(false);
[Outbound] public IActivity Body { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
@ -58,7 +58,8 @@ public class While : Activity
private async ValueTask HandleIterationAsync(ActivityExecutionContext context)
{
var loop = context.Get(Condition);
var loop = await context.EvaluateInputPropertyAsync<While, bool>(x => x.Condition);
//var loop = context.Get(Condition);
if (loop)
context.ScheduleActivity(Body, OnBodyCompleted);

View file

@ -76,4 +76,10 @@ public class InputAttribute : Attribute
/// A value indicating whether this property should be visible.
/// </summary>
public bool IsBrowsable { get; set; } = true;
/// <summary>
/// True if the activity invoker should evaluate the expression, false otherwise.
/// When set to false, it is up to the activity itself to evaluate its input before using it.
/// </summary>
public bool AutoEvaluate { get; set; } = true;
}

View file

@ -1,3 +1,4 @@
using System.Linq.Expressions;
using Elsa.Contracts;
using Elsa.Models;
using Microsoft.Extensions.Logging;
@ -26,5 +27,55 @@ public static class ActivityExecutionContextExtensions
public static Variable SetVariable(this ActivityExecutionContext context, string name, object? value) => context.ExpressionExecutionContext.SetVariable(name, value);
public static T? GetVariable<T>(this ActivityExecutionContext context, string name) => context.ExpressionExecutionContext.GetVariable<T?>(name);
/// <summary>
/// Evaluates each input property of the activity.
/// </summary>
public static async Task EvaluateInputPropertiesAsync(this ActivityExecutionContext context)
{
var activity = context.Activity;
var inputs = activity.GetInputs();
var assignedInputs = inputs.Where(x => x.LocationReference != null!).ToList();
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var expressionExecutionContext = context.ExpressionExecutionContext;
foreach (var input in assignedInputs)
{
var locationReference = input.LocationReference;
var value = await evaluator.EvaluateAsync(input, expressionExecutionContext);
locationReference.Set(context, value);
}
}
public static async Task<T?> EvaluateInputPropertyAsync<TActivity, T>(this ActivityExecutionContext context, Expression<Func<TActivity, Input<T>>> propertyExpression)
{
var inputName = propertyExpression.GetProperty()!.Name;
var input = await EvaluateInputPropertyAsync(context, inputName);
return context.Get((Input<T>)input);
}
/// <summary>
/// Evaluates a specific input property of the activity.
/// </summary>
public static async Task<Input> EvaluateInputPropertyAsync(this ActivityExecutionContext context, string inputName)
{
var activity = context.Activity;
var input = activity.GetInput(inputName);
if (input == null)
throw new Exception($"No input with name {inputName} could be found");
if (input.LocationReference == null!)
throw new Exception("Input not initialized");
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var expressionExecutionContext = context.ExpressionExecutionContext;
var locationReference = input.LocationReference;
var value = await evaluator.EvaluateAsync(input, expressionExecutionContext);
locationReference.Set(context, value);
return input;
}
public static ILogger GetLogger(this ActivityExecutionContext context) => (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(context.Activity.GetType()));
}

View file

@ -19,6 +19,16 @@ public static class ActivityExtensions
return query.Select(x => x!).ToList();
}
public static Input? GetInput(this IActivity activity, string inputName)
{
var inputProp = activity.GetType().GetProperties().FirstOrDefault(x => typeof(Input).IsAssignableFrom(x.PropertyType) && x.Name == inputName);
if (inputProp == null)
return null;
return (Input?)inputProp.GetValue(activity);
}
public static IEnumerable<Output> GetOutputs(this IActivity activity)
{

View file

@ -84,13 +84,47 @@ public class ActivityExecutionContext
WorkflowExecutionContext.Schedule(activity, owner, completionCallback, locationReferences, tag);
}
public void PostActivities(params IActivity?[] activities) => PostActivities((IEnumerable<IActivity?>)activities);
public void ScheduleActivities(params IActivity?[] activities) => ScheduleActivities((IEnumerable<IActivity?>)activities);
public void PostActivities(IEnumerable<IActivity?> activities, ActivityCompletionCallback? completionCallback = default)
public void ScheduleActivities(IEnumerable<IActivity?> activities, ActivityCompletionCallback? completionCallback = default)
{
foreach (var activity in activities)
ScheduleActivity(activity, completionCallback);
}
/// <summary>
/// Send a signal up the current branch.
/// </summary>
public async ValueTask SignalAsync(object signal)
{
var ancestorContexts = GetAncestors();
foreach (var ancestorContext in ancestorContexts)
{
var signalContext = new SignalContext(ancestorContext, this, CancellationToken);
if (ancestorContext.Activity is not ISignalHandler handler)
continue;
await handler.HandleSignalAsync(signal, signalContext);
if (signalContext.StopPropagationRequested)
return;
}
}
/// <summary>
/// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion.
/// </summary>
public async ValueTask CompleteActivityAsync()
{
// Send a signal.
await SignalAsync(new ActivityCompleted());
// Remove the context.
WorkflowExecutionContext.ActivityExecutionContexts.Remove(this);
}
public void CreateBookmarks(IEnumerable<object> bookmarkData, ExecuteActivityDelegate? callback = default)
{
@ -169,39 +203,6 @@ public class ActivityExecutionContext
/// </summary>
public void PreventContinuation() => Continue = false;
/// <summary>
/// Send a signal up the current branch.
/// </summary>
public async ValueTask SignalAsync(object signal)
{
var ancestorContexts = GetAncestors();
foreach (var ancestorContext in ancestorContexts)
{
var signalContext = new SignalContext(ancestorContext, this, CancellationToken);
if (ancestorContext.Activity is not ISignalHandler handler)
continue;
await handler.HandleSignalAsync(signal, signalContext);
if (signalContext.StopPropagationRequested)
return;
}
}
/// <summary>
/// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion.
/// </summary>
public async ValueTask CompleteActivityAsync()
{
// Send a signal.
await SignalAsync(new ActivityCompleted());
// Remove the context.
WorkflowExecutionContext.ActivityExecutionContexts.Remove(this);
}
/// <summary>
/// Returns a flattened list of the current context's ancestors.
/// </summary>

View file

@ -28,7 +28,7 @@ public class ActivityInvokerMiddleware : IActivityExecutionMiddleware
var workflowExecution = context.WorkflowExecutionContext;
// Evaluate input properties.
await EvaluateInputPropertiesAsync(context);
await context.EvaluateInputPropertiesAsync();
var activity = context.Activity;
// Execute activity.
@ -62,20 +62,4 @@ public class ActivityInvokerMiddleware : IActivityExecutionMiddleware
}
private void LogExecutionRecord(ActivityExecutionContext context, string eventName, string? message = default, string? source = default, object? payload = default) => context.AddExecutionLogEntry(eventName, message, source, payload);
private async Task EvaluateInputPropertiesAsync(ActivityExecutionContext context)
{
var activity = context.Activity;
var inputs = activity.GetInputs();
var assignedInputs = inputs.Where(x => x.LocationReference != null!).ToList();
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var expressionExecutionContext = context.ExpressionExecutionContext;
foreach (var input in assignedInputs)
{
var locationReference = input.LocationReference;
var value = await evaluator.EvaluateAsync(input, expressionExecutionContext);
locationReference.Set(context, value);
}
}
}

View file

@ -35,6 +35,6 @@ public class Flowchart : Container
var outboundConnections = Connections.Where(x => x.Source == parent).ToList();
var children = outboundConnections.Select(x => x.Target).ToList();
context.PostActivities(children, OnChildCompleted);
context.ScheduleActivities(children, OnChildCompleted);
}
}

View file

@ -21,24 +21,14 @@ public class ComplexIfWorkflow : IWorkflow
Activities =
{
new WriteLine("Start"),
new If(_condition)
new If
{
Then = new Sequence
{
Activities =
{
new WriteLine("Executing"),
new WriteLine("True!"),
}
},
Else = new Sequence
{
Activities =
{
new WriteLine("Executing"),
new WriteLine("False!"),
}
}
Then = new WriteLine(""),
Else = new Sequence(
new WriteLine("Executing"),
new WriteLine("False!")
)
},
new WriteLine("End")
}

View file

@ -12,36 +12,21 @@ public class BreakWhileBlockForkWorkflow : IWorkflow
{
var currentValue = new Variable<int?>(0);
workflow.WithRoot(new Sequence
workflow.WithVariable(currentValue);
workflow.WithRoot(new While(context => currentValue.Get(context) < 3)
{
Variables = { currentValue },
Activities =
{
While.True(new Sequence
Body =
new Sequence
{
Activities =
{
new WriteLine(context => $"Current value: {currentValue.Get<int>(context)}"),
new SetVariable<int?>(currentValue, context => currentValue.Get(context) + 1),
new Fork
{
Branches =
{
new Sequence
{
Activities =
{
new SetVariable
{
Variable = currentValue,
Value = new Input<object?>(context => currentValue.Get(context) + 1)
},
new If(context => currentValue.Get(context) == 3)
{
Then = new Break()
},
new WriteLine("Branch 1"),
}
},
new WriteLine("Branch 1"),
new Sequence
{
Activities =
@ -54,8 +39,7 @@ public class BreakWhileBlockForkWorkflow : IWorkflow
}
}
}
}),
}
}
});
}
}