From 97113ea30bd1ffaeb166482af872448f1b057bdb Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Wed, 3 Jul 2024 01:03:12 +0800 Subject: [PATCH] Restrict states to have only one-way dependencies #16 --- .../ProcessManagementWebUnifiedModule.cs | 9 +-- .../Options/ProcessDefinition.cs | 30 ++++------ .../Options/ProcessStateDefinition.cs | 28 ++++++++- .../Processes/ProcessManager.cs | 2 +- .../ProcessManagementOptionsTests.cs | 57 +++++++++++-------- .../ProcessManagementTestBaseModule.cs | 32 +++++------ 6 files changed, 89 insertions(+), 69 deletions(-) diff --git a/host/EasyAbp.ProcessManagement.Web.Unified/ProcessManagementWebUnifiedModule.cs b/host/EasyAbp.ProcessManagement.Web.Unified/ProcessManagementWebUnifiedModule.cs index 3743a0d..85903ef 100644 --- a/host/EasyAbp.ProcessManagement.Web.Unified/ProcessManagementWebUnifiedModule.cs +++ b/host/EasyAbp.ProcessManagement.Web.Unified/ProcessManagementWebUnifiedModule.cs @@ -89,10 +89,11 @@ public override void ConfigureServices(ServiceConfigurationContext context) Configure(options => { var definition = new ProcessDefinition("FakeExport", "Fake export") - .AddState(new ProcessStateDefinition("Ready", "Ready"), null) - .AddState(new ProcessStateDefinition("Exporting", "Exporting"), ["Ready", "Exporting"]) - .AddState(new ProcessStateDefinition("Failed", "Failed"), ["Ready", "Exporting"]) - .AddState(new ProcessStateDefinition("Succeeded", "Succeeded"), "Exporting"); + .AddState(new ProcessStateDefinition("Ready", "Ready", null)) + .AddState(new ProcessStateDefinition("FailedToStartExporting", "Failed", "Ready")) + .AddState(new ProcessStateDefinition("Exporting", "Exporting", "Ready")) + .AddState(new ProcessStateDefinition("ExportFailed", "Failed", "Exporting")) + .AddState(new ProcessStateDefinition("Succeeded", "Succeeded", "Exporting")); options.AddOrUpdateProcessDefinition(definition); }); diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs index 9fca477..4f0c1bd 100644 --- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs +++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Volo.Abp; namespace EasyAbp.ProcessManagement.Options; @@ -19,11 +20,11 @@ public ProcessDefinition(string name, string? displayName) DisplayName = displayName; } - public IEnumerable GetChildStateNames(string currentStateName) + public List GetChildrenStateNames(string currentStateName) { Check.NotNullOrWhiteSpace(currentStateName, nameof(currentStateName)); - return StateDefinitions[currentStateName].NextStateNames; + return StateDefinitions[currentStateName].ChildrenStateNames.ToList(); } public ProcessStateDefinition GetState(string stateName) @@ -33,13 +34,7 @@ public ProcessStateDefinition GetState(string stateName) return StateDefinitions[stateName]; } - /// - /// Add a state. - /// - /// The state definition object. - /// Names of the parent states. Stages can only transition from their parent states. - /// - public ProcessDefinition AddState(ProcessStateDefinition stateDefinition, params string[]? parentStateNames) + public ProcessDefinition AddState(ProcessStateDefinition stateDefinition) { Check.NotNull(stateDefinition, nameof(stateDefinition)); @@ -51,32 +46,29 @@ public ProcessDefinition AddState(ProcessStateDefinition stateDefinition, params StateDefinitions.Add(stateDefinition.Name, stateDefinition); - if (parentStateNames is null) + if (stateDefinition.FatherStateName is null) { - SetInitialState(stateDefinition.Name); + SetAsInitialState(stateDefinition.Name); } else { - foreach (var parentStateName in parentStateNames) - { - LinkStates(stateDefinition.Name, parentStateName); - } + SetAsChildState(stateDefinition.Name, stateDefinition.FatherStateName); } return this; } - private void LinkStates(string stateName, string parentStateName) + private void SetAsChildState(string stateName, string fatherStateName) { Check.NotNullOrWhiteSpace(stateName, nameof(stateName)); - Check.NotNullOrWhiteSpace(parentStateName, nameof(parentStateName)); + Check.NotNullOrWhiteSpace(fatherStateName, nameof(fatherStateName)); var stateDefinition = StateDefinitions[stateName]; - StateDefinitions[parentStateName].NextStateNames.Add(stateDefinition.Name); + StateDefinitions[fatherStateName].ChildrenStateNames.Add(stateDefinition.Name); } - private void SetInitialState(string stateName) + private void SetAsInitialState(string stateName) { Check.NotNullOrWhiteSpace(stateName, nameof(stateName)); diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessStateDefinition.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessStateDefinition.cs index e82e461..6d9c378 100644 --- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessStateDefinition.cs +++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessStateDefinition.cs @@ -5,15 +5,39 @@ namespace EasyAbp.ProcessManagement.Options; public class ProcessStateDefinition { + /// + /// Unique state name. + /// public string Name { get; } + /// + /// Localized display name. + /// todo: use ILocalizableString. + /// public string? DisplayName { get; } - internal HashSet NextStateNames { get; } = new(); + /// + /// Name of the father state. Stages can only transition from their father state. + /// If null, this state is the initial state. A process can have only one initial state. + /// + internal string? FatherStateName { get; } - public ProcessStateDefinition(string name, string? displayName) + /// + /// Names of the children states. Stages can only transition from their father state. + /// + internal HashSet ChildrenStateNames { get; } = new(); + + /// + /// + /// + /// Localized display name. + /// Localized display name. + /// Name of the father state. Stages can only transition from their father state. + /// If null, this state is the initial state. A process can have only one initial state. + public ProcessStateDefinition(string name, string? displayName, string? fatherStateName) { Name = Check.NotNullOrWhiteSpace(name, nameof(name)); DisplayName = displayName; + FatherStateName = fatherStateName; } } \ No newline at end of file diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/ProcessManager.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/ProcessManager.cs index cb9080f..d24cf33 100644 --- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/ProcessManager.cs +++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/ProcessManager.cs @@ -33,7 +33,7 @@ public virtual Task UpdateStateAsync(Process process, IProcessState nextState) { var processDefinition = Options.GetProcessDefinition(process.ProcessName); - var nextStates = processDefinition.GetChildStateNames(process.StateName); + var nextStates = processDefinition.GetChildrenStateNames(process.StateName); if (!nextStates.Contains(nextState.StateName)) { diff --git a/test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs b/test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs index 74bb501..a2ac775 100644 --- a/test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs +++ b/test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Shouldly; +using Volo.Abp; using Xunit; namespace EasyAbp.ProcessManagement; @@ -13,32 +14,38 @@ public class ProcessManagementOptionsTests : ProcessManagementDomainTestBase [Fact] public void Should_Get_Definitions() { - var options = ServiceProvider.GetService>(); - - options.ShouldNotBeNull(); - - var processDefinition = options.Value.GetProcessDefinition("MyDemoProcess"); - - processDefinition.GetState("Startup").ShouldNotBeNull(); - processDefinition.GetState("Step1").ShouldNotBeNull(); - processDefinition.GetState("Step2").ShouldNotBeNull(); - processDefinition.GetState("Step3").ShouldNotBeNull(); - processDefinition.GetState("Step4").ShouldNotBeNull(); - processDefinition.GetState("Step5").ShouldNotBeNull(); - processDefinition.GetState("Step6").ShouldNotBeNull(); - processDefinition.GetState("Step7").ShouldNotBeNull(); - processDefinition.GetState("Step8").ShouldNotBeNull(); + var options = ServiceProvider.GetRequiredService>().Value; + + var processDefinition = options.GetProcessDefinition("FakeExport"); + + processDefinition.GetState("Ready").ShouldNotBeNull(); + processDefinition.GetState("FailedToStartExporting").ShouldNotBeNull(); + processDefinition.GetState("Exporting").ShouldNotBeNull(); + processDefinition.GetState("ExportFailed").ShouldNotBeNull(); + processDefinition.GetState("Succeeded").ShouldNotBeNull(); Should.Throw(() => processDefinition.GetState("Step10000")); - processDefinition.InitialStateName.ShouldBe("Startup"); - processDefinition.GetChildStateNames("Startup").ToArray().ShouldBeEquivalentTo(new[] { "Step1", "Step2" }); - processDefinition.GetChildStateNames("Step1").ToArray().ShouldBeEquivalentTo(new[] { "Step3" }); - processDefinition.GetChildStateNames("Step2").ToArray().ShouldBeEquivalentTo(new[] { "Step3" }); - processDefinition.GetChildStateNames("Step3").ToArray().ShouldBeEquivalentTo(new[] { "Step4", "Step5" }); - processDefinition.GetChildStateNames("Step4").ToArray().ShouldBeEquivalentTo(new[] { "Step6" }); - processDefinition.GetChildStateNames("Step5").ToArray().ShouldBeEquivalentTo(new[] { "Step7" }); - processDefinition.GetChildStateNames("Step6").ToArray().ShouldBeEquivalentTo(new[] { "Step4" }); - processDefinition.GetChildStateNames("Step7").ToArray().ShouldBeEquivalentTo(new[] { "Step8" }); - processDefinition.GetChildStateNames("Step8").ToArray().ShouldBeEquivalentTo(new[] { "Step5" }); + processDefinition.InitialStateName.ShouldBe("Ready"); + processDefinition.GetChildrenStateNames("Ready").ToArray() + .ShouldBeEquivalentTo(new[] { "FailedToStartExporting", "Exporting" }); + processDefinition.GetChildrenStateNames("Exporting").ToArray() + .ShouldBeEquivalentTo(new[] { "ExportFailed", "Succeeded" }); + processDefinition.GetChildrenStateNames("FailedToStartExporting").ToArray().ShouldBeEmpty(); + processDefinition.GetChildrenStateNames("Succeeded").ToArray().ShouldBeEmpty(); + processDefinition.GetChildrenStateNames("ExportFailed").ToArray().ShouldBeEmpty(); + } + + [Fact] + public void Should_Not_Add_Duplicate_State() + { + var options = ServiceProvider.GetRequiredService>().Value; + + var processDefinition = options.GetProcessDefinition("FakeExport"); + + Should.Throw(() => + processDefinition.AddState(new ProcessStateDefinition("Ready", "Ready", null))); + + Should.Throw(() => + processDefinition.AddState(new ProcessStateDefinition("Exporting", "Exporting", "Ready"))); } } \ No newline at end of file diff --git a/test/EasyAbp.ProcessManagement.TestBase/ProcessManagementTestBaseModule.cs b/test/EasyAbp.ProcessManagement.TestBase/ProcessManagementTestBaseModule.cs index e478f43..3685387 100644 --- a/test/EasyAbp.ProcessManagement.TestBase/ProcessManagementTestBaseModule.cs +++ b/test/EasyAbp.ProcessManagement.TestBase/ProcessManagementTestBaseModule.cs @@ -26,24 +26,20 @@ public override void ConfigureServices(ServiceConfigurationContext context) private void ConfigureDemoProcessDefinitions(ServiceConfigurationContext context) { - var processDefinition = new ProcessDefinition("MyDemoProcess", "My Demo Process") - .AddState(new ProcessStateDefinition("Startup", "Startup")) - .AddState(new ProcessStateDefinition("Step1", "Step1"), ["Startup"]) - .AddState(new ProcessStateDefinition("Step2", "Step2"), ["Startup"]) - .AddState(new ProcessStateDefinition("Step3", "Step3"), ["Step1", "Step2"]) - .AddState(new ProcessStateDefinition("Step4", "Step4"), ["Step3", "Step6"]) - .AddState(new ProcessStateDefinition("Step5", "Step5"), ["Step3", "Step8"]) - .AddState(new ProcessStateDefinition("Step6", "Step6"), ["Step4"]) - .AddState(new ProcessStateDefinition("Step7", "Step7"), ["Step5"]) - .AddState(new ProcessStateDefinition("Step8", "Step8"), ["Step7"]); - - // Step1 Step4 ⇌ Step6 - // ↗ ↘ ↗ - // Startup Step3 - // ↘ ↗ ↘ - // Step2 Step5 ➔ Step7 - // ↖ ↙ - // Step8 + /* Succeeded + * ↗ + * Exporting + * ↗ ↘ + * Ready ExportFailed + * ↘ + * FailedToStartExporting + */ + var processDefinition = new ProcessDefinition("FakeExport", "Fake export") + .AddState(new ProcessStateDefinition("Ready", "Ready", null)) + .AddState(new ProcessStateDefinition("FailedToStartExporting", "Failed", "Ready")) + .AddState(new ProcessStateDefinition("Exporting", "Exporting", "Ready")) + .AddState(new ProcessStateDefinition("ExportFailed", "Failed", "Exporting")) + .AddState(new ProcessStateDefinition("Succeeded", "Succeeded", "Exporting")); context.Services.Configure(options => {