diff --git a/src/core/Elsa.Core/Activities/Fork.cs b/src/core/Elsa.Core/Activities/Fork.cs index cf40416bd..8d12162a6 100644 --- a/src/core/Elsa.Core/Activities/Fork.cs +++ b/src/core/Elsa.Core/Activities/Fork.cs @@ -26,7 +26,7 @@ public class Fork : Activity [Outbound] public ICollection Branches { get; set; } = new List(); - 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) { diff --git a/src/core/Elsa.Core/Activities/While.cs b/src/core/Elsa.Core/Activities/While.cs index 3e42f434a..0a8f990b2 100644 --- a/src/core/Elsa.Core/Activities/While.cs +++ b/src/core/Elsa.Core/Activities/While.cs @@ -43,7 +43,7 @@ public class While : Activity { } - [Input] public Input Condition { get; set; } = new(false); + [Input(AutoEvaluate = false)] public Input 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(x => x.Condition); + //var loop = context.Get(Condition); if (loop) context.ScheduleActivity(Body, OnBodyCompleted); diff --git a/src/core/Elsa.Core/Attributes/InputAttribute.cs b/src/core/Elsa.Core/Attributes/InputAttribute.cs index b67c8e8dd..49fb6b854 100644 --- a/src/core/Elsa.Core/Attributes/InputAttribute.cs +++ b/src/core/Elsa.Core/Attributes/InputAttribute.cs @@ -76,4 +76,10 @@ public class InputAttribute : Attribute /// A value indicating whether this property should be visible. /// public bool IsBrowsable { get; set; } = true; + + /// + /// 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. + /// + public bool AutoEvaluate { get; set; } = true; } \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/core/Elsa.Core/Extensions/ActivityExecutionContextExtensions.cs index 4fbaee413..b2d00288c 100644 --- a/src/core/Elsa.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -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(this ActivityExecutionContext context, string name) => context.ExpressionExecutionContext.GetVariable(name); + /// + /// Evaluates each input property of the activity. + /// + 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(); + 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 EvaluateInputPropertyAsync(this ActivityExecutionContext context, Expression>> propertyExpression) + { + var inputName = propertyExpression.GetProperty()!.Name; + var input = await EvaluateInputPropertyAsync(context, inputName); + return context.Get((Input)input); + } + + /// + /// Evaluates a specific input property of the activity. + /// + public static async Task 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(); + 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())); } \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ActivityExtensions.cs b/src/core/Elsa.Core/Extensions/ActivityExtensions.cs index 7a9832177..054e88f43 100644 --- a/src/core/Elsa.Core/Extensions/ActivityExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ActivityExtensions.cs @@ -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 GetOutputs(this IActivity activity) { diff --git a/src/core/Elsa.Core/Models/ActivityExecutionContext.cs b/src/core/Elsa.Core/Models/ActivityExecutionContext.cs index 96aa6a859..0a305bd50 100644 --- a/src/core/Elsa.Core/Models/ActivityExecutionContext.cs +++ b/src/core/Elsa.Core/Models/ActivityExecutionContext.cs @@ -84,13 +84,47 @@ public class ActivityExecutionContext WorkflowExecutionContext.Schedule(activity, owner, completionCallback, locationReferences, tag); } - public void PostActivities(params IActivity?[] activities) => PostActivities((IEnumerable)activities); + public void ScheduleActivities(params IActivity?[] activities) => ScheduleActivities((IEnumerable)activities); - public void PostActivities(IEnumerable activities, ActivityCompletionCallback? completionCallback = default) + public void ScheduleActivities(IEnumerable activities, ActivityCompletionCallback? completionCallback = default) { foreach (var activity in activities) ScheduleActivity(activity, completionCallback); } + + /// + /// Send a signal up the current branch. + /// + 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; + } + } + + /// + /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. + /// + public async ValueTask CompleteActivityAsync() + { + // Send a signal. + await SignalAsync(new ActivityCompleted()); + + // Remove the context. + WorkflowExecutionContext.ActivityExecutionContexts.Remove(this); + } public void CreateBookmarks(IEnumerable bookmarkData, ExecuteActivityDelegate? callback = default) { @@ -169,39 +203,6 @@ public class ActivityExecutionContext /// public void PreventContinuation() => Continue = false; - /// - /// Send a signal up the current branch. - /// - 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; - } - } - - /// - /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. - /// - public async ValueTask CompleteActivityAsync() - { - // Send a signal. - await SignalAsync(new ActivityCompleted()); - - // Remove the context. - WorkflowExecutionContext.ActivityExecutionContexts.Remove(this); - } - /// /// Returns a flattened list of the current context's ancestors. /// diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs index 602c2779f..a3f2ed672 100644 --- a/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs @@ -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(); - var expressionExecutionContext = context.ExpressionExecutionContext; - - foreach (var input in assignedInputs) - { - var locationReference = input.LocationReference; - var value = await evaluator.EvaluateAsync(input, expressionExecutionContext); - locationReference.Set(context, value); - } - } } \ No newline at end of file diff --git a/src/modules/Elsa.Modules.Activities/Activities/Workflows/Flowchart.cs b/src/modules/Elsa.Modules.Activities/Activities/Workflows/Flowchart.cs index 616c66662..0abf9f18a 100644 --- a/src/modules/Elsa.Modules.Activities/Activities/Workflows/Flowchart.cs +++ b/src/modules/Elsa.Modules.Activities/Activities/Workflows/Flowchart.cs @@ -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); } } \ No newline at end of file diff --git a/test/Elsa.IntegrationTests/Activities/If/ComplexIfWorkflow.cs b/test/Elsa.IntegrationTests/Activities/If/ComplexIfWorkflow.cs index 0349a0dfa..7c25a4a11 100644 --- a/test/Elsa.IntegrationTests/Activities/If/ComplexIfWorkflow.cs +++ b/test/Elsa.IntegrationTests/Activities/If/ComplexIfWorkflow.cs @@ -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") } diff --git a/test/Elsa.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows.cs b/test/Elsa.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows.cs index f3389a423..3310ab4cc 100644 --- a/test/Elsa.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows.cs +++ b/test/Elsa.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows.cs @@ -12,36 +12,21 @@ public class BreakWhileBlockForkWorkflow : IWorkflow { var currentValue = new Variable(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(context)}"), + new SetVariable(currentValue, context => currentValue.Get(context) + 1), new Fork { Branches = { - new Sequence - { - Activities = - { - new SetVariable - { - Variable = currentValue, - Value = new Input(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 } } } - }), - } + } }); } } \ No newline at end of file