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