Skip to content

Commit

Permalink
Merge pull request #20 from EasyAbp/state-update
Browse files Browse the repository at this point in the history
State update refactor
  • Loading branch information
gdlcf88 authored Jul 3, 2024
2 parents 9c2a57f + 77b13f1 commit 373e474
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,37 @@ 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; }

public UpdateProcessStateModel()

Check warning on line 23 in src/EasyAbp.ProcessManagement.Domain.Shared/EasyAbp/ProcessManagement/Processes/UpdateProcessStateModel.cs

View workflow job for this annotation

GitHub Actions / publish

Non-nullable property 'StateName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
{
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -58,6 +60,23 @@ public ProcessDefinition AddState(ProcessStateDefinition stateDefinition)
return this;
}

/// <summary>
/// If the specified state is a child, grandchild, or further descendant of the current state, it returns true.
/// </summary>
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`")
{
}
}
Original file line number Diff line number Diff line change
@@ -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<ProcessStateHistory, Guid>
{
}
Task<List<ProcessStateHistory>> GetHistoriesByStateNameAsync(Guid processId, string stateName);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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")
{
}
}
Original file line number Diff line number Diff line change
@@ -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<IOptions<ProcessManagementOptions>>().Value;

public ProcessManager(IOptions<ProcessManagementOptions> options)
{
Options = options.Value;
}
protected IProcessStateHistoryRepository ProcessStateHistoryRepository => LazyServiceProvider
.LazyGetRequiredService<IProcessStateHistoryRepository>();

public virtual Task<Process> CreateAsync(CreateProcessModel model, DateTime now)
public virtual async Task<Process> 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<ProcessStateHistory> RecordStateHistoryAsync(Guid processId, IProcessState state)
{
return await ProcessStateHistoryRepository.InsertAsync(
new ProcessStateHistory(GuidGenerator.Create(), CurrentTenant.Id, processId, state), true);
}
}
Original file line number Diff line number Diff line change
@@ -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.")
{
}
}
Original file line number Diff line number Diff line change
@@ -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})")
{
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
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<IProcessManagementDbContext, ProcessStateHistory, Guid>, IProcessStateHistoryRepository
public class ProcessStateHistoryRepository : EfCoreRepository<IProcessManagementDbContext, ProcessStateHistory, Guid>,
IProcessStateHistoryRepository
{
public ProcessStateHistoryRepository(IDbContextProvider<IProcessManagementDbContext> dbContextProvider) : base(dbContextProvider)
public ProcessStateHistoryRepository(IDbContextProvider<IProcessManagementDbContext> dbContextProvider) : base(
dbContextProvider)
{
}

public override async Task<IQueryable<ProcessStateHistory>> WithDetailsAsync()
{
return (await GetQueryableAsync()).IncludeDetails();
}

public virtual async Task<List<ProcessStateHistory>> GetHistoriesByStateNameAsync(Guid processId, string stateName)
{
return await (await GetQueryableAsync())
.Where(x => x.ProcessId == processId && x.StateName == stateName)
.ToListAsync();
}
}

This file was deleted.

Loading

0 comments on commit 373e474

Please sign in to comment.