diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f12cbd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,206 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### + +root = true + +# All files +[*] +indent_style = space +# Code files +[*.{cs}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +############################### +# .NET Coding Conventions # +############################### +[*.{cs}] +# Organize usings +dotnet_sort_system_directives_first = true + +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = true:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:error + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion + +dotnet_style_explicit_tuple_names = true:error +dotnet_style_prefer_inferred_tuple_names = true:suggestion + +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent + +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:silent + +############################### +# Naming Conventions # +############################### +# Symbols +# Naming Symbols +dotnet_naming_symbols.public_symbols.applicable_kinds = property,method,field,event,delegate +dotnet_naming_symbols.public_symbols.applicable_accessibilities = public + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_accessibilities = private + +# Style Definitions +dotnet_naming_style.pascal.capitalization = pascal_case + +dotnet_naming_style.uppercase.capitalization = all_upper + +dotnet_naming_style.pascal_starts_with_underscore.capitalization = pascal_case +dotnet_naming_style.pascal_starts_with_underscore.required_prefix = _ + +dotnet_naming_style.camel_starts_with_underscore.capitalization = camel_case +dotnet_naming_style.camel_starts_with_underscore.required_prefix = _ + +# Public members must be in pascal casing (public_members_pascal_case) +dotnet_naming_rule.public_members_pascal_case.symbols = public_symbols +dotnet_naming_rule.public_members_pascal_case.style = pascal +dotnet_naming_rule.public_members_pascal_case.severity = error + +# Private constants must be in uppercase (private_constants_must_be_uppercase) +dotnet_naming_rule.private_constants_must_be_uppercase.symbols = private_constants +dotnet_naming_rule.private_constants_must_be_uppercase.style = uppercase +dotnet_naming_rule.private_constants_must_be_uppercase.severity = error + +# Private fields must be start with an underscore (private_fields_must_start_with_underscore) +dotnet_naming_rule.private_fields_must_start_with_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_must_start_with_underscore.style = camel_starts_with_underscore +dotnet_naming_rule.private_fields_must_start_with_underscore.severity = error + +############################### +# C# Coding Conventions # +############################### +[*.cs] + +# 'using' directive placement +csharp_using_directive_placement = outside_namespace + +# var preferences +csharp_style_var_for_built_in_types = true:none +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_elsewhere = true:none + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Expression-level preferences +csharp_prefer_braces = true:none +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +############################### +# Ignored diagnostics # +############################### +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = none + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = none + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = none + +# CA2234: Pass system uri objects instead of strings +dotnet_diagnostic.CA2234.severity = none + +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = none + +# CA2227: Collection properties should be read only +dotnet_diagnostic.CA2227.severity = none + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none + +# CA1707: Remove the underscores from member names +dotnet_diagnostic.CA1707.severity = none + +# CS1591: XML Comments +dotnet_diagnostic.CS1591.severity = none; diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bbfb89a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +--- +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + target-branch: dev + schedule: + interval: "daily" + reviewers: + - "ditkrg/devops" + + # Maintain dependencies for nuget + - package-ecosystem: "nuget" + directory: "/" + target-branch: dev + schedule: + interval: "daily" + reviewers: + - "ditkrg/digital-development-net" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e73e5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,349 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Tt]emp/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml +**/coverage +**/lcov.info + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ diff --git a/DIT.Workflower.sln b/DIT.Workflower.sln new file mode 100644 index 0000000..861a01c --- /dev/null +++ b/DIT.Workflower.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32516.85 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIT.Workflower", "src\DIT.Workflower\DIT.Workflower.csproj", "{4823C6DF-53BF-4764-BF88-12D90BF1486B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIT.Workflower.Tests", "tests\DIT.Workflower.Tests\DIT.Workflower.Tests.csproj", "{FAE6F48D-66BF-48E6-AA04-CE0D1A81390B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIT.Workflower.DependencyInjection", "src\DIT.Workflower.DependencyInjection\DIT.Workflower.DependencyInjection.csproj", "{FE7914CE-8104-4AA6-88B3-B2B31ED6757C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4823C6DF-53BF-4764-BF88-12D90BF1486B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4823C6DF-53BF-4764-BF88-12D90BF1486B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4823C6DF-53BF-4764-BF88-12D90BF1486B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4823C6DF-53BF-4764-BF88-12D90BF1486B}.Release|Any CPU.Build.0 = Release|Any CPU + {FAE6F48D-66BF-48E6-AA04-CE0D1A81390B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAE6F48D-66BF-48E6-AA04-CE0D1A81390B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAE6F48D-66BF-48E6-AA04-CE0D1A81390B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAE6F48D-66BF-48E6-AA04-CE0D1A81390B}.Release|Any CPU.Build.0 = Release|Any CPU + {FE7914CE-8104-4AA6-88B3-B2B31ED6757C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE7914CE-8104-4AA6-88B3-B2B31ED6757C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE7914CE-8104-4AA6-88B3-B2B31ED6757C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE7914CE-8104-4AA6-88B3-B2B31ED6757C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {97523804-E9A2-4291-89CA-FE3236E4D9EE} + EndGlobalSection +EndGlobal diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c7da092 --- /dev/null +++ b/NOTICE @@ -0,0 +1,7 @@ +========================================================================= +== Example NOTICE file for use with the Apache License, Version 2.0, == +== in this case for the Apache httpd-2.0 distribution. == +========================================================================= + +DIT.Workflower +Copyright 2022 Department of Information Technology - KRG diff --git a/src/DIT.Workflower.DependencyInjection/Abstractions/IWorkflow.cs b/src/DIT.Workflower.DependencyInjection/Abstractions/IWorkflow.cs new file mode 100644 index 0000000..48432b9 --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/Abstractions/IWorkflow.cs @@ -0,0 +1,23 @@ +namespace DIT.Workflower.DependencyInjection.Abstractions; + +public interface IWorkflow + where TState : struct + where TCommand : struct +{ + int Version { get; } + + /// + /// Gets a list of allowed transitions without any condition checks. + /// + /// The incoming state + /// A list of available transitions + public List> GetAllowedTransitions(TState from); + + /// + /// Gets a list of allowed transitions evaluated for the current context. + /// + /// The given context + /// The incoming state + /// A list of available transitions for the current context + public List> GetAllowedTransitions(TContext context, TState from); +} diff --git a/src/DIT.Workflower.DependencyInjection/Abstractions/IWorkflowFactory.cs b/src/DIT.Workflower.DependencyInjection/Abstractions/IWorkflowFactory.cs new file mode 100644 index 0000000..2124641 --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/Abstractions/IWorkflowFactory.cs @@ -0,0 +1,12 @@ +namespace DIT.Workflower.DependencyInjection.Abstractions; + +public interface IWorkflowFactory + where TState : struct + where TCommand : struct +{ + + public IWorkflow CreateWorkflow(); + + public IWorkflow CreateWorkflow(int version); + +} diff --git a/src/DIT.Workflower.DependencyInjection/DIT.Workflower.DependencyInjection.csproj b/src/DIT.Workflower.DependencyInjection/DIT.Workflower.DependencyInjection.csproj new file mode 100644 index 0000000..2600550 --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/DIT.Workflower.DependencyInjection.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/DIT.Workflower.DependencyInjection/DefaultWorkflowFactory.cs b/src/DIT.Workflower.DependencyInjection/DefaultWorkflowFactory.cs new file mode 100644 index 0000000..1001df2 --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/DefaultWorkflowFactory.cs @@ -0,0 +1,29 @@ + +namespace DIT.Workflower.DependencyInjection; + +public class DefaultWorkflowFactory : IWorkflowFactory + where TState : struct + where TCommand : struct +{ + + private readonly IServiceProvider _serviceProvider; + + public DefaultWorkflowFactory(IServiceProvider sp) + { + _serviceProvider = sp; + } + + public IWorkflow CreateWorkflow() + => CreateWorkflow(version: 1); + + public IWorkflow CreateWorkflow(int version) + { + var service = _serviceProvider.GetServices>() + .FirstOrDefault(x => x.Version == version); + + if (service is null) + throw new ArgumentOutOfRangeException(nameof(version), $"Version {version} of workflow does not exist"); + + return service; + } +} diff --git a/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs b/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..c316228 --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace DIT.Workflower.DependencyInjection.Extensions; + +public static class IServiceCollectionExtensions +{ + + public static WorkflowDefinitionBuilder AddWorkflowDefinition(this IServiceCollection services) + where TState : struct + where TCommand : struct + { + return AddWorkflowDefinition(services, version: 1); + } + + public static WorkflowDefinitionBuilder AddWorkflowDefinition(this IServiceCollection services, int version) + where TState : struct + where TCommand : struct + { + var builder = new WorkflowDefinitionBuilder(); + + services.TryAddSingleton, DefaultWorkflowFactory>(); + + services.AddSingleton, WorkflowDefinitionWrapper>(sp => + { + var workflow = builder.Build(); + var wrapper = new WorkflowDefinitionWrapper(builder, version); + return wrapper; + }); + + return builder; + } +} diff --git a/src/DIT.Workflower.DependencyInjection/Usings.cs b/src/DIT.Workflower.DependencyInjection/Usings.cs new file mode 100644 index 0000000..7198182 --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/Usings.cs @@ -0,0 +1,2 @@ +global using DIT.Workflower.DependencyInjection.Abstractions; +global using Microsoft.Extensions.DependencyInjection; diff --git a/src/DIT.Workflower.DependencyInjection/WorkflowDefinitionWrapper.cs b/src/DIT.Workflower.DependencyInjection/WorkflowDefinitionWrapper.cs new file mode 100644 index 0000000..31a917f --- /dev/null +++ b/src/DIT.Workflower.DependencyInjection/WorkflowDefinitionWrapper.cs @@ -0,0 +1,16 @@ +namespace DIT.Workflower.DependencyInjection; + +public record WorkflowDefinitionWrapper : WorkflowDefinition, IWorkflow + where TState : struct + where TCommand : struct +{ + + public int Version { get; } + + public WorkflowDefinitionWrapper(WorkflowDefinitionBuilder builder, int version) + : base(builder.Transitions) + { + Version = version; + } + +} diff --git a/src/DIT.Workflower/DIT.Workflower.csproj b/src/DIT.Workflower/DIT.Workflower.csproj new file mode 100644 index 0000000..15526bf --- /dev/null +++ b/src/DIT.Workflower/DIT.Workflower.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.1 + enable + enable + latest + + + diff --git a/src/DIT.Workflower/Transition.cs b/src/DIT.Workflower/Transition.cs new file mode 100644 index 0000000..2c33ca1 --- /dev/null +++ b/src/DIT.Workflower/Transition.cs @@ -0,0 +1,46 @@ +namespace DIT.Workflower; + +public record Transition + where TState : struct + where TCommand : struct +{ + public TState From { get; set; } + + public TState To { get; set; } + + public TCommand Command { get; set; } + + public object? Meta { get; set; } +} + +public record TransitionDefinition + where TState : struct + where TCommand : struct +{ + public TState From { get; set; } + + public TState To { get; set; } + + public TCommand Command { get; set; } + + public object? Meta { get; set; } + + public List>? Conditions { get; internal set; } + + public Transition ToTransition() + { + return (Transition)this; + } + + public static explicit operator Transition(TransitionDefinition definition) + { + return new() + { + From = definition.From, + To = definition.To, + Command = definition.Command, + Meta = definition.Meta, + }; + } + +} diff --git a/src/DIT.Workflower/WorkflowDefinition.cs b/src/DIT.Workflower/WorkflowDefinition.cs new file mode 100644 index 0000000..1b55ded --- /dev/null +++ b/src/DIT.Workflower/WorkflowDefinition.cs @@ -0,0 +1,36 @@ +namespace DIT.Workflower; + +public record WorkflowDefinition + where TState : struct + where TCommand : struct +{ + + private readonly List> _transitions; + + public WorkflowDefinition(List> transitions) + { + _transitions = transitions; + } + + /// + /// Lists all allowed transitions from current state for the given context. + /// + public List> GetAllowedTransitions(TState from) + { + var query = _transitions.Where(doc => doc.From.Equals(from)); + + return query.Select(x => x.ToTransition()).ToList(); + } + + /// + /// Lists all allowed transitions from current state for the given context. + /// + public List> GetAllowedTransitions(TContext context, TState from) + { + var query = _transitions.Where(doc => doc.From.Equals(from)); + + query = query.Where(doc => doc.Conditions is null || !doc.Conditions.Any(cond => !cond(context))); + + return query.Select(x => x.ToTransition()).ToList(); + } +} diff --git a/src/DIT.Workflower/WorkflowDefinitionBuilder.cs b/src/DIT.Workflower/WorkflowDefinitionBuilder.cs new file mode 100644 index 0000000..15ea22d --- /dev/null +++ b/src/DIT.Workflower/WorkflowDefinitionBuilder.cs @@ -0,0 +1,75 @@ +namespace DIT.Workflower; + +public class WorkflowDefinitionBuilder + where TState : struct + where TCommand : struct +{ + private TransitionDefinition? _current; + + public List> Transitions { get; } = new(); + + + public WorkflowDefinitionBuilder From(in TState state) + { + if (_current != null) + Transitions.Add(_current); + + _current = new() { From = state }; + + return this; + } + + public WorkflowDefinitionBuilder To(in TState state) + { + if (_current == null) + throw new InvalidOperationException($"From needs to be called first"); + + _current = _current with { To = state }; + + return this; + } + + public WorkflowDefinitionBuilder 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) + { + if (_current == null) + throw new InvalidOperationException($"From needs to be called first"); + + _current.Meta = meta; + + return this; + } + + public WorkflowDefinitionBuilder When(Func condition) + { + if (_current == null) + throw new InvalidOperationException($"From needs to be called first"); + + if (_current.Conditions is null) + _current.Conditions = new(); + + _current.Conditions.Add(condition); + + return this; + } + + public WorkflowDefinition Build() + { + if (_current != null) + Transitions.Add(_current); + + if (!Transitions.Any()) + throw new InvalidOperationException("No transitions are added"); + + return new(Transitions); + } +} diff --git a/tests/DIT.Workflower.Tests/DIT.Workflower.Tests.csproj b/tests/DIT.Workflower.Tests/DIT.Workflower.Tests.csproj new file mode 100644 index 0000000..74a7f33 --- /dev/null +++ b/tests/DIT.Workflower.Tests/DIT.Workflower.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs b/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs new file mode 100644 index 0000000..53288a9 --- /dev/null +++ b/tests/DIT.Workflower.Tests/DependencyInjection/DependencyInjectionTests.cs @@ -0,0 +1,65 @@ +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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace DIT.Workflower.Tests.DependencyInjection; + +public class DependencyInjectionTests +{ + + [Fact] + public void Test() + { + + var sc = new ServiceCollection(); + + sc.AddWorkflowDefinition(version: 1) + + .From(PhoneState.Idle) + .On(PhoneCommand.IncomingCall) + .To(PhoneState.Ringing) + + .From(PhoneState.Ringing) + .On(PhoneCommand.Decline) + .To(PhoneState.Connected) + ; + + sc.AddWorkflowDefinition(version: 2) + + .From(PhoneState.Idle) + .On(PhoneCommand.IncomingCall) + .To(PhoneState.Ringing) + + .From(PhoneState.Ringing) + .On(PhoneCommand.Accept) + .To(PhoneState.Connected) + + .From(PhoneState.Ringing) + .On(PhoneCommand.Decline) + .To(PhoneState.Declined) + ; + + var sp = sc.BuildServiceProvider(); + + var workflowFactory = sp.GetService>(); + var v1 = workflowFactory?.CreateWorkflow(); + var v2 = workflowFactory?.CreateWorkflow(version: 2); + + Assert.NotNull(workflowFactory); + Assert.NotNull(v1); + Assert.NotNull(v2); + + + Assert.Single(v1!.GetAllowedTransitions(PhoneState.Idle)); + Assert.Single(v2!.GetAllowedTransitions(PhoneState.Idle)); + + Assert.Single(v1.GetAllowedTransitions(PhoneState.Ringing)); + Assert.Equal(2, v2.GetAllowedTransitions(PhoneState.Ringing).Count); + } + +} diff --git a/tests/DIT.Workflower.Tests/Models.cs b/tests/DIT.Workflower.Tests/Models.cs new file mode 100644 index 0000000..1917ea1 --- /dev/null +++ b/tests/DIT.Workflower.Tests/Models.cs @@ -0,0 +1,22 @@ +namespace DIT.Workflower.Tests; + +public enum PhoneState +{ + Idle, + Ringing, + Connected, + Declined, + OnHold, +} + +public enum PhoneCommand +{ + IncomingCall, + Accept, + Decline, + Hold, + Resume, + Disconnect, +} + +public record PhoneCall(bool Active = true); diff --git a/tests/DIT.Workflower.Tests/Usings.cs b/tests/DIT.Workflower.Tests/Usings.cs new file mode 100644 index 0000000..b739c5a --- /dev/null +++ b/tests/DIT.Workflower.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using DIT.Workflower; +global using Xunit; diff --git a/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs b/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs new file mode 100644 index 0000000..7ae4f6b --- /dev/null +++ b/tests/DIT.Workflower.Tests/WorkflowConditionTests.cs @@ -0,0 +1,54 @@ +namespace DIT.Workflower.Tests; + +public class WorkflowConditionTests +{ + private WorkflowDefinitionBuilder GetDefaultBuilder() + { + return new(); + } + + [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"); + + builder2 + .From(PhoneState.Idle) + .When((res) => a == "b" && res.Active is false); + + Assert.Empty(builder1.Build().GetAllowedTransitions(phone, PhoneState.Idle)); + Assert.Single(builder2.Build().GetAllowedTransitions(phone, PhoneState.Idle)); + } + + [Fact] + public void MultiConditionTests() + { + var phone = new PhoneCall(); + var builder1 = GetDefaultBuilder(); + var builder2 = GetDefaultBuilder(); + + var a = "b"; + var other = a; + + builder1 + .From(PhoneState.Idle) + .When((res) => a == "c") + .When((res) => other == a); + + builder2 + .From(PhoneState.Idle) + .When((res) => a == "b") + .When((res) => other == a); + + 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 new file mode 100644 index 0000000..b859abf --- /dev/null +++ b/tests/DIT.Workflower.Tests/WorkflowMetaTests.cs @@ -0,0 +1,14 @@ +namespace DIT.Workflower.Tests; + +public class WorkflowMetaTests +{ + + [Fact] + public void WorkflowNeedsAtLeastOneTransition() + { + var builder = new WorkflowDefinitionBuilder(); + + Assert.Throws(builder.Build); + } + +}