From 1942bcfe8487336c4bab40aed6e6baf2aa884881 Mon Sep 17 00:00:00 2001 From: "Shkar T. Noori" Date: Sun, 22 May 2022 09:21:44 +0300 Subject: [PATCH] WIP --- README.md | 96 ++++++++++++++++++- .../IServiceCollectionExtensions.cs | 12 +-- .../Usings.cs | 5 +- src/DIT.Workflower/Abstractions.cs | 35 +++++++ src/DIT.Workflower/Transition.cs | 6 +- .../WorkflowDefinitionBuilder.cs | 56 ++++++----- .../DependencyInjectionTests.cs | 8 +- tests/DIT.Workflower.Tests/Usings.cs | 1 + .../WorkflowConditionTests.cs | 36 +++---- .../DIT.Workflower.Tests/WorkflowMetaTests.cs | 14 --- 10 files changed, 194 insertions(+), 75 deletions(-) create mode 100644 src/DIT.Workflower/Abstractions.cs delete mode 100644 tests/DIT.Workflower.Tests/WorkflowMetaTests.cs diff --git a/README.md b/README.md index aef586b..0cac1ca 100644 --- a/README.md +++ b/README.md @@ -1 +1,95 @@ -# DIT.Workflower \ No newline at end of file +# 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() + .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(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 _workflow; + + public SampleController(IWorkflowFactory factory) + { + _workflow = factory.CreateWorkflow(version: 1); // Optional version param to support versioning on workflows. + } + + [HttpGet("{state}")] + [ProducesResponseType(typeof(JsonApiSingleDataResponse), StatusCodes.Status200OK)] + public ActionResult GetRequest(PhoneState state, bool isActive) + { + var phoneCall = new PhoneCall(isActive) + var transitions = _workflow.GetAllowedTransitions(context: phoneCall, state); + return Ok(transitions) + } +} +``` diff --git a/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs b/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs index c316228..57cf280 100644 --- a/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs +++ b/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs @@ -5,25 +5,25 @@ namespace DIT.Workflower.DependencyInjection.Extensions; public static class IServiceCollectionExtensions { - public static WorkflowDefinitionBuilder AddWorkflowDefinition(this IServiceCollection services) + public static ITransitionOn AddWorkflowDefinition(this IServiceCollection services, TState initial) where TState : struct where TCommand : struct { - return AddWorkflowDefinition(services, version: 1); + return AddWorkflowDefinition(services, initial, version: 1); } - public static WorkflowDefinitionBuilder AddWorkflowDefinition(this IServiceCollection services, int version) + public static ITransitionOn AddWorkflowDefinition(this IServiceCollection services, TState initial, int version) where TState : struct where TCommand : struct { - var builder = new WorkflowDefinitionBuilder(); + var builder = WorkflowDefinitionBuilder.Initial(initial); services.TryAddSingleton, DefaultWorkflowFactory>(); services.AddSingleton, WorkflowDefinitionWrapper>(sp => { - var workflow = builder.Build(); - var wrapper = new WorkflowDefinitionWrapper(builder, version); + var definition = ((WorkflowDefinitionBuilder)builder); + var wrapper = new WorkflowDefinitionWrapper(definition, version); return wrapper; }); diff --git a/src/DIT.Workflower.DependencyInjection/Usings.cs b/src/DIT.Workflower.DependencyInjection/Usings.cs index 7198182..18ea7bf 100644 --- a/src/DIT.Workflower.DependencyInjection/Usings.cs +++ b/src/DIT.Workflower.DependencyInjection/Usings.cs @@ -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; diff --git a/src/DIT.Workflower/Abstractions.cs b/src/DIT.Workflower/Abstractions.cs new file mode 100644 index 0000000..179b142 --- /dev/null +++ b/src/DIT.Workflower/Abstractions.cs @@ -0,0 +1,35 @@ +namespace DIT.Workflower.Abstractions; + +public interface ITransitionOn + where TState : struct + where TCommand : struct +{ + ITransitionCondition On(in TCommand command); +} + +public interface ITransitionExit + where TState : struct + where TCommand : struct +{ + ITransitionDone To(in TState command); +} + +public interface ITransitionCondition : ITransitionExit + where TState : struct + where TCommand : struct +{ + ITransitionCondition WithMeta(in object meta); + + ITransitionCondition When(Func condition); + +} + +public interface ITransitionDone : ITransitionOn + where TState : struct + where TCommand : struct +{ + + ITransitionOn From(in TState state); + + WorkflowDefinition Build(); +} diff --git a/src/DIT.Workflower/Transition.cs b/src/DIT.Workflower/Transition.cs index 2c33ca1..811bfde 100644 --- a/src/DIT.Workflower/Transition.cs +++ b/src/DIT.Workflower/Transition.cs @@ -4,11 +4,11 @@ public record Transition 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; } } diff --git a/src/DIT.Workflower/WorkflowDefinitionBuilder.cs b/src/DIT.Workflower/WorkflowDefinitionBuilder.cs index 15ea22d..97eba4b 100644 --- a/src/DIT.Workflower/WorkflowDefinitionBuilder.cs +++ b/src/DIT.Workflower/WorkflowDefinitionBuilder.cs @@ -1,59 +1,64 @@ -namespace DIT.Workflower; +using DIT.Workflower.Abstractions; + +namespace DIT.Workflower; + +public sealed class WorkflowDefinitionBuilder : + ITransitionOn, + ITransitionExit, + ITransitionCondition, + ITransitionDone -public class WorkflowDefinitionBuilder where TState : struct where TCommand : struct { - private TransitionDefinition? _current; + private TransitionDefinition _current; public List> Transitions { get; } = new(); + #region Constructor - public WorkflowDefinitionBuilder From(in TState state) + private WorkflowDefinitionBuilder(TState initialState) + => _current = new() { From = initialState }; + + public static ITransitionOn Initial(TState initialState) + => new WorkflowDefinitionBuilder(initialState); + + #endregion + + #region Methods + + public ITransitionOn From(in TState state) { - if (_current != null) - Transitions.Add(_current); - _current = new() { From = state }; return this; } - public WorkflowDefinitionBuilder To(in TState state) + public ITransitionDone 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 On(in TCommand command) + public ITransitionCondition 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 WithMeta(in object meta) + public ITransitionCondition WithMeta(in object meta) { - if (_current == null) - throw new InvalidOperationException($"From needs to be called first"); - _current.Meta = meta; return this; } - public WorkflowDefinitionBuilder When(Func condition) + public ITransitionCondition When(Func condition) { - if (_current == null) - throw new InvalidOperationException($"From needs to be called first"); - if (_current.Conditions is null) _current.Conditions = new(); @@ -64,12 +69,13 @@ public class WorkflowDefinitionBuilder public WorkflowDefinition Build() { - if (_current != null) - Transitions.Add(_current); + Transitions.Add(_current); if (!Transitions.Any()) throw new InvalidOperationException("No transitions are added"); return new(Transitions); } + + #endregion } diff --git a/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs b/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs index 53288a9..f202f22 100644 --- a/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs +++ b/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs @@ -18,9 +18,7 @@ public class DependencyInjectionTests var sc = new ServiceCollection(); - sc.AddWorkflowDefinition(version: 1) - - .From(PhoneState.Idle) + sc.AddWorkflowDefinition(initial: PhoneState.Idle, version: 1) .On(PhoneCommand.IncomingCall) .To(PhoneState.Ringing) @@ -29,9 +27,7 @@ public class DependencyInjectionTests .To(PhoneState.Connected) ; - sc.AddWorkflowDefinition(version: 2) - - .From(PhoneState.Idle) + sc.AddWorkflowDefinition(initial: PhoneState.Idle, version: 2) .On(PhoneCommand.IncomingCall) .To(PhoneState.Ringing) diff --git a/tests/DIT.Workflower.Tests/Usings.cs b/tests/DIT.Workflower.Tests/Usings.cs index b739c5a..b60bfde 100644 --- a/tests/DIT.Workflower.Tests/Usings.cs +++ b/tests/DIT.Workflower.Tests/Usings.cs @@ -1,2 +1,3 @@ global using DIT.Workflower; +global using DIT.Workflower.Abstractions; global using Xunit; diff --git a/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs b/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs index 7ae4f6b..60a7ecf 100644 --- a/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs +++ b/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs @@ -2,27 +2,27 @@ namespace DIT.Workflower.Tests; public class WorkflowConditionTests { - private WorkflowDefinitionBuilder GetDefaultBuilder() + private ITransitionOn GetDefaultBuilder() { - return new(); + return WorkflowDefinitionBuilder.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)); diff --git a/tests/DIT.Workflower.Tests/WorkflowMetaTests.cs b/tests/DIT.Workflower.Tests/WorkflowMetaTests.cs deleted file mode 100644 index b859abf..0000000 --- a/tests/DIT.Workflower.Tests/WorkflowMetaTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DIT.Workflower.Tests; - -public class WorkflowMetaTests -{ - - [Fact] - public void WorkflowNeedsAtLeastOneTransition() - { - var builder = new WorkflowDefinitionBuilder(); - - Assert.Throws(builder.Build); - } - -}