diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index a1bb1883d..144497879 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -701,6 +701,13 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// True if the memory block exists, false otherwise. public bool TryGet(MemoryBlockReference blockReference, out object? value) { + // Handle Literal references directly - they hold their value and don't need to be in the memory register + if (blockReference is Literal literal) + { + value = literal.Value; + return true; + } + var memoryBlock = GetMemoryBlock(blockReference); if (memoryBlock != null) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 57b5fae04..56509f366 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -639,6 +639,14 @@ public partial class WorkflowExecutionContext : IExecutionContext { RemoveActivityExecutionContexts(x => x is { IsCompleted: true, ParentActivityExecutionContext: not null }); } + + /// + /// Clears all activity completion callback entries from the workflow execution context. + /// + public void ClearCompletionCallbacks() + { + _completionCallbackEntries.Clear(); + } public IEnumerable GetActiveActivityExecutionContexts() { diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Core/LiteralInputTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Core/LiteralInputTests.cs new file mode 100644 index 000000000..4570790f6 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Core/LiteralInputTests.cs @@ -0,0 +1,119 @@ +using Elsa.Expressions.Models; +using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Memory; +using Elsa.Workflows.Models; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Core; + +/// +/// Tests for ActivityExecutionContext handling of Literal inputs in real scenarios +/// +public class LiteralInputTests +{ + private readonly IServiceProvider _services; + private readonly IWorkflowRunner _workflowRunner; + + public LiteralInputTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper).Build(); + _workflowRunner = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Activity should be able to access Input created with Literal")] + public async Task ActivityShouldAccessLiteralInput() + { + // Arrange - Create a custom activity that uses another activity with literal input + await _services.PopulateRegistriesAsync(); + + var workflow = new TestWorkflow(builder => + { + builder.Root = new CompositeActivityWithLiteralInput(); + }); + + // Act & Assert - Should not throw + var result = await _workflowRunner.RunAsync(workflow); + Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); + } + + [Fact(DisplayName = "ActivityExecutionContext.TryGet should return true and value for Literal")] + public async Task TryGet_ShouldHandleLiteralDirectly() + { + // Arrange - Create a workflow and activity execution context + await _services.PopulateRegistriesAsync(); + + var activity = new WriteLine("Test"); + var workflow = new Workflow { Root = activity }; + + var workflowGraphBuilder = _services.GetRequiredService(); + var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow); + var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_services, workflowGraph, "test"); + var activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity); + + // Create a Literal and use it as a MemoryBlockReference + var expectedValue = "Hello World"; + var literal = new Literal(expectedValue); + var blockReference = (MemoryBlockReference)literal; + + // Act - Call TryGet directly with the Literal + var success = activityExecutionContext.TryGet(blockReference, out var actualValue); + + // Assert - Should succeed and return the literal's value + Assert.True(success, "TryGet should return true for Literal references"); + Assert.Equal(expectedValue, actualValue); + } + + [Fact(DisplayName = "ActivityExecutionContext.Get with Input containing Literal should work")] + public async Task Get_ShouldWorkWithInputContainingLiteral() + { + // Arrange + await _services.PopulateRegistriesAsync(); + + var activity = new WriteLine("Test"); + var workflow = new Workflow { Root = activity }; + + var workflowGraphBuilder = _services.GetRequiredService(); + var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow); + var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_services, workflowGraph, "test"); + var activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity); + + // Create an Input with a Literal value + var expectedValue = 42; + var literal = new Literal(expectedValue); + var input = new Input(literal); + + // Act - Get the value through the Input (which internally uses TryGet) + var actualValue = activityExecutionContext.Get(input); + + // Assert + Assert.Equal(expectedValue, actualValue); + } +} + +/// +/// A composite activity that creates inputs with literal values and tries to read them +/// This simulates the use case described in the issue where activities re-use other activities' execute methods +/// +public class CompositeActivityWithLiteralInput : CodeActivity +{ + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Create an Input with a Literal value - this is a common pattern when programmatically + // creating activities and setting their inputs + var literal = new Literal("Test Value"); + var input = new Input(literal); + + // Try to get the value - this should work but will fail without the Literal handling in TryGet + // The issue is that when Input is created with a Literal, the Literal becomes the MemoryBlockReference + // When Get is called, it tries to find this in the memory register, but Literals hold values directly + var value = context.Get(input); + + // If we got here without exception, the test passes + if (value != "Test Value") + throw new Exception($"Expected 'Test Value' but got '{value}'"); + + return ValueTask.CompletedTask; + } +}