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