diff --git a/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateEto.cs b/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateEto.cs
index 2c747df..6965e05 100644
--- a/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateEto.cs
+++ b/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateEto.cs
@@ -15,9 +15,16 @@ public UpdateProcessStateEto()
{
}
- public UpdateProcessStateEto(Guid? tenantId, string correlationId, string? actionName, ProcessStateFlag stateFlag,
- string? stateSummaryText, DateTime stateUpdateTime, string stateName, string? stateDetailsText) : base(
- actionName, stateFlag, stateSummaryText, stateUpdateTime, stateName, stateDetailsText)
+ public UpdateProcessStateEto(Guid? tenantId, string correlationId, DateTime stateUpdateTime, string stateName,
+ string? actionName, ProcessStateFlag stateFlag, string? stateSummaryText, string? stateDetailsText) : base(
+ stateUpdateTime, stateName, actionName, stateFlag, stateSummaryText, stateDetailsText)
+ {
+ TenantId = tenantId;
+ CorrelationId = Check.NotNullOrWhiteSpace(correlationId, nameof(correlationId));
+ }
+
+ public UpdateProcessStateEto(Guid? tenantId, string correlationId, DateTime stateUpdateTime, string stateName) :
+ base(stateUpdateTime, stateName)
{
TenantId = tenantId;
CorrelationId = Check.NotNullOrWhiteSpace(correlationId, nameof(correlationId));
diff --git a/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateModel.cs b/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateModel.cs
index 6880c06..dd18061 100644
--- a/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateModel.cs
+++ b/src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateModel.cs
@@ -7,15 +7,16 @@ namespace EasyAbp.ProcessManagement.Processes;
[Serializable]
public class UpdateProcessStateModel : ExtensibleObject, IProcessState
{
+ public DateTime StateUpdateTime { get; set; }
+
+ public string StateName { get; set; }
+
public string? ActionName { get; set; }
public ProcessStateFlag StateFlag { get; set; }
public string? StateSummaryText { get; set; }
- public DateTime StateUpdateTime { get; set; }
-
- public string StateName { get; set; }
public string? StateDetailsText { get; set; }
@@ -23,14 +24,20 @@ public UpdateProcessStateModel()
{
}
- public UpdateProcessStateModel(string? actionName, ProcessStateFlag stateFlag, string? stateSummaryText,
- DateTime stateUpdateTime, string stateName, string? stateDetailsText)
+ public UpdateProcessStateModel(DateTime stateUpdateTime, string stateName)
+ {
+ StateUpdateTime = stateUpdateTime;
+ StateName = Check.NotNullOrWhiteSpace(stateName, nameof(stateName));
+ }
+
+ public UpdateProcessStateModel(DateTime stateUpdateTime, string stateName, string? actionName,
+ ProcessStateFlag stateFlag, string? stateSummaryText, string? stateDetailsText)
{
+ StateUpdateTime = stateUpdateTime;
+ StateName = Check.NotNullOrWhiteSpace(stateName, nameof(stateName));
ActionName = actionName;
StateFlag = stateFlag;
StateSummaryText = stateSummaryText;
- StateUpdateTime = stateUpdateTime;
- StateName = Check.NotNullOrWhiteSpace(stateName, nameof(stateName));
StateDetailsText = stateDetailsText;
}
}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs
index 4f0c1bd..e32ae2f 100644
--- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/ProcessDefinition.cs
@@ -31,7 +31,9 @@ public ProcessStateDefinition GetState(string stateName)
{
Check.NotNullOrWhiteSpace(stateName, nameof(stateName));
- return StateDefinitions[stateName];
+ return StateDefinitions.TryGetValue(stateName, out var stateDefinition)
+ ? stateDefinition
+ : throw new UndefinedProcessStateException(stateName, Name);
}
public ProcessDefinition AddState(ProcessStateDefinition stateDefinition)
@@ -58,6 +60,23 @@ public ProcessDefinition AddState(ProcessStateDefinition stateDefinition)
return this;
}
+ ///
+ /// If the specified state is a child, grandchild, or further descendant of the current state, it returns true.
+ ///
+ public bool IsDescendantState(string stateName, string currentStateName)
+ {
+ var currentStateDefinition = StateDefinitions[currentStateName];
+
+ if (currentStateDefinition.ChildrenStateNames.Contains(stateName))
+ {
+ return true;
+ }
+
+ return currentStateDefinition.ChildrenStateNames
+ .SelectMany(x => StateDefinitions[x].ChildrenStateNames)
+ .Contains(stateName);
+ }
+
private void SetAsChildState(string stateName, string fatherStateName)
{
Check.NotNullOrWhiteSpace(stateName, nameof(stateName));
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/UndefinedProcessStateException.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/UndefinedProcessStateException.cs
new file mode 100644
index 0000000..e549c27
--- /dev/null
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Options/UndefinedProcessStateException.cs
@@ -0,0 +1,11 @@
+using Volo.Abp;
+
+namespace EasyAbp.ProcessManagement.Options;
+
+public class UndefinedProcessStateException : AbpException
+{
+ public UndefinedProcessStateException(string stateName, string processName) : base(
+ $"State `{stateName}` is undefined for the process `{processName}`")
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/IProcessStateHistoryRepository.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/IProcessStateHistoryRepository.cs
index 719c46a..bf59292 100644
--- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/IProcessStateHistoryRepository.cs
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/IProcessStateHistoryRepository.cs
@@ -1,8 +1,11 @@
using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace EasyAbp.ProcessManagement.ProcessStateHistories;
public interface IProcessStateHistoryRepository : IRepository
{
-}
+ Task> GetHistoriesByStateNameAsync(Guid processId, string stateName);
+}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/ProcessStateChangedEventHandler.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/ProcessStateChangedEventHandler.cs
deleted file mode 100644
index 2f4d38b..0000000
--- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/ProcessStateHistories/ProcessStateChangedEventHandler.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Threading.Tasks;
-using EasyAbp.ProcessManagement.Processes;
-using Volo.Abp.DependencyInjection;
-using Volo.Abp.EventBus;
-using Volo.Abp.Guids;
-using Volo.Abp.Uow;
-
-namespace EasyAbp.ProcessManagement.ProcessStateHistories;
-
-public class ProcessStateChangedEventHandler : ILocalEventHandler, ITransientDependency
-{
- private readonly IGuidGenerator _guidGenerator;
- private readonly IProcessStateHistoryRepository _processStateHistoryRepository;
-
- public ProcessStateChangedEventHandler(
- IGuidGenerator guidGenerator,
- IProcessStateHistoryRepository processStateHistoryRepository)
- {
- _guidGenerator = guidGenerator;
- _processStateHistoryRepository = processStateHistoryRepository;
- }
-
- [UnitOfWork]
- public virtual async Task HandleEventAsync(ProcessStateChangedEto eventData)
- {
- var history = new ProcessStateHistory(
- _guidGenerator.Create(), eventData.TenantId, eventData.ProcessId, eventData.NewState);
-
- await _processStateHistoryRepository.InsertAsync(history, true);
- }
-}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/InvalidStateUpdateTimeException.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/InvalidStateUpdateTimeException.cs
new file mode 100644
index 0000000..f568393
--- /dev/null
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/InvalidStateUpdateTimeException.cs
@@ -0,0 +1,13 @@
+using System;
+using Volo.Abp;
+
+namespace EasyAbp.ProcessManagement.Processes;
+
+public class InvalidStateUpdateTimeException : AbpException
+{
+ public InvalidStateUpdateTimeException(string stateName, string processName, Guid processId) : base(
+ $"Failed to update to the state `{stateName}` for the process " +
+ $"`{processName}`(id: {processId}) since the StateUpdateTime is less than the current")
+ {
+ }
+}
\ 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 d24cf33..310ae89 100644
--- a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/ProcessManager.cs
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/ProcessManager.cs
@@ -1,49 +1,115 @@
using System;
-using System.Linq;
using System.Threading.Tasks;
using EasyAbp.ProcessManagement.Options;
+using EasyAbp.ProcessManagement.ProcessStateHistories;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Domain.Services;
+using Volo.Abp.Uow;
namespace EasyAbp.ProcessManagement.Processes;
public class ProcessManager : DomainService
{
- protected ProcessManagementOptions Options { get; }
+ protected ProcessManagementOptions Options =>
+ LazyServiceProvider.LazyGetRequiredService>().Value;
- public ProcessManager(IOptions options)
- {
- Options = options.Value;
- }
+ protected IProcessStateHistoryRepository ProcessStateHistoryRepository => LazyServiceProvider
+ .LazyGetRequiredService();
- public virtual Task CreateAsync(CreateProcessModel model, DateTime now)
+ public virtual async Task CreateAsync(CreateProcessModel model, DateTime now)
{
var processDefinition = Options.GetProcessDefinition(model.ProcessName);
var id = GuidGenerator.Create();
- return Task.FromResult(new Process(id, CurrentTenant.Id, processDefinition, now, model.GroupKey,
- model.CorrelationId ?? id.ToString(), model));
+ var process = new Process(id, CurrentTenant.Id, processDefinition, now, model.GroupKey,
+ model.CorrelationId ?? id.ToString(), model);
+
+ await RecordStateHistoryAsync(process.Id, process);
+
+ return process;
}
- public virtual Task UpdateStateAsync(Process process, IProcessState nextState)
+ [UnitOfWork]
+ public virtual async Task UpdateStateAsync(Process process, IProcessState nextState)
{
if (nextState.StateName != process.StateName)
{
- var processDefinition = Options.GetProcessDefinition(process.ProcessName);
+ await UpdateToDifferentStateAsync(process, nextState);
+ }
+ else
+ {
+ await UpdateStateCustomInfoAsync(process, nextState);
+ }
+ }
- var nextStates = processDefinition.GetChildrenStateNames(process.StateName);
+ [UnitOfWork]
+ protected virtual async Task UpdateToDifferentStateAsync(Process process, IProcessState state)
+ {
+ var processDefinition = Options.GetProcessDefinition(process.ProcessName);
+
+ var availableStates = processDefinition.GetChildrenStateNames(process.StateName);
- if (!nextStates.Contains(nextState.StateName))
+ if (availableStates.Contains(state.StateName))
+ {
+ if (state.StateUpdateTime <= process.StateUpdateTime)
{
- throw new AbpException(
- $"The specified state `{nextState.StateName}` is invalid for the process `{process.ProcessName}`");
+ throw new InvalidStateUpdateTimeException(state.StateName, process.ProcessName, process.Id);
}
+
+ process.SetState(state);
+
+ await RecordStateHistoryAsync(process.Id, state);
}
+ else
+ {
+ // get or throw.
+ processDefinition.GetState(state.StateName);
- process.SetState(nextState);
+ /* If this incoming state is a descendant of the current state, it will be accepted in the future.
+ * So we throw an exception and skip handling it this time.
+ * The next time the event handling is attempted, it may succeed.
+ */
+ if (processDefinition.IsDescendantState(state.StateName, process.StateName))
+ {
+ throw new UpdatingToFutureStateException(state.StateName, process.ProcessName, process.Id);
+ }
+
+ /*
+ * Or, the process has been updated to this incoming state before, we just record the state history.
+ */
+ if ((await ProcessStateHistoryRepository.GetHistoriesByStateNameAsync(
+ process.Id, state.StateName)).Count != 0)
+ {
+ await RecordStateHistoryAsync(process.Id, state);
+ return;
+ }
+
+ /*
+ * Otherwise, this incoming state will never succeed, we don't handle it.
+ */
+ throw new UpdatingToNonDescendantStateException(state.StateName, process.ProcessName, process.Id);
+ }
+ }
- return Task.CompletedTask;
+ protected virtual async Task UpdateStateCustomInfoAsync(Process process, IProcessState state)
+ {
+ /* If it receives a state update event out of order (event.StateUpdateTime < process.StateUpdateTime),
+ * we will only add a new state history entity without updating the process entity properties.
+ */
+ if (state.StateUpdateTime > process.StateUpdateTime)
+ {
+ process.SetState(state);
+ }
+
+ await RecordStateHistoryAsync(process.Id, state);
+ }
+
+ [UnitOfWork]
+ protected virtual async Task RecordStateHistoryAsync(Guid processId, IProcessState state)
+ {
+ return await ProcessStateHistoryRepository.InsertAsync(
+ new ProcessStateHistory(GuidGenerator.Create(), CurrentTenant.Id, processId, state), true);
}
}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/UpdatingToFutureStateException.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/UpdatingToFutureStateException.cs
new file mode 100644
index 0000000..1734139
--- /dev/null
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/UpdatingToFutureStateException.cs
@@ -0,0 +1,14 @@
+using System;
+using Volo.Abp;
+
+namespace EasyAbp.ProcessManagement.Processes;
+
+public class UpdatingToFutureStateException : AbpException
+{
+ public UpdatingToFutureStateException(string stateName, string processName, Guid processId) : base(
+ $"Failed to update to the state `{stateName}` for the process `{processName}` (id: {processId}) since " +
+ $"it's not a child. However, it's a descendant state, this error may be caused by the event disorder, " +
+ $"so the next time the event handling is attempted, it may succeed.")
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/UpdatingToNonDescendantStateException.cs b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/UpdatingToNonDescendantStateException.cs
new file mode 100644
index 0000000..f2f5dc2
--- /dev/null
+++ b/src/EasyAbp.ProcessManagement.Domain/EasyAbp/ProcessManagement/Processes/UpdatingToNonDescendantStateException.cs
@@ -0,0 +1,13 @@
+using System;
+using Volo.Abp;
+
+namespace EasyAbp.ProcessManagement.Processes;
+
+public class UpdatingToNonDescendantStateException : AbpException
+{
+ public UpdatingToNonDescendantStateException(string stateName, string processName, Guid processId) : base(
+ $"Skipping the event handling because the specified state `{stateName}` is not a descendant state of " +
+ $"the current state for the process `{processName}` (id: {processId})")
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/EasyAbp.ProcessManagement.EntityFrameworkCore/EasyAbp/ProcessManagement/EntityFrameworkCore/ProcessStateHistories/ProcessStateHistoryRepository.cs b/src/EasyAbp.ProcessManagement.EntityFrameworkCore/EasyAbp/ProcessManagement/EntityFrameworkCore/ProcessStateHistories/ProcessStateHistoryRepository.cs
index 400928d..5dc78d0 100644
--- a/src/EasyAbp.ProcessManagement.EntityFrameworkCore/EasyAbp/ProcessManagement/EntityFrameworkCore/ProcessStateHistories/ProcessStateHistoryRepository.cs
+++ b/src/EasyAbp.ProcessManagement.EntityFrameworkCore/EasyAbp/ProcessManagement/EntityFrameworkCore/ProcessStateHistories/ProcessStateHistoryRepository.cs
@@ -1,15 +1,19 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.ProcessManagement.ProcessStateHistories;
+using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace EasyAbp.ProcessManagement.EntityFrameworkCore.ProcessStateHistories;
-public class ProcessStateHistoryRepository : EfCoreRepository, IProcessStateHistoryRepository
+public class ProcessStateHistoryRepository : EfCoreRepository,
+ IProcessStateHistoryRepository
{
- public ProcessStateHistoryRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
+ public ProcessStateHistoryRepository(IDbContextProvider dbContextProvider) : base(
+ dbContextProvider)
{
}
@@ -17,4 +21,11 @@ public override async Task> WithDetailsAsync()
{
return (await GetQueryableAsync()).IncludeDetails();
}
+
+ public virtual async Task> GetHistoriesByStateNameAsync(Guid processId, string stateName)
+ {
+ return await (await GetQueryableAsync())
+ .Where(x => x.ProcessId == processId && x.StateName == stateName)
+ .ToListAsync();
+ }
}
\ No newline at end of file
diff --git a/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessDomainTests.cs b/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessDomainTests.cs
deleted file mode 100644
index 09c322e..0000000
--- a/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessDomainTests.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace EasyAbp.ProcessManagement.Processes;
-
-public class ProcessDomainTests : ProcessManagementDomainTestBase
-{
- public ProcessDomainTests()
- {
- }
-
- /*
- [Fact]
- public async Task Test1()
- {
- // Arrange
-
- // Assert
-
- // Assert
- }
- */
-}
\ No newline at end of file
diff --git a/test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs b/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessManagementOptionsTests.cs
similarity index 97%
rename from test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs
rename to test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessManagementOptionsTests.cs
index a2ac775..56af210 100644
--- a/test/EasyAbp.ProcessManagement.Domain.Tests/ProcessManagementOptionsTests.cs
+++ b/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessManagementOptionsTests.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
using EasyAbp.ProcessManagement.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -7,7 +6,7 @@
using Volo.Abp;
using Xunit;
-namespace EasyAbp.ProcessManagement;
+namespace EasyAbp.ProcessManagement.Processes;
public class ProcessManagementOptionsTests : ProcessManagementDomainTestBase
{
diff --git a/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessManagerTests.cs b/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessManagerTests.cs
new file mode 100644
index 0000000..ff21613
--- /dev/null
+++ b/test/EasyAbp.ProcessManagement.Domain.Tests/Processes/ProcessManagerTests.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using EasyAbp.ProcessManagement.Options;
+using EasyAbp.ProcessManagement.ProcessStateHistories;
+using Shouldly;
+using Volo.Abp;
+using Xunit;
+
+namespace EasyAbp.ProcessManagement.Processes;
+
+public class ProcessManagerTests : ProcessManagementDomainTestBase
+{
+ protected ProcessManager ProcessManager { get; }
+ protected IProcessRepository ProcessRepository { get; }
+ protected IProcessStateHistoryRepository ProcessStateHistoryRepository { get; }
+
+ public ProcessManagerTests()
+ {
+ ProcessManager = GetRequiredService();
+ ProcessRepository = GetRequiredService();
+ ProcessStateHistoryRepository = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task Should_Create_State()
+ {
+ var now = DateTime.Now;
+
+ var process = await ProcessManager.CreateAsync(new CreateProcessModel("FakeExport", null, "groupKey"), now);
+
+ await ProcessRepository.InsertAsync(process, true);
+
+ process.ProcessName.ShouldBe("FakeExport");
+ process.CorrelationId.ShouldBe(process.Id.ToString());
+ process.GroupKey.ShouldBe("groupKey");
+ process.StateUpdateTime.ShouldBe(now);
+
+ var histories = await ProcessStateHistoryRepository.GetListAsync(x => x.ProcessId == process.Id);
+
+ histories.Count.ShouldBe(1);
+ histories.ShouldContain(x => x.StateName == "Ready" && x.StateUpdateTime == now);
+ }
+
+ [Fact]
+ public async Task Should_Update_To_Child_State()
+ {
+ var process = await ProcessManager.CreateAsync(
+ new CreateProcessModel("FakeExport", null, "groupKey"), DateTime.Now);
+
+ await ProcessRepository.InsertAsync(process, true);
+
+ var updateTime = DateTime.Now;
+
+ await ProcessManager.UpdateStateAsync(process, new UpdateProcessStateModel(updateTime, "Exporting"));
+
+ await ProcessRepository.UpdateAsync(process, true);
+
+ process.StateName.ShouldBe("Exporting");
+ process.StateUpdateTime.ShouldBe(updateTime);
+
+ var histories = await ProcessStateHistoryRepository.GetListAsync(x => x.ProcessId == process.Id);
+
+ histories.Count.ShouldBe(2);
+ histories.ShouldContain(x => x.StateName == "Ready");
+ histories.ShouldContain(x => x.StateName == "Exporting" && x.StateUpdateTime == updateTime);
+ }
+
+ [Fact]
+ public async Task Should_Update_State_Custom_Info()
+ {
+ var process = await ProcessManager.CreateAsync(
+ new CreateProcessModel("FakeExport", null, "groupKey"), DateTime.Now);
+
+ await ProcessRepository.InsertAsync(process, true);
+
+ var updateTime = DateTime.Now;
+
+ // Not updating from Exporting to Ready, but add a history for Ready.
+ await ProcessManager.UpdateStateAsync(process,
+ new UpdateProcessStateModel(updateTime, "Ready", "balalala", ProcessStateFlag.Running, null, null));
+
+ var histories = await ProcessStateHistoryRepository.GetListAsync(x => x.ProcessId == process.Id);
+
+ histories.Count.ShouldBe(2);
+ histories.Count(x => x.StateName == "Ready").ShouldBe(2);
+ histories.ShouldContain(x =>
+ x.StateName == "Ready" && x.ActionName == "balalala" && x.StateUpdateTime == updateTime);
+ }
+
+ [Fact]
+ public async Task Should_Update_State_Custom_Info_Even_If_Disordered()
+ {
+ var process = await ProcessManager.CreateAsync(
+ new CreateProcessModel("FakeExport", null, "groupKey"), DateTime.Now);
+
+ await ProcessRepository.InsertAsync(process, true);
+
+ // Ready -> Exporting
+ await ProcessManager.UpdateStateAsync(process, new UpdateProcessStateModel(DateTime.Now, "Exporting"));
+
+ var updateTime = DateTime.Now;
+
+ // Not updating from Exporting to Ready, but add a history for Ready.
+ await ProcessManager.UpdateStateAsync(process,
+ new UpdateProcessStateModel(updateTime, "Ready", "balalala", ProcessStateFlag.Running, null, null));
+
+ var histories = await ProcessStateHistoryRepository.GetListAsync(x => x.ProcessId == process.Id);
+
+ histories.Count.ShouldBe(3);
+ histories.Count(x => x.StateName == "Ready").ShouldBe(2);
+ histories.ShouldContain(x =>
+ x.StateName == "Ready" && x.ActionName == "balalala" && x.StateUpdateTime == updateTime);
+ histories.ShouldContain(x => x.StateName == "Exporting");
+ }
+
+ [Fact]
+ public async Task Should_Not_Update_To_Invalid_State()
+ {
+ var process = await ProcessManager.CreateAsync(
+ new CreateProcessModel("FakeExport", null, "groupKey"), DateTime.Now);
+
+ await ProcessRepository.InsertAsync(process, true);
+
+ // Update to an undefined state.
+ await Should.ThrowAsync(() =>
+ ProcessManager.UpdateStateAsync(process, new UpdateProcessStateModel(DateTime.Now, "Balalala")));
+
+ // Update to a grandchild state.
+ await Should.ThrowAsync(() =>
+ ProcessManager.UpdateStateAsync(process, new UpdateProcessStateModel(DateTime.Now, "Succeeded")));
+
+ await ProcessManager.UpdateStateAsync(process, new UpdateProcessStateModel(DateTime.Now, "Exporting"));
+
+ // Update to a non-descendant state.
+ await Should.ThrowAsync(() =>
+ ProcessManager.UpdateStateAsync(process,
+ new UpdateProcessStateModel(DateTime.Now, "FailedToStartExporting")));
+ }
+}
\ No newline at end of file