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:
parent
a0001d3891
commit
52516aa0ff
|
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue