mirror of
https://github.com/ditkrg/DIT.Workflower.git
synced 2026-01-22 13:56:43 +00:00
commit
3add480296
18
.config/dotnet-tools.json
Normal file
18
.config/dotnet-tools.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-sonarscanner": {
|
||||
"version": "5.6.0",
|
||||
"commands": [
|
||||
"dotnet-sonarscanner"
|
||||
]
|
||||
},
|
||||
"dotnet-coverage": {
|
||||
"version": "17.3.2",
|
||||
"commands": [
|
||||
"dotnet-coverage"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
78
.github/workflows/tests-base.yaml
vendored
Normal file
78
.github/workflows/tests-base.yaml
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
|
||||
- ".github/**"
|
||||
- "!.github/workflows/tests-base.yaml"
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
sonarqube:
|
||||
type: boolean
|
||||
required: false
|
||||
default: fals
|
||||
|
||||
secrets:
|
||||
SONARQUBE_HOST:
|
||||
required: false
|
||||
SONARQUBE_TOKEN:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
name: Run Tests
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
PROJECT_KEY: ditkrg_DIT.Workflower_AYF14rjSb80e2b0bns3t
|
||||
SONARQUBE_HOST: ${{ secrets.SONARQUBE_HOST }}
|
||||
SONARQUBE_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
|
||||
ASPNETCORE_ENVIRONMENT: Testing
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: "6.0.x"
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ !inputs.sonarqube }}
|
||||
|
||||
- name: Run tests
|
||||
if: ${{ !inputs.sonarqube }}
|
||||
run: dotnet test
|
||||
|
||||
###############################
|
||||
########## SONARQUBE ##########
|
||||
###############################
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
if: ${{ inputs.sonarqube }}
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ inputs.sonarqube }}
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Restore tools
|
||||
if: ${{ inputs.sonarqube }}
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: Run tests (SonarQube)
|
||||
if: ${{ inputs.sonarqube }}
|
||||
run: |
|
||||
dotnet tool run dotnet-sonarscanner begin -k:"$PROJECT_KEY" \
|
||||
-d:sonar.login="$SONARQUBE_TOKEN" \
|
||||
-d:sonar.host.url="$SONARQUBE_HOST" \
|
||||
-d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml
|
||||
|
||||
dotnet build --no-incremental
|
||||
dotnet dotnet-coverage collect "dotnet test" -f xml -o "coverage.xml"
|
||||
|
||||
dotnet tool run dotnet-sonarscanner end -d:sonar.login="$SONARQUBE_TOKEN"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,6 +10,8 @@
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
.sonarqube
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
@ -147,6 +149,7 @@ _TeamCity*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
coverage.xml
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
**/coverage
|
||||
|
||||
102
README.md
102
README.md
@ -1 +1,101 @@
|
||||
# DIT.Workflower
|
||||
# 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
|
||||
|
||||
```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
|
||||
|
||||
In the `Program.cs`, register the workflow factory, defining one or more workflow definitions.
|
||||
|
||||
|
||||
```csharp
|
||||
public record PhoneCall(bool Active);
|
||||
|
||||
services.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(id: "constant-id", 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(id: "constant-id", 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: If you do not specify an id for the workflow, the default id is:
|
||||
```csharp
|
||||
$"{typeof(TState).Name}_{typeof(TCommand).Name}_{typeof(TContext).Name}";
|
||||
```
|
||||
|
||||
@ -4,8 +4,12 @@ public interface IWorkflow<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
string Id { get; }
|
||||
|
||||
int Version { get; }
|
||||
|
||||
string Reference => $"{Id}.v{Version}";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of allowed transitions without any condition checks.
|
||||
/// </summary>
|
||||
|
||||
@ -4,9 +4,8 @@ public interface IWorkflowFactory<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow(int version = 1);
|
||||
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow();
|
||||
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow(int version);
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow(string id, int version = 1);
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
|
||||
namespace DIT.Workflower.DependencyInjection;
|
||||
namespace DIT.Workflower.DependencyInjection;
|
||||
|
||||
public class DefaultWorkflowFactory<TState, TCommand, TContext> : IWorkflowFactory<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
@ -13,16 +12,20 @@ public class DefaultWorkflowFactory<TState, TCommand, TContext> : IWorkflowFacto
|
||||
_serviceProvider = sp;
|
||||
}
|
||||
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow()
|
||||
=> CreateWorkflow(version: 1);
|
||||
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow(int version)
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow(int version = 1)
|
||||
{
|
||||
var id = WorkflowDefinitionWrapper<TState, TCommand, TContext>.GetDefaultId();
|
||||
return CreateWorkflow(id, version);
|
||||
}
|
||||
|
||||
public IWorkflow<TState, TCommand, TContext> CreateWorkflow(string id, int version = 1)
|
||||
{
|
||||
var reference = $"{id}.v{version}";
|
||||
var service = _serviceProvider.GetServices<IWorkflow<TState, TCommand, TContext>>()
|
||||
.FirstOrDefault(x => x.Version == version);
|
||||
.FirstOrDefault(x => x.Reference == reference);
|
||||
|
||||
if (service is null)
|
||||
throw new ArgumentOutOfRangeException(nameof(version), $"Version {version} of workflow does not exist");
|
||||
throw new KeyNotFoundException($"Workflow reference {id}.v{version} does not exist");
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
@ -5,25 +5,33 @@ namespace DIT.Workflower.DependencyInjection.Extensions;
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
|
||||
public static WorkflowDefinitionBuilder<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services)
|
||||
public static ITransitionStart<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, in int version = 1)
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
return AddWorkflowDefinition<TState, TCommand, TContext>(services, version: 1);
|
||||
var id = WorkflowDefinitionWrapper<TState, TCommand, TContext>.GetDefaultId();
|
||||
return AddWorkflowDefinition<TState, TCommand, TContext>(services, id, version);
|
||||
}
|
||||
|
||||
public static WorkflowDefinitionBuilder<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, int version)
|
||||
public static ITransitionStart<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, in string id)
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
var builder = new WorkflowDefinitionBuilder<TState, TCommand, TContext>();
|
||||
return AddWorkflowDefinition<TState, TCommand, TContext>(services, id, version: 1);
|
||||
}
|
||||
|
||||
public static ITransitionStart<TState, TCommand, TContext> AddWorkflowDefinition<TState, TCommand, TContext>(this IServiceCollection services, string id, int version)
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
var builder = WorkflowDefinitionBuilder<TState, TCommand, TContext>.Create();
|
||||
|
||||
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, id, 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;
|
||||
|
||||
@ -5,12 +5,20 @@ public record WorkflowDefinitionWrapper<TState, TCommand, TContext> : WorkflowDe
|
||||
where TCommand : struct
|
||||
{
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public int Version { get; }
|
||||
|
||||
public WorkflowDefinitionWrapper(WorkflowDefinitionBuilder<TState, TCommand, TContext> builder, int version)
|
||||
public WorkflowDefinitionWrapper(WorkflowDefinitionBuilder<TState, TCommand, TContext> builder, string id, int version)
|
||||
: base(builder.Transitions)
|
||||
{
|
||||
Id = id;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public static string GetDefaultId()
|
||||
{
|
||||
return $"{typeof(TState).Name}_{typeof(TCommand).Name}_{typeof(TContext).Name}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
42
src/DIT.Workflower/Abstractions.cs
Normal file
42
src/DIT.Workflower/Abstractions.cs
Normal file
@ -0,0 +1,42 @@
|
||||
namespace DIT.Workflower.Abstractions;
|
||||
|
||||
public interface ITransitionStart<TState, TCommand, TContext>
|
||||
where TState : struct
|
||||
where TCommand : struct
|
||||
{
|
||||
ITransitionOn<TState, TCommand, TContext> From(in TState state);
|
||||
}
|
||||
|
||||
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 state);
|
||||
}
|
||||
|
||||
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>
|
||||
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,65 @@
|
||||
namespace DIT.Workflower;
|
||||
using DIT.Workflower.Abstractions;
|
||||
|
||||
namespace DIT.Workflower;
|
||||
|
||||
public sealed class WorkflowDefinitionBuilder<TState, TCommand, TContext> :
|
||||
ITransitionStart<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()
|
||||
=> _current = new();
|
||||
|
||||
public static ITransitionStart<TState, TCommand, TContext> Create()
|
||||
=> new WorkflowDefinitionBuilder<TState, TCommand, TContext>();
|
||||
|
||||
#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,12 +70,11 @@ public class WorkflowDefinitionBuilder<TState, TCommand, TContext>
|
||||
|
||||
public WorkflowDefinition<TState, TCommand, TContext> Build()
|
||||
{
|
||||
if (_current != null)
|
||||
Transitions.Add(_current);
|
||||
|
||||
if (!Transitions.Any())
|
||||
throw new InvalidOperationException("No transitions are added");
|
||||
|
||||
return new(Transitions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DIT.Workflower.DependencyInjection.Abstractions;
|
||||
using DIT.Workflower.DependencyInjection.Abstractions;
|
||||
using DIT.Workflower.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -17,9 +12,9 @@ public class DependencyInjectionTests
|
||||
{
|
||||
|
||||
var sc = new ServiceCollection();
|
||||
var id = "test";
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 1)
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(id, version: 1)
|
||||
.From(PhoneState.Idle)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing)
|
||||
@ -29,8 +24,7 @@ public class DependencyInjectionTests
|
||||
.To(PhoneState.Connected)
|
||||
;
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 2)
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(id, version: 2)
|
||||
.From(PhoneState.Idle)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing)
|
||||
@ -46,9 +40,11 @@ public class DependencyInjectionTests
|
||||
|
||||
var sp = sc.BuildServiceProvider();
|
||||
|
||||
var workflowFactory = sp.GetService<IWorkflowFactory<PhoneState, PhoneCommand, PhoneCall>>();
|
||||
var v1 = workflowFactory?.CreateWorkflow();
|
||||
var v2 = workflowFactory?.CreateWorkflow(version: 2);
|
||||
var workflowFactory = sp.GetRequiredService<IWorkflowFactory<PhoneState, PhoneCommand, PhoneCall>>();
|
||||
|
||||
|
||||
var v1 = workflowFactory.CreateWorkflow(id);
|
||||
var v2 = workflowFactory.CreateWorkflow(id, version: 2);
|
||||
|
||||
Assert.NotNull(workflowFactory);
|
||||
Assert.NotNull(v1);
|
||||
@ -62,4 +58,36 @@ public class DependencyInjectionTests
|
||||
Assert.Equal(2, v2.GetAllowedTransitions(PhoneState.Ringing).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IdGenerationTest()
|
||||
{
|
||||
var sc = new ServiceCollection();
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 1)
|
||||
.From(PhoneState.Idle)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing);
|
||||
|
||||
var sp = sc.BuildServiceProvider();
|
||||
var workflowFactory = sp.GetRequiredService<IWorkflowFactory<PhoneState, PhoneCommand, PhoneCall>>();
|
||||
var workflow = workflowFactory.CreateWorkflow();
|
||||
|
||||
Assert.Equal("PhoneState_PhoneCommand_PhoneCall", workflow.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownWorkflowReferenceThrows()
|
||||
{
|
||||
var sc = new ServiceCollection();
|
||||
|
||||
sc.AddWorkflowDefinition<PhoneState, PhoneCommand, PhoneCall>(version: 1)
|
||||
.From(PhoneState.Idle)
|
||||
.On(PhoneCommand.IncomingCall)
|
||||
.To(PhoneState.Ringing);
|
||||
|
||||
var sp = sc.BuildServiceProvider();
|
||||
var workflowFactory = sp.GetRequiredService<IWorkflowFactory<PhoneState, PhoneCommand, PhoneCall>>();
|
||||
Assert.Throws<KeyNotFoundException>(() => workflowFactory.CreateWorkflow("unknown"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
global using DIT.Workflower;
|
||||
global using DIT.Workflower.Abstractions;
|
||||
global using Xunit;
|
||||
|
||||
@ -2,53 +2,71 @@ namespace DIT.Workflower.Tests;
|
||||
|
||||
public class WorkflowConditionTests
|
||||
{
|
||||
private WorkflowDefinitionBuilder<PhoneState, PhoneCommand, PhoneCall> GetDefaultBuilder()
|
||||
private static ITransitionStart<PhoneState, PhoneCommand, PhoneCall> GetDefaultBuilder()
|
||||
{
|
||||
return new();
|
||||
return WorkflowDefinitionBuilder<PhoneState, PhoneCommand, PhoneCall>.Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleConditionTests()
|
||||
{
|
||||
var phone = new PhoneCall(Active: false);
|
||||
var builder1 = GetDefaultBuilder();
|
||||
var builder2 = GetDefaultBuilder();
|
||||
var meta = "String";
|
||||
|
||||
var a = "b";
|
||||
|
||||
builder1
|
||||
.From(PhoneState.Idle)
|
||||
.When((res) => a == "n");
|
||||
var builder1 = GetDefaultBuilder()
|
||||
.From(PhoneState.Ringing)
|
||||
.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()
|
||||
.From(PhoneState.Ringing)
|
||||
.On(PhoneCommand.Decline)
|
||||
.When((res) => a == "b" && res.Active is false)
|
||||
.WithMeta(meta)
|
||||
.To(PhoneState.OnHold);
|
||||
|
||||
Assert.Empty(builder1.Build().GetAllowedTransitions(phone, PhoneState.Idle));
|
||||
Assert.Single(builder2.Build().GetAllowedTransitions(phone, PhoneState.Idle));
|
||||
Assert.Empty(builder1.Build().GetAllowedTransitions(phone, PhoneState.Ringing));
|
||||
Assert.Single(builder2.Build().GetAllowedTransitions(phone, PhoneState.Ringing));
|
||||
|
||||
// Check meta
|
||||
Assert.Equal(meta, builder2.Build().GetAllowedTransitions(phone, PhoneState.Ringing).First().Meta);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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()
|
||||
.From(PhoneState.OnHold)
|
||||
.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()
|
||||
.From(PhoneState.OnHold)
|
||||
.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));
|
||||
Assert.Empty(builder1.Build().GetAllowedTransitions(phone, from: PhoneState.OnHold));
|
||||
Assert.Single(builder2.Build().GetAllowedTransitions(phone, from: PhoneState.OnHold));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyBuildThrowsError()
|
||||
{
|
||||
var builder1 = (WorkflowDefinitionBuilder<PhoneState, PhoneCommand, PhoneCall>)GetDefaultBuilder();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => builder1.Build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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