diff --git a/.assets/aggregate.jpg b/.assets/aggregate.jpg new file mode 100644 index 0000000..99c1b26 Binary files /dev/null and b/.assets/aggregate.jpg differ diff --git a/.assets/aggregates.png b/.assets/aggregates.png new file mode 100644 index 0000000..932f6c7 Binary files /dev/null and b/.assets/aggregates.png differ diff --git a/.assets/decider-impl.png b/.assets/decider-impl.png new file mode 100644 index 0000000..adeb92b Binary files /dev/null and b/.assets/decider-impl.png differ diff --git a/.assets/decider-test.png b/.assets/decider-test.png new file mode 100644 index 0000000..6c4f564 Binary files /dev/null and b/.assets/decider-test.png differ diff --git a/.assets/decider.jpg b/.assets/decider.jpg new file mode 100644 index 0000000..e90cc74 Binary files /dev/null and b/.assets/decider.jpg differ diff --git a/.assets/decider.png b/.assets/decider.png new file mode 100644 index 0000000..3405f79 Binary files /dev/null and b/.assets/decider.png differ diff --git a/.assets/es-aggregate.jpg b/.assets/es-aggregate.jpg new file mode 100644 index 0000000..e6240ca Binary files /dev/null and b/.assets/es-aggregate.jpg differ diff --git a/.assets/es-aggregate.png b/.assets/es-aggregate.png new file mode 100644 index 0000000..6a749ca Binary files /dev/null and b/.assets/es-aggregate.png differ diff --git a/.assets/es-ss-system.png b/.assets/es-ss-system.png new file mode 100644 index 0000000..b9ec907 Binary files /dev/null and b/.assets/es-ss-system.png differ diff --git a/.assets/event-modeling.png b/.assets/event-modeling.png new file mode 100644 index 0000000..9dec436 Binary files /dev/null and b/.assets/event-modeling.png differ diff --git a/.assets/information-flow.jpg b/.assets/information-flow.jpg new file mode 100644 index 0000000..af090b3 Binary files /dev/null and b/.assets/information-flow.jpg differ diff --git a/.assets/kotlin-actors.png b/.assets/kotlin-actors.png new file mode 100644 index 0000000..d509dd8 Binary files /dev/null and b/.assets/kotlin-actors.png differ diff --git a/.assets/logo/gradient_128x128.png b/.assets/logo/gradient_128x128.png new file mode 100644 index 0000000..6b3b316 Binary files /dev/null and b/.assets/logo/gradient_128x128.png differ diff --git a/.assets/mviews.png b/.assets/mviews.png new file mode 100644 index 0000000..40822c3 Binary files /dev/null and b/.assets/mviews.png differ diff --git a/.assets/onion.png b/.assets/onion.png new file mode 100644 index 0000000..cbbdbe0 Binary files /dev/null and b/.assets/onion.png differ diff --git a/.assets/saga.jpg b/.assets/saga.jpg new file mode 100644 index 0000000..459d87d Binary files /dev/null and b/.assets/saga.jpg differ diff --git a/.assets/saga.png b/.assets/saga.png new file mode 100644 index 0000000..b5554bb Binary files /dev/null and b/.assets/saga.png differ diff --git a/.assets/ss-aggregate.jpg b/.assets/ss-aggregate.jpg new file mode 100644 index 0000000..6ed1ead Binary files /dev/null and b/.assets/ss-aggregate.jpg differ diff --git a/.assets/ss-aggregate.png b/.assets/ss-aggregate.png new file mode 100644 index 0000000..845aabd Binary files /dev/null and b/.assets/ss-aggregate.png differ diff --git a/.assets/view.jpg b/.assets/view.jpg new file mode 100644 index 0000000..c170488 Binary files /dev/null and b/.assets/view.jpg differ diff --git a/.assets/view.png b/.assets/view.png new file mode 100644 index 0000000..a35f717 Binary files /dev/null and b/.assets/view.png differ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1d20e66 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,76 @@ +root = true +; EditorConfig helps developers define and maintain consistent +; coding styles between different editors and IDEs. + +; For more visit http://editorconfig.org. + +; Choose between lf or rf on "end_of_line" property +[*.proto] +indent_style = tab +indent_size = tab +tab_width = 4 + +[*.{asax,ascx,aspx,cs,cshtml,css,htm,html,js,jsx,master,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style = space +indent_size = 4 +tab_width = 4 + +[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style = space +indent_size = 2 +tab_width = 2 + +[*] + +# Standard properties +end_of_line = native +insert_final_newline = false + +# Microsoft .NET properties +csharp_indent_braces = false +csharp_indent_switch_labels = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_prefer_braces = true +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_preserve_single_line_blocks = true +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_constructors = true:suggestion +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_var_elsewhere = true:hint +csharp_style_var_for_built_in_types = true:hint +csharp_style_var_when_type_is_apparent = true:hint +csharp_using_directive_placement = outside_namespace:silent +dotnet_style_predefined_type_for_locals_parameters_members = true:hint +dotnet_style_predefined_type_for_member_access = true:hint +dotnet_style_qualification_for_event = false:hint +dotnet_style_qualification_for_field = false:hint +dotnet_style_qualification_for_method = false:hint +dotnet_style_qualification_for_property = false:hint +dotnet_style_require_accessibility_modifiers = for_non_interface_members:hint diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ed3c6d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: + Bug report about: + Create a report to help us improve title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5fe926b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request about: Suggest an idea for this project title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d411f1e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup dotnet 8.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.302' + - name: Build and Test + run: ./Build.ps1 + shell: pwsh + - name: Push to MyGet + env: + NUGET_URL: https://api.nuget.org/v3/index.json + NUGET_API_KEY: ${{ secrets.NUGET_FRAKTALIO_CI_API_KEY }} + run: ./Push.ps1 + shell: pwsh + - name: Artifacts + uses: actions/upload-artifact@v2 + with: + name: artifacts + path: artifacts/**/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f94d134 --- /dev/null +++ b/.gitignore @@ -0,0 +1,263 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.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 + +# 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 + +# 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 +# TODO: 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 +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +#VerifyTests +*.received.* + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# 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 +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc +.mfractor/.txf.tmd.db +/.mfractor +TestResults diff --git a/Build.ps1 b/Build.ps1 new file mode 100644 index 0000000..d2af93b --- /dev/null +++ b/Build.ps1 @@ -0,0 +1,36 @@ +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} + +$artifacts = ".\artifacts" + +if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse } + +exec { & dotnet clean -c Release } + +exec { & dotnet build -c Release } + +exec { & dotnet test -c Release --no-build -l trx --verbosity=normal } + +exec { & dotnet pack .\src\Fraktalio.FModel\Fraktalio.FModel.csproj -c Release -o $artifacts --no-build } + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f37f16e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,102 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, +or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing +the community in public spaces. Examples of representing our community include using an official e-mail address, posting +via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible +for enforcement at info@fraktalio.com. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem +in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the +community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation +and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified +period of time. No public or private interaction with the people involved, including unsolicited interaction with those +enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate +behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired +by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..6fa1306 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,39 @@ + + + + Ivan Dugalić, Srđan Živojinović + Copyright © 2024 Fraktalio. All rights reserved. + Fraktalio + + net8.0 + enable + enable + true + + True + true + true + true + strict + latest + 9999 + + domain-modeling;event-sourcing;eventsourcing;cqrs + gradient_128x128.png + true + https://github.com/fraktalio/fmodel-csharp + git + README.md + v + true + snupkg + true + true + true + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..e872d44 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,21 @@ + + + true + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Fraktalio.FModel.sln b/Fraktalio.FModel.sln new file mode 100644 index 0000000..7726e7e --- /dev/null +++ b/Fraktalio.FModel.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{92A047C2-3835-42E8-A055-3F94DE935E8F}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + nuget.config = nuget.config + LICENSE = LICENSE + CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md + README.md = README.md + Build.ps1 = Build.ps1 + Push.ps1 = Push.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DF314C79-FDC8-4210-98D7-ED7AB13848D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio.FModel", "src\Fraktalio.FModel\Fraktalio.FModel.csproj", "{419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio.FModel.Contracts", "src\Fraktalio.FModel.Contracts\Fraktalio.FModel.Contracts.csproj", "{262EE234-6FFF-4C7D-9D29-FE133D3BBF46}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8B68360D-A2C7-416C-BFFA-D0C063E231FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio.FModel.Tests", "test\Fraktalio.FModel.Tests\Fraktalio.FModel.Tests.csproj", "{445D6510-F66E-4805-B9C8-B03E3A3C0571}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{57E3D8FE-526A-4BB0-BDB2-0E97CAEEC06A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{97650C4C-B1E0-4E17-AB33-C9D82056CE51}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Release|Any CPU.Build.0 = Release|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Release|Any CPU.Build.0 = Release|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D} = {DF314C79-FDC8-4210-98D7-ED7AB13848D2} + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46} = {DF314C79-FDC8-4210-98D7-ED7AB13848D2} + {445D6510-F66E-4805-B9C8-B03E3A3C0571} = {8B68360D-A2C7-416C-BFFA-D0C063E231FE} + {97650C4C-B1E0-4E17-AB33-C9D82056CE51} = {57E3D8FE-526A-4BB0-BDB2-0E97CAEEC06A} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE index 261eeb9..9557691 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,10 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright 2023 Fraktalio D.O.O. All rights reserved. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at - 1. Definitions. + http://www.apache.org/licenses/LICENSE-2.0 - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " +AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. diff --git a/Push.ps1 b/Push.ps1 new file mode 100644 index 0000000..9663563 --- /dev/null +++ b/Push.ps1 @@ -0,0 +1,14 @@ +$scriptName = $MyInvocation.MyCommand.Name +$artifacts = "./artifacts" + +if ([string]::IsNullOrEmpty($Env:NUGET_API_KEY)) { + Write-Host "${scriptName}: NUGET_API_KEY is empty or not set. Skipped pushing package(s)." +} else { + Get-ChildItem $artifacts -Filter "*.nupkg" | ForEach-Object { + Write-Host "$($scriptName): Pushing $($_.Name)" + dotnet nuget push $_ --source $Env:NUGET_URL --api-key $Env:NUGET_API_KEY + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } + } +} diff --git a/README.md b/README.md index c8aaa78..2fcfa4e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,700 @@ -# fmodel-c- -Domain modeling in C# - f(model) +# **f`(`model`)`** - Functional and Reactive Domain Modeling + +When you’re developing an information system to automate the activities of the business, you are modeling the business. +The abstractions that you design, the behaviors that you implement, and the UI interactions that you build all reflect +the business — together, they constitute the model of the domain. + +## `IOR` + +This project can be used as a multiplatform library, or as an inspiration, or both. **It provides just enough tactical +Domain-Driven Design patterns, optimised for Event Sourcing and CQRS.** + +- The `domain` model library is fully isolated from the application layer and API-related concerns. It represents a pure + declaration of the program logic. It is written in [Kotlin](https://kotlinlang.org/) programming language, without + additional + dependencies. [![Maven Central - domain](https://img.shields.io/maven-central/v/com.fraktalio.fmodel/domain.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.fraktalio.fmodel%22%20AND%20a:%22domain%22) +- The `application` libraries orchestrates the execution of the logic by loading state, executing `domain` components + and storing new state. It is written in [Kotlin](https://kotlinlang.org/) programming language. Two flavors ( + extensions of `Application` module) are available: + [![Maven Central - application](https://img.shields.io/maven-central/v/com.fraktalio.fmodel/application.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.fraktalio.fmodel%22%20AND%20a:%22application%22) + - `application-vanilla` is using plain/vanilla Kotlin to implement the application layer in order to load the state, + orchestrate the execution of the logic and save new state. + - `application-arrow` is using [Arrow](https://arrow-kt.io/) and Kotlin to implement the application layer in order + to load the state, orchestrate the execution of the logic and save new state - managing errors much better (using + Either). + +The libraries are non-intrusive, and you can select any flavor, or choose both (`vanila` and `arrow`). You can use +only `domain` library and model the orchestration (`application` library) on your own. Or, you can simply be inspired by +this project :) + +![event-modeling](.assets/event-modeling.png) + +## Table of Contents + +* [f(model) - Functional domain modeling](#fmodel---functional-domain-modeling) + * [Multiplatform](#multiplatform) + * [Abstraction and generalization](#abstraction-and-generalization) + * [decide: (C, S) -> Flow<E>](#decide-c-s---flowe) + * [evolve: (S, E) -> S](#evolve-s-e---s) + * [Event-sourced or State-stored systems](#event-sourced-or-state-stored-systems) + * [Decider](#decider) + * [Decider extensions and functions](#decider-extensions-and-functions) + * [Event-sourcing aggregate](#event-sourcing-aggregate) + * [State-stored aggregate](#state-stored-aggregate) + * [View](#view) + * [View extensions and functions](#view-extensions-and-functions) + * [Materialized View](#materialized-view) + * [Saga](#saga) + * [Saga extensions and functions](#saga-extensions-and-functions) + * [Saga Manager](#saga-manager) + * [Kotlin](#kotlin) + * [Examples](#start-using-the-libraries) + * [References and further reading](#references-and-further-reading) + +## Multiplatform + +Support for multiplatform programming is one of Kotlin’s key benefits. It reduces time spent writing and maintaining the +same code for different platforms while retaining the flexibility and benefits of native programming. + +## Abstraction and generalization + +Abstractions can hide irrelevant details and use names to reference objects. It emphasizes what an object is or does +rather than how it is represented or how it works. + +Generalization reduces complexity by replacing multiple entities which perform similar functions with a single +construct. + +Abstraction and generalization are often used together. Abstracts are generalized through parameterization to provide +more excellent utility. + +## `decide: (C, S) -> Flow` + +On a higher level of abstraction, any information system is responsible for handling the intent (`Command`) and based on +the current `State`, produce new facts (`Events`): + +- given the current `State/S` *on the input*, +- when `Command/C` is handled *on the input*, +- expect `flow` of new `Events/E` to be published/emitted *on the output* + +## `evolve: (S, E) -> S` + +The new state is always evolved out of the current state `S` and the current event `E`: + +- given the current `State/S` *on the input*, +- when `Event/E` is handled *on the input*, +- expect new `State/S` to be published *on the output* + +## Event-sourced or State-stored systems + +- State-stored systems are traditional systems that are only storing the current State by overwriting the previous State + in the storage. +- Event-sourced systems are storing the events in immutable storage by only appending. + +### A statement: + +Both types of systems can be designed by using only these two functions and three generic parameters: + +- `decide: (C, S) -> Flow` +- `evolve: (S, E) -> S` + +![event sourced vs state stored](.assets/es-ss-system.png) + +There is more to it! You can switch from one system type to another or have both flavors included within your systems +landscape. + +
+ A proof + +We can fold/recreate the new state out of the flow of events by using `evolve` function `(S, E) -> S` and providing the +initialState of type S as a starting point. + +- `Flow.fold(initialState: S, ((S, E) -> S)): S` + +Essentially, this `fold` is a function that is mapping a flow of Events to the State: + +- `(Flow) -> S` + +We can now use this function `(Flow) -> S` to: + +- contra-map our `decide` function (`(C, S) -> Flow`) over `S` type to: `(C, Flow) -> Flow` - **this is an + event-sourced system** +- or to map our `decide` function (`(C, S) -> Flow`) over `E` type to: `(C, S) -> S` - **this is a state-stored + system** + +
+ +Two functions are wrapped in a datatype class (algebraic data structure), which is generalized with three generic +parameters: + +```kotlin +data class Decider( + val decide: (C, S) -> Flow, + val evolve: (S, E) -> S, +) +``` + +`Decider` is the most important datatype, but it is not the only one. There are others: + +![onion architecture image](.assets/onion.png) + +## Decider + +`Decider` is a datatype that represents the main decision-making algorithm. It belongs to the Domain layer. It has three +generic parameters `C`, `S`, `E` , representing the type of the values that `Decider` may contain or use. +`Decider` can be specialized for any type `C` or `S` or `E` because these types do not affect its +behavior. `Decider` behaves the same for `C`=`Int` or `C`=`YourCustomType`, for example. + +`Decider` is a pure domain component. + +- `C` - Command +- `S` - State +- `E` - Event + +```kotlin +data class Decider( + override val decide: (C, S) -> Flow, + override val evolve: (S, E) -> S, + override val initialState: S +) : IDecider +``` + +Additionally, `initialState` of the Decider is introduced to gain more control over the initial state of the Decider. +Notice that `Decider` implements an interface `IDecider` to communicate the contract. + +
+ Example + +```kotlin +fun restaurantOrderDecider() = Decider( + // Initial state of the Restaurant Order is `null`. It does not exist. + initialState = null, + // Exhaustive command handler(s): for each type of [RestaurantCommand] you are going to publish specific events/facts, as required by the current state/s of the [RestaurantOrder]. + decide = { c, s -> + when (c) { + is CreateRestaurantOrderCommand -> + // ** positive flow ** + if (s == null) flowOf(RestaurantOrderCreatedEvent(c.identifier, c.lineItems, c.restaurantIdentifier)) + // ** negative flow ** + else flowOf(RestaurantOrderRejectedEvent(c.identifier, "Restaurant order already exists")) + is MarkRestaurantOrderAsPreparedCommand -> + // ** positive flow ** + if ((s != null && CREATED == s.status)) flowOf(RestaurantOrderPreparedEvent(c.identifier)) + // ** negative flow ** + else flowOf( + RestaurantOrderNotPreparedEvent( + c.identifier, + "Restaurant order does not exist or not in CREATED state" + ) + ) + null -> emptyFlow() // We ignore the `null` command by emitting the empty flow. Only the Decider that can handle `null` command can be combined (Monoid) with other Deciders. + } + }, + // Exhaustive event-sourcing handler(s): for each event of type [RestaurantEvent] you are going to evolve from the current state/s of the [RestaurantOrder] to a new state of the [RestaurantOrder] + evolve = { s, e -> + when (e) { + is RestaurantOrderCreatedEvent -> RestaurantOrder(e.identifier, e.restaurantId, CREATED, e.lineItems) + is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED) + is RestaurantOrderErrorEvent -> s // Error events are not changing the state / We return current state instead. + null -> s // Null events are not changing the state / We return current state instead. Only the Decider that can handle `null` event can be combined (Monoid) with other Deciders. + } + } +) +``` + +
+ +![decider image](.assets/decider.png) + +### Decider extensions and functions + +#### Contravariant + +- `Decider.mapLeftOnCommand(f: (Cn) -> C): Decider` + +#### Profunctor (Contravariant and Covariant) + +- `Decider.dimapOnEvent(fl: (En) -> E, fr: (E) -> En): Decider` +- `Decider.dimapOnState(fl: (Sn) -> S, fr: (S) -> Sn): Decider` + +#### *Commutative* Monoid + +- ` Decider.combine( + y: Decider + ): Decider, E_SUPER>` + +- with identity element `Decider` + +> A monoid is a type together with a binary operation (`combine`) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. +> +> `combine` operation is also commutative. This means that the order in which deciders are combined does not affect the +> result. + + +We can now construct event-sourcing or/and state-storing aggregate by using the same `decider`. + +### Event-sourcing aggregate + +[Event sourcing aggregate](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EventSourcingAggregate.kt) +is using/delegating a `Decider` to handle commands and produce events. It belongs to the Application layer. In order to +handle the command, aggregate needs to fetch the current state (represented as a list of events) +via `EventRepository.fetchEvents` function, and then delegate the command to the decider which can produce new events as +a result. Produced events are then stored via `EventRepository.save` suspending function. + +![event sourced aggregate](.assets/es-aggregate.png) + +`EventSourcingAggregate` extends `IDecider` and `EventRepository` interfaces, clearly communicating that it is composed +out of these two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`eventSourcingAggregate` function is a good example: + +```kotlin +fun eventSourcingAggregate( + decider: IDecider, + eventRepository: EventRepository +): EventSourcingAggregate = + object : + EventSourcingAggregate, + EventRepository by eventRepository, + IDecider by decider {} +``` + +
+ Example + +```kotlin +typealias RestaurantOrderAggregate = EventSourcingAggregate + +fun restaurantOrderAggregate( + restaurantOrderDecider: RestaurantOrderDecider, + eventRepository: EventRepository +): RestaurantOrderAggregate = eventSourcingAggregate( + decider = restaurantOrderDecider, + eventRepository = eventRepository, +) +``` + +
+ +### State-stored aggregate + +[State stored aggregate](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/StateStoredAggregate.kt) is +using/delegating a `Decider` to handle commands and produce new state. It belongs to the Application layer. In order to +handle the command, aggregate needs to fetch the current state via `StateRepository.fetchState` function first, and then +delegate the command to the decider which can produce new state as a result. New state is then stored +via `StateRepository.save` suspending function. + +![state storedaggregate](.assets/ss-aggregate.png) + +`StateStoredAggregate` extends `IDecider` and `StateRepository` interfaces, clearly communicating that it is composed +out of these two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`stateStoredAggregate` function is a good example: + +```kotlin +fun stateStoredAggregate( + decider: IDecider, + stateRepository: StateRepository +): StateStoredAggregate = + object : + StateStoredAggregate, + StateRepository by stateRepository, + IDecider by decider {} +``` + +
+ Example + +```kotlin +typealias RestaurantOrderAggregate = StateStoredAggregate + +fun restaurantOrderAggregate( + restaurantOrderDecider: RestaurantOrderDecider, + aggregateRepository: StateRepository +): RestaurantOrderAggregate = stateStoredAggregate( + decider = restaurantOrderDecider, + stateRepository = aggregateRepository +) +``` + +
+ +*The logic is orchestrated on the application layer. The components/functions are composed in different ways to support +variety of requirements.* + +![aggregates-application-layer](.assets/aggregates.png) + +Check, [application-vanilla](application-vanilla) and [application-arrow](application-arrow) modules/libraries for +scenarios that are offered out of the box. + +## View + +`View` is a datatype that represents the event handling algorithm, responsible for translating the events into +denormalized state, which is more adequate for querying. It belongs to the Domain layer. It is usually used to create +the view/query side of the CQRS pattern. Obviously, the command side of the CQRS is usually event-sourced aggregate. + +It has two generic parameters `S`, `E`, representing the type of the values that `View` may contain or use. +`View` can be specialized for any type of `S`, `E` because these types do not affect its behavior. +`View` behaves the same for `E`=`Int` or `E`=`YourCustomType`, for example. + +`View` is a pure domain component. + +- `S` - State +- `E` - Event + +```kotlin +data class View( + override val evolve: (S, E) -> S, + override val initialState: S +) : IView +``` + +Notice that `View` implements an interface `IView` to communicate the contract. + +
+ Example + +```kotlin +fun restaurantOrderView() = View( + // Initial state of the [RestaurantOrderViewState] is `null`. It does not exist. + initialState = null, + // Exhaustive event-sourcing handling part: for each event of type [RestaurantOrderEvent] you are going to evolve from the current state/s of the [RestaurantOrderViewState] to a new state of the [RestaurantOrderViewState]. + evolve = { s, e -> + when (e) { + is RestaurantOrderCreatedEvent -> RestaurantOrderViewState( + e.identifier, + e.restaurantId, + CREATED, + e.lineItems + ) + is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED) + is RestaurantOrderErrorEvent -> s // We ignore the `error` event by returning current State/s. + null -> s // We ignore the `null` event by returning current State/s. Only the View that can handle `null` event can be combined (Monoid) with other Views. + + } + } +) +``` + +
+ +![view image](.assets/view.png) + +### View extensions and functions + +#### Contravariant + +- `View.mapLeftOnEvent(f: (En) -> E): View` + +#### Profunctor (Contravariant and Covariant) + +- `View.dimapOnState(fl: (Sn) -> S, fr: (S) -> Sn): View` + +#### *Commutative* Monoid + +- ` View.combine(y: View): View, E_SUPER>` +- with identity element `View` + +> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. +> +> `combine` operation is also commutative. This means that the order in which views are combined does not affect the +> result. + + +We can now construct `materialized` view by using this `view`. + +### Materialized View + +A [Materialized view](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedView.kt) is +using/delegating a `View` to handle events of type `E` and to maintain a state of denormalized projection(s) as a +result. Essentially, it represents the query/view side of the CQRS pattern. It belongs to the Application layer. + +In order to handle the event, materialized view needs to fetch the current state via `ViewStateRepository.fetchState` +suspending function first, and then delegate the event to the view, which can produce new state as a result. New state +is then stored via `ViewStateRepository.save` suspending function. + +`MaterializedView` extends `IView` and `ViewStateRepository` interfaces, clearly communicating that it is composed out +of these two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`materializedView` function is a good example: + +```kotlin +fun materializedView( + view: IView, + viewStateRepository: ViewStateRepository, +): MaterializedView = + object : MaterializedView, ViewStateRepository by viewStateRepository, IView by view {} +``` + +
+ Example + +```kotlin +typealias RestaurantOrderMaterializedView = MaterializedView + +fun restaurantOrderMaterializedView( + restaurantOrderView: RestaurantOrderView, + viewStateRepository: ViewStateRepository +): RestaurantOrderMaterializedView = materializedView( + view = restaurantOrderView, + viewStateRepository = viewStateRepository +) +``` + +
+ +*The logic is orchestrated on the application layer. The components/functions are composed in different ways to support +variety of requirements.* + +![materialized-views-application-layer](.assets/mviews.png) + +Check, [application-vanilla](application-vanilla) and [application-arrow](application-arrow) modules/libraries for +scenarios that are offered out of the box. + +## Saga + +`Saga` is a datatype that represents the central point of control, deciding what to execute next (`A`). It is +responsible for mapping different events from many aggregates into action results `AR` that the `Saga` then can use to +calculate the next actions `A` to be mapped to commands of other aggregates. + +`Saga` is stateless, it does not maintain the state. + +It has two generic parameters `AR`, `A`, representing the type of the values that `Saga` may contain or use. +`Saga` can be specialized for any type of `AR`, `A` because these types do not affect its behavior. +`Saga` behaves the same for `AR`=`Int` or `AR`=`YourCustomType`, for example. + +`Saga` is a pure domain component. + +- `AR` - Action Result +- `A` - Action + +```kotlin +data class Saga( + val react: (AR) -> Flow +) : I_Saga +``` + +Notice that `Saga` implements an interface `ISaga` to communicate the contract. + +
+ Example + +```kotlin + +fun restaurantOrderSaga() = Saga( + react = { e -> + when (e) { + is RestaurantOrderPlacedAtRestaurantEvent -> flowOf( + CreateRestaurantOrderCommand( + e.restaurantOrderId, + e.identifier, + e.lineItems + ) + ) + is RestaurantCreatedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantMenuActivatedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantMenuChangedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantMenuPassivatedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantErrorEvent -> emptyFlow() // We choose to ignore this event, in our case. + null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas. + } + } +) + +fun restaurantSaga() = Saga( + react = { e -> + when (e) { + //TODO evolve the example ;), it does not do much at the moment. + is RestaurantOrderCreatedEvent -> emptyFlow() + is RestaurantOrderPreparedEvent -> emptyFlow() + is RestaurantOrderErrorEvent -> emptyFlow() + null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas. + } + } +) + + ``` + +
+ +![saga image](.assets/saga.png) + +### Saga extensions and functions + +#### Contravariant + +- `Saga.mapLeftOnActionResult(f: (ARn) -> AR): Saga` + +#### Covariant + +- `Saga.mapOnAction(f: (A) -> An): Saga` + +#### Monoid + +- ` Saga.combine(y: Saga): Saga` +- with identity element `Saga` + +> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. +> +> `combine` operation is also commutative. This means that the order in which sagas are combined does not affect the +> result. + + +We can now construct `Saga Manager` by using this `saga`. + +### Saga Manager + +[Saga manager](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManager.kt) is a stateless process +orchestrator. It is reacting on Action Results of type `AR` and produces new actions `A` based on them. + +Saga manager is using/delegating a `Saga` to react on Action Results of type `AR` and produce new actions `A` which are +going to be published via `ActionPublisher.publish` suspending function. + +It belongs to the Application layer. + +`SagaManager` extends `ISaga` and `ActionPublisher` interfaces, clearly communicating that it is composed out of these +two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`sagaManager` function is a good example: + +```kotlin +fun sagaManager( + saga: ISaga, + actionPublisher: ActionPublisher
+): SagaManager = + object : SagaManager, ActionPublisher by actionPublisher, ISaga by saga {} +``` + +
+ Example + +```kotlin + +typealias OrderRestaurantSagaManager = SagaManager + +fun sagaManager( + restaurantOrderSaga: RestaurantOrderSaga, + restaurantSaga: RestaurantSaga, + actionPublisher: ActionPublisher +): OrderRestaurantSagaManager = sagaManager( + // Combining individual choreography Sagas into one orchestrating Saga. + saga = restaurantOrderSaga.combine(restaurantSaga), + // How and where do you want to publish new commands. + actionPublisher = actionPublisher +) +``` + +
+ +### Experimental features + +#### Actors (only on [JVM](https://github.com/fraktalio/fmodel/tree/main/application-vanilla/src/jvmMain/kotlin/com/fraktalio/fmodel/application)) + +Coroutines can be executed parallelly. It presents all the usual parallelism problems. The main problem being +synchronization of access to shared mutable +state. [Actors](https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html#actors) to the rescue! + +![kotlin actors](.assets/kotlin-actors.png) + +[Dive into the implementation ...](https://github.com/fraktalio/fmodel/tree/main/application-vanilla/src/jvmMain/kotlin/com/fraktalio/fmodel/application) + +```kotlin +private fun CoroutineScope.commandActor( + fanInChannel: SendChannel, + capacity: Int = Channel.RENDEZVOUS, + start: CoroutineStart = CoroutineStart.DEFAULT, + context: CoroutineContext = EmptyCoroutineContext, + handle: (C) -> Flow +) = actor(context, capacity, start) { + for (msg in channel) { + handle(msg).collect { fanInChannel.send(it) } + } +} +``` + +> [Actors](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html) +> are marked as @ObsoleteCoroutinesApi by Kotlin at the moment. + +## Kotlin + +*"Kotlin has both object-oriented and functional constructs. You can use it in both OO and FP styles, or mix elements of +the two. With first-class support for features such as higher-order functions, function types and lambdas, Kotlin is a +great choice if you’re doing or exploring functional programming."* + +## Start using the libraries + +All `fmodel` components/libraries are released to [Maven Central](https://repo1.maven.org/maven2/com/fraktalio/fmodel/) + +### Maven coordinates + +``` + + com.fraktalio.fmodel + domain + 3.5.0 + + + + com.fraktalio.fmodel + application-vanilla + 3.5.0 + + + + com.fraktalio.fmodel + application-arrow + 3.5.0 + +``` + +### Examples + +![decider demo implementation](.assets/decider-impl.png) + +![decider demo test](.assets/decider-test.png) + +- Browse the [tests](domain/src/commonTest/kotlin/com/fraktalio/fmodel/domain/DeciderTest.kt) +- Learn by example on the [playground](https://fraktalio.com/blog/playground) +- Read the [blog](https://fraktalio.com/blog/) +- Check the demos + - [Spring, R2DBC, Event Sourcing, CQRS, Postgres](https://github.com/fraktalio/fmodel-spring-demo) + - [Spring, R2DBC, State-Stored, Postgres](https://github.com/fraktalio/fmodel-spring-state-stored-demo) + - [Ktor, R2DBC, Event Sourcing, CQRS, Postgres](https://github.com/fraktalio/fmodel-ktor-demo) + +### FModel in other languages + +- [FModel in TypeScript](https://github.com/fraktalio/fmodel-ts) +- [FModel in Rust](https://github.com/fraktalio/fmodel-rust) + +## References and further reading + +- https://www.youtube.com/watch?v=kgYGMVDHQHs +- https://www.manning.com/books/functional-and-reactive-domain-modeling +- https://www.manning.com/books/functional-programming-in-kotlin +- https://www.47deg.com/blog/functional-domain-modeling/ +- https://www.47deg.com/blog/functional-domain-modeling-part-2/ +- https://www.youtube.com/watch?v=I8LbkfSSR58&list=PLbgaMIhjbmEnaH_LTkxLI7FMa2HsnawM_ + +## Credits + +Special credits to `Jérémie Chassaing` for sharing his [research](https://www.youtube.com/watch?v=kgYGMVDHQHs) +and `Adam Dymitruk` for hosting the meetup. + +--- +Created with :heart: by [Fraktalio](https://fraktalio.com/) diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..8fbd4a0 --- /dev/null +++ b/nuget.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Fraktalio.FModel.Contracts/Fraktalio.FModel.Contracts.csproj b/src/Fraktalio.FModel.Contracts/Fraktalio.FModel.Contracts.csproj new file mode 100644 index 0000000..332ec44 --- /dev/null +++ b/src/Fraktalio.FModel.Contracts/Fraktalio.FModel.Contracts.csproj @@ -0,0 +1,17 @@ + + + + Fraktalio.FModel + Contracts package for ISaga, IView, IDecider + Fraktalio.FModel + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Fraktalio.FModel.Contracts/IDecider.cs b/src/Fraktalio.FModel.Contracts/IDecider.cs new file mode 100644 index 0000000..5c6733f --- /dev/null +++ b/src/Fraktalio.FModel.Contracts/IDecider.cs @@ -0,0 +1,14 @@ +namespace Fraktalio.FModel; + +/// +/// Decider Interface +/// +/// C Command +/// S State +/// E Event +public interface IDecider +{ + Func> Decide { get; } + Func Evolve { get; } + S InitialState { get; } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel.Contracts/ISaga.cs b/src/Fraktalio.FModel.Contracts/ISaga.cs new file mode 100644 index 0000000..6f22e4c --- /dev/null +++ b/src/Fraktalio.FModel.Contracts/ISaga.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; + +namespace Fraktalio.FModel; + +/// +/// An interface of the Saga +/// +/// Action Result type +/// Action Type +[PublicAPI] +public interface ISaga +{ + IEnumerable
React(AR actionResult); +} \ No newline at end of file diff --git a/src/Fraktalio.FModel.Contracts/IView.cs b/src/Fraktalio.FModel.Contracts/IView.cs new file mode 100644 index 0000000..aae2221 --- /dev/null +++ b/src/Fraktalio.FModel.Contracts/IView.cs @@ -0,0 +1,12 @@ +namespace Fraktalio.FModel; + +/// +/// View interface +/// +/// +/// +public interface IView +{ + Func Evolve { get; } + S InitialState { get; } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel.Contracts/packages.lock.json b/src/Fraktalio.FModel.Contracts/packages.lock.json new file mode 100644 index 0000000..f35d02e --- /dev/null +++ b/src/Fraktalio.FModel.Contracts/packages.lock.json @@ -0,0 +1,33 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "JetBrains.Annotations": { + "type": "Direct", + "requested": "[2023.3.0, )", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + } + } + } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/Decider.cs b/src/Fraktalio.FModel/Decider.cs new file mode 100644 index 0000000..b2ab66c --- /dev/null +++ b/src/Fraktalio.FModel/Decider.cs @@ -0,0 +1,63 @@ +namespace Fraktalio.FModel; + +/// +/// [Decider] is a datatype that represents the main decision-making algorithm. +/// +/// It has three generic parameters `C`, `S`, `E` , representing the type of the values that [Decider] may contain or use. +/// [Decider] can be specialized for any type `C` or `S` or `E` because these types does not affect its behavior. +/// [Decider] behaves the same for `C`=[Int] or `C`=`OddNumberCommand`. +/// +/// A function/lambda that takes command of type [C] and input state of type [S] as parameters, and returns/emits the list of output events [E] +/// A function/lambda that takes input state of type [S] and input event of type [E] as parameters, and returns the output/new state [S] +/// A starting point / An initial state of type [S] +/// Command +/// State +/// Event +public class Decider(Func> decide, Func evolve, S initialState) + : IDecider +{ + public Func> Decide { get; } = decide; + public Func Evolve { get; } = evolve; + public S InitialState { get; } = initialState; + + /// + /// Left map on C/Command parameter - Contravariant + /// + /// + /// + /// + public Decider MapLeftOnCommand(Func f) + { + var internalDecider = new InternalDecider(Decide, Evolve, InitialState); + var mappedInternalDecider = internalDecider.MapLeftOnCommand(f); + return mappedInternalDecider.AsDecider(); + } + + /// + /// Di-map on E/Event parameter + /// + /// + /// + /// + /// + public Decider DimapOnEvent(Func fl, Func fr) + { + var internalDecider = new InternalDecider(Decide, Evolve, InitialState); + var mappedInternalDecider = internalDecider.DimapOnEvent(fl, fr); + return mappedInternalDecider.AsDecider(); + } + + /// + /// Di-map on S/State parameter + /// + /// + /// + /// + /// + public Decider DimapOnState(Func fl, Func fr) + { + var internalDecider = new InternalDecider(Decide, Evolve, InitialState); + var mappedInternalDecider = internalDecider.DimapOnState(fl, fr); + return mappedInternalDecider.AsDecider(); + } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/DeciderExtensions.cs b/src/Fraktalio.FModel/DeciderExtensions.cs new file mode 100644 index 0000000..37514ad --- /dev/null +++ b/src/Fraktalio.FModel/DeciderExtensions.cs @@ -0,0 +1,37 @@ +namespace Fraktalio.FModel; + +public static class DeciderExtensions +{ + /// + /// Combine [Decider]s into one [Decider] + /// + /// Possible to use when: + /// - [E] and [E2] have common superclass [E_SUPER] + /// - [C] and [C2] have common superclass [C_SUPER] + /// + /// First decider + /// Second decider + /// Command type of the first Decider + /// State type of the first Decider + /// Event type of the first Decider + /// Command type of the second Decider + /// Input_State type of the second Decider + /// Event type of the second Decider + /// super type of the command types C and C2 + /// super type of the E and E2 types + /// Combined decider + public static Decider, E_SUPER?> Combine( + this Decider x, Decider y) + where C : class, C_SUPER + where C2 : class, C_SUPER + where E : class, E_SUPER + where E2 : class, E_SUPER + { + var internalDeciderX = new InternalDecider(x.Decide, x.Evolve, x.InitialState); + var internalDeciderY = new InternalDecider(y.Decide, y.Evolve, y.InitialState); + var combinedInternalDecider = + internalDeciderX.Combine( + internalDeciderY); + return combinedInternalDecider.AsDecider(); + } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/Fraktalio.FModel.csproj b/src/Fraktalio.FModel/Fraktalio.FModel.csproj new file mode 100644 index 0000000..93f20d5 --- /dev/null +++ b/src/Fraktalio.FModel/Fraktalio.FModel.csproj @@ -0,0 +1,29 @@ + + + + true + Fraktalio.FModel + Fraktalio.FModel + Functional Domain Modeling with C# + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Fraktalio.FModel/InternalDecider.cs b/src/Fraktalio.FModel/InternalDecider.cs new file mode 100644 index 0000000..afb075a --- /dev/null +++ b/src/Fraktalio.FModel/InternalDecider.cs @@ -0,0 +1,154 @@ +namespace Fraktalio.FModel; + +/// +/// [InternalDecider] is a datatype that represents the main decision-making algorithm. +/// It has five generic parameters [C], [Si], [So], [Ei], [Eo] , representing the type of the values that [InternalDecider] may contain or use. +/// [InternalDecider] can be specialized for any type [C] or [Si] or [So] or [Ei] or [Eo] because these types does not affect its behavior. +/// [InternalDecider] behaves the same for [C]=[Int] or [C]=YourCustomType, for example. +/// +/// [InternalDecider] is a pure domain component. +/// +/// C Command type +/// Si Input State type +/// Output State type +/// Input Event type +/// Output Event type +internal class InternalDecider +{ + /// + /// [InternalDecider] is a datatype that represents the main decision-making algorithm. + /// It has five generic parameters [C], [Si], [So], [Ei], [Eo] , representing the type of the values that [InternalDecider] may contain or use. + /// [InternalDecider] can be specialized for any type [C] or [Si] or [So] or [Ei] or [Eo] because these types does not affect its behavior. + /// [InternalDecider] behaves the same for [C]=[Int] or [C]=YourCustomType, for example. + /// + /// [InternalDecider] is a pure domain component. + /// + /// C Command type + /// Si Input State type + /// Output State type + /// Input Event type + /// Output Event type + internal InternalDecider(Func> decide, + Func evolve, + So initialState) + { + Decide = decide; + Evolve = evolve; + InitialState = initialState; + } + + /// + /// A function/lambda that takes command of type [C] and input state of type [Si] as parameters, and returns/emits the list of output events + /// + internal Func> Decide { get; } + + /// + /// A function/lambda that takes input state of type [Si] and input event of type [Ei] as parameters, and returns the output/new state [So] + /// + internal Func Evolve { get; } + + /// + /// A starting point / An initial state of type [So] + /// + internal So InitialState { get; } + + /// + /// Left map on C/Command parameter - Contravariant + /// + /// + /// Cn Command new + /// + internal InternalDecider MapLeftOnCommand(Func f) => + new( + (cn, si) => Decide(f(cn), si), + (si, ei) => Evolve(si, ei), + InitialState + ); + + /// + /// Dimap on E/Event parameter - Contravariant on input event and Covariant on output event = Profunctor + /// + /// + /// + /// Event input new + /// Event output new + /// + internal InternalDecider DimapOnEvent(Func fl, Func fr) => + new( + (c, si) => Decide(c, si).Select(fr), + (si, ein) => Evolve(si, fl(ein)), + InitialState + ); + + /// + /// Left map on E/Event parameter - Contravariant + /// + /// + /// + /// + internal InternalDecider MapLeftOnEvent(Func f) => + DimapOnEvent(f, eo => eo); + + /// + /// Right map on E/Event parameter - Covariant + /// + /// + /// + /// + internal InternalDecider MapOnEvent(Func f) => DimapOnEvent(ei => ei, f); + + /// + /// Dimap on S/State parameter - Contravariant on input state (Si) and Covariant on output state (So) = Profunctor + /// + /// + /// + /// State input new + /// State output new + /// + internal InternalDecider DimapOnState(Func fl, Func fr) => + new( + (c, sin) => Decide(c, fl(sin)), + (sin, ei) => fr(Evolve(fl(sin), ei)), + fr(InitialState) + ); + + /// + /// Left map on S/State parameter - Contravariant + /// + /// + /// Sin State input new + /// + internal InternalDecider MapLeftOnState(Func f) => + DimapOnState(f, so => so); + + /// + /// Right map on S/State parameter - Covariant + /// + /// + /// + /// + private InternalDecider MapOnState(Func f) => DimapOnState(si => si, f); + + /// + /// Apply on S/State - Applicative + /// + /// + /// + /// + private InternalDecider ApplyOnState(InternalDecider, Ei, Eo> ff) => + new( + (c, si) => ff.Decide(c, si).Concat(Decide(c, si)), + (si, ei) => ff.Evolve(si, ei)(Evolve(si, ei)), + ff.InitialState(InitialState) + ); + + /// + /// Product on S/State parameter - Applicative + /// + /// + /// + /// + internal InternalDecider, Ei, Eo> + ProductOnState(InternalDecider fb) => + ApplyOnState(fb.MapOnState(b => new Func>(a => new Tuple(a, b)))); +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/InternalDeciderExtensions.cs b/src/Fraktalio.FModel/InternalDeciderExtensions.cs new file mode 100644 index 0000000..b5c9f34 --- /dev/null +++ b/src/Fraktalio.FModel/InternalDeciderExtensions.cs @@ -0,0 +1,53 @@ +namespace Fraktalio.FModel; + +internal static class InternalDeciderExtensions +{ + /// + /// Combine [InternalDecider]s into one big [InternalDecider] + /// + /// Possible to use when: + /// - [Ei] and [Ei2] have common superclass [Ei_SUPER] + /// - [Eo] and [Eo2] have common superclass [Eo_SUPER] + /// - [C] and [C2] have common superclass [C_SUPER] + /// + /// First Decider + /// Second Decider + /// Command type of the first Decider + /// Input_State type of the first Decider + /// Output_State type of the first Decider + /// Input_Event type of the first Decider + /// Output_Event type of the first Decider + /// Command type of the second Decider + /// Input_State type of the second Decider + /// Output_State type of the second Decider + /// Input_Event type of the second Decider + /// Output_Event type of the second Decider + /// super type of the command types C and C2 + /// Super type of the Ei and Ei2 types + /// super type of the Eo and Eo2 types + /// + internal static InternalDecider, Tuple, Ei_SUPER, Eo_SUPER?> Combine( + this InternalDecider x, + InternalDecider y) + where C : class?, C_SUPER? + where C2 : class, C_SUPER + where Ei : class?, Ei_SUPER? + where Eo : Eo_SUPER + where Ei2 : class, Ei_SUPER + where Eo2 : Eo_SUPER + { + var deciderX = x.MapLeftOnCommand(c => c as C) + .MapLeftOnState>(pair => pair.Item1) + .DimapOnEvent(ei => ei as Ei, eo => eo); + + var deciderY = y.MapLeftOnCommand(c => c as C2) + .MapLeftOnState>(pair => pair.Item2) + .DimapOnEvent(ei => ei as Ei2, eo => eo); + + return deciderX.ProductOnState(deciderY); + } + + internal static Decider AsDecider(this InternalDecider internalDecider) => + new(internalDecider.Decide, internalDecider.Evolve, internalDecider.InitialState); +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/InternalView.cs b/src/Fraktalio.FModel/InternalView.cs new file mode 100644 index 0000000..ae263bd --- /dev/null +++ b/src/Fraktalio.FModel/InternalView.cs @@ -0,0 +1,82 @@ +namespace Fraktalio.FModel; + +/// +/// [InternalView] is a datatype that represents the event handling algorithm, +/// responsible for translating the events into denormalized state, +/// which is more adequate for querying. +/// +/// It has three generic parameters [Si], [So], [E], representing the type of the values that [InternalView] may contain or use. +/// [InternalView] can be specialized for any type of [Si], [So], [E] because these types does not affect its behavior. +/// [InternalView] behaves the same for [E]=[Int] or [E]=YourCustomType, for example. +/// +/// [InternalView] is a pure domain component +/// +/// A pure function/lambda that takes input state of type [Si] and input event of type [E] as parameters, and returns the output/new state [So] +/// A starting point / An initial state of type [So] +/// Input State type +/// Output State type +/// Event type +internal class InternalView(Func evolve, So initialState) +{ + internal Func Evolve { get; } = evolve; + internal So InitialState { get; } = initialState; + + /// + /// Left map on E/Event parameter - Contravariant + /// + /// Map function + /// En Event new + /// Mapped view + internal InternalView MapLeftOnEvent(Func f) => + new( + (si, en) => Evolve(si, f(en)), + InitialState + ); + + /// + /// Dimap on S/State parameter - Contravariant on the Si (input State) - Covariant on the So (output State) = Profunctor + /// + /// + /// + /// Sin State input new + /// Son State output new + /// + public InternalView DimapOnState(Func fl, Func fr) => + new( + (sin, e) => fr(Evolve(fl(sin), e)), + fr(InitialState) + ); + + /// + /// Left map on S/State parameter - Contravariant + /// + /// + /// Sin State input new + /// + public InternalView MapLeftOnState(Func f) => DimapOnState(f, so => so); + + /// + /// Right map on S/State parameter - Covariant + /// + /// + /// + /// + public InternalView MapOnState(Func f) => DimapOnState(si => si, f); + + /// + /// Apply on S/State parameter - Applicative + /// + /// + /// Son State output new type + /// + public InternalView ApplyOnState(InternalView, E> ff) => + new( + (si, e) => ff.Evolve(si, e)(Evolve(si, e)), + ff.InitialState(InitialState) + ); + + internal InternalView, E> ProductOnState(InternalView fb) => + ApplyOnState(fb.MapOnState(b => new Func>(a => new Tuple(a, b)))); + + +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/InternalViewExtensions.cs b/src/Fraktalio.FModel/InternalViewExtensions.cs new file mode 100644 index 0000000..6f99616 --- /dev/null +++ b/src/Fraktalio.FModel/InternalViewExtensions.cs @@ -0,0 +1,30 @@ +namespace Fraktalio.FModel; + +internal static class InternalViewExtensions +{ + /// + /// Combines [InternalView]s into one bigger [InternalView] + /// + /// first view + /// second view + /// Si State input of the first View + /// So State output of the first View + /// E Event of the first View + /// Si2 State input of the second View + /// So2 State output of the second View + /// E2 Event of the second View + /// E_SUPER super type for [E] and [E2] + /// new View of type [InternalView]<[Pair]<[Si], [Si2]>, [Pair]<[So], [So2]>, [E_SUPER]> + internal static InternalView, Tuple, E_SUPER> Combine( + this InternalView x, InternalView y) + where E : class, E_SUPER + where E2 : class, E_SUPER + { + var viewX = x.MapLeftOnEvent(e => e as E).MapLeftOnState>(pair => pair.Item1); + var viewY = y.MapLeftOnEvent(e => e as E2).MapLeftOnState>(pair => pair.Item2); + + return viewX.ProductOnState(viewY); + } + + internal static View AsView(this InternalView view) => new(view.Evolve, view.InitialState); +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/Saga.cs b/src/Fraktalio.FModel/Saga.cs new file mode 100644 index 0000000..2c546bc --- /dev/null +++ b/src/Fraktalio.FModel/Saga.cs @@ -0,0 +1,19 @@ +namespace Fraktalio.FModel; + +/// +/// Saga is a datatype that represents the central point of control deciding what to execute next ([A]) +/// It is responsible for mapping different events into action results ([AR]) that the [Saga] then can use to calculate the next actions ([A]) to be mapped to command(s). +/// +/// Saga does not maintain the state. +/// +/// A function/lambda that takes input state of type [AR], and returns the flow of actions. +/// Action Result type +/// Action type +public class Saga(Func> react) : ISaga +{ + public IEnumerable React(AR actionResult) => react(actionResult); + + public Saga MapLeftOnActionResult(Func f) => new(arn => react(f(arn))); + + public Saga MapOnAction(Func f) => new(ar => react(ar).Select(f)); +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/SagaExtensions.cs b/src/Fraktalio.FModel/SagaExtensions.cs new file mode 100644 index 0000000..4be2775 --- /dev/null +++ b/src/Fraktalio.FModel/SagaExtensions.cs @@ -0,0 +1,45 @@ +namespace Fraktalio.FModel; + +public static class SagaExtensions +{ + /// + /// Saga DSL - A convenient builder DSL for the see cref="Saga{AR,A}"/> + /// + /// + /// + /// + /// + public static Saga ToSaga(this Func> react) => new(react); + + /// + /// Combines [Saga]s into one [Saga] + /// + /// Specially convenient when: + /// - [AR] and [AR2] have common superclass [AR_SUPER], or + /// - [A] and [A2] have common superclass [A_SUPER] + /// + /// first saga + /// second saga + /// Action Result (usually event) of the first Saga + /// Action (usually command) of the first Saga + /// Action Result (usually event) of the second Saga + /// Action (usually command) of the second Saga + /// common superclass for [AR] and [AR2] + /// common superclass for [A] and [A2] + /// new Saga of type Saga`[AR_SUPER], [A_SUPER]>` + public static Saga Combine(this Saga sagaX, + Saga sagaY) + where AR : AR_SUPER + where A : A_SUPER + where AR2 : AR_SUPER + where A2 : A_SUPER + { + var newSagaX = sagaX.MapLeftOnActionResult(it => it is AR ar ? ar : default) + .MapOnAction(it => it); + + var newSagaY = sagaY.MapLeftOnActionResult(it => it is AR2 ar2 ? ar2 : default) + .MapOnAction(it => it); + + return new Saga(eitherAr => newSagaX.React(eitherAr).Concat(newSagaY.React(eitherAr))); + } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/View.cs b/src/Fraktalio.FModel/View.cs new file mode 100644 index 0000000..463d4e0 --- /dev/null +++ b/src/Fraktalio.FModel/View.cs @@ -0,0 +1,65 @@ +namespace Fraktalio.FModel; + +/// +/// [View] is a datatype that represents the event handling algorithm, +/// responsible for translating the events into denormalized state, +/// which is more adequate for querying. +/// +/// It has two generic parameters `S`, `E`, representing the type of the values that [View] may contain or use. +/// [View] can be specialized for any type of `S`, `E` because these types does not affect its behavior. +/// [View] behaves the same for `E`=[Int] or `E`=`YourCustomType`. +/// +/// evolve A pure function/lambda that takes input state of type [S] and input event of type [E] as parameters, and returns the output/new state [S] +/// initialState A starting point / An initial state of type [S] +/// State type +/// Event type +public class View(Func evolve, S initialState) : IView +{ + public Func Evolve { get; } = evolve; + public S InitialState { get; } = initialState; + + /// + /// Left map on E/Event + /// + /// Function that maps type `En` to `E` + /// En Event new + /// New View of type [View]<[S], [En]> + public View MapLeftOnEvent(Func f) => + new InternalView(Evolve, InitialState).MapLeftOnEvent(f).AsView(); + + /// + /// Di-map on S/State + /// + /// Function that maps type `Sn` to `S` + /// Function that maps type `S` to `Sn` + /// Sn State new + /// New View of type [View]<[Sn], [E]> + public View DimapOnState(Func fl, Func fr) => + new InternalView(Evolve, InitialState).DimapOnState(fl, fr).AsView(); +} + +public static class ViewExtensions +{ + /// + /// Combines [View]s into one [View] + /// + /// Possible to use when [E] and [E2] have common superclass [E_SUPER] + /// + /// + /// + /// State of the first View + /// Event of the first View + /// State of the second View + /// Event of the second View + /// Super type for [E] and [E2] + /// + public static View, E_SUPER> Combine(this View x, View y) + where E : class, E_SUPER + where E2 : class, E_SUPER + { + var internalViewX = new InternalView(x.Evolve, x.InitialState); + var internalViewY = new InternalView(y.Evolve, y.InitialState); + var combined = internalViewX.Combine(internalViewY); + return combined.AsView(); + } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/ViewBuilder.cs b/src/Fraktalio.FModel/ViewBuilder.cs new file mode 100644 index 0000000..9fc53ab --- /dev/null +++ b/src/Fraktalio.FModel/ViewBuilder.cs @@ -0,0 +1,13 @@ +namespace Fraktalio.FModel; + +internal class ViewBuilder +{ + private Func Evolve { get; set; } = (s, _) => s; + private Func InitialState { get; set; } = () => throw new Exception("Initial State is not initialized"); + + public void SetEvolve(Func value) => Evolve = value; + + public void SetInitialState(Func value) => InitialState = value; + + public View Build() => new(Evolve, InitialState()); +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/ViewFactory.cs b/src/Fraktalio.FModel/ViewFactory.cs new file mode 100644 index 0000000..08cb262 --- /dev/null +++ b/src/Fraktalio.FModel/ViewFactory.cs @@ -0,0 +1,11 @@ +namespace Fraktalio.FModel; + +internal static class ViewFactory +{ + public static View CreateView(Action> buildAction) + { + var builder = new ViewBuilder(); + buildAction(builder); + return builder.Build(); + } +} \ No newline at end of file diff --git a/src/Fraktalio.FModel/packages.lock.json b/src/Fraktalio.FModel/packages.lock.json new file mode 100644 index 0000000..a7af438 --- /dev/null +++ b/src/Fraktalio.FModel/packages.lock.json @@ -0,0 +1,45 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "MinVer": { + "type": "Direct", + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "ybkgpQMtt0Fo91l5rYtE3TZtD+Nmy5Ko091xvfXXOosQdMi30XO2EZ2+ShZt89gdu7RMmJqZaJ+e1q6d+6+KNw==" + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "fraktalio.fmodel.contracts": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2023.3.0, )" + } + }, + "JetBrains.Annotations": { + "type": "CentralTransitive", + "requested": "[2023.3.0, )", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + } + } + } +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/CombinedDeciderTest.cs b/test/Fraktalio.FModel.Tests/CombinedDeciderTest.cs new file mode 100644 index 0000000..2eb6a2f --- /dev/null +++ b/test/Fraktalio.FModel.Tests/CombinedDeciderTest.cs @@ -0,0 +1,56 @@ +using Fraktalio.FModel.Tests.Examples.Numbers; +using Fraktalio.FModel.Tests.Examples.Numbers.Even; +using Fraktalio.FModel.Tests.Examples.Numbers.Odd; +using Fraktalio.FModel.Tests.Extensions; +using EvenNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.EvenNumberCommand; +using OddNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.OddNumberCommand; + + +namespace Fraktalio.FModel.Tests; + +[Category("unit")] +public class CombinedDeciderTest +{ + private readonly EvenNumberDecider _evenDecider = new(); + private readonly OddNumberDecider _oddDecider = new(); + private Decider, NumberEvent?> _combinedDecider = null!; + + [SetUp] + public void Setup() => + _combinedDecider = _evenDecider + .Combine( + _oddDecider); + + [Test] + public void GivenEmptyEvents_AddEvenNumber() => + _combinedDecider + .GivenEvents([], + () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenEvents([new EvenNumberAdded(Description.Create("2"), Number.Create(2))]); + + [Test] + public void GivenEmptyState_AddEvenNumber() => + _combinedDecider + .GivenState(null, + () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenState(Tuple.Create(new EvenNumberState(Description.Create("Initial state + 2"), Number.Create(2)), + _oddDecider.InitialState)); + + [Test] + public void GivenEvents_AddEvenNumber() => + _combinedDecider + .GivenEvents(new[] { new EvenNumberAdded(Description.Create("2"), Number.Create(2)) }, + () => new EvenNumberCommand.AddEvenNumber(Description.Create("4"), Number.Create(4))) + .ThenEvents([new EvenNumberAdded(Description.Create("4"), Number.Create(6))]); + + [Test] + public void GivenState_AddEvenNumber() => + _combinedDecider + .GivenState( + Tuple.Create(new EvenNumberState(Description.Create("2"), Number.Create(2)), + _oddDecider.InitialState), + () => new EvenNumberCommand.AddEvenNumber(Description.Create("4"), Number.Create(4))) + .ThenState(Tuple.Create(new EvenNumberState(Description.Create("2 + 4"), Number.Create(6)), + _oddDecider.InitialState)); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/EventSourcedDeciderTest.cs b/test/Fraktalio.FModel.Tests/EventSourcedDeciderTest.cs new file mode 100644 index 0000000..10f9122 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/EventSourcedDeciderTest.cs @@ -0,0 +1,99 @@ +using Fraktalio.FModel.Tests.Examples.Numbers; +using Fraktalio.FModel.Tests.Examples.Numbers.Even; +using Fraktalio.FModel.Tests.Examples.Numbers.Odd; +using Fraktalio.FModel.Tests.Extensions; +using EvenNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.EvenNumberCommand; +using OddNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.OddNumberCommand; + +namespace Fraktalio.FModel.Tests; + +[Category("unit")] +public class EventSourcedDeciderTest +{ + private readonly EvenNumberDecider _evenDecider = new(); + private readonly OddNumberDecider _oddDecider = new(); + + [Test] + public void GivenEmptyEvents_AddEvenNumber() => + _evenDecider + .GivenEvents([], + () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenEvents([new EvenNumberAdded(Description.Create("2"), Number.Create(2))]); + + [Test] + public void GivenEvents_AddEvenNumber() => + _evenDecider + .GivenEvents(new[] { new EvenNumberAdded(Description.Create("2"), Number.Create(2)) }, + () => new EvenNumberCommand.AddEvenNumber(Description.Create("4"), Number.Create(4))) + .ThenEvents([new EvenNumberAdded(Description.Create("4"), Number.Create(6))]); + + [Test] + public void GivenEvents_SubtractEvenNumber() => + _evenDecider + .GivenEvents(new EvenNumberAdded[] { new(Description.Create("8"), Number.Create(8)) }, + () => new EvenNumberCommand.SubtractEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenEvents([new EvenNumberSubtracted(Description.Create("2"), Number.Create(6))]); + + [Test] + public void GivenEvents_AddOddNumber() => + _oddDecider + .GivenEvents(new OddNumberAdded[] { new(Description.Create("3"), Number.Create(3)) }, + () => new OddNumberCommand.AddOddNumber(Description.Create("1"), Number.Create(1))) + .ThenEvents([new OddNumberAdded(Description.Create("1"), Number.Create(4))]); + + [Test] + public void GivenEvents_SubtractOddNumber() => + _oddDecider + .GivenEvents(new OddNumberAdded[] { new(Description.Create("3"), Number.Create(3)) }, + () => new OddNumberCommand.SubtractOddNumber(Description.Create("1"), Number.Create(1))) + .ThenEvents([new OddNumberAdded(Description.Create("1"), Number.Create(2))]); + + [Test] + public void GivenEvents_LeftMapOverCommand_AddEvenNumber() => + _evenDecider.MapLeftOnCommand(cn => + new EvenNumberCommand.AddEvenNumber(Description.Create(cn.ToString()), Number.Create(cn))) + .GivenEvents([], + () => 2) + .ThenEvents([new EvenNumberAdded(Description.Create("2"), Number.Create(2))]); + + [Test] + public void GivenState_LeftMapOverCommand_AddEvenNumber() => + _evenDecider.MapLeftOnCommand(cn => + new EvenNumberCommand.AddEvenNumber(Description.Create(cn.ToString()), Number.Create(cn))) + .GivenState(null, + () => 2) + .ThenState(new EvenNumberState(Description.Create("Initial state + 2"), Number.Create(2))); + + [Test] + //TODO ID: check if this is correct + public void GivenEmptyEvents_DimapOverEventParameter_AddEvenNumber() => + _evenDecider.DimapOnEvent( + fl => fl != null ? fl with { Value = fl.Value} : null, + fr => fr != null + ? new EvenNumberAdded(Description.Create(fr.Value.Value.ToString()), Number.Create(fr.Value)) + : null) + .GivenEvents([], () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenEvents([new EvenNumberAdded(Description.Create("2"), Number.Create(2))]); + + [Test] + //TODO ID: check if this is correct + public void GivenEmptyEvents_DimapOverStateParameter_AddEvenNumber() => + _evenDecider.DimapOnState( + fl => fl with { Value = fl.Value}, + fr => + new EvenNumberState(Description.Create(fr.Value.Value.ToString()), Number.Create(fr.Value)) + ) + .GivenEvents([], () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenEvents([new EvenNumberAdded(Description.Create("2"), Number.Create(2))]); + + [Test] + //TODO ID: check if this is correct + public void GivenEmptyState_DimapOverStateParameter_AddEvenNumber() => + _evenDecider.DimapOnState( + fl => fl with { Value = fl.Value}, + fr => + new EvenNumberState(Description.Create(fr.Value.Value.ToString()), Number.Create(fr.Value)) + ) + .GivenState(null, () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenState(new EvenNumberState(Description.Create("2"), Number.Create(2))); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/NumberSagaFactory.cs b/test/Fraktalio.FModel.Tests/Examples/NumberSagaFactory.cs new file mode 100644 index 0000000..febe0b2 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/NumberSagaFactory.cs @@ -0,0 +1,71 @@ +using Fraktalio.FModel.Tests.Examples.Numbers; + +namespace Fraktalio.FModel.Tests.Examples; + +using OddNumberCommand = NumberCommand.OddNumberCommand; +using EvenNumberCommand = NumberCommand.EvenNumberCommand; + +public static class NumberSagaFactory +{ + public static Saga EvenNumberSaga() => EvenNumberSagaReact().ToSaga(); + + /// + /// Even number saga + /// + /// It reacts on Action Results of type of any [NumberEvent.EvenNumberEvent] and issue a Command/Action of type [NumberCommand.OddNumberCommand] + /// + /// The event + /// List of commands + private static Func> EvenNumberSagaReact() => + numberEvent => numberEvent switch + { + EvenNumberAdded evenNumberAdded => new OddNumberCommand[] + { + new OddNumberCommand.AddOddNumber( + new Description($"{evenNumberAdded.Value.Value - 1}"), + new Number(evenNumberAdded.Value.Value - 1) + ) + }, + + EvenNumberSubtracted evenNumberSubtracted => + [ + new OddNumberCommand.SubtractOddNumber( + new Description($"{evenNumberSubtracted.Value.Value - 1}"), + new Number(evenNumberSubtracted.Value.Value - 1) + ) + ], + + _ => [] + }; + + public static Saga OddNumberSaga() => OddNumberSagaReact().ToSaga(); + + /// + /// Odd number saga + /// + /// It reacts on Action Results of type of any [NumberEvent.OddNumberEvent] and issue a Command/Action of type [NumberCommand.EvenNumberCommand] + /// + /// The event + /// List of commands + private static Func> OddNumberSagaReact() => + numberEvent => numberEvent switch + { + OddNumberAdded oddNumberAdded => new EvenNumberCommand[] + { + new EvenNumberCommand.AddEvenNumber( + new Description($"{oddNumberAdded.Value.Value + 1}"), + new Number(oddNumberAdded.Value.Value + 1) + ) + }, + + OddNumberSubtracted oddNumberSubtracted => + [ + new EvenNumberCommand.SubtractEvenNumber( + new Description($"{oddNumberSubtracted.Value.Value - 1}"), + new Number(oddNumberSubtracted.Value.Value - 1) + ) + ], + + _ => [] + }; +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/Description.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/Description.cs new file mode 100644 index 0000000..b1b4070 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/Description.cs @@ -0,0 +1,12 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers; + +public record Description(string Value) +{ + public static Description operator +(Description a, Description b) => new($"{a.Value} + {b.Value}"); + + public static Description operator -(Description a, Description b) => new($"{a.Value} - {b.Value}"); + + public static Description Create(string value) => new(value); + + public static implicit operator string(Description value) => value.Value; +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/Even/EvenNumberDecider.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/Even/EvenNumberDecider.cs new file mode 100644 index 0000000..494721a --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/Even/EvenNumberDecider.cs @@ -0,0 +1,36 @@ +using EvenNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.EvenNumberCommand; + +namespace Fraktalio.FModel.Tests.Examples.Numbers.Even; + +public class EvenNumberDecider() : Decider( + initialState: new EvenNumberState(Description.Create("Initial state"), Number.Create(0)), + decide: (c, s) => + { + if (c != null && c.Number > 1000) + { + throw new NotSupportedException("Sorry"); + } + + return c switch + { + EvenNumberCommand.AddEvenNumber add => + [ + new EvenNumberAdded(Description.Create(add.Description), + s.Value + add.Number) + ], + EvenNumberCommand.SubtractEvenNumber subtract => + [ + new EvenNumberSubtracted(Description.Create(subtract.Description), + s.Value - subtract.Number) + ], + _ => [] + }; + }, + evolve: (s, e) => e switch + { + EvenNumberAdded added => new EvenNumberState( + s.Description + added.Description, added.Value), + EvenNumberSubtracted subtracted => new EvenNumberState( + s.Description - subtracted.Description, subtracted.Value), + _ => s + }); \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/Even/EvenNumberView.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/Even/EvenNumberView.cs new file mode 100644 index 0000000..8de5ade --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/Even/EvenNumberView.cs @@ -0,0 +1,15 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers.Even; + +public class EvenNumberView() : View( + (s, e) => + { + return e switch + { + EvenNumberAdded => new EvenNumberState(s.Description + e.Description, + s.Value + e.Value), + EvenNumberSubtracted => new EvenNumberState(s.Description - e.Description, + s.Value - e.Value), + _ => s + }; + }, + new EvenNumberState(Description.Create("Initial state"), Number.Create(0))); \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/Number.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/Number.cs new file mode 100644 index 0000000..03b5aaa --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/Number.cs @@ -0,0 +1,12 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers; + +public record Number(int Value) +{ + public static Number operator +(Number a, Number b) => Create(a.Value + b.Value); + + public static Number operator -(Number a, Number b) => Create(a.Value - b.Value); + + public static Number Create(int value) => new(value); + + public static implicit operator int(Number value) => value.Value; +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberCommand.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberCommand.cs new file mode 100644 index 0000000..5c9da6e --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberCommand.cs @@ -0,0 +1,24 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers; + +public abstract class NumberCommand(Description description, Number number) +{ + public Description Description { get; } = description; + public Number Number { get; } = number; + + public abstract class EvenNumberCommand(Description description, Number number) : NumberCommand(description, number) + { + public sealed class AddEvenNumber(Description description, Number number) + : EvenNumberCommand(description, number); + + public sealed class SubtractEvenNumber(Description description, Number number) + : EvenNumberCommand(description, number); + } + + public abstract class OddNumberCommand(Description description, Number number) : NumberCommand(description, number) + { + public sealed class AddOddNumber(Description description, Number number) : OddNumberCommand(description, number); + + public sealed class SubtractOddNumber(Description description, Number number) + : OddNumberCommand(description, number); + } +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberEvent.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberEvent.cs new file mode 100644 index 0000000..70ca028 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberEvent.cs @@ -0,0 +1,16 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers; + +public abstract record NumberEvent(Description Description, Number Value); + +public abstract record EvenNumberEvent(Description Description, Number Value) : NumberEvent(Description, Value); + +public sealed record EvenNumberAdded(Description Description, Number Value) : EvenNumberEvent(Description, Value); + +public sealed record EvenNumberSubtracted(Description Description, Number Value) : EvenNumberEvent(Description, Value); + +public abstract record OddNumberEvent(Description Description, Number Value) : NumberEvent(Description, Value); + +public sealed record OddNumberAdded(Description Description, Number Value) : OddNumberEvent(Description, Value); + +public sealed record OddNumberSubtracted(Description Description, Number Value) + : OddNumberEvent(Description, Value); \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberState.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberState.cs new file mode 100644 index 0000000..e4c98e7 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/NumberState.cs @@ -0,0 +1,7 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers; + +public abstract record NumberState(Description Description, Number Value); + +public sealed record OddNumberState(Description Description, Number Value) : NumberState(Description, Value); + +public sealed record EvenNumberState(Description Description, Number Value) : NumberState(Description, Value); \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/Odd/OddNumberDecider.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/Odd/OddNumberDecider.cs new file mode 100644 index 0000000..79f7008 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/Odd/OddNumberDecider.cs @@ -0,0 +1,36 @@ +using OddNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.OddNumberCommand; + +namespace Fraktalio.FModel.Tests.Examples.Numbers.Odd; + +public class OddNumberDecider() : Decider( + initialState: new OddNumberState(Description.Create("Initial state"), Number.Create(0)), + decide: (c, s) => + { + if (c != null && c.Number > 1000) + { + throw new NotSupportedException("Sorry"); + } + + return c switch + { + OddNumberCommand.AddOddNumber add => + [ + new OddNumberAdded(add.Description, + s.Value + add.Number) + ], + OddNumberCommand.SubtractOddNumber subtract => + [ + new OddNumberSubtracted(subtract.Description, + s.Value - subtract.Number) + ], + _ => [] + }; + }, + evolve: (s, e) => e switch + { + OddNumberAdded added => new OddNumberState( + s.Description + added.Description, added.Value), + OddNumberSubtracted subtracted => new OddNumberState( + s.Description - subtracted.Description, subtracted.Value), + _ => s + }); \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Examples/Numbers/Odd/OddNumberView.cs b/test/Fraktalio.FModel.Tests/Examples/Numbers/Odd/OddNumberView.cs new file mode 100644 index 0000000..7987b5d --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Examples/Numbers/Odd/OddNumberView.cs @@ -0,0 +1,15 @@ +namespace Fraktalio.FModel.Tests.Examples.Numbers.Odd; + +public class OddNumberView() : View( + (s, e) => + { + return e switch + { + OddNumberAdded => new OddNumberState(s.Description + e.Description, + s.Value + e.Value), + OddNumberSubtracted => new OddNumberState(s.Description - e.Description, + s.Value - e.Value), + _ => s + }; + }, + new OddNumberState(Description.Create("Initial state"), Number.Create(0))); \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Extensions/DeciderExtensions.cs b/test/Fraktalio.FModel.Tests/Extensions/DeciderExtensions.cs new file mode 100644 index 0000000..38844d6 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Extensions/DeciderExtensions.cs @@ -0,0 +1,18 @@ +namespace Fraktalio.FModel.Tests.Extensions; + +public static class DeciderExtensions +{ + public static IEnumerable GivenEvents(this IDecider decider, IEnumerable events, + Func command) + { + var currentState = events.Aggregate(decider.InitialState, (s, e) => decider.Evolve(s, e)); + return decider.Decide(command(), currentState); + } + + public static S GivenState(this IDecider decider, S? state, Func command) + { + var currentState = state != null ? state : decider.InitialState; + var events = decider.Decide(command(), currentState); + return events.Aggregate(currentState, (current, e) => decider.Evolve(current, e)); + } +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Extensions/EnumerableExtensions.cs b/test/Fraktalio.FModel.Tests/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..be75933 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Extensions/EnumerableExtensions.cs @@ -0,0 +1,20 @@ +using FluentAssertions; + +namespace Fraktalio.FModel.Tests.Extensions; + +public static class EnumerableExtensions +{ + public static void ExpectActions(this IEnumerable flow, params A[] expected) + { + var list = flow.ToList(); + list.Should().BeEquivalentTo(expected); + } + + public static void ThenEvents(this IEnumerable flow, params E[] expected) + { + var list = flow.ToList(); + list.Should().BeEquivalentTo(expected); + } + + public static void ThenState(this S state, U expected) where U : S => state.Should().Be(expected); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Extensions/SagaExtensions.cs b/test/Fraktalio.FModel.Tests/Extensions/SagaExtensions.cs new file mode 100644 index 0000000..68fd01b --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Extensions/SagaExtensions.cs @@ -0,0 +1,7 @@ +namespace Fraktalio.FModel.Tests.Extensions; + +public static class SagaExtensions +{ + public static IEnumerable WhenActionResult(this ISaga saga, AR actionResults) => + saga.React(actionResults); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Extensions/ViewExtensions.cs b/test/Fraktalio.FModel.Tests/Extensions/ViewExtensions.cs new file mode 100644 index 0000000..38ae09d --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Extensions/ViewExtensions.cs @@ -0,0 +1,7 @@ +namespace Fraktalio.FModel.Tests.Extensions; + +internal static class ViewExtensions +{ + public static S GivenEvents(this IView view, IEnumerable events) => + events.Aggregate(view.InitialState, (s, e) => view.Evolve(s, e)); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/Fraktalio.FModel.Tests.csproj b/test/Fraktalio.FModel.Tests/Fraktalio.FModel.Tests.csproj new file mode 100644 index 0000000..53c7651 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/Fraktalio.FModel.Tests.csproj @@ -0,0 +1,29 @@ + + + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/test/Fraktalio.FModel.Tests/GlobalUsings.cs b/test/Fraktalio.FModel.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/SagaTest.cs b/test/Fraktalio.FModel.Tests/SagaTest.cs new file mode 100644 index 0000000..4ad7c2e --- /dev/null +++ b/test/Fraktalio.FModel.Tests/SagaTest.cs @@ -0,0 +1,93 @@ +using Fraktalio.FModel.Tests.Examples.Numbers; +using Fraktalio.FModel.Tests.Extensions; +using OddNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.OddNumberCommand; +using EvenNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.EvenNumberCommand; +using static Fraktalio.FModel.Tests.Examples.NumberSagaFactory; + +namespace Fraktalio.FModel.Tests; + +[Category("unit")] +public class SagaTest +{ + private Saga _evenSaga; + private Saga _oddSaga; + + [SetUp] + public void Setup() + { + _evenSaga = EvenNumberSaga(); + _oddSaga = OddNumberSaga(); + } + + [Test] + public void EvenSaga() => + _evenSaga.WhenActionResult( + new EvenNumberAdded(Description.Create("2"), Number.Create(2))) + .ExpectActions( + new OddNumberCommand.AddOddNumber( + Description.Create("1"), Number.Create(1) + ) + ); + + [Test] + public void Given_EvenNumberAdded_CombinedSaga_CreatesAddOddNumberCommand() + { + var combinedSaga = + _evenSaga + .Combine(_oddSaga); + + combinedSaga.WhenActionResult( + new EvenNumberAdded(Description.Create("2"), Number.Create(2))) + .ExpectActions( + new OddNumberCommand.AddOddNumber( + Description.Create("1"), + Number.Create(1) + ) + ); + } + + [Test] + public void Given_OddNumberAdded_CombinedSaga_CreatesAddEvenNumberCommand() + { + var combinedSaga = + _evenSaga + .Combine(_oddSaga); + + combinedSaga.WhenActionResult( + new OddNumberAdded(Description.Create("1"), Number.Create(1))) + .ExpectActions( + new EvenNumberCommand.AddEvenNumber( + Description.Create("2"), + Number.Create(2) + ) + ); + } + + [Test] + public void MapLeftOnActionResult() => + _evenSaga.MapLeftOnActionResult(arn => + new EvenNumberAdded(Description.Create(arn.ToString()), Number.Create(arn))) + .WhenActionResult( + 2) + .ExpectActions( + new OddNumberCommand.AddOddNumber( + Description.Create("1"), + Number.Create(1) + ) + ); + + [Test] + public void MapOnAction() => + _evenSaga.MapOnAction(a => a.Number.Value) + .WhenActionResult( + new EvenNumberAdded( + Description.Create("2"), + Number.Create(2) + ) + ) + .ExpectActions( + 1 + ); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/StateStoredDeciderTest.cs b/test/Fraktalio.FModel.Tests/StateStoredDeciderTest.cs new file mode 100644 index 0000000..cb70460 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/StateStoredDeciderTest.cs @@ -0,0 +1,50 @@ +using Fraktalio.FModel.Tests.Examples.Numbers; +using Fraktalio.FModel.Tests.Examples.Numbers.Even; +using Fraktalio.FModel.Tests.Examples.Numbers.Odd; +using Fraktalio.FModel.Tests.Extensions; +using EvenNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.EvenNumberCommand; +using OddNumberCommand = Fraktalio.FModel.Tests.Examples.Numbers.NumberCommand.OddNumberCommand; + +namespace Fraktalio.FModel.Tests; + +[Category("unit")] +public class StateStoredDeciderTest +{ + private readonly EvenNumberDecider _evenDecider = new(); + private readonly OddNumberDecider _oddDecider = new(); + + [Test] + public void GivenEmptyState_AddEvenNumber() => + _evenDecider + .GivenState(null, + () => new EvenNumberCommand.AddEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenState(new EvenNumberState(Description.Create("Initial state + 2"), Number.Create(2))); + + [Test] + public void GivenState_AddEvenNumber() => + _evenDecider + .GivenState(new EvenNumberState(Description.Create("2"), Number.Create(2)), + () => new EvenNumberCommand.AddEvenNumber(Description.Create("4"), Number.Create(4))) + .ThenState(new EvenNumberState(Description.Create("2 + 4"), Number.Create(6))); + + [Test] + public void GivenState_SubtractEvenNumber() => + _evenDecider + .GivenState(new EvenNumberState(Description.Create("8"), Number.Create(8)), + () => new EvenNumberCommand.SubtractEvenNumber(Description.Create("2"), Number.Create(2))) + .ThenState(new EvenNumberState(Description.Create("8 - 2"), Number.Create(6))); + + [Test] + public void GivenState_AddOddNumber() => + _oddDecider + .GivenState(new OddNumberState(Description.Create("3"), Number.Create(3)), + () => new OddNumberCommand.AddOddNumber(Description.Create("1"), Number.Create(1))) + .ThenState(new OddNumberState(Description.Create("3 + 1"), Number.Create(4))); + + [Test] + public void GivenState_SubtractOddNumber() => + _oddDecider + .GivenState(new OddNumberState(Description.Create("3"), Number.Create(3)), + () => new OddNumberCommand.SubtractOddNumber(Description.Create("1"), Number.Create(1))) + .ThenState(new OddNumberState(Description.Create("3 - 1"), Number.Create(2))); +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/ViewTest.cs b/test/Fraktalio.FModel.Tests/ViewTest.cs new file mode 100644 index 0000000..2005ac8 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/ViewTest.cs @@ -0,0 +1,70 @@ +using Fraktalio.FModel.Tests.Examples.Numbers; +using Fraktalio.FModel.Tests.Examples.Numbers.Even; +using Fraktalio.FModel.Tests.Examples.Numbers.Odd; +using Fraktalio.FModel.Tests.Extensions; + +namespace Fraktalio.FModel.Tests; + +[Category("unit")] +public class ViewTest +{ + private readonly EvenNumberView _evenView = new(); + private readonly OddNumberView _oddNumberView = new(); + + [Test] + public void GivenSingleEvent_EvenNumberState() => + _evenView + .GivenEvents([new EvenNumberAdded(Description.Create("2"), Number.Create(2))]) + .ThenState(new EvenNumberState(Description.Create("Initial state + 2"), Number.Create(2))); + + [Test] + public void GivenMultipleEvents_EvenNumberState() => + _evenView + .GivenEvents([ + new EvenNumberAdded(Description.Create("2"), Number.Create(2)), + new EvenNumberAdded(Description.Create("4"), Number.Create(4)) + ]) + .ThenState(new EvenNumberState(Description.Create("Initial state + 2 + 4"), Number.Create(6))); + + [Test] + public void MapLefOnEvent_EvenNumbersAdded() + { + var mappedEvenView = _evenView.MapLeftOnEvent(number => + new EvenNumberAdded(Description.Create(number.ToString()), Number.Create(number))); + mappedEvenView.GivenEvents([ + 2, + 4 + ]) + .ThenState(new EvenNumberState(Description.Create("Initial state + 2 + 4"), Number.Create(6))); + } + + [Test] + public void DimapOnState_EvenNumbersAdded() + { + var mappedEvenView = + _evenView.DimapOnState(fl => + new EvenNumberState(Description.Create(fl.ToString()), Number.Create(fl)), + fr => fr.Value.Value); + + mappedEvenView.GivenEvents([ + new EvenNumberAdded(Description.Create("2"), Number.Create(2)), + new EvenNumberAdded(Description.Create("4"), Number.Create(4)) + ]).ThenState(6); + } + + [Test] + public void CombinedView_EvenAndOddNumbersAdded() + { + var combinedView = + _evenView.Combine( + _oddNumberView); + + combinedView.GivenEvents([ + new EvenNumberAdded(Description.Create("2"), Number.Create(2)), + new OddNumberAdded(Description.Create("3"), Number.Create(3)), + new EvenNumberAdded(Description.Create("4"), Number.Create(4)) + ]).ThenState(Tuple.Create( + new EvenNumberState(Description.Create("Initial state + 2 + 4"), Number.Create(6)), + new OddNumberState(Description.Create("Initial state + 3"), Number.Create(3)))); + } +} \ No newline at end of file diff --git a/test/Fraktalio.FModel.Tests/packages.lock.json b/test/Fraktalio.FModel.Tests/packages.lock.json new file mode 100644 index 0000000..bf20912 --- /dev/null +++ b/test/Fraktalio.FModel.Tests/packages.lock.json @@ -0,0 +1,200 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[6.12.0, )", + "resolved": "6.12.0", + "contentHash": "ZXhHT2YwP9lajrwSKbLlFqsmCCvFJMoRSK9t7sImfnCyd0OB3MhgxdoMcVqxbq1iyxD6mD2fiackWmBb7ayiXQ==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.4.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.8.0, )", + "resolved": "17.8.0", + "contentHash": "BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==", + "dependencies": { + "Microsoft.CodeCoverage": "17.8.0", + "Microsoft.TestPlatform.TestHost": "17.8.0" + } + }, + "NUnit": { + "type": "Direct", + "requested": "[3.14.0, )", + "resolved": "3.14.0", + "contentHash": "R7iPwD7kbOaP3o2zldWJbWeMQAvDKD0uld27QvA3PAALl1unl7x0v2J7eGiJOYjimV/BuGT4VJmr45RjS7z4LA==", + "dependencies": { + "NETStandard.Library": "2.0.0" + } + }, + "NUnit.Analyzers": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "Odd1RusSMnfswIiCPbokAqmlcCCXjQ20poaXWrw+CWDnBY1vQ/x6ZGqgyJXpebPq5Uf8uEBe5iOAySsCdSrWdQ==" + }, + "NUnit3TestAdapter": { + "type": "Direct", + "requested": "[4.5.0, )", + "resolved": "4.5.0", + "contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A==" + }, + "Verify.NUnit": { + "type": "Direct", + "requested": "[22.5.0, )", + "resolved": "22.5.0", + "contentHash": "zCf6FaOdyJaOp01wUja9JgREVLICt0XwzbtnQPrrwzXaooyQyrAJrhAOc+hY1nO4eisDfJ15kDqv3HUfzIfdJg==", + "dependencies": { + "EmptyFiles": "5.0.0", + "NUnit": "3.14.0", + "Verify": "22.5.0" + } + }, + "Argon": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "KTbzEEvCC6QX0eEO6C49WBeHdkGT0Hq1EeCk7gd0GhTK+fMpRr8DvmDaqDFK58Fvvwo0dx7K5NhFUexXv+aVuQ==" + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "13.0.0", + "contentHash": "FVGXLxBFCULWVA3Esqi6IwE1HRTMpNmRyAH7KRobY+wo8BwwSNmE9a8znoY0UXoHwN1FTQFtgW+NiGYIZZ8FgQ==", + "dependencies": { + "EmptyFiles": "4.6.0", + "System.Management": "6.0.2" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "HGmJfvJ+8htlyvHKdaN7olLch/U3nrYa0rJ0xK7mKNja4YANdhcQEHfHzT93ehgG9TAUSj1MJaXZIB62HHEuiQ==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==", + "dependencies": { + "NuGet.Frameworks": "6.5.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.8.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" + }, + "SimpleInfoName": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "jZwJajqdF56n0HcwvM8wqrwhr8tBqp7E3dWVoa5XWMC4tqSXP4+rIfYipeVIavRUI5MSj5vYPCdwtF3h4RJvxA==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.2", + "contentHash": "s6c9x2Kghd+ncEDnT6ApYVOacDXr/Y57oSUSx6wjegMOfKxhtrXn3PdASPNU59y3kB9OJ1yb3l5k6uKr3bhqew==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" + }, + "Verify": { + "type": "Transitive", + "resolved": "22.5.0", + "contentHash": "E8KnPsAqtNdB8Sr++0/zGw5vyk7j9nBYLsJflAZnA6xG5ndaiq5+EUtjcM+JQetirLnE9f4wEU7/RnIDCnW//Q==", + "dependencies": { + "Argon": "0.13.0", + "DiffEngine": "13.0.0", + "EmptyFiles": "5.0.0", + "SimpleInfoName": "2.2.0", + "System.IO.Hashing": "8.0.0" + } + }, + "fraktalio.fmodel": { + "type": "Project", + "dependencies": { + "Fraktalio.FModel.Contracts": "[1.0.0, )" + } + }, + "fraktalio.fmodel.contracts": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2023.3.0, )" + } + }, + "JetBrains.Annotations": { + "type": "CentralTransitive", + "requested": "[2023.3.0, )", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + } + } + } +} \ No newline at end of file