Skip to content

Commit d296896

Browse files
author
Jakab László
committed
State machine classes
1 parent d387887 commit d296896

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

src/AutSoft.Core/AutSoft.Common.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
@@ -8,6 +8,8 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
11+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" />
12+
<PackageReference Include="Stateless" Version="5.12.0" />
1113
</ItemGroup>
1214

1315
</Project>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using AutSoft.Common.Exceptions;
2+
using AutSoft.Common.Time;
3+
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.Infrastructure;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
using Stateless;
9+
10+
using System.Linq.Expressions;
11+
12+
namespace AutSoft.Common.StateMachines;
13+
14+
/// <summary>
15+
/// Abstract base class of state machines, which add functionalities for Stateless StateMachine class
16+
/// </summary>
17+
/// <typeparam name="TState">Type of state machine's states</typeparam>
18+
/// <typeparam name="TTrigger">Type of state machine's triggers</typeparam>
19+
/// <typeparam name="TEntity">The type of entity whose state is described by the state machine</typeparam>
20+
public abstract class EntityStateMachineBase<TState, TTrigger, TEntity> : StateMachine<TState, TTrigger>
21+
where TState : struct, Enum
22+
where TTrigger : Enum
23+
where TEntity : class
24+
{
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="EntityStateMachineBase{TState, TTrigger, TEntity}"/> class.
27+
/// </summary>
28+
/// <param name="entity">The entity whose state is described by the state machine</param>
29+
/// <param name="statePropertySelector">The state property of the entity described by the state machine</param>
30+
/// <param name="stateModifiedDatePropertySelector">The property of entity, which contains the time of the last change of state</param>
31+
/// <param name="timeProvider">An instance of <see cref="ITimeProvider"/></param>
32+
/// <param name="dbContext">An instance of <see cref="DbContext"/></param>
33+
/// <param name="exceptionMessage">Message of exception</param>
34+
/// <exception cref="ArgumentNullException">This exception is thrown if the added properties aren't available</exception>
35+
/// <exception cref="BusinessException">This exception is thrown if a state transition does not permitted</exception>
36+
protected EntityStateMachineBase(
37+
TEntity entity,
38+
Expression<Func<TEntity, TState>> statePropertySelector,
39+
Expression<Func<TEntity, DateTimeOffset>>? stateModifiedDatePropertySelector = null,
40+
ITimeProvider? timeProvider = null,
41+
DbContext? dbContext = null,
42+
string? exceptionMessage = null)
43+
: base(
44+
stateAccessor: () => (TState)statePropertySelector.GetPropertyAccess().GetValue(entity)!,
45+
stateMutator: state => statePropertySelector.GetPropertyAccess().SetValue(entity, state))
46+
{
47+
if (stateModifiedDatePropertySelector != null && timeProvider == null)
48+
throw new ArgumentNullException(nameof(timeProvider), $"{nameof(stateModifiedDatePropertySelector)} megadása esetén a {nameof(timeProvider)} paraméter nem lehet null");
49+
50+
if (stateModifiedDatePropertySelector != null && dbContext == null)
51+
throw new ArgumentNullException(nameof(timeProvider), $"{nameof(stateModifiedDatePropertySelector)} megadása esetén a {nameof(dbContext)} paraméter nem lehet null");
52+
53+
OnTransitionedAsync(async _ =>
54+
{
55+
stateModifiedDatePropertySelector?.GetPropertyAccess().SetValue(entity, timeProvider?.Now);
56+
57+
if (dbContext != null)
58+
await dbContext.SaveChangesAsync();
59+
});
60+
61+
OnUnhandledTrigger((s, t) =>
62+
{
63+
var id = entity.GetType().GetProperty("Id")?.GetValue(entity);
64+
throw new BusinessException(
65+
exceptionMessage ?? $"A kívánt állapotátmenet nem engedélyezett! (entitás: {entity.GetType().Name} id: {id} állapot: {s} trigger: {t})", "Hibás művelet!");
66+
});
67+
}
68+
69+
/// <summary>
70+
/// Gives that an trigger can be fired with specified arguments or not
71+
/// </summary>
72+
/// <param name="trigger">The trigger object</param>
73+
/// <param name="arguments">The specified arguments</param>
74+
/// <returns>The trigger can be fired with specified arguments or not</returns>
75+
public bool CanFire(TTrigger trigger, params object[] arguments)
76+
{
77+
if (arguments == null || arguments.Length == 0)
78+
return base.CanFire(trigger);
79+
80+
return GetPermittedTriggers(arguments).Any(x => Equals(x, trigger));
81+
}
82+
83+
/// <summary>
84+
/// Base abstract class of state machine factories
85+
/// </summary>
86+
/// <typeparam name="TStateMachine">Type of state machine to be created</typeparam>
87+
public abstract class FactoryBase<TStateMachine>
88+
where TStateMachine : EntityStateMachineBase<TState, TTrigger, TEntity>
89+
{
90+
private readonly IServiceProvider _serviceProvider;
91+
92+
/// <summary>
93+
/// Initializes a new instance of the <see cref="FactoryBase{TStateMachine}"/> class
94+
/// </summary>
95+
/// <param name="serviceProvider">Instance of an IServiceProvider</param>
96+
protected FactoryBase(IServiceProvider serviceProvider)
97+
{
98+
_serviceProvider = serviceProvider;
99+
}
100+
101+
/// <summary>
102+
/// Create a state machine with specified type
103+
/// </summary>
104+
/// <param name="entity">The entity whose state is described by the state machine</param>
105+
/// <returns>The created state machine</returns>
106+
public TStateMachine CreateStateMachine(TEntity entity)
107+
{
108+
return ActivatorUtilities.CreateInstance<TStateMachine>(_serviceProvider, entity);
109+
}
110+
}
111+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Stateless;
2+
using Stateless.Reflection;
3+
4+
namespace AutSoft.Common.StateMachines;
5+
6+
/// <summary>
7+
/// Extension methods for state configuration with Stateless package
8+
/// </summary>
9+
public static class StateConfigurationExtensions
10+
{
11+
/// <summary>
12+
/// Permit manually changes in state machine
13+
/// </summary>
14+
/// <typeparam name="TState">Type of state machine's states</typeparam>
15+
/// <typeparam name="TTrigger">Type of state machine's triggers</typeparam>
16+
/// <param name="config">Configuration instance of a state of the state machine</param>
17+
/// <param name="trigger">The trigger, which triggers the state transition</param>
18+
/// <param name="permittedDestinationStates">The permitted target states</param>
19+
/// <returns>The configured configuration instance</returns>
20+
public static StateMachine<TState, TTrigger>.StateConfiguration PermitManualChange<TState, TTrigger>(
21+
this StateMachine<TState, TTrigger>.StateConfiguration config,
22+
TTrigger trigger,
23+
TState[] permittedDestinationStates)
24+
{
25+
var dynamicStateInfos = new DynamicStateInfos();
26+
dynamicStateInfos.AddRange(permittedDestinationStates.Select(s => new DynamicStateInfo(s.ToString(), string.Empty)));
27+
28+
return config.PermitDynamicIf(
29+
new StateMachine<TState, TTrigger>.TriggerWithParameters<TState>(trigger),
30+
newState => newState,
31+
newState => permittedDestinationStates.Contains(newState),
32+
possibleDestinationStates: dynamicStateInfos);
33+
}
34+
}

src/Directory.Packages.props

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
</PackageReference>
2020
</ItemGroup>
2121
<ItemGroup>
22+
<PackageVersion Include="Microsoft.EntityFrameworkCore">
23+
<Version>6.0.10</Version>
24+
</PackageVersion>
2225
<PackageVersion Include="Microsoft.NET.Test.Sdk">
2326
<Version>17.3.0</Version>
2427
</PackageVersion>
28+
<PackageVersion Include="Stateless">
29+
<Version>5.12.0</Version>
30+
</PackageVersion>
2531
<PackageVersion Include="xunit">
2632
<Version>2.4.2</Version>
2733
</PackageVersion>

0 commit comments

Comments
 (0)