mirror of
https://github.com/ditkrg/DIT.Workflower.git
synced 2026-01-22 22:06:42 +00:00
WIP
This commit is contained in:
parent
e66733b46a
commit
1942bcfe84
94
README.md
94
README.md
@ -1 +1,95 @@
|
||||
# DIT.Workflower
|
||||
|
||||
Workflower is a library based on .NET Standard, To handle Finite State Machines (FSM) and workflow management.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Nuget
|
||||
|
||||
Install the latest nuget package into your ASP.NET Core application.
|
||||
|
||||
```sh
|
||||
dotnet add package DIT.Workflower
|
||||
```
|
||||
|
||||
You can also download support for Dependency Injection:
|
||||
|
||||
```sh
|
||||
dotnet add package DIT.Workflower.DependencyInjection
|
||||
```
|
||||
|
||||
### Workflow Builder
|
||||
In the `Program.cs`, register the Swagger generator, defining one or more Swagger documents.
|
||||
|
||||
```csharp
|
||||
using DIT.Workflower;
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
||||
var builder = new WorkflowDefinitionBuilder<TState, TCommand, TContext>()
|
||||
.From(TState.State_1)
|
||||
.On(TCommand.GoNext)
|
||||
.To(TState.State_2)
|
||||
|
||||
.From(TState.State_2)
|
||||
.On(TCommand.GoNext)
|
||||
.To(TState.State_3)
|
||||
.AndOn(TCommand.GoBack)
|
||||
.To(TState.State_1)
|
||||
|
||||
var workflow = builder.Build();
|
||||
|
||||
var allowedTransitions = workflow.GetAllowedTransitions(from: TState.State_2);
|
||||
|
||||
// allowedTransitions will be 2 transitions
|
||||
// State_2 -> State_3 (GoNext)
|
||||
// State_2 -> State_1 (GoBack)
|
||||
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
|
||||
```csharp
|
||||
public record PhoneCall(bool Active);
|
||||
|
||||
services.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 1)
|
||||
// Idle -> Ringing (Incoming)
|
||||
.From(PhoneState.Idle)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing)
|
||||
|
||||
// Ringing -> Connected (Accept & ctx.Active)
|
||||
.From(PhoneState.Ringing)
|
||||
.On(PhoneCommand.Accept)
|
||||
.To(PhoneState.Connected)
|
||||
.When(ctx => ctx.Active) // An instance of PhoneCall will become the ctx.
|
||||
|
||||
// Ringing -> Declined (Decline)
|
||||
.From(PhoneState.Ringing)
|
||||
.On(PhoneCommand.Decline)
|
||||
.To(PhoneState.Declined)
|
||||
```
|
||||
```csharp
|
||||
[Route("[controller]")]
|
||||
public class SampleController : JsonApiControllerBase
|
||||
{
|
||||
|
||||
private readonly IWorkflow<PhoneState, PhoneCommand, PhoneCall> _workflow;
|
||||
|
||||
public SampleController(IWorkflowFactory<PhoneState, PhoneCommand, PhoneCall> factory)
|
||||
{
|
||||
_workflow = factory.CreateWorkflow(version: 1); // Optional version param to support versioning on workflows.
|
||||
}
|
||||
|
||||
[HttpGet("{state}")]
|
||||
[ProducesResponseType(typeof(JsonApiSingleDataResponse<InitiateRequest>), StatusCodes.Status200OK)]
|
||||
public ActionResult GetRequest(PhoneState state, bool isActive)
|
||||
{
|
||||
var phoneCall = new PhoneCall(isActive)
|
||||
var transitions = _workflow.GetAllowedTransitions(context: phoneCall, state);
|
||||
return Ok(transitions)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -5,25 +5,25 @@ namespace DIT.Workflower.DependencyInjection.Extensions;
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
|
||||
public static WorkflowDefinitionBuilder<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services)
|
||||
public static ITransitionOn<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, TState initial)
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
return AddWorkflowDefinition<TState, TCommand, TContext>(services, version: 1);
|
||||
return AddWorkflowDefinition<TState, TCommand, TContext>(services, initial, version: 1);
|
||||
}
|
||||
|
||||
public static WorkflowDefinitionBuilder<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, int version)
|
||||
public static ITransitionOn<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, TState initial, int version)
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
var builder = new WorkflowDefinitionBuilder<TState, TCommand, TContext>();
|
||||
var builder = WorkflowDefinitionBuilder<TState, TCommand, TContext>.Initial(initial);
|
||||
|
||||
services.TryAddSingleton<IWorkflowFactory<TState, TCommand, TContext>, DefaultWorkflowFactory<TState, TCommand, TContext>>();
|
||||
|
||||
services.AddSingleton<IWorkflow<TState, TCommand, TContext>, WorkflowDefinitionWrapper<TState, TCommand, TContext>>(sp =>
|
||||
{
|
||||
var workflow = builder.Build();
|
||||
var wrapper = new WorkflowDefinitionWrapper<TState, TCommand, TContext>(builder, version);
|
||||
var definition = ((WorkflowDefinitionBuilder<TState, TCommand, TContext>)builder);
|
||||
var wrapper = new WorkflowDefinitionWrapper<TState, TCommand, TContext>(definition, version);
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
global using DIT.Workflower.DependencyInjection.Abstractions;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using DIT.Workflower.Abstractions;
|
||||
global using DIT.Workflower.DependencyInjection.Abstractions;
|
||||
|
||||
35
src/DIT.Workflower/Abstractions.cs
Normal file
35
src/DIT.Workflower/Abstractions.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace DIT.Workflower.Abstractions;
|
||||
|
||||
public interface ITransitionOn<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
ITransitionCondition<TState, TCommand, TContext> On(in TCommand command);
|
||||
}
|
||||
|
||||
public interface ITransitionExit<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
ITransitionDone<TState, TCommand, TContext> To(in TState command);
|
||||
}
|
||||
|
||||
public interface ITransitionCondition<TState, TCommand, TContext> : ITransitionExit<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
ITransitionCondition<TState, TCommand, TContext> WithMeta(in object meta);
|
||||
|
||||
ITransitionCondition<TState, TCommand, TContext> When(Func<TContext, bool> condition);
|
||||
|
||||
}
|
||||
|
||||
public interface ITransitionDone<TState, TCommand, TContext> : ITransitionOn<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
|
||||
ITransitionOn<TState, TCommand, TContext> From(in TState state);
|
||||
|
||||
WorkflowDefinition<TState, TCommand, TContext> Build();
|
||||
}
|
||||
@ -4,11 +4,11 @@ public record Transition<TState, TCommand>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
public TState From { get; set; }
|
||||
public TState From { get; set; } = default!;
|
||||
|
||||
public TState To { get; set; }
|
||||
public TState To { get; set; } = default!;
|
||||
|
||||
public TCommand Command { get; set; }
|
||||
public TCommand Command { get; set; } = default!;
|
||||
|
||||
public object? Meta { get; set; }
|
||||
}
|
||||
|
||||
@ -1,59 +1,64 @@
|
||||
namespace DIT.Workflower;
|
||||
using DIT.Workflower.Abstractions;
|
||||
|
||||
namespace DIT.Workflower;
|
||||
|
||||
public sealed class WorkflowDefinitionBuilder<TState, TCommand, TContext> :
|
||||
ITransitionOn<TState, TCommand, TContext>,
|
||||
ITransitionExit<TState, TCommand, TContext>,
|
||||
ITransitionCondition<TState, TCommand, TContext>,
|
||||
ITransitionDone<TState, TCommand, TContext>
|
||||
|
||||
public class WorkflowDefinitionBuilder<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
private TransitionDefinition<TState, TCommand, TContext>? _current;
|
||||
private TransitionDefinition<TState, TCommand, TContext> _current;
|
||||
|
||||
public List<TransitionDefinition<TState, TCommand, TContext>> Transitions { get; } = new();
|
||||
|
||||
#region Constructor
|
||||
|
||||
public WorkflowDefinitionBuilder<TState, TCommand, TContext> From(in TState state)
|
||||
private WorkflowDefinitionBuilder(TState initialState)
|
||||
=> _current = new() { From = initialState };
|
||||
|
||||
public static ITransitionOn<TState, TCommand, TContext> Initial(TState initialState)
|
||||
=> new WorkflowDefinitionBuilder<TState, TCommand, TContext>(initialState);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
public ITransitionOn<TState, TCommand, TContext> From(in TState state)
|
||||
{
|
||||
if (_current != null)
|
||||
Transitions.Add(_current);
|
||||
|
||||
_current = new() { From = state };
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public WorkflowDefinitionBuilder<TState, TCommand, TContext> To(in TState state)
|
||||
public ITransitionDone<TState, TCommand, TContext> To(in TState state)
|
||||
{
|
||||
if (_current == null)
|
||||
throw new InvalidOperationException($"From needs to be called first");
|
||||
|
||||
_current = _current with { To = state };
|
||||
|
||||
Transitions.Add(_current);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public WorkflowDefinitionBuilder<TState, TCommand, TContext> On(in TCommand command)
|
||||
public ITransitionCondition<TState, TCommand, TContext> On(in TCommand command)
|
||||
{
|
||||
if (_current == null)
|
||||
throw new InvalidOperationException($"From needs to be called first");
|
||||
|
||||
_current = _current with { Command = command };
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public WorkflowDefinitionBuilder<TState, TCommand, TContext> WithMeta(in object meta)
|
||||
public ITransitionCondition<TState, TCommand, TContext> WithMeta(in object meta)
|
||||
{
|
||||
if (_current == null)
|
||||
throw new InvalidOperationException($"From needs to be called first");
|
||||
|
||||
_current.Meta = meta;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public WorkflowDefinitionBuilder<TState, TCommand, TContext> When(Func<TContext, bool> condition)
|
||||
public ITransitionCondition<TState, TCommand, TContext> When(Func<TContext, bool> condition)
|
||||
{
|
||||
if (_current == null)
|
||||
throw new InvalidOperationException($"From needs to be called first");
|
||||
|
||||
if (_current.Conditions is null)
|
||||
_current.Conditions = new();
|
||||
|
||||
@ -64,7 +69,6 @@ public class WorkflowDefinitionBuilder<TState, TCommand, TContext>
|
||||
|
||||
public WorkflowDefinition<TState, TCommand, TContext> Build()
|
||||
{
|
||||
if (_current != null)
|
||||
Transitions.Add(_current);
|
||||
|
||||
if (!Transitions.Any())
|
||||
@ -72,4 +76,6 @@ public class WorkflowDefinitionBuilder<TState, TCommand, TContext>
|
||||
|
||||
return new(Transitions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -18,9 +18,7 @@ public class DependencyInjectionTests
|
||||
|
||||
var sc = new ServiceCollection();
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 1)
|
||||
|
||||
.From(PhoneState.Idle)
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(initial: PhoneState.Idle, version: 1)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing)
|
||||
|
||||
@ -29,9 +27,7 @@ public class DependencyInjectionTests
|
||||
.To(PhoneState.Connected)
|
||||
;
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 2)
|
||||
|
||||
.From(PhoneState.Idle)
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(initial: PhoneState.Idle, version: 2)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing)
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
global using DIT.Workflower;
|
||||
global using DIT.Workflower.Abstractions;
|
||||
global using Xunit;
|
||||
|
||||
@ -2,27 +2,27 @@ namespace DIT.Workflower.Tests;
|
||||
|
||||
public class WorkflowConditionTests
|
||||
{
|
||||
private WorkflowDefinitionBuilder<PhoneState, PhoneCommand, PhoneCall> GetDefaultBuilder()
|
||||
private ITransitionOn<PhoneState, PhoneCommand, PhoneCall> GetDefaultBuilder()
|
||||
{
|
||||
return new();
|
||||
return WorkflowDefinitionBuilder<PhoneState, PhoneCommand, PhoneCall>.Initial(PhoneState.Idle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleConditionTests()
|
||||
{
|
||||
var phone = new PhoneCall(Active: false);
|
||||
var builder1 = GetDefaultBuilder();
|
||||
var builder2 = GetDefaultBuilder();
|
||||
|
||||
var a = "b";
|
||||
|
||||
builder1
|
||||
.From(PhoneState.Idle)
|
||||
.When((res) => a == "n");
|
||||
var builder1 = GetDefaultBuilder()
|
||||
.On(PhoneCommand.Decline)
|
||||
.When((res) => a == "n")
|
||||
.To(PhoneState.Declined);
|
||||
|
||||
builder2
|
||||
.From(PhoneState.Idle)
|
||||
.When((res) => a == "b" && res.Active is false);
|
||||
var builder2 = GetDefaultBuilder()
|
||||
.On(PhoneCommand.Decline)
|
||||
.When((res) => a == "b" && res.Active is false)
|
||||
.To(PhoneState.OnHold);
|
||||
|
||||
Assert.Empty(builder1.Build().GetAllowedTransitions(phone, PhoneState.Idle));
|
||||
Assert.Single(builder2.Build().GetAllowedTransitions(phone, PhoneState.Idle));
|
||||
@ -32,21 +32,21 @@ public class WorkflowConditionTests
|
||||
public void MultiConditionTests()
|
||||
{
|
||||
var phone = new PhoneCall();
|
||||
var builder1 = GetDefaultBuilder();
|
||||
var builder2 = GetDefaultBuilder();
|
||||
|
||||
var a = "b";
|
||||
var other = a;
|
||||
|
||||
builder1
|
||||
.From(PhoneState.Idle)
|
||||
var builder1 = GetDefaultBuilder()
|
||||
.On(PhoneCommand.Resume)
|
||||
.When((res) => a == "c")
|
||||
.When((res) => other == a);
|
||||
.When((res) => other == a)
|
||||
.To(PhoneState.Connected);
|
||||
|
||||
builder2
|
||||
.From(PhoneState.Idle)
|
||||
var builder2 = GetDefaultBuilder()
|
||||
.On(PhoneCommand.Resume)
|
||||
.When((res) => a == "b")
|
||||
.When((res) => other == a);
|
||||
.When((res) => other == a)
|
||||
.To(PhoneState.Connected);
|
||||
|
||||
Assert.Empty(builder1.Build().GetAllowedTransitions(phone, PhoneState.Idle));
|
||||
Assert.Single(builder2.Build().GetAllowedTransitions(phone, PhoneState.Idle));
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
namespace DIT.Workflower.Tests;
|
||||
|
||||
public class WorkflowMetaTests
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public void WorkflowNeedsAtLeastOneTransition()
|
||||
{
|
||||
var builder = new WorkflowDefinitionBuilder<PhoneState, PhoneCommand, PhoneCall>();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(builder.Build);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user