Merge pull request #6495 from elsa-workflows/enh/explicit-deterministic-variable-id

Refactor variable initialization for clarity and consistency
This commit is contained in:
Sipke Schoorstra 2025-03-14 23:05:35 +01:00 committed by GitHub
commit d8208d2fe7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 90 additions and 97 deletions

View file

@ -52,7 +52,7 @@ public abstract class WorkflowBase<TResult> : WorkflowBase
/// <inheritdoc />
protected WorkflowBase()
{
Result = new Variable<TResult>();
Result = new("Result", default!);
}
/// <summary>

View file

@ -10,7 +10,7 @@ namespace Elsa.Workflows.Activities;
public class ParallelForEach : ParallelForEach<object>
{
/// <inheritdoc />
public ParallelForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
public ParallelForEach([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
}

View file

@ -20,7 +20,7 @@ public class ParallelForEach<T> : Activity
private const string CompletedTagsProperty = nameof(CompletedTagsProperty);
/// <inheritdoc />
public ParallelForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
public ParallelForEach([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
@ -34,7 +34,7 @@ public class ParallelForEach<T> : Activity
/// The <see cref="IActivity"/> to execute each iteration.
/// </summary>
[Port]
public IActivity Body { get; set; } = default!;
public IActivity Body { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)

View file

@ -72,9 +72,10 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer
}
/// <inheritdoc />
[Obsolete("Use the overload that takes a name instead. This overload will be removed in a future version.")]
public Variable<T> WithVariable<T>()
{
var variable = new Variable<T>();
var variable = new Variable<T>(null!, default!);
Variables.Add(variable);
variable.WithWorkflowStorage();
variable.Id = null!; // This ensures that a deterministic ID is assigned by the builder.
@ -84,13 +85,13 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer
/// <inheritdoc />
public Variable<T> WithVariable<T>(string name, T value)
{
var variable = WithVariable<T>();
variable.Name = name;
variable.Value = value;
var variable = new Variable<T>(name, value);
Variables.Add(variable);
return variable;
}
/// <inheritdoc />
[Obsolete("Use the overload that takes a name instead. This overload will be removed in a future version.")]
public Variable<T> WithVariable<T>(T value)
{
var variable = WithVariable<T>();

View file

@ -1,5 +1,7 @@
using System.Text.Json.Serialization;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Humanizer;
namespace Elsa.Workflows.Memory;
@ -11,19 +13,23 @@ public class Variable : MemoryBlockReference
/// <inheritdoc />
public Variable()
{
Id = Guid.NewGuid().ToString("N");
}
/// <inheritdoc />
public Variable(string name) : this()
public Variable(string name)
{
Id = GetIdFromName(name);
Name = name;
}
/// <inheritdoc />
public Variable(string name, object? value = null) : this()
public Variable(string name, object? value = null) : this(name)
{
Value = value;
}
public Variable(string name, object? value = null, string? id = null) : this(name, value)
{
Name = name;
Value = value;
}
@ -45,6 +51,8 @@ public class Variable : MemoryBlockReference
/// <inheritdoc />
public override MemoryBlock Declare() => new(Value, new VariableBlockMetadata(this, StorageDriverType, false));
private string GetIdFromName(string? name) => $"{name?.Camelize() ?? "Unnamed"}{nameof(Variable)}";
}
/// <summary>
@ -59,16 +67,19 @@ public class Variable<T> : Variable
}
/// <inheritdoc />
[Obsolete("Use the constructor that takes a name parameter instead.", true)]
public Variable(T value)
{
Value = value;
}
/// <inheritdoc />
public Variable(string name, T value)
public Variable(string name, T value) : base(name, value)
{
}
public Variable(string name, T value, string? id = null) : base(name, value, id)
{
Name = name;
Value = value;
}
/// <summary>
@ -95,6 +106,18 @@ public class Variable<T> : Variable
Id = id;
return this;
}
public Variable<T> WithName(string name)
{
Name = name;
return this;
}
public Variable<T> WithValue(T value)
{
Value = value;
return this;
}
}
/// <summary>

View file

@ -11,6 +11,6 @@ public class JavaScriptVariablesWorkflow1 : WorkflowBase
{
builder.WithDefinitionId(DefinitionId);
builder.WithVariable("MagicNumber", 3).WithWorkflowStorage();
builder.Root = new RunJavaScript("setMagicNumber(42)", default, default);
builder.Root = new RunJavaScript("setMagicNumber(42)", null, null);
}
}

View file

@ -11,6 +11,6 @@ public class JavaScriptVariablesWorkflow2 : WorkflowBase
{
builder.WithDefinitionId(DefinitionId);
builder.WithVariable("MagicNumber", 3).WithWorkflowStorage();
builder.Root = new RunJavaScript("variables.MagicNumber = 42", default, default);
builder.Root = new RunJavaScript("variables.MagicNumber = 42", null, null);
}
}

View file

@ -11,6 +11,6 @@ public class JavaScriptVariablesWorkflow3 : WorkflowBase
{
builder.WithDefinitionId(DefinitionId);
builder.WithVariable("MagicNumber", 3).WithWorkflowStorage();
builder.Root = new RunJavaScript("setVariable('MagicNumber', 42)", default, default);
builder.Root = new RunJavaScript("setVariable('MagicNumber', 42)", null, null);
}
}

View file

@ -30,7 +30,7 @@ public class JavaScriptVariablesWorkflowTests(App app) : AppComponentTest(app)
var workflowState = workflowInstance!.WorkflowState;
var rootWorkflowActivityExecutionContext = workflowState.ActivityExecutionContexts.Single(x => x.ParentContextId == null);
var variables = GetVariablesDictionary(rootWorkflowActivityExecutionContext);
var magicNumber = variables["Workflow1:variable-1"].ConvertTo<int>();
var magicNumber = variables["magicNumberVariable"].ConvertTo<int>();
Assert.Equal(42, magicNumber);
}

View file

@ -1,3 +1,4 @@
using Elsa.Common.Models;
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.Workflows.ComponentTests.Abstractions;
@ -25,11 +26,11 @@ public class CountdownWorkflowTests(App app) : AppComponentTest(app)
var bookmarkStore = Scope.ServiceProvider.GetRequiredService<IBookmarkStore>();
var runAndCreateRequest = new CreateAndRunWorkflowInstanceRequest
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(CountdownWorkflow.DefinitionId),
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(CountdownWorkflow.DefinitionId, VersionOptions.Latest),
};
var runResponse = await workflowClient.CreateAndRunInstanceAsync(runAndCreateRequest);
var workflowInstanceId = runResponse.WorkflowInstanceId;
var createdBookmarks = await bookmarkStore.FindManyAsync(new BookmarkFilter
var createdBookmarks = await bookmarkStore.FindManyAsync(new()
{
WorkflowInstanceId = workflowInstanceId
});
@ -42,7 +43,7 @@ public class CountdownWorkflowTests(App app) : AppComponentTest(app)
var workflowState = workflowInstance!.WorkflowState;
var rootWorkflowActivityExecutionContext = workflowState.ActivityExecutionContexts.Single(x => x.ParentContextId == null);
var variables = GetVariablesDictionary(rootWorkflowActivityExecutionContext);
var actualCounter = variables["Workflow1:variable-1"].ConvertTo<int>();
var actualCounter = variables["counterVariable"].ConvertTo<int>();
Assert.Equal(--expectedCounter, actualCounter);
var bookmark = bookmarks.Pop();
@ -53,7 +54,7 @@ public class CountdownWorkflowTests(App app) : AppComponentTest(app)
await workflowClient.RunInstanceAsync(runRequest);
createdBookmarks = await bookmarkStore.FindManyAsync(new BookmarkFilter
createdBookmarks = await bookmarkStore.FindManyAsync(new()
{
WorkflowInstanceId = workflowInstanceId
});

View file

@ -17,12 +17,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
public async Task TestJsonObjectPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var jsonVariable = new Variable<object>
{
Name = "JsonVariable"
};
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "{\"name\": \"John\", \"age\": 30}";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
@ -36,12 +32,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
public async Task TestJsonArrayPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var jsonVariable = new Variable<object>
{
Name = "JsonVariable"
};
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "[1, 2, 3, 4, 5, 6]";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
@ -55,12 +47,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
public async Task TestStringPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var jsonVariable = new Variable<object>
{
Name = "JsonVariable"
};
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "\"I'm just a string\"";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
@ -74,12 +62,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
public async Task TestBooleanPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var jsonVariable = new Variable<object>
{
Name = "JsonVariable"
};
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "false";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
@ -93,11 +77,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
public async Task TestNestedJsonPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var jsonVariable = new Variable<object>
{
Name = "JsonVariable"
};
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = @"
{

View file

@ -31,10 +31,7 @@ public class ToJsonTests(ITestOutputHelper testOutputHelper)
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var unicodeString = UnicodeRangeGenerator.GenerateUnicodeString();
var payloadVariable = new Variable<object>
{
Name = "Payload"
};
var payloadVariable = new Variable<object>("Payload", null!);
var payload = new
{
Text = unicodeString

View file

@ -1,6 +1,5 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
@ -9,7 +8,7 @@ class BreakForEachWorkflow : WorkflowBase
protected override void Build(IWorkflowBuilder workflow)
{
var items = new[] { "C#", "Rust", "Go" };
var currentItem = new Variable<string>();
var currentItem = new Variable<string>("CurrentItem", "");
workflow.Root = new Sequence
{
@ -18,7 +17,7 @@ class BreakForEachWorkflow : WorkflowBase
new WriteLine("Start"),
new ForEach<string>
{
Items = new Input<ICollection<string>>(items),
Items = new(items),
CurrentValue = new (currentItem),
Body = new Sequence
{

View file

@ -7,7 +7,7 @@ class BreakForWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>();
var currentValue = new Variable<int?>("CurrentValue", null);
workflow.Root = new Sequence
{

View file

@ -8,11 +8,7 @@ public class BreakWhileForkWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>
{
Name = "CurrentValue",
Value = 0
};
var currentValue = new Variable<int?>("CurrentValue", 0);
workflow.Root = new Sequence
{

View file

@ -7,7 +7,7 @@ public class BreakWhileWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>(0);
var currentValue = new Variable<int?>("CurrentValue", 0);
workflow.Root = new Sequence
{

View file

@ -1,6 +1,5 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Workflows.IntegrationTests.Activities;
@ -15,10 +14,7 @@ class ForEachWorkflow : WorkflowBase
protected override void Build(IWorkflowBuilder workflow)
{
var currentItem = new Variable<string>
{
Name = "CurrentItem"
};
var currentItem = new Variable<string>("CurrentItem", "");
workflow.Root = new Sequence
{
@ -27,8 +23,8 @@ class ForEachWorkflow : WorkflowBase
{
new ForEach<string>
{
Items = new Input<ICollection<string>>(_items),
CurrentValue = new Output<string?>(currentItem),
Items = new(_items),
CurrentValue = new(currentItem),
Body = new WriteLine(currentItem)
},
}

View file

@ -9,8 +9,8 @@ class NestedForEachWithBreakWorkflow : WorkflowBase
{
var outerItems = new[] { "C#", "Rust", "Go" };
var innerItems = new[] { "Classes", "Functions", "Modules" };
var currentOuterItem = new Variable<string>();
var currentInnerItem = new Variable<string>();
var currentOuterItem = new Variable<string>("CurrentOuterItem", "");
var currentInnerItem = new Variable<string>("CurrentInnerItem", "");
workflow.Root = new ForEach<string>(outerItems)
{

View file

@ -8,8 +8,8 @@ public class SumWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var a = new Variable<int>();
var b = new Variable<int>();
var a = new Variable<int>("A", 0);
var b = new Variable<int>("B", 0);
var sumActivity = new SumActivity(a, b);

View file

@ -8,7 +8,7 @@ public class BreakWhileFromForkWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>(0);
var currentValue = new Variable<int?>("CurrentValue", 0);
workflow.WithVariable(currentValue);

View file

@ -8,7 +8,7 @@ public class WaitAllForkWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>(0);
var currentValue = new Variable<int?>("CurrentValue", 0);
workflow.WithVariable(currentValue);

View file

@ -8,7 +8,7 @@ public class WaitAnyForkWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>(0);
var currentValue = new Variable<int?>("CurrentValue", 0);
workflow.WithVariable(currentValue);

View file

@ -13,12 +13,12 @@ public class Sum : Composite<int>
{
private readonly RunJavaScript _runJavaScript;
public Input<int> A { get; set; } = default!;
public Input<int> B { get; set; } = default!;
public Input<int> A { get; set; } = null!;
public Input<int> B { get; set; } = null!;
public Sum()
{
_runJavaScript = new RunJavaScript
_runJavaScript = new()
{
Script = new("getA() + getB();"),
};
@ -42,7 +42,7 @@ public class Sum : Composite<int>
// If the call site set a result variable, assign it to the JavaScript activity's result.
if (Result != null)
_runJavaScript.Result = new Output<object>(Result.MemoryBlockReference)!;
_runJavaScript.Result = new(Result.MemoryBlockReference)!;
}
}
@ -53,11 +53,10 @@ public class SumWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var sum = new Variable<int>();
var sum = workflow.WithVariable<int>("Sum", 0);
workflow.Root = new Sequence
{
Variables = { sum },
Activities =
{
new Sum

View file

@ -12,8 +12,8 @@ public class JavascriptAndLiquidWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
var products = new Variable<object> { Name = "Products", StorageDriverType = typeof(WorkflowInstanceStorageDriver) };
var product = new Variable<object> { Name = "Product", StorageDriverType = typeof(WorkflowInstanceStorageDriver) };
var products = new Variable<object>("Products", null!).WithStorageDriver<WorkflowInstanceStorageDriver>();
var product = new Variable<object>("Product", null!).WithStorageDriver<WorkflowInstanceStorageDriver>();
builder.Root = new Sequence
{

View file

@ -24,13 +24,13 @@ public class ParallelJoinCompletesTests
await _services.PopulateRegistriesAsync();
// Import workflow.
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImplicitJoins/Workflows/parallel-join.json");
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync("Scenarios/ImplicitJoins/Workflows/parallel-join.json");
// Execute.
var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
// Assert.
var journal = await _services.GetRequiredService<IWorkflowExecutionLogStore>().FindManyAsync(new WorkflowExecutionLogRecordFilter
var journal = await _services.GetRequiredService<IWorkflowExecutionLogStore>().FindManyAsync(new()
{
WorkflowInstanceId = state.Id,
ActivityId = "70fc1183cd5800f2",

View file

@ -8,7 +8,7 @@ class SetGetVariableWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var variable1 = new Variable<string>();
var variable1 = new Variable<string>("Variable1", "");
workflow.Root = new Sequence
{
@ -30,8 +30,8 @@ class SetGetVariablesWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var variable1 = new Variable<string>();
var variable2 = new Variable<string>();
var variable1 = new Variable<string>("Variable1", "");
var variable2 = new Variable<string>("Variable2", "");
workflow.Root = new Sequence
{

View file

@ -8,7 +8,7 @@ class SampleWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var variable1 = new Variable<string>();
var variable1 = new Variable<string>("Variable1", "");
workflow.Root = new Sequence
{

View file

@ -23,7 +23,7 @@ public class Tests
{
await _services.PopulateRegistriesAsync();
var expectedValue = "Some value";
var variable1 = new Variable();
var variable1 = new Variable("Variable1");
var workflow = Workflow.FromActivity(new Sequence
{
@ -46,7 +46,7 @@ public class Tests
{
await _services.PopulateRegistriesAsync();
var expectedValue = "Some value";
var variable = new Variable<string>();
var variable = new Variable<string>("Variable", "");
var workflow = Workflow.FromActivity(new Sequence
{

View file

@ -24,7 +24,7 @@ public class Tests
var model = new VariablesContainer(variables);
// Create a typed variable.
var variable = new Variable<bool>();
var variable = new Variable<bool>("Variable", false);
// Add variable to collection.
variables.Add(variable);