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/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..de0dea2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,32 @@
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
+[*]
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+# Xml project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
+indent_size = 2
+
+[*.{csproj,vbproj,proj,nativeproj,locproj}]
+charset = utf-8
+
+# Xml files
+[*.{xml,stylecop,resx,ruleset}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+# YAML config files
+[*.{yml,yaml}]
+indent_size = 2
\ No newline at end of file
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..e6d70cf
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,37 @@
+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.204'
+ - name: Build and Test
+ run: ./Build.ps1
+ shell: pwsh
+ - name: Push to MyGet
+ if: false
+ env:
+ NUGET_URL: https://www.myget.org/F/TODO/api/v3/index.json
+ NUGET_API_KEY: ${{ secrets.MYGET_FRAKTALIO_CI_API_KEY }}
+ run: ./Push.ps1
+ shell: pwsh
+ - name: Artifacts
+ if: false
+ 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..e2a9ed2
--- /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\Fraktalio.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..b77da40
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,15 @@
+
+
+
+ Copyright © 2023 Fraktalio. All rights reserved.
+ Fraktalio
+ Ivan Dugalić
+ net8.0
+ enable
+ enable
+ True
+ true
+ strict
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..aa6cb94
--- /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..4d0f0a9
--- /dev/null
+++ b/Fraktalio.Fmodel.sln
@@ -0,0 +1,57 @@
+
+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
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DF314C79-FDC8-4210-98D7-ED7AB13848D2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio", "src\Fraktalio\Fraktalio.csproj", "{419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio.Contracts", "src\Fraktalio.Contracts\Fraktalio.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.Tests", "test\Fraktalio.Tests\Fraktalio.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/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.Contracts/Fraktalio.Contracts.csproj b/src/Fraktalio.Contracts/Fraktalio.Contracts.csproj
new file mode 100644
index 0000000..e0673e6
--- /dev/null
+++ b/src/Fraktalio.Contracts/Fraktalio.Contracts.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Fraktalio.Contracts/ISaga.cs b/src/Fraktalio.Contracts/ISaga.cs
new file mode 100644
index 0000000..fed704b
--- /dev/null
+++ b/src/Fraktalio.Contracts/ISaga.cs
@@ -0,0 +1,16 @@
+using JetBrains.Annotations;
+
+namespace Fraktalio.Contracts;
+
+using System.Collections.Generic;
+
+///
+/// An interface of the Saga
+///
+/// Action Result type
+/// Action Type
+[PublicAPI]
+public interface ISaga
+{
+ IEnumerable React(AR actionResult);
+}
diff --git a/src/Fraktalio.Contracts/packages.lock.json b/src/Fraktalio.Contracts/packages.lock.json
new file mode 100644
index 0000000..733402a
--- /dev/null
+++ b/src/Fraktalio.Contracts/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net8.0": {
+ "JetBrains.Annotations": {
+ "type": "Direct",
+ "requested": "[2023.3.0, )",
+ "resolved": "2023.3.0",
+ "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Fraktalio/Fraktalio.csproj b/src/Fraktalio/Fraktalio.csproj
new file mode 100644
index 0000000..4595f12
--- /dev/null
+++ b/src/Fraktalio/Fraktalio.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Fraktalio/Saga.cs b/src/Fraktalio/Saga.cs
new file mode 100644
index 0000000..3684dbb
--- /dev/null
+++ b/src/Fraktalio/Saga.cs
@@ -0,0 +1,30 @@
+using Fraktalio.Contracts;
+
+namespace Fraktalio;
+
+///
+/// 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)
+ {
+ return react(actionResult);
+ }
+
+ public Saga MapLeftOnActionResult(Func f)
+ {
+ return new Saga(arn => react(f(arn)));
+ }
+
+ public Saga MapOnAction(Func f)
+ {
+ return new Saga(ar => react(ar).Select(f));
+ }
+}
diff --git a/src/Fraktalio/SagaExtensions.cs b/src/Fraktalio/SagaExtensions.cs
new file mode 100644
index 0000000..d40382f
--- /dev/null
+++ b/src/Fraktalio/SagaExtensions.cs
@@ -0,0 +1,35 @@
+namespace Fraktalio;
+
+public static class SagaExtensions
+{
+ ///
+ /// 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)));
+ }
+}
diff --git a/src/Fraktalio/SagaFactory.cs b/src/Fraktalio/SagaFactory.cs
new file mode 100644
index 0000000..39b745f
--- /dev/null
+++ b/src/Fraktalio/SagaFactory.cs
@@ -0,0 +1,16 @@
+namespace Fraktalio;
+
+public static class SagaFactory
+{
+ ///
+ /// Saga DSL - A convenient builder DSL for the see cref="Saga{AR,A}"/>
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Saga Create(Func> react)
+ {
+ return new Saga(react);
+ }
+}
diff --git a/src/Fraktalio/packages.lock.json b/src/Fraktalio/packages.lock.json
new file mode 100644
index 0000000..3ad82aa
--- /dev/null
+++ b/src/Fraktalio/packages.lock.json
@@ -0,0 +1,19 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net8.0": {
+ "fraktalio.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.Tests/EnumerableExtensions.cs b/test/Fraktalio.Tests/EnumerableExtensions.cs
new file mode 100644
index 0000000..a138784
--- /dev/null
+++ b/test/Fraktalio.Tests/EnumerableExtensions.cs
@@ -0,0 +1,12 @@
+using FluentAssertions;
+
+namespace Fraktalio.Tests;
+
+public static class EnumerableExtensions
+{
+ public static void ExpectActions(this IEnumerable flow, params A[] expected)
+ {
+ var list = flow.ToList();
+ list.Should().BeEquivalentTo(expected);
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/NumberSagaFactory.cs b/test/Fraktalio.Tests/Examples/NumberSagaFactory.cs
new file mode 100644
index 0000000..57a48e1
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/NumberSagaFactory.cs
@@ -0,0 +1,137 @@
+using Fraktalio.Tests.Examples.Numbers;
+
+namespace Fraktalio.Tests.Examples;
+
+public static class NumberSagaFactory
+{
+ public static Saga CreateNumberSaga()
+ {
+ var react = NumberSaga;
+ return SagaFactory.Create(react);
+ }
+
+ public static Saga CreateEvenNumberSaga()
+ {
+ var react = EvenNumberSaga;
+ return SagaFactory.Create(react);
+ }
+
+ public static Saga CreateOddNumberSaga()
+ {
+ var react = OddNumberSaga;
+ return SagaFactory.Create(react);
+ }
+
+ ///
+ /// Very simple Number saga, just for fun ;)
+ ///
+ /// It reacts on Action Results of type of any Event (Even or Odd) and issue a Command/Action (Odd or Even)
+ /// For example if the EvenNumberAdded happened with value 4, a new command of type AddOddNumber will be published with value EvenNumberAdded-1=3
+ ///
+ /// The event
+ /// List of commands
+ private static IEnumerable NumberSaga(NumberEvent numberEvent)
+ {
+ return numberEvent switch
+ {
+ NumberEvent.EvenNumberEvent.EvenNumberAdded evenNumberAdded => new NumberCommand[]
+ {
+ new NumberCommand.OddNumberCommand.AddOddNumber(
+ new Description($"{evenNumberAdded.Value.Value - 1}"),
+ new NumberValue(evenNumberAdded.Value.Value - 1)
+ )
+ },
+
+ NumberEvent.EvenNumberEvent.EvenNumberSubtracted evenNumberSubtracted => new NumberCommand[]
+ {
+ new NumberCommand.OddNumberCommand.SubtractOddNumber(
+ new Description($"{evenNumberSubtracted.Value.Value - 1}"),
+ new NumberValue(evenNumberSubtracted.Value.Value - 1)
+ )
+ },
+
+ NumberEvent.OddNumberEvent.OddNumberAdded oddNumberAdded => new NumberCommand[]
+ {
+ new NumberCommand.EvenNumberCommand.AddEvenNumber(
+ new Description($"{oddNumberAdded.Value.Value + 1}"),
+ new NumberValue(oddNumberAdded.Value.Value + 1)
+ )
+ },
+
+ NumberEvent.OddNumberEvent.OddNumberSubtracted oddNumberSubtracted => new NumberCommand[]
+ {
+ new NumberCommand.EvenNumberCommand.SubtractEvenNumber(
+ new Description($"{oddNumberSubtracted.Value.Value + 1}"),
+ new NumberValue(oddNumberSubtracted.Value.Value + 1)
+ )
+ },
+
+ _ => Enumerable.Empty()
+ };
+ }
+
+ ///
+ /// 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 IEnumerable EvenNumberSaga(
+ this NumberEvent.EvenNumberEvent? numberEvent)
+ {
+ return numberEvent switch
+ {
+ NumberEvent.EvenNumberEvent.EvenNumberAdded evenNumberAdded => new NumberCommand.OddNumberCommand[]
+ {
+ new NumberCommand.OddNumberCommand.AddOddNumber(
+ new Description($"{evenNumberAdded.Value.Value - 1}"),
+ new NumberValue(evenNumberAdded.Value.Value - 1)
+ )
+ },
+
+ NumberEvent.EvenNumberEvent.EvenNumberSubtracted evenNumberSubtracted => new NumberCommand.OddNumberCommand
+ []
+ {
+ new NumberCommand.OddNumberCommand.SubtractOddNumber(
+ new Description($"{evenNumberSubtracted.Value.Value - 1}"),
+ new NumberValue(evenNumberSubtracted.Value.Value - 1)
+ )
+ },
+
+ _ => Enumerable.Empty()
+ };
+ }
+
+ ///
+ /// 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
+ public static IEnumerable OddNumberSaga(
+ this NumberEvent.OddNumberEvent? numberEvent)
+ {
+ return numberEvent switch
+ {
+ NumberEvent.OddNumberEvent.OddNumberAdded oddNumberAdded => new NumberCommand.EvenNumberCommand[]
+ {
+ new NumberCommand.EvenNumberCommand.AddEvenNumber(
+ new Description($"{oddNumberAdded.Value.Value + 1}"),
+ new NumberValue(oddNumberAdded.Value.Value + 1)
+ )
+ },
+
+ NumberEvent.OddNumberEvent.OddNumberSubtracted oddNumberSubtracted => new NumberCommand.EvenNumberCommand[]
+ {
+ new NumberCommand.EvenNumberCommand.SubtractEvenNumber(
+ new Description($"{oddNumberSubtracted.Value.Value + 1}"),
+ new NumberValue(oddNumberSubtracted.Value.Value + 1)
+ )
+ },
+
+ _ => Enumerable.Empty()
+ };
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/AddEvenNumberBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/AddEvenNumberBuilder.cs
new file mode 100644
index 0000000..21f2f56
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/AddEvenNumberBuilder.cs
@@ -0,0 +1,32 @@
+namespace Fraktalio.Tests.Examples.Numbers.Builders;
+
+public class AddEvenNumberBuilder
+{
+ private Description DescriptionValue { get; set; } = new("");
+ private NumberValue NumberValue { get; set; } = new(0);
+
+ public void Description(Func lambda)
+ {
+ DescriptionValue = lambda();
+ }
+
+ public void Value(Func lambda)
+ {
+ NumberValue = lambda();
+ }
+
+ public NumberCommand.EvenNumberCommand.AddEvenNumber Build()
+ {
+ return new NumberCommand.EvenNumberCommand.AddEvenNumber(DescriptionValue, NumberValue);
+ }
+}
+
+public static class AddEvenNumberExtensions
+{
+ public static NumberCommand.EvenNumberCommand.AddEvenNumber AddEvenNumber(this AddEvenNumberBuilder builder, Action block)
+ {
+ block(builder);
+ return builder.Build();
+ }
+}
+
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberAddedBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberAddedBuilder.cs
new file mode 100644
index 0000000..add07c3
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberAddedBuilder.cs
@@ -0,0 +1,32 @@
+namespace Fraktalio.Tests.Examples.Numbers.Builders;
+
+public class EvenNumberAddedBuilder
+{
+ private Description DescriptionValue { get; set; } = new("");
+ private NumberValue NumberValue { get; set; } = new(0);
+
+ public void Description(Func lambda)
+ {
+ DescriptionValue = lambda();
+ }
+
+ public void Value(Func lambda)
+ {
+ NumberValue = lambda();
+ }
+
+ public NumberEvent.EvenNumberEvent.EvenNumberAdded Build()
+ {
+ return new NumberEvent.EvenNumberEvent.EvenNumberAdded(DescriptionValue, NumberValue);
+ }
+}
+
+public static class EvenNumberAddedExtensions
+{
+ public static NumberEvent.EvenNumberEvent.EvenNumberAdded EvenNumberAdded(this EvenNumberAddedBuilder builder,
+ Action block)
+ {
+ block(builder);
+ return builder.Build();
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberStateBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberStateBuilder.cs
new file mode 100644
index 0000000..ec01c5c
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberStateBuilder.cs
@@ -0,0 +1,41 @@
+namespace Fraktalio.Tests.Examples.Numbers.Builders;
+
+public class EvenNumberStateBuilder
+{
+ private Description DescriptionValue { get; set; } = new("");
+ private NumberValue NumberValue { get; set; } = new(0);
+
+ public void Description(Func lambda)
+ {
+ DescriptionValue = lambda();
+ }
+
+ public void DescriptionString(Func lambda)
+ {
+ DescriptionValue = new Description(lambda());
+ }
+
+ public void Value(Func lambda)
+ {
+ NumberValue = lambda();
+ }
+
+ public void ValueInt(Func lambda)
+ {
+ NumberValue = new NumberValue(lambda());
+ }
+
+ public EvenNumberState Build()
+ {
+ return new EvenNumberState(DescriptionValue, NumberValue);
+ }
+}
+
+public static class EvenNumberStateExtensions
+{
+ public static EvenNumberState EvenNumberState(this EvenNumberStateBuilder builder, Action block)
+ {
+ block(builder);
+ return builder.Build();
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberSubtractedBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberSubtractedBuilder.cs
new file mode 100644
index 0000000..d7e4ce7
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberSubtractedBuilder.cs
@@ -0,0 +1,32 @@
+namespace Fraktalio.Tests.Examples.Numbers.Builders;
+
+public class EvenNumberSubtractedBuilder
+{
+ private Description DescriptionValue { get; set; } = new("");
+ private NumberValue NumberValue { get; set; } = new(0);
+
+ public void Description(Func lambda)
+ {
+ DescriptionValue = lambda();
+ }
+
+ public void Value(Func lambda)
+ {
+ NumberValue = lambda();
+ }
+
+ public NumberEvent.EvenNumberEvent.EvenNumberSubtracted Build()
+ {
+ return new NumberEvent.EvenNumberEvent.EvenNumberSubtracted(DescriptionValue, NumberValue);
+ }
+}
+
+public static class EvenNumberSubtractedExtensions
+{
+ public static NumberEvent.EvenNumberEvent.EvenNumberSubtracted EvenNumberSubtracted(this EvenNumberSubtractedBuilder builder,
+ Action block)
+ {
+ block(builder);
+ return builder.Build();
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/OddNumberStateBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/OddNumberStateBuilder.cs
new file mode 100644
index 0000000..d3263b1
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/OddNumberStateBuilder.cs
@@ -0,0 +1,41 @@
+namespace Fraktalio.Tests.Examples.Numbers.Builders;
+
+public class OddNumberStateBuilder
+{
+ private Description DescriptionValue { get; set; } = new("");
+ private NumberValue NumberValue { get; set; } = new(0);
+
+ public void Description(Func lambda)
+ {
+ DescriptionValue = lambda();
+ }
+
+ public void DescriptionString(Func lambda)
+ {
+ DescriptionValue = new Description(lambda());
+ }
+
+ public void Value(Func lambda)
+ {
+ NumberValue = lambda();
+ }
+
+ public void ValueInt(Func lambda)
+ {
+ NumberValue = new NumberValue(lambda());
+ }
+
+ public OddNumberState Build()
+ {
+ return new OddNumberState(DescriptionValue, NumberValue);
+ }
+}
+
+public static class OddNumberStateExtensions
+{
+ public static OddNumberState OddNumberState(this OddNumberStateBuilder builder, Action block)
+ {
+ block(builder);
+ return builder.Build();
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/SubstractEvenNumberBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/SubstractEvenNumberBuilder.cs
new file mode 100644
index 0000000..0527ac9
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/SubstractEvenNumberBuilder.cs
@@ -0,0 +1,32 @@
+namespace Fraktalio.Tests.Examples.Numbers.Builders;
+
+public class SubtractEvenNumberBuilder
+{
+ private Description DescriptionValue { get; set; } = new("");
+ private NumberValue NumberValue { get; set; } = new(0);
+
+ public void Description(Func lambda)
+ {
+ DescriptionValue = lambda();
+ }
+
+ public void Value(Func lambda)
+ {
+ NumberValue = lambda();
+ }
+
+ public NumberCommand.EvenNumberCommand.SubtractEvenNumber Build()
+ {
+ return new NumberCommand.EvenNumberCommand.SubtractEvenNumber(DescriptionValue, NumberValue);
+ }
+}
+
+public static class SubtractEvenNumberExtensions
+{
+ public static NumberCommand.EvenNumberCommand.SubtractEvenNumber SubtractEvenNumber(this SubtractEvenNumberBuilder builder, Action block)
+ {
+ block(builder);
+ return builder.Build();
+ }
+}
+
diff --git a/test/Fraktalio.Tests/Examples/Numbers/Description.cs b/test/Fraktalio.Tests/Examples/Numbers/Description.cs
new file mode 100644
index 0000000..5165bec
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/Description.cs
@@ -0,0 +1,17 @@
+namespace Fraktalio.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 implicit operator string(Description value) => value.Value;
+}
+
+public static class DescriptionFactory
+{
+ public static Description Description(Func block) => new(block());
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberCommand.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberCommand.cs
new file mode 100644
index 0000000..714ac85
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/NumberCommand.cs
@@ -0,0 +1,37 @@
+namespace Fraktalio.Tests.Examples.Numbers;
+
+public abstract class NumberCommand
+{
+ public abstract Description Description { get; }
+ public abstract NumberValue Value { get; }
+
+ public abstract class EvenNumberCommand : NumberCommand
+ {
+ public sealed class AddEvenNumber(Description description, NumberValue value) : EvenNumberCommand
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+
+ public sealed class SubtractEvenNumber(Description description, NumberValue value) : EvenNumberCommand
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+ }
+
+ public abstract class OddNumberCommand : NumberCommand
+ {
+ public sealed class AddOddNumber(Description description, NumberValue value) : OddNumberCommand
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+
+ public sealed class SubtractOddNumber(Description description, NumberValue value) : OddNumberCommand
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberEvent.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberEvent.cs
new file mode 100644
index 0000000..73566b3
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/NumberEvent.cs
@@ -0,0 +1,37 @@
+namespace Fraktalio.Tests.Examples.Numbers;
+
+public abstract class NumberEvent
+{
+ public abstract Description Description { get; }
+ public abstract NumberValue Value { get; }
+
+ public abstract class EvenNumberEvent : NumberEvent
+ {
+ public sealed class EvenNumberAdded(Description description, NumberValue value) : EvenNumberEvent
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+
+ public sealed class EvenNumberSubtracted(Description description, NumberValue value) : EvenNumberEvent
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+ }
+
+ public abstract class OddNumberEvent : NumberEvent
+ {
+ public sealed class OddNumberAdded(Description description, NumberValue value) : OddNumberEvent
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+
+ public sealed class OddNumberSubtracted(Description description, NumberValue value) : OddNumberEvent
+ {
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+ }
+ }
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberState.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberState.cs
new file mode 100644
index 0000000..9db2e09
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/NumberState.cs
@@ -0,0 +1,19 @@
+namespace Fraktalio.Tests.Examples.Numbers;
+
+public abstract class NumberState
+{
+ public abstract Description Description { get; }
+ public abstract NumberValue Value { get; }
+}
+
+public sealed class EvenNumberState(Description description, NumberValue value) : NumberState
+{
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+}
+
+public sealed class OddNumberState(Description description, NumberValue value) : NumberState
+{
+ public override Description Description { get; } = description;
+ public override NumberValue Value { get; } = value;
+}
diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberValue.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberValue.cs
new file mode 100644
index 0000000..6a7a2d7
--- /dev/null
+++ b/test/Fraktalio.Tests/Examples/Numbers/NumberValue.cs
@@ -0,0 +1,17 @@
+namespace Fraktalio.Tests.Examples.Numbers;
+
+public record NumberValue(int Value)
+{
+ public static Description operator +(NumberValue a, NumberValue b) =>
+ new($"{a.Value} + {b.Value}");
+
+ public static Description operator -(NumberValue a, NumberValue b) =>
+ new($"{a.Value} - {b.Value}");
+
+ public static implicit operator int(NumberValue value) => value.Value;
+}
+
+public static class NumberValueFactory
+{
+ public static NumberValue Value(Func block) => new(block());
+}
diff --git a/test/Fraktalio.Tests/Fraktalio.Tests.csproj b/test/Fraktalio.Tests/Fraktalio.Tests.csproj
new file mode 100644
index 0000000..d118379
--- /dev/null
+++ b/test/Fraktalio.Tests/Fraktalio.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.Tests/GlobalUsings.cs b/test/Fraktalio.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..3244567
--- /dev/null
+++ b/test/Fraktalio.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
diff --git a/test/Fraktalio.Tests/SagaExtensions.cs b/test/Fraktalio.Tests/SagaExtensions.cs
new file mode 100644
index 0000000..ce6ba81
--- /dev/null
+++ b/test/Fraktalio.Tests/SagaExtensions.cs
@@ -0,0 +1,11 @@
+using Fraktalio.Contracts;
+
+namespace Fraktalio.Tests;
+
+public static class SagaExtensions
+{
+ public static IEnumerable WhenActionResult(this ISaga saga, AR actionResults)
+ {
+ return saga.React(actionResults);
+ }
+}
diff --git a/test/Fraktalio.Tests/SagaTest.cs b/test/Fraktalio.Tests/SagaTest.cs
new file mode 100644
index 0000000..3c13da4
--- /dev/null
+++ b/test/Fraktalio.Tests/SagaTest.cs
@@ -0,0 +1,22 @@
+using Fraktalio.Tests.Examples;
+using Fraktalio.Tests.Examples.Numbers;
+
+namespace Fraktalio.Tests;
+
+public class SagaTest
+{
+ [Test]
+ public void EvenSagaTest()
+ {
+ var evenSaga = NumberSagaFactory.CreateEvenNumberSaga();
+
+ evenSaga.WhenActionResult(
+ new NumberEvent.EvenNumberEvent.EvenNumberAdded(new Description("2"), new NumberValue(2)))
+ .ExpectActions(
+ new NumberCommand.OddNumberCommand.AddOddNumber(
+ new Description("1"),
+ new NumberValue(1)
+ )
+ );
+ }
+}
diff --git a/test/Fraktalio.Tests/packages.lock.json b/test/Fraktalio.Tests/packages.lock.json
new file mode 100644
index 0000000..7188ea2
--- /dev/null
+++ b/test/Fraktalio.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": {
+ "type": "Project",
+ "dependencies": {
+ "Fraktalio.Contracts": "[1.0.0, )"
+ }
+ },
+ "fraktalio.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