diff --git a/.gitignore b/.gitignore
index abf72e513..99e1e6c41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,4 +62,7 @@ FodyWeavers.xsd
# Elsa Stencil Components
src/designer/elsa-workflows-studio/dist
src/designer/elsa-workflows-studio/www
-src/designer/elsa-workflows-studio/loader
\ No newline at end of file
+src/designer/elsa-workflows-studio/loader
+
+# Created by developers using GNU/Linux
+.directory
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 000000000..2f2312032
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,33 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "/property:GenerateFullPaths=true"
+ ],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "test",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "test",
+ "/property:GenerateFullPaths=true"
+ ],
+ "group": {
+ "kind": "test",
+ "isDefault": true
+ },
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Elsa.code-workspace b/Elsa.code-workspace
new file mode 100644
index 000000000..876a1499c
--- /dev/null
+++ b/Elsa.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/Elsa.sln b/Elsa.sln
index 567e3f8e6..3fedfc86d 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -233,6 +233,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Timers.Quartz", "src\samples\worker\Elsa.Samples.Timers.Quartz\Elsa.Samples.Timers.Quartz.csproj", "{FE724143-B24C-43DA-BBF5-15431BCA6E27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AutoConnectNextActivityConsole", "src\samples\console\Elsa.Samples.AutoConnectNextActivityConsole\Elsa.Samples.AutoConnectNextActivityConsole.csproj", "{DB42A8DA-06CA-4402-9C87-8F7F055AB37E}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{F471267A-DA3A-48C7-8784-F8E4E46203A2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.UnitTests", "test\unit\Elsa.UnitTests\Elsa.UnitTests.csproj", "{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -560,6 +563,10 @@ Global
{DB42A8DA-06CA-4402-9C87-8F7F055AB37E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB42A8DA-06CA-4402-9C87-8F7F055AB37E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB42A8DA-06CA-4402-9C87-8F7F055AB37E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -671,6 +678,8 @@ Global
{45A06CA3-199B-4650-8880-30DDF2E38E19} = {8B6B40A8-DF21-4CD2-BABD-474B79D0C3AF}
{FE724143-B24C-43DA-BBF5-15431BCA6E27} = {E42743A0-FBDD-4150-9D53-6000496D9B87}
{DB42A8DA-06CA-4402-9C87-8F7F055AB37E} = {FC9F520F-BA51-4AD2-BFEE-EF787798E734}
+ {F471267A-DA3A-48C7-8784-F8E4E46203A2} = {AB1AE008-6FD6-414C-8E88-D735F42E1FA6}
+ {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A} = {F471267A-DA3A-48C7-8784-F8E4E46203A2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}
diff --git a/src/core/Elsa.Abstractions/Models/Variables.cs b/src/core/Elsa.Abstractions/Models/Variables.cs
index fdcc6a5be..bfbc52d96 100644
--- a/src/core/Elsa.Abstractions/Models/Variables.cs
+++ b/src/core/Elsa.Abstractions/Models/Variables.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
namespace Elsa.Models
@@ -29,11 +30,24 @@ namespace Elsa.Models
return this;
}
+ ///
+ /// Removes a variable of the specified name if it is present.
+ ///
+ /// The variable name
+ /// A reference to this same instance, so calls may be chained.
public Variables Remove(string name)
{
- if (Has(name))
- Data.Remove(name);
+ Data.Remove(name);
+ return this;
+ }
+ ///
+ /// Removes all of the variables from the current instance, clearing it.
+ ///
+ /// A reference to this same instance, so calls may be chained.
+ public Variables RemoveAll()
+ {
+ Data.Clear();
return this;
}
diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs
index 29caf1d65..d5a6d4e3b 100644
--- a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs
+++ b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs
@@ -116,6 +116,14 @@ namespace Elsa.Services.Models
public object? GetVariable(string name) => WorkflowExecutionContext.GetVariable(name);
public T? GetVariable(string name) => WorkflowExecutionContext.GetVariable(name);
public T? GetVariable() => GetVariable(typeof(T).Name);
+
+ ///
+ /// Clears all of the variables associated with the current .
+ ///
+ ///
+ ///
+ public void PurgeVariables() => WorkflowExecutionContext.PurgeVariables();
+
public void SetTransientVariable(string name, object? value) => WorkflowExecutionContext.SetTransientVariable(name, value);
public object? GetTransientVariable(string name) => WorkflowExecutionContext.GetTransientVariable(name);
public T? GetTransientVariable(string name) => WorkflowExecutionContext.GetTransientVariable(name);
diff --git a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs
index 238ace335..253d1711e 100644
--- a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs
+++ b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs
@@ -146,6 +146,13 @@ namespace Elsa.Services.Models
///
public object? GetWorkflowVariable(string name) => WorkflowInstance.Variables.Get(name);
+ ///
+ /// Clears all of the variables associated with the current .
+ ///
+ ///
+ public void PurgeVariables() => WorkflowInstance.Variables.RemoveAll();
+
+
public ActivityScope CurrentScope => WorkflowInstance.Scopes.Peek();
public ActivityScope GetScope(string activityId) => WorkflowInstance.Scopes.First(x => x.ActivityId == activityId);
diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs
new file mode 100644
index 000000000..bee3715cb
--- /dev/null
+++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Reflection;
+using AutoFixture;
+using AutoFixture.Kernel;
+using AutoFixture.Xunit2;
+using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders;
+
+namespace Elsa.Testing.Shared.AutoFixture.Attributes
+{
+ ///
+ /// Customizes the parameter so that it creates an
+ /// using Moq. That mock service provider gets services by resolving them from Autofixture.
+ ///
+ ///
+ public class AutofixtureServiceProviderAttribute : CustomizeAttribute
+ {
+ public override ICustomization GetCustomization(ParameterInfo parameter)
+ => new AutofixtureServiceProviderCustomization(parameter);
+
+ class AutofixtureServiceProviderCustomization : ICustomization
+ {
+ readonly ParameterInfo parameter;
+
+ public void Customize(IFixture fixture)
+ {
+ fixture.Customizations.Insert(0, GetSpecimenBuilder());
+ }
+
+ ISpecimenBuilder GetSpecimenBuilder()
+ {
+ var paramSpec = new ParameterSpecification(parameter.ParameterType, parameter.Name);
+ var specimenBuilder = new AutofixtureServiceProviderSpecimenBuilder();
+ return new FilteringSpecimenBuilder(specimenBuilder, paramSpec);
+ }
+
+ public AutofixtureServiceProviderCustomization(ParameterInfo parameter)
+ {
+ this.parameter = parameter;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs
new file mode 100644
index 000000000..cec1c2887
--- /dev/null
+++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs
@@ -0,0 +1,25 @@
+using System.Linq;
+using System.Reflection;
+using AutoFixture;
+using AutoFixture.Xunit2;
+
+namespace Elsa.Testing.Shared.AutoFixture.Attributes
+{
+ public class OmitOnRecursionAttribute : CustomizeAttribute
+ {
+ public override ICustomization GetCustomization(ParameterInfo parameter)
+ => new OmitOnRecursionCustomization();
+
+ class OmitOnRecursionCustomization : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ var throwingBehaviours = fixture.Behaviors.OfType().ToList();
+ foreach(var behaviour in throwingBehaviours)
+ fixture.Behaviors.Remove(behaviour);
+
+ fixture.Behaviors.Add(new OmitOnRecursionBehavior());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs
new file mode 100644
index 000000000..4a124efa8
--- /dev/null
+++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Reflection;
+using AutoFixture.Kernel;
+using Moq;
+
+namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders
+{
+ ///
+ /// Creates a mock which gets services
+ /// by resolving them from Autofixture.
+ ///
+ public class AutofixtureServiceProviderSpecimenBuilder : ISpecimenBuilder
+ {
+ public object Create(object request, ISpecimenContext context)
+ {
+ if(Equals(request, typeof(IServiceProvider)))
+ return GetServiceProvider(context);
+ if(request is ParameterInfo paramInfo && paramInfo.ParameterType == typeof(IServiceProvider))
+ return GetServiceProvider(context);
+
+ return new NoSpecimen();
+ }
+
+ static object GetServiceProvider(ISpecimenContext context)
+ {
+ var provider = new Mock();
+
+ provider.Name = $"Autofixture_{nameof(IServiceProvider)}-{Guid.NewGuid()}";
+ provider
+ .Setup(x => x.GetService(It.IsAny()))
+ .Returns((Type t) => context.Resolve(t));
+
+ return provider.Object;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj
index b95b976e1..5df535cd8 100644
--- a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj
+++ b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj
@@ -13,6 +13,8 @@
+
+
diff --git a/test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs b/test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs
new file mode 100644
index 000000000..643588758
--- /dev/null
+++ b/test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs
@@ -0,0 +1,13 @@
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using AutoFixture.Xunit2;
+
+namespace Elsa
+{
+ public class AutoMoqDataAttribute : AutoDataAttribute
+ {
+ public AutoMoqDataAttribute() : base(() => new Fixture().Customize(new AutoMoqCustomization()))
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj b/test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj
new file mode 100644
index 000000000..68d5d38b9
--- /dev/null
+++ b/test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net5.0
+ false
+ Elsa
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/test/unit/Elsa.UnitTests/Models/VariablesTests.cs b/test/unit/Elsa.UnitTests/Models/VariablesTests.cs
new file mode 100644
index 000000000..283ebdcf8
--- /dev/null
+++ b/test/unit/Elsa.UnitTests/Models/VariablesTests.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using Xunit;
+
+namespace Elsa.Models
+{
+ public class VariablesTests
+ {
+ [Theory(DisplayName = "The Remove method should remove a variable if such a variable exists"), AutoMoqData]
+ public void Remove_removes_a_variable_by_name_if_present(string variableName, object variableData)
+ {
+ var sut = new Variables(new Dictionary { { variableName, variableData } });
+
+ sut.Remove(variableName);
+
+ Assert.Empty(sut.Data);
+ }
+
+ [Theory(DisplayName = "The Remove method should not throw if used with a non-existent variable"), AutoMoqData]
+ public void Remove_does_not_throw_when_trying_to_remove_a_variable_which_does_not_exist(string variableName)
+ {
+ var sut = new Variables();
+
+ sut.Remove(variableName);
+
+ // No assertion, if the line about doesn't throw then this test passed
+ }
+
+ [Theory(DisplayName = "The Remove method should support call-chaining by returning a self-reference"), AutoMoqData]
+ public void Remove_returns_a_reference_to_itself(Variables sut, string variableName)
+ {
+ var result = sut.Remove(variableName);
+ Assert.Same(sut, result);
+ }
+
+ [Theory(DisplayName = "The RemoveAll method should leave the variables collection empty"), AutoMoqData]
+ public void RemoveAll_clears_all_variables(string variableName1,
+ string variableName2,
+ string variableName3,
+ object variableData)
+ {
+ var sut = new Variables(new Dictionary {
+ { variableName1, variableData },
+ { variableName2, variableData },
+ { variableName3, variableData },
+ });
+
+ sut.RemoveAll();
+
+ Assert.Empty(sut.Data);
+ }
+
+ [Theory(DisplayName = "The RemoveAll method should support call-chaining by returning a self-reference"), AutoMoqData]
+ public void RemoveAll_returns_a_reference_to_itself(Variables sut)
+ {
+ var result = sut.RemoveAll();
+ Assert.Same(sut, result);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs b/test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs
new file mode 100644
index 000000000..18c9455ce
--- /dev/null
+++ b/test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs
@@ -0,0 +1,23 @@
+using Xunit;
+using Elsa.Testing.Shared.AutoFixture.Attributes;
+using AutoFixture.Xunit2;
+using System;
+
+namespace Elsa.Services.Models
+{
+ public class ActivityExecutionContextTests
+ {
+ [Theory(DisplayName = "The PurgeVariables method should clear the Variables instance associated with the WorkflowInstance associated with the Workflow Execution Context"), AutoMoqData]
+ public void PurgeVariables_clears_workflow_execution_context_workflow_instance_variables([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider,
+ [OmitOnRecursion,NoAutoProperties] ActivityExecutionContext sut,
+ string variableName,
+ object variableValue)
+ {
+ sut.WorkflowExecutionContext.WorkflowInstance.Variables.Set(variableName, variableValue);
+
+ sut.PurgeVariables();
+
+ Assert.Empty(sut.WorkflowExecutionContext.WorkflowInstance.Variables.Data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs b/test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs
new file mode 100644
index 000000000..160ff796b
--- /dev/null
+++ b/test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs
@@ -0,0 +1,23 @@
+using Xunit;
+using Elsa.Testing.Shared.AutoFixture.Attributes;
+using AutoFixture.Xunit2;
+using System;
+
+namespace Elsa.Services.Models
+{
+ public class WorkflowExecutionContextTests
+ {
+ [Theory(DisplayName = "The PurgeVariables method should clear the Variables instance associated with the WorkflowInstance"), AutoMoqData]
+ public void PurgeVariables_clears_workflow_execution_context_workflow_instance_variables([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider,
+ [OmitOnRecursion,NoAutoProperties] WorkflowExecutionContext sut,
+ string variableName,
+ object variableValue)
+ {
+ sut.WorkflowInstance.Variables.Set(variableName, variableValue);
+
+ sut.PurgeVariables();
+
+ Assert.Empty(sut.WorkflowInstance.Variables.Data);
+ }
+ }
+}
\ No newline at end of file