Fix Bulk Dispatch Missing Input (#6052)

* Remove outdated sample workflows and activities

Deleted CompositeExample, SampleWorkflow, and SlowActivity classes as they are no longer needed. These deletions help clean up the codebase and maintain relevance in the code structure.

* Handle null WorkflowBuilderType in ClrWorkflowMaterializer

Introduce a fallback NotFoundWorkflowbuilder class when WorkflowBuilderType is null. This ensures the MaterializeAsync method functions even if the provided context lacks a specific workflow builder type.

* Add merging of itemDictionary to input

This change ensures that the itemDictionary's contents are also merged into the input dictionary, preventing possible data loss. It builds on existing logic by adding an additional merge operation to incorporate all necessary data.

* Add new workflows to BulkDispatchWorkflows and modify tests

Introduce `FruitWorkflow` and `MixFruitsWorkflow` for bulk dispatch scenarios. Refactor `BulkDispatchWorkflowsTests` to include new tests and rename signals for clarity. Optimize `BulkDispatchWorkflows` activity by removing redundant dictionary merge.
This commit is contained in:
Sipke Schoorstra 2024-10-23 16:06:28 +02:00 committed by GitHub
parent a0001d3891
commit 52516aa0ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 145 additions and 142 deletions

View file

@ -1,40 +0,0 @@
using Elsa.AzureServiceBus.Activities;
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
namespace Elsa.Server.Web.Activities;
[Activity("Elsa", "Example")]
public class CompositeExample : Composite
{
/// <summary>
/// The name of the queue or topic to read from.
/// </summary>
[Input(Description = "The name of the queue or topic to read from.")]
public Input<string> QueueOrTopic { get; set; } = default!;
private MessageReceived _messageReceived = default!;
/// <inheritdoc />
public override void Setup()
{
_messageReceived = new MessageReceived
{
QueueOrTopic = QueueOrTopic,
CanStartWorkflow = true
};
var writeLine = new WriteLine("Hello World!");
Root = new Sequence
{
Activities =
{
_messageReceived,
writeLine
}
};
}
}

View file

@ -1,37 +0,0 @@
using System.Reflection;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Models;
namespace Elsa.Server.Web.Activities;
/// <summary>
/// Simulate an activity that takes a long time to complete.
/// </summary>
[Activity("Testing", "Testing", "Simulate an activity that takes a long time to complete.")]
public class SlowActivity : CodeActivity, IActivityPropertyDefaultValueProvider
{
/// <summary>
/// The delay.
/// </summary>
[Input(Description = "The delay.", DefaultValueProvider = typeof(SlowActivity))]
public Input<TimeSpan> Delay { get; set; } = new(TimeSpan.FromSeconds(1));
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var delay = Delay.Get(context);
await Task.Delay(delay, context.CancellationToken);
}
object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property)
{
return property.Name switch
{
nameof(Delay) => TimeSpan.FromSeconds(1),
_ => default
};
}
}

View file

@ -1,41 +0,0 @@
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
namespace Elsa.Server.Web;
public class SampleWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
// The WithVariable method ensures that the created variable will be added to the Workflow's Variables collection, which is required for persistent variables.
var variable1 = workflow.WithVariable<string>("Foo").WithWorkflowStorage();
workflow.Variables =
[
variable1
];
workflow.Root = new Sequence
{
Activities =
{
new StartAt(DateTimeOffset.UtcNow + TimeSpan.FromSeconds(5))
{
CanStartWorkflow = true
},
new WriteLine(variable1),
new SetVariable
{
Variable = variable1,
Value = new (Literal.From("Bar"))
},
new Delay(TimeSpan.FromSeconds(1)),
new WriteLine(variable1)
}
};
}
}

View file

@ -42,7 +42,7 @@ public class ClrWorkflowMaterializer : IWorkflowMaterializer
public async ValueTask<Workflow> MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
var providerContext = _payloadSerializer.Deserialize<ClrWorkflowMaterializerContext>(definition.MaterializerContext!);
var workflowBuilderType = providerContext.WorkflowBuilderType;
var workflowBuilderType = providerContext.WorkflowBuilderType == null! ? typeof(NotFoundWorkflowbuilder) : providerContext.WorkflowBuilderType;
var workflowBuilder = (IWorkflow)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, workflowBuilderType);
var workflowDefinitionBuilder = _workflowBuilderFactory.CreateBuilder();
var workflow = await workflowDefinitionBuilder.BuildWorkflowAsync(workflowBuilder, cancellationToken);
@ -58,4 +58,11 @@ public class ClrWorkflowMaterializer : IWorkflowMaterializer
/// Provides context for the CLR workflow materializer.
/// </summary>
/// <param name="WorkflowBuilderType">The type of the workflow builder.</param>
public record ClrWorkflowMaterializerContext(Type WorkflowBuilderType);
public record ClrWorkflowMaterializerContext(Type WorkflowBuilderType);
/// <summary>
/// A workflow builder that is used when the workflow builder type is not found.
/// </summary>
public class NotFoundWorkflowbuilder : WorkflowBase
{
}

View file

@ -195,7 +195,7 @@ public class BulkDispatchWorkflows : Activity
Arguments = itemDictionary
};
var inputDictionary = item as IDictionary<string, object> ?? new Dictionary<string, object>();
var inputDictionary = item as IDictionary<string, object> ?? itemDictionary;
input["ParentInstanceId"] = parentInstanceId;
input.Merge(inputDictionary);

View file

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers_005Ccontracts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers_005Ceventargs/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

View file

@ -1,17 +0,0 @@
namespace Elsa.Workflows.ComponentTests.Activities;
public class SignalResetEvent : CodeActivity
{
public SignalResetEvent(string eventName)
{
EventName = eventName;
}
public string EventName { get; set; }
protected override void Execute(ActivityExecutionContext context)
{
var testEventManager = context.GetRequiredService<ISignalManager>();
testEventManager.Trigger(EventName);
}
}

View file

@ -0,0 +1,55 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Workflows.ComponentTests.Activities;
public class TriggerSignal : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
private TriggerSignal(string? source = default, int? line = default) : base(source, line)
{
}
/// <inheritdoc />
public TriggerSignal(string eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Literal<string>(eventName), source, line)
{
}
/// <inheritdoc />
public TriggerSignal(Func<string> eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
: this(Expression.DelegateExpression(eventName), source, line)
{
}
/// <inheritdoc />
public TriggerSignal(Func<ExpressionExecutionContext, string?> eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
: this(Expression.DelegateExpression(eventName), source, line)
{
}
/// <inheritdoc />
public TriggerSignal(Variable<string> variable, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = new Input<string>(variable);
/// <inheritdoc />
public TriggerSignal(Literal<string> literal, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = new Input<string>(literal);
/// <inheritdoc />
public TriggerSignal(Expression expression, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = new Input<string>(expression, new MemoryBlockReference());
/// <inheritdoc />
public TriggerSignal(Input<string> eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = eventName;
public Input<string> EventName { get; set; }
protected override void Execute(ActivityExecutionContext context)
{
var testEventManager = context.GetRequiredService<ISignalManager>();
var eventName = EventName.Get(context);
testEventManager.Trigger(eventName);
}
}

View file

@ -8,7 +8,12 @@ public class SignalManager : ISignalManager
public async Task<T> WaitAsync<T>(object signal, int millisecondsTimeout = 5000)
{
return await WaitAsync(signal, millisecondsTimeout) is T result ? result : throw new InvalidCastException($"Signal '{signal}' was not of type '{typeof(T).Name}'.");
var result = await WaitAsync(signal, millisecondsTimeout);
if(result is not T typedResult)
throw new InvalidCastException($"Signal '{signal}' was not of type '{typeof(T).Name}'.");
return typedResult;
}
public async Task<object?> WaitAsync(object signal, int millisecondsTimeout = 5000)

View file

@ -9,7 +9,7 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
private readonly IWorkflowEvents _workflowEvents;
private readonly ISignalManager _signalManager;
private readonly IWorkflowRuntime _workflowRuntime;
private static readonly object ParentWorkflowCompletedSignal = new();
private static readonly object GreetEmployeesWorkflowCompletedSignal = new();
public BulkDispatchWorkflowsTests(App app) : base(app)
{
@ -26,10 +26,22 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete()
{
await _workflowRuntime.StartWorkflowAsync(GreetEmployeesWorkflow.DefinitionId);
var parentWorkflowInstanceArgs = await _signalManager.WaitAsync<WorkflowInstanceSavedEventArgs>(ParentWorkflowCompletedSignal);
var parentWorkflowInstanceArgs = await _signalManager.WaitAsync<WorkflowInstanceSavedEventArgs>(GreetEmployeesWorkflowCompletedSignal);
Assert.Equal(WorkflowStatus.Finished, parentWorkflowInstanceArgs.WorkflowInstance.Status);
}
/// <summary>
/// Individual items are sent as input to child workflows.
/// </summary>
[Fact]
public async Task DispatchWorkflows_ChildWorkflowsShouldReceiveCurrentItem()
{
await _workflowRuntime.StartWorkflowAsync(MixFruitsWorkflow.DefinitionId);
await _signalManager.WaitAsync("Apple");
await _signalManager.WaitAsync("Banana");
await _signalManager.WaitAsync("Cherry");
}
private void OnWorkflowInstanceSaved(object? sender, WorkflowInstanceSavedEventArgs e)
{
@ -37,7 +49,7 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
return;
if(e.WorkflowInstance.DefinitionId == GreetEmployeesWorkflow.DefinitionId)
_signalManager.Trigger(ParentWorkflowCompletedSignal, e);
_signalManager.Trigger(GreetEmployeesWorkflowCompletedSignal, e);
}
protected override void OnDispose()

View file

@ -0,0 +1,27 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.Activities;
using Elsa.Workflows.Contracts;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
[UsedImplicitly]
public class FruitWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var item = builder.WithInput<string>("Item");
builder.Root = new Sequence
{
Activities =
{
new WriteLine(x => $"Mixing {x.GetInput<string>(item)}"),
new TriggerSignal(x => x.GetInput<string>(item))
}
};
}
}

View file

@ -0,0 +1,31 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
public class MixFruitsWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
var fruits = new[]
{
"Apple", "Banana", "Cherry"
};
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new Runtime.Activities.BulkDispatchWorkflows
{
WorkflowDefinitionId = new(FruitWorkflow.DefinitionId),
Items = new(fruits),
WaitForCompletion = new(true)
}
}
};
}
}