diff --git a/.assets/aggregate.jpg b/.assets/aggregate.jpg new file mode 100644 index 0000000..99c1b26 Binary files /dev/null and b/.assets/aggregate.jpg differ diff --git a/.assets/aggregates.png b/.assets/aggregates.png new file mode 100644 index 0000000..932f6c7 Binary files /dev/null and b/.assets/aggregates.png differ diff --git a/.assets/decider-impl.png b/.assets/decider-impl.png new file mode 100644 index 0000000..adeb92b Binary files /dev/null and b/.assets/decider-impl.png differ diff --git a/.assets/decider-test.png b/.assets/decider-test.png new file mode 100644 index 0000000..6c4f564 Binary files /dev/null and b/.assets/decider-test.png differ diff --git a/.assets/decider.jpg b/.assets/decider.jpg new file mode 100644 index 0000000..e90cc74 Binary files /dev/null and b/.assets/decider.jpg differ diff --git a/.assets/decider.png b/.assets/decider.png new file mode 100644 index 0000000..3405f79 Binary files /dev/null and b/.assets/decider.png differ diff --git a/.assets/es-aggregate.jpg b/.assets/es-aggregate.jpg new file mode 100644 index 0000000..e6240ca Binary files /dev/null and b/.assets/es-aggregate.jpg differ diff --git a/.assets/es-aggregate.png b/.assets/es-aggregate.png new file mode 100644 index 0000000..6a749ca Binary files /dev/null and b/.assets/es-aggregate.png differ diff --git a/.assets/es-ss-system.png b/.assets/es-ss-system.png new file mode 100644 index 0000000..b9ec907 Binary files /dev/null and b/.assets/es-ss-system.png differ diff --git a/.assets/event-modeling.png b/.assets/event-modeling.png new file mode 100644 index 0000000..9dec436 Binary files /dev/null and b/.assets/event-modeling.png differ diff --git a/.assets/information-flow.jpg b/.assets/information-flow.jpg new file mode 100644 index 0000000..af090b3 Binary files /dev/null and b/.assets/information-flow.jpg differ diff --git a/.assets/kotlin-actors.png b/.assets/kotlin-actors.png new file mode 100644 index 0000000..d509dd8 Binary files /dev/null and b/.assets/kotlin-actors.png differ diff --git a/.assets/mviews.png b/.assets/mviews.png new file mode 100644 index 0000000..40822c3 Binary files /dev/null and b/.assets/mviews.png differ diff --git a/.assets/onion.png b/.assets/onion.png new file mode 100644 index 0000000..cbbdbe0 Binary files /dev/null and b/.assets/onion.png differ diff --git a/.assets/saga.jpg b/.assets/saga.jpg new file mode 100644 index 0000000..459d87d Binary files /dev/null and b/.assets/saga.jpg differ diff --git a/.assets/saga.png b/.assets/saga.png new file mode 100644 index 0000000..b5554bb Binary files /dev/null and b/.assets/saga.png differ diff --git a/.assets/ss-aggregate.jpg b/.assets/ss-aggregate.jpg new file mode 100644 index 0000000..6ed1ead Binary files /dev/null and b/.assets/ss-aggregate.jpg differ diff --git a/.assets/ss-aggregate.png b/.assets/ss-aggregate.png new file mode 100644 index 0000000..845aabd Binary files /dev/null and b/.assets/ss-aggregate.png differ diff --git a/.assets/view.jpg b/.assets/view.jpg new file mode 100644 index 0000000..c170488 Binary files /dev/null and b/.assets/view.jpg differ diff --git a/.assets/view.png b/.assets/view.png new file mode 100644 index 0000000..a35f717 Binary files /dev/null and b/.assets/view.png differ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..de0dea2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ed3c6d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: + Bug report about: + Create a report to help us improve title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5fe926b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request about: Suggest an idea for this project title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e6d70cf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup dotnet 8.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.204' + - name: Build and Test + run: ./Build.ps1 + shell: pwsh + - name: Push to MyGet + if: false + env: + NUGET_URL: https://www.myget.org/F/TODO/api/v3/index.json + NUGET_API_KEY: ${{ secrets.MYGET_FRAKTALIO_CI_API_KEY }} + run: ./Push.ps1 + shell: pwsh + - name: Artifacts + if: false + uses: actions/upload-artifact@v2 + with: + name: artifacts + path: artifacts/**/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f94d134 --- /dev/null +++ b/.gitignore @@ -0,0 +1,263 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +#VerifyTests +*.received.* + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc +.mfractor/.txf.tmd.db +/.mfractor +TestResults diff --git a/Build.ps1 b/Build.ps1 new file mode 100644 index 0000000..e2a9ed2 --- /dev/null +++ b/Build.ps1 @@ -0,0 +1,36 @@ +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} + +$artifacts = ".\artifacts" + +if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse } + +exec { & dotnet clean -c Release } + +exec { & dotnet build -c Release } + +exec { & dotnet test -c Release --no-build -l trx --verbosity=normal } + +# exec { & dotnet pack .\src\Fraktalio\Fraktalio.csproj -c Release -o $artifacts --no-build } + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f37f16e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,102 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, +or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing +the community in public spaces. Examples of representing our community include using an official e-mail address, posting +via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible +for enforcement at info@fraktalio.com. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem +in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the +community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation +and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified +period of time. No public or private interaction with the people involved, including unsolicited interaction with those +enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate +behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired +by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..b77da40 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + + Copyright © 2023 Fraktalio. All rights reserved. + Fraktalio + Ivan Dugalić + net8.0 + enable + enable + True + true + strict + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..aa6cb94 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,21 @@ + + + true + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Fraktalio.Fmodel.sln b/Fraktalio.Fmodel.sln new file mode 100644 index 0000000..4d0f0a9 --- /dev/null +++ b/Fraktalio.Fmodel.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{92A047C2-3835-42E8-A055-3F94DE935E8F}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + nuget.config = nuget.config + LICENSE = LICENSE + CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md + README.md = README.md + Build.ps1 = Build.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DF314C79-FDC8-4210-98D7-ED7AB13848D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio", "src\Fraktalio\Fraktalio.csproj", "{419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio.Contracts", "src\Fraktalio.Contracts\Fraktalio.Contracts.csproj", "{262EE234-6FFF-4C7D-9D29-FE133D3BBF46}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8B68360D-A2C7-416C-BFFA-D0C063E231FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fraktalio.Tests", "test\Fraktalio.Tests\Fraktalio.Tests.csproj", "{445D6510-F66E-4805-B9C8-B03E3A3C0571}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{57E3D8FE-526A-4BB0-BDB2-0E97CAEEC06A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{97650C4C-B1E0-4E17-AB33-C9D82056CE51}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D}.Release|Any CPU.Build.0 = Release|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46}.Release|Any CPU.Build.0 = Release|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {445D6510-F66E-4805-B9C8-B03E3A3C0571}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {419C4D5B-DFB7-4387-90A0-97D5DBCDB04D} = {DF314C79-FDC8-4210-98D7-ED7AB13848D2} + {262EE234-6FFF-4C7D-9D29-FE133D3BBF46} = {DF314C79-FDC8-4210-98D7-ED7AB13848D2} + {445D6510-F66E-4805-B9C8-B03E3A3C0571} = {8B68360D-A2C7-416C-BFFA-D0C063E231FE} + {97650C4C-B1E0-4E17-AB33-C9D82056CE51} = {57E3D8FE-526A-4BB0-BDB2-0E97CAEEC06A} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE index 261eeb9..9557691 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,10 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright 2023 Fraktalio D.O.O. All rights reserved. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at - 1. Definitions. + http://www.apache.org/licenses/LICENSE-2.0 - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " +AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. diff --git a/README.md b/README.md index c8aaa78..2fcfa4e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,700 @@ -# fmodel-c- -Domain modeling in C# - f(model) +# **f`(`model`)`** - Functional and Reactive Domain Modeling + +When you’re developing an information system to automate the activities of the business, you are modeling the business. +The abstractions that you design, the behaviors that you implement, and the UI interactions that you build all reflect +the business — together, they constitute the model of the domain. + +## `IOR` + +This project can be used as a multiplatform library, or as an inspiration, or both. **It provides just enough tactical +Domain-Driven Design patterns, optimised for Event Sourcing and CQRS.** + +- The `domain` model library is fully isolated from the application layer and API-related concerns. It represents a pure + declaration of the program logic. It is written in [Kotlin](https://kotlinlang.org/) programming language, without + additional + dependencies. [![Maven Central - domain](https://img.shields.io/maven-central/v/com.fraktalio.fmodel/domain.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.fraktalio.fmodel%22%20AND%20a:%22domain%22) +- The `application` libraries orchestrates the execution of the logic by loading state, executing `domain` components + and storing new state. It is written in [Kotlin](https://kotlinlang.org/) programming language. Two flavors ( + extensions of `Application` module) are available: + [![Maven Central - application](https://img.shields.io/maven-central/v/com.fraktalio.fmodel/application.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.fraktalio.fmodel%22%20AND%20a:%22application%22) + - `application-vanilla` is using plain/vanilla Kotlin to implement the application layer in order to load the state, + orchestrate the execution of the logic and save new state. + - `application-arrow` is using [Arrow](https://arrow-kt.io/) and Kotlin to implement the application layer in order + to load the state, orchestrate the execution of the logic and save new state - managing errors much better (using + Either). + +The libraries are non-intrusive, and you can select any flavor, or choose both (`vanila` and `arrow`). You can use +only `domain` library and model the orchestration (`application` library) on your own. Or, you can simply be inspired by +this project :) + +![event-modeling](.assets/event-modeling.png) + +## Table of Contents + +* [f(model) - Functional domain modeling](#fmodel---functional-domain-modeling) + * [Multiplatform](#multiplatform) + * [Abstraction and generalization](#abstraction-and-generalization) + * [decide: (C, S) -> Flow<E>](#decide-c-s---flowe) + * [evolve: (S, E) -> S](#evolve-s-e---s) + * [Event-sourced or State-stored systems](#event-sourced-or-state-stored-systems) + * [Decider](#decider) + * [Decider extensions and functions](#decider-extensions-and-functions) + * [Event-sourcing aggregate](#event-sourcing-aggregate) + * [State-stored aggregate](#state-stored-aggregate) + * [View](#view) + * [View extensions and functions](#view-extensions-and-functions) + * [Materialized View](#materialized-view) + * [Saga](#saga) + * [Saga extensions and functions](#saga-extensions-and-functions) + * [Saga Manager](#saga-manager) + * [Kotlin](#kotlin) + * [Examples](#start-using-the-libraries) + * [References and further reading](#references-and-further-reading) + +## Multiplatform + +Support for multiplatform programming is one of Kotlin’s key benefits. It reduces time spent writing and maintaining the +same code for different platforms while retaining the flexibility and benefits of native programming. + +## Abstraction and generalization + +Abstractions can hide irrelevant details and use names to reference objects. It emphasizes what an object is or does +rather than how it is represented or how it works. + +Generalization reduces complexity by replacing multiple entities which perform similar functions with a single +construct. + +Abstraction and generalization are often used together. Abstracts are generalized through parameterization to provide +more excellent utility. + +## `decide: (C, S) -> Flow` + +On a higher level of abstraction, any information system is responsible for handling the intent (`Command`) and based on +the current `State`, produce new facts (`Events`): + +- given the current `State/S` *on the input*, +- when `Command/C` is handled *on the input*, +- expect `flow` of new `Events/E` to be published/emitted *on the output* + +## `evolve: (S, E) -> S` + +The new state is always evolved out of the current state `S` and the current event `E`: + +- given the current `State/S` *on the input*, +- when `Event/E` is handled *on the input*, +- expect new `State/S` to be published *on the output* + +## Event-sourced or State-stored systems + +- State-stored systems are traditional systems that are only storing the current State by overwriting the previous State + in the storage. +- Event-sourced systems are storing the events in immutable storage by only appending. + +### A statement: + +Both types of systems can be designed by using only these two functions and three generic parameters: + +- `decide: (C, S) -> Flow` +- `evolve: (S, E) -> S` + +![event sourced vs state stored](.assets/es-ss-system.png) + +There is more to it! You can switch from one system type to another or have both flavors included within your systems +landscape. + +
+ A proof + +We can fold/recreate the new state out of the flow of events by using `evolve` function `(S, E) -> S` and providing the +initialState of type S as a starting point. + +- `Flow.fold(initialState: S, ((S, E) -> S)): S` + +Essentially, this `fold` is a function that is mapping a flow of Events to the State: + +- `(Flow) -> S` + +We can now use this function `(Flow) -> S` to: + +- contra-map our `decide` function (`(C, S) -> Flow`) over `S` type to: `(C, Flow) -> Flow` - **this is an + event-sourced system** +- or to map our `decide` function (`(C, S) -> Flow`) over `E` type to: `(C, S) -> S` - **this is a state-stored + system** + +
+ +Two functions are wrapped in a datatype class (algebraic data structure), which is generalized with three generic +parameters: + +```kotlin +data class Decider( + val decide: (C, S) -> Flow, + val evolve: (S, E) -> S, +) +``` + +`Decider` is the most important datatype, but it is not the only one. There are others: + +![onion architecture image](.assets/onion.png) + +## Decider + +`Decider` is a datatype that represents the main decision-making algorithm. It belongs to the Domain layer. It has three +generic parameters `C`, `S`, `E` , representing the type of the values that `Decider` may contain or use. +`Decider` can be specialized for any type `C` or `S` or `E` because these types do not affect its +behavior. `Decider` behaves the same for `C`=`Int` or `C`=`YourCustomType`, for example. + +`Decider` is a pure domain component. + +- `C` - Command +- `S` - State +- `E` - Event + +```kotlin +data class Decider( + override val decide: (C, S) -> Flow, + override val evolve: (S, E) -> S, + override val initialState: S +) : IDecider +``` + +Additionally, `initialState` of the Decider is introduced to gain more control over the initial state of the Decider. +Notice that `Decider` implements an interface `IDecider` to communicate the contract. + +
+ Example + +```kotlin +fun restaurantOrderDecider() = Decider( + // Initial state of the Restaurant Order is `null`. It does not exist. + initialState = null, + // Exhaustive command handler(s): for each type of [RestaurantCommand] you are going to publish specific events/facts, as required by the current state/s of the [RestaurantOrder]. + decide = { c, s -> + when (c) { + is CreateRestaurantOrderCommand -> + // ** positive flow ** + if (s == null) flowOf(RestaurantOrderCreatedEvent(c.identifier, c.lineItems, c.restaurantIdentifier)) + // ** negative flow ** + else flowOf(RestaurantOrderRejectedEvent(c.identifier, "Restaurant order already exists")) + is MarkRestaurantOrderAsPreparedCommand -> + // ** positive flow ** + if ((s != null && CREATED == s.status)) flowOf(RestaurantOrderPreparedEvent(c.identifier)) + // ** negative flow ** + else flowOf( + RestaurantOrderNotPreparedEvent( + c.identifier, + "Restaurant order does not exist or not in CREATED state" + ) + ) + null -> emptyFlow() // We ignore the `null` command by emitting the empty flow. Only the Decider that can handle `null` command can be combined (Monoid) with other Deciders. + } + }, + // Exhaustive event-sourcing handler(s): for each event of type [RestaurantEvent] you are going to evolve from the current state/s of the [RestaurantOrder] to a new state of the [RestaurantOrder] + evolve = { s, e -> + when (e) { + is RestaurantOrderCreatedEvent -> RestaurantOrder(e.identifier, e.restaurantId, CREATED, e.lineItems) + is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED) + is RestaurantOrderErrorEvent -> s // Error events are not changing the state / We return current state instead. + null -> s // Null events are not changing the state / We return current state instead. Only the Decider that can handle `null` event can be combined (Monoid) with other Deciders. + } + } +) +``` + +
+ +![decider image](.assets/decider.png) + +### Decider extensions and functions + +#### Contravariant + +- `Decider.mapLeftOnCommand(f: (Cn) -> C): Decider` + +#### Profunctor (Contravariant and Covariant) + +- `Decider.dimapOnEvent(fl: (En) -> E, fr: (E) -> En): Decider` +- `Decider.dimapOnState(fl: (Sn) -> S, fr: (S) -> Sn): Decider` + +#### *Commutative* Monoid + +- ` Decider.combine( + y: Decider + ): Decider, E_SUPER>` + +- with identity element `Decider` + +> A monoid is a type together with a binary operation (`combine`) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. +> +> `combine` operation is also commutative. This means that the order in which deciders are combined does not affect the +> result. + + +We can now construct event-sourcing or/and state-storing aggregate by using the same `decider`. + +### Event-sourcing aggregate + +[Event sourcing aggregate](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EventSourcingAggregate.kt) +is using/delegating a `Decider` to handle commands and produce events. It belongs to the Application layer. In order to +handle the command, aggregate needs to fetch the current state (represented as a list of events) +via `EventRepository.fetchEvents` function, and then delegate the command to the decider which can produce new events as +a result. Produced events are then stored via `EventRepository.save` suspending function. + +![event sourced aggregate](.assets/es-aggregate.png) + +`EventSourcingAggregate` extends `IDecider` and `EventRepository` interfaces, clearly communicating that it is composed +out of these two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`eventSourcingAggregate` function is a good example: + +```kotlin +fun eventSourcingAggregate( + decider: IDecider, + eventRepository: EventRepository +): EventSourcingAggregate = + object : + EventSourcingAggregate, + EventRepository by eventRepository, + IDecider by decider {} +``` + +
+ Example + +```kotlin +typealias RestaurantOrderAggregate = EventSourcingAggregate + +fun restaurantOrderAggregate( + restaurantOrderDecider: RestaurantOrderDecider, + eventRepository: EventRepository +): RestaurantOrderAggregate = eventSourcingAggregate( + decider = restaurantOrderDecider, + eventRepository = eventRepository, +) +``` + +
+ +### State-stored aggregate + +[State stored aggregate](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/StateStoredAggregate.kt) is +using/delegating a `Decider` to handle commands and produce new state. It belongs to the Application layer. In order to +handle the command, aggregate needs to fetch the current state via `StateRepository.fetchState` function first, and then +delegate the command to the decider which can produce new state as a result. New state is then stored +via `StateRepository.save` suspending function. + +![state storedaggregate](.assets/ss-aggregate.png) + +`StateStoredAggregate` extends `IDecider` and `StateRepository` interfaces, clearly communicating that it is composed +out of these two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`stateStoredAggregate` function is a good example: + +```kotlin +fun stateStoredAggregate( + decider: IDecider, + stateRepository: StateRepository +): StateStoredAggregate = + object : + StateStoredAggregate, + StateRepository by stateRepository, + IDecider by decider {} +``` + +
+ Example + +```kotlin +typealias RestaurantOrderAggregate = StateStoredAggregate + +fun restaurantOrderAggregate( + restaurantOrderDecider: RestaurantOrderDecider, + aggregateRepository: StateRepository +): RestaurantOrderAggregate = stateStoredAggregate( + decider = restaurantOrderDecider, + stateRepository = aggregateRepository +) +``` + +
+ +*The logic is orchestrated on the application layer. The components/functions are composed in different ways to support +variety of requirements.* + +![aggregates-application-layer](.assets/aggregates.png) + +Check, [application-vanilla](application-vanilla) and [application-arrow](application-arrow) modules/libraries for +scenarios that are offered out of the box. + +## View + +`View` is a datatype that represents the event handling algorithm, responsible for translating the events into +denormalized state, which is more adequate for querying. It belongs to the Domain layer. It is usually used to create +the view/query side of the CQRS pattern. Obviously, the command side of the CQRS is usually event-sourced aggregate. + +It has two generic parameters `S`, `E`, representing the type of the values that `View` may contain or use. +`View` can be specialized for any type of `S`, `E` because these types do not affect its behavior. +`View` behaves the same for `E`=`Int` or `E`=`YourCustomType`, for example. + +`View` is a pure domain component. + +- `S` - State +- `E` - Event + +```kotlin +data class View( + override val evolve: (S, E) -> S, + override val initialState: S +) : IView +``` + +Notice that `View` implements an interface `IView` to communicate the contract. + +
+ Example + +```kotlin +fun restaurantOrderView() = View( + // Initial state of the [RestaurantOrderViewState] is `null`. It does not exist. + initialState = null, + // Exhaustive event-sourcing handling part: for each event of type [RestaurantOrderEvent] you are going to evolve from the current state/s of the [RestaurantOrderViewState] to a new state of the [RestaurantOrderViewState]. + evolve = { s, e -> + when (e) { + is RestaurantOrderCreatedEvent -> RestaurantOrderViewState( + e.identifier, + e.restaurantId, + CREATED, + e.lineItems + ) + is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED) + is RestaurantOrderErrorEvent -> s // We ignore the `error` event by returning current State/s. + null -> s // We ignore the `null` event by returning current State/s. Only the View that can handle `null` event can be combined (Monoid) with other Views. + + } + } +) +``` + +
+ +![view image](.assets/view.png) + +### View extensions and functions + +#### Contravariant + +- `View.mapLeftOnEvent(f: (En) -> E): View` + +#### Profunctor (Contravariant and Covariant) + +- `View.dimapOnState(fl: (Sn) -> S, fr: (S) -> Sn): View` + +#### *Commutative* Monoid + +- ` View.combine(y: View): View, E_SUPER>` +- with identity element `View` + +> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. +> +> `combine` operation is also commutative. This means that the order in which views are combined does not affect the +> result. + + +We can now construct `materialized` view by using this `view`. + +### Materialized View + +A [Materialized view](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedView.kt) is +using/delegating a `View` to handle events of type `E` and to maintain a state of denormalized projection(s) as a +result. Essentially, it represents the query/view side of the CQRS pattern. It belongs to the Application layer. + +In order to handle the event, materialized view needs to fetch the current state via `ViewStateRepository.fetchState` +suspending function first, and then delegate the event to the view, which can produce new state as a result. New state +is then stored via `ViewStateRepository.save` suspending function. + +`MaterializedView` extends `IView` and `ViewStateRepository` interfaces, clearly communicating that it is composed out +of these two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`materializedView` function is a good example: + +```kotlin +fun materializedView( + view: IView, + viewStateRepository: ViewStateRepository, +): MaterializedView = + object : MaterializedView, ViewStateRepository by viewStateRepository, IView by view {} +``` + +
+ Example + +```kotlin +typealias RestaurantOrderMaterializedView = MaterializedView + +fun restaurantOrderMaterializedView( + restaurantOrderView: RestaurantOrderView, + viewStateRepository: ViewStateRepository +): RestaurantOrderMaterializedView = materializedView( + view = restaurantOrderView, + viewStateRepository = viewStateRepository +) +``` + +
+ +*The logic is orchestrated on the application layer. The components/functions are composed in different ways to support +variety of requirements.* + +![materialized-views-application-layer](.assets/mviews.png) + +Check, [application-vanilla](application-vanilla) and [application-arrow](application-arrow) modules/libraries for +scenarios that are offered out of the box. + +## Saga + +`Saga` is a datatype that represents the central point of control, deciding what to execute next (`A`). It is +responsible for mapping different events from many aggregates into action results `AR` that the `Saga` then can use to +calculate the next actions `A` to be mapped to commands of other aggregates. + +`Saga` is stateless, it does not maintain the state. + +It has two generic parameters `AR`, `A`, representing the type of the values that `Saga` may contain or use. +`Saga` can be specialized for any type of `AR`, `A` because these types do not affect its behavior. +`Saga` behaves the same for `AR`=`Int` or `AR`=`YourCustomType`, for example. + +`Saga` is a pure domain component. + +- `AR` - Action Result +- `A` - Action + +```kotlin +data class Saga( + val react: (AR) -> Flow +) : I_Saga +``` + +Notice that `Saga` implements an interface `ISaga` to communicate the contract. + +
+ Example + +```kotlin + +fun restaurantOrderSaga() = Saga( + react = { e -> + when (e) { + is RestaurantOrderPlacedAtRestaurantEvent -> flowOf( + CreateRestaurantOrderCommand( + e.restaurantOrderId, + e.identifier, + e.lineItems + ) + ) + is RestaurantCreatedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantMenuActivatedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantMenuChangedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantMenuPassivatedEvent -> emptyFlow() // We choose to ignore this event, in our case. + is RestaurantErrorEvent -> emptyFlow() // We choose to ignore this event, in our case. + null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas. + } + } +) + +fun restaurantSaga() = Saga( + react = { e -> + when (e) { + //TODO evolve the example ;), it does not do much at the moment. + is RestaurantOrderCreatedEvent -> emptyFlow() + is RestaurantOrderPreparedEvent -> emptyFlow() + is RestaurantOrderErrorEvent -> emptyFlow() + null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas. + } + } +) + + ``` + +
+ +![saga image](.assets/saga.png) + +### Saga extensions and functions + +#### Contravariant + +- `Saga.mapLeftOnActionResult(f: (ARn) -> AR): Saga` + +#### Covariant + +- `Saga.mapOnAction(f: (A) -> An): Saga` + +#### Monoid + +- ` Saga.combine(y: Saga): Saga` +- with identity element `Saga` + +> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. +> +> `combine` operation is also commutative. This means that the order in which sagas are combined does not affect the +> result. + + +We can now construct `Saga Manager` by using this `saga`. + +### Saga Manager + +[Saga manager](application/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManager.kt) is a stateless process +orchestrator. It is reacting on Action Results of type `AR` and produces new actions `A` based on them. + +Saga manager is using/delegating a `Saga` to react on Action Results of type `AR` and produce new actions `A` which are +going to be published via `ActionPublisher.publish` suspending function. + +It belongs to the Application layer. + +`SagaManager` extends `ISaga` and `ActionPublisher` interfaces, clearly communicating that it is composed out of these +two behaviours. + +The Delegation pattern has proven to be a good alternative to `implementation inheritance`, and Kotlin supports it +natively requiring zero boilerplate code. +`sagaManager` function is a good example: + +```kotlin +fun sagaManager( + saga: ISaga, + actionPublisher: ActionPublisher
+): SagaManager = + object : SagaManager, ActionPublisher by actionPublisher, ISaga by saga {} +``` + +
+ Example + +```kotlin + +typealias OrderRestaurantSagaManager = SagaManager + +fun sagaManager( + restaurantOrderSaga: RestaurantOrderSaga, + restaurantSaga: RestaurantSaga, + actionPublisher: ActionPublisher +): OrderRestaurantSagaManager = sagaManager( + // Combining individual choreography Sagas into one orchestrating Saga. + saga = restaurantOrderSaga.combine(restaurantSaga), + // How and where do you want to publish new commands. + actionPublisher = actionPublisher +) +``` + +
+ +### Experimental features + +#### Actors (only on [JVM](https://github.com/fraktalio/fmodel/tree/main/application-vanilla/src/jvmMain/kotlin/com/fraktalio/fmodel/application)) + +Coroutines can be executed parallelly. It presents all the usual parallelism problems. The main problem being +synchronization of access to shared mutable +state. [Actors](https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html#actors) to the rescue! + +![kotlin actors](.assets/kotlin-actors.png) + +[Dive into the implementation ...](https://github.com/fraktalio/fmodel/tree/main/application-vanilla/src/jvmMain/kotlin/com/fraktalio/fmodel/application) + +```kotlin +private fun CoroutineScope.commandActor( + fanInChannel: SendChannel, + capacity: Int = Channel.RENDEZVOUS, + start: CoroutineStart = CoroutineStart.DEFAULT, + context: CoroutineContext = EmptyCoroutineContext, + handle: (C) -> Flow +) = actor(context, capacity, start) { + for (msg in channel) { + handle(msg).collect { fanInChannel.send(it) } + } +} +``` + +> [Actors](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html) +> are marked as @ObsoleteCoroutinesApi by Kotlin at the moment. + +## Kotlin + +*"Kotlin has both object-oriented and functional constructs. You can use it in both OO and FP styles, or mix elements of +the two. With first-class support for features such as higher-order functions, function types and lambdas, Kotlin is a +great choice if you’re doing or exploring functional programming."* + +## Start using the libraries + +All `fmodel` components/libraries are released to [Maven Central](https://repo1.maven.org/maven2/com/fraktalio/fmodel/) + +### Maven coordinates + +``` + + com.fraktalio.fmodel + domain + 3.5.0 + + + + com.fraktalio.fmodel + application-vanilla + 3.5.0 + + + + com.fraktalio.fmodel + application-arrow + 3.5.0 + +``` + +### Examples + +![decider demo implementation](.assets/decider-impl.png) + +![decider demo test](.assets/decider-test.png) + +- Browse the [tests](domain/src/commonTest/kotlin/com/fraktalio/fmodel/domain/DeciderTest.kt) +- Learn by example on the [playground](https://fraktalio.com/blog/playground) +- Read the [blog](https://fraktalio.com/blog/) +- Check the demos + - [Spring, R2DBC, Event Sourcing, CQRS, Postgres](https://github.com/fraktalio/fmodel-spring-demo) + - [Spring, R2DBC, State-Stored, Postgres](https://github.com/fraktalio/fmodel-spring-state-stored-demo) + - [Ktor, R2DBC, Event Sourcing, CQRS, Postgres](https://github.com/fraktalio/fmodel-ktor-demo) + +### FModel in other languages + +- [FModel in TypeScript](https://github.com/fraktalio/fmodel-ts) +- [FModel in Rust](https://github.com/fraktalio/fmodel-rust) + +## References and further reading + +- https://www.youtube.com/watch?v=kgYGMVDHQHs +- https://www.manning.com/books/functional-and-reactive-domain-modeling +- https://www.manning.com/books/functional-programming-in-kotlin +- https://www.47deg.com/blog/functional-domain-modeling/ +- https://www.47deg.com/blog/functional-domain-modeling-part-2/ +- https://www.youtube.com/watch?v=I8LbkfSSR58&list=PLbgaMIhjbmEnaH_LTkxLI7FMa2HsnawM_ + +## Credits + +Special credits to `Jérémie Chassaing` for sharing his [research](https://www.youtube.com/watch?v=kgYGMVDHQHs) +and `Adam Dymitruk` for hosting the meetup. + +--- +Created with :heart: by [Fraktalio](https://fraktalio.com/) diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..8fbd4a0 --- /dev/null +++ b/nuget.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Fraktalio.Contracts/Fraktalio.Contracts.csproj b/src/Fraktalio.Contracts/Fraktalio.Contracts.csproj new file mode 100644 index 0000000..e0673e6 --- /dev/null +++ b/src/Fraktalio.Contracts/Fraktalio.Contracts.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Fraktalio.Contracts/ISaga.cs b/src/Fraktalio.Contracts/ISaga.cs new file mode 100644 index 0000000..fed704b --- /dev/null +++ b/src/Fraktalio.Contracts/ISaga.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; + +namespace Fraktalio.Contracts; + +using System.Collections.Generic; + +/// +/// An interface of the Saga +/// +/// Action Result type +/// Action Type +[PublicAPI] +public interface ISaga +{ + IEnumerable
React(AR actionResult); +} diff --git a/src/Fraktalio.Contracts/packages.lock.json b/src/Fraktalio.Contracts/packages.lock.json new file mode 100644 index 0000000..733402a --- /dev/null +++ b/src/Fraktalio.Contracts/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "JetBrains.Annotations": { + "type": "Direct", + "requested": "[2023.3.0, )", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + } + } + } +} \ No newline at end of file diff --git a/src/Fraktalio/Fraktalio.csproj b/src/Fraktalio/Fraktalio.csproj new file mode 100644 index 0000000..4595f12 --- /dev/null +++ b/src/Fraktalio/Fraktalio.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Fraktalio/Saga.cs b/src/Fraktalio/Saga.cs new file mode 100644 index 0000000..3684dbb --- /dev/null +++ b/src/Fraktalio/Saga.cs @@ -0,0 +1,30 @@ +using Fraktalio.Contracts; + +namespace Fraktalio; + +/// +/// Saga is a datatype that represents the central point of control deciding what to execute next ([A]) +/// It is responsible for mapping different events into action results ([AR]) that the [Saga] then can use to calculate the next actions ([A]) to be mapped to command(s). +/// +/// Saga does not maintain the state. +/// +/// A function/lambda that takes input state of type [AR], and returns the flow of actions. +/// Action Result type +/// Action type +public class Saga(Func> react) : ISaga +{ + public IEnumerable React(AR actionResult) + { + return react(actionResult); + } + + public Saga MapLeftOnActionResult(Func f) + { + return new Saga(arn => react(f(arn))); + } + + public Saga MapOnAction(Func f) + { + return new Saga(ar => react(ar).Select(f)); + } +} diff --git a/src/Fraktalio/SagaExtensions.cs b/src/Fraktalio/SagaExtensions.cs new file mode 100644 index 0000000..d40382f --- /dev/null +++ b/src/Fraktalio/SagaExtensions.cs @@ -0,0 +1,35 @@ +namespace Fraktalio; + +public static class SagaExtensions +{ + /// + /// Combines [Saga]s into one [Saga] + /// + /// Specially convenient when: + /// - [AR] and [AR2] have common superclass [AR_SUPER], or + /// - [A] and [A2] have common superclass [A_SUPER] + /// + /// first saga + /// second saga + /// Action Result (usually event) of the first Saga + /// Action (usually command) of the first Saga + /// Action Result (usually event) of the second Saga + /// Action (usually command) of the second Saga + /// common superclass for [AR] and [AR2] + /// common superclass for [A] and [A2] + /// new Saga of type Saga`[AR_SUPER], [A_SUPER]>` + public static Saga Combine(this Saga sagaX, Saga sagaY) + where AR : AR_SUPER + where A : A_SUPER + where AR2 : AR_SUPER + where A2 : A_SUPER + { + var newSagaX = sagaX.MapLeftOnActionResult(it => it is AR ar ? ar : default) + .MapOnAction(it => it); + + var newSagaY = sagaY.MapLeftOnActionResult(it => it is AR2 ar2 ? ar2 : default) + .MapOnAction(it => it); + + return new Saga(eitherAr => newSagaX.React(eitherAr).Concat(newSagaY.React(eitherAr))); + } +} diff --git a/src/Fraktalio/SagaFactory.cs b/src/Fraktalio/SagaFactory.cs new file mode 100644 index 0000000..39b745f --- /dev/null +++ b/src/Fraktalio/SagaFactory.cs @@ -0,0 +1,16 @@ +namespace Fraktalio; + +public static class SagaFactory +{ + /// + /// Saga DSL - A convenient builder DSL for the see cref="Saga{AR,A}"/> + /// + /// + /// + /// + /// + public static Saga Create(Func> react) + { + return new Saga(react); + } +} diff --git a/src/Fraktalio/packages.lock.json b/src/Fraktalio/packages.lock.json new file mode 100644 index 0000000..3ad82aa --- /dev/null +++ b/src/Fraktalio/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "fraktalio.contracts": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2023.3.0, )" + } + }, + "JetBrains.Annotations": { + "type": "CentralTransitive", + "requested": "[2023.3.0, )", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + } + } + } +} \ No newline at end of file diff --git a/test/Fraktalio.Tests/EnumerableExtensions.cs b/test/Fraktalio.Tests/EnumerableExtensions.cs new file mode 100644 index 0000000..a138784 --- /dev/null +++ b/test/Fraktalio.Tests/EnumerableExtensions.cs @@ -0,0 +1,12 @@ +using FluentAssertions; + +namespace Fraktalio.Tests; + +public static class EnumerableExtensions +{ + public static void ExpectActions(this IEnumerable flow, params A[] expected) + { + var list = flow.ToList(); + list.Should().BeEquivalentTo(expected); + } +} diff --git a/test/Fraktalio.Tests/Examples/NumberSagaFactory.cs b/test/Fraktalio.Tests/Examples/NumberSagaFactory.cs new file mode 100644 index 0000000..57a48e1 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/NumberSagaFactory.cs @@ -0,0 +1,137 @@ +using Fraktalio.Tests.Examples.Numbers; + +namespace Fraktalio.Tests.Examples; + +public static class NumberSagaFactory +{ + public static Saga CreateNumberSaga() + { + var react = NumberSaga; + return SagaFactory.Create(react); + } + + public static Saga CreateEvenNumberSaga() + { + var react = EvenNumberSaga; + return SagaFactory.Create(react); + } + + public static Saga CreateOddNumberSaga() + { + var react = OddNumberSaga; + return SagaFactory.Create(react); + } + + /// + /// Very simple Number saga, just for fun ;) + /// + /// It reacts on Action Results of type of any Event (Even or Odd) and issue a Command/Action (Odd or Even) + /// For example if the EvenNumberAdded happened with value 4, a new command of type AddOddNumber will be published with value EvenNumberAdded-1=3 + /// + /// The event + /// List of commands + private static IEnumerable NumberSaga(NumberEvent numberEvent) + { + return numberEvent switch + { + NumberEvent.EvenNumberEvent.EvenNumberAdded evenNumberAdded => new NumberCommand[] + { + new NumberCommand.OddNumberCommand.AddOddNumber( + new Description($"{evenNumberAdded.Value.Value - 1}"), + new NumberValue(evenNumberAdded.Value.Value - 1) + ) + }, + + NumberEvent.EvenNumberEvent.EvenNumberSubtracted evenNumberSubtracted => new NumberCommand[] + { + new NumberCommand.OddNumberCommand.SubtractOddNumber( + new Description($"{evenNumberSubtracted.Value.Value - 1}"), + new NumberValue(evenNumberSubtracted.Value.Value - 1) + ) + }, + + NumberEvent.OddNumberEvent.OddNumberAdded oddNumberAdded => new NumberCommand[] + { + new NumberCommand.EvenNumberCommand.AddEvenNumber( + new Description($"{oddNumberAdded.Value.Value + 1}"), + new NumberValue(oddNumberAdded.Value.Value + 1) + ) + }, + + NumberEvent.OddNumberEvent.OddNumberSubtracted oddNumberSubtracted => new NumberCommand[] + { + new NumberCommand.EvenNumberCommand.SubtractEvenNumber( + new Description($"{oddNumberSubtracted.Value.Value + 1}"), + new NumberValue(oddNumberSubtracted.Value.Value + 1) + ) + }, + + _ => Enumerable.Empty() + }; + } + + /// + /// Even number saga + /// + /// It reacts on Action Results of type of any [NumberEvent.EvenNumberEvent] and issue a Command/Action of type [NumberCommand.OddNumberCommand] + /// + /// The event + /// List of commands + private static IEnumerable EvenNumberSaga( + this NumberEvent.EvenNumberEvent? numberEvent) + { + return numberEvent switch + { + NumberEvent.EvenNumberEvent.EvenNumberAdded evenNumberAdded => new NumberCommand.OddNumberCommand[] + { + new NumberCommand.OddNumberCommand.AddOddNumber( + new Description($"{evenNumberAdded.Value.Value - 1}"), + new NumberValue(evenNumberAdded.Value.Value - 1) + ) + }, + + NumberEvent.EvenNumberEvent.EvenNumberSubtracted evenNumberSubtracted => new NumberCommand.OddNumberCommand + [] + { + new NumberCommand.OddNumberCommand.SubtractOddNumber( + new Description($"{evenNumberSubtracted.Value.Value - 1}"), + new NumberValue(evenNumberSubtracted.Value.Value - 1) + ) + }, + + _ => Enumerable.Empty() + }; + } + + /// + /// Odd number saga + /// + /// It reacts on Action Results of type of any [NumberEvent.OddNumberEvent] and issue a Command/Action of type [NumberCommand.EvenNumberCommand] + /// + /// The event + /// List of commands + public static IEnumerable OddNumberSaga( + this NumberEvent.OddNumberEvent? numberEvent) + { + return numberEvent switch + { + NumberEvent.OddNumberEvent.OddNumberAdded oddNumberAdded => new NumberCommand.EvenNumberCommand[] + { + new NumberCommand.EvenNumberCommand.AddEvenNumber( + new Description($"{oddNumberAdded.Value.Value + 1}"), + new NumberValue(oddNumberAdded.Value.Value + 1) + ) + }, + + NumberEvent.OddNumberEvent.OddNumberSubtracted oddNumberSubtracted => new NumberCommand.EvenNumberCommand[] + { + new NumberCommand.EvenNumberCommand.SubtractEvenNumber( + new Description($"{oddNumberSubtracted.Value.Value + 1}"), + new NumberValue(oddNumberSubtracted.Value.Value + 1) + ) + }, + + _ => Enumerable.Empty() + }; + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/AddEvenNumberBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/AddEvenNumberBuilder.cs new file mode 100644 index 0000000..21f2f56 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/AddEvenNumberBuilder.cs @@ -0,0 +1,32 @@ +namespace Fraktalio.Tests.Examples.Numbers.Builders; + +public class AddEvenNumberBuilder +{ + private Description DescriptionValue { get; set; } = new(""); + private NumberValue NumberValue { get; set; } = new(0); + + public void Description(Func lambda) + { + DescriptionValue = lambda(); + } + + public void Value(Func lambda) + { + NumberValue = lambda(); + } + + public NumberCommand.EvenNumberCommand.AddEvenNumber Build() + { + return new NumberCommand.EvenNumberCommand.AddEvenNumber(DescriptionValue, NumberValue); + } +} + +public static class AddEvenNumberExtensions +{ + public static NumberCommand.EvenNumberCommand.AddEvenNumber AddEvenNumber(this AddEvenNumberBuilder builder, Action block) + { + block(builder); + return builder.Build(); + } +} + diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberAddedBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberAddedBuilder.cs new file mode 100644 index 0000000..add07c3 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberAddedBuilder.cs @@ -0,0 +1,32 @@ +namespace Fraktalio.Tests.Examples.Numbers.Builders; + +public class EvenNumberAddedBuilder +{ + private Description DescriptionValue { get; set; } = new(""); + private NumberValue NumberValue { get; set; } = new(0); + + public void Description(Func lambda) + { + DescriptionValue = lambda(); + } + + public void Value(Func lambda) + { + NumberValue = lambda(); + } + + public NumberEvent.EvenNumberEvent.EvenNumberAdded Build() + { + return new NumberEvent.EvenNumberEvent.EvenNumberAdded(DescriptionValue, NumberValue); + } +} + +public static class EvenNumberAddedExtensions +{ + public static NumberEvent.EvenNumberEvent.EvenNumberAdded EvenNumberAdded(this EvenNumberAddedBuilder builder, + Action block) + { + block(builder); + return builder.Build(); + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberStateBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberStateBuilder.cs new file mode 100644 index 0000000..ec01c5c --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberStateBuilder.cs @@ -0,0 +1,41 @@ +namespace Fraktalio.Tests.Examples.Numbers.Builders; + +public class EvenNumberStateBuilder +{ + private Description DescriptionValue { get; set; } = new(""); + private NumberValue NumberValue { get; set; } = new(0); + + public void Description(Func lambda) + { + DescriptionValue = lambda(); + } + + public void DescriptionString(Func lambda) + { + DescriptionValue = new Description(lambda()); + } + + public void Value(Func lambda) + { + NumberValue = lambda(); + } + + public void ValueInt(Func lambda) + { + NumberValue = new NumberValue(lambda()); + } + + public EvenNumberState Build() + { + return new EvenNumberState(DescriptionValue, NumberValue); + } +} + +public static class EvenNumberStateExtensions +{ + public static EvenNumberState EvenNumberState(this EvenNumberStateBuilder builder, Action block) + { + block(builder); + return builder.Build(); + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberSubtractedBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberSubtractedBuilder.cs new file mode 100644 index 0000000..d7e4ce7 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/EvenNumberSubtractedBuilder.cs @@ -0,0 +1,32 @@ +namespace Fraktalio.Tests.Examples.Numbers.Builders; + +public class EvenNumberSubtractedBuilder +{ + private Description DescriptionValue { get; set; } = new(""); + private NumberValue NumberValue { get; set; } = new(0); + + public void Description(Func lambda) + { + DescriptionValue = lambda(); + } + + public void Value(Func lambda) + { + NumberValue = lambda(); + } + + public NumberEvent.EvenNumberEvent.EvenNumberSubtracted Build() + { + return new NumberEvent.EvenNumberEvent.EvenNumberSubtracted(DescriptionValue, NumberValue); + } +} + +public static class EvenNumberSubtractedExtensions +{ + public static NumberEvent.EvenNumberEvent.EvenNumberSubtracted EvenNumberSubtracted(this EvenNumberSubtractedBuilder builder, + Action block) + { + block(builder); + return builder.Build(); + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/OddNumberStateBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/OddNumberStateBuilder.cs new file mode 100644 index 0000000..d3263b1 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/OddNumberStateBuilder.cs @@ -0,0 +1,41 @@ +namespace Fraktalio.Tests.Examples.Numbers.Builders; + +public class OddNumberStateBuilder +{ + private Description DescriptionValue { get; set; } = new(""); + private NumberValue NumberValue { get; set; } = new(0); + + public void Description(Func lambda) + { + DescriptionValue = lambda(); + } + + public void DescriptionString(Func lambda) + { + DescriptionValue = new Description(lambda()); + } + + public void Value(Func lambda) + { + NumberValue = lambda(); + } + + public void ValueInt(Func lambda) + { + NumberValue = new NumberValue(lambda()); + } + + public OddNumberState Build() + { + return new OddNumberState(DescriptionValue, NumberValue); + } +} + +public static class OddNumberStateExtensions +{ + public static OddNumberState OddNumberState(this OddNumberStateBuilder builder, Action block) + { + block(builder); + return builder.Build(); + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/Builders/SubstractEvenNumberBuilder.cs b/test/Fraktalio.Tests/Examples/Numbers/Builders/SubstractEvenNumberBuilder.cs new file mode 100644 index 0000000..0527ac9 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Builders/SubstractEvenNumberBuilder.cs @@ -0,0 +1,32 @@ +namespace Fraktalio.Tests.Examples.Numbers.Builders; + +public class SubtractEvenNumberBuilder +{ + private Description DescriptionValue { get; set; } = new(""); + private NumberValue NumberValue { get; set; } = new(0); + + public void Description(Func lambda) + { + DescriptionValue = lambda(); + } + + public void Value(Func lambda) + { + NumberValue = lambda(); + } + + public NumberCommand.EvenNumberCommand.SubtractEvenNumber Build() + { + return new NumberCommand.EvenNumberCommand.SubtractEvenNumber(DescriptionValue, NumberValue); + } +} + +public static class SubtractEvenNumberExtensions +{ + public static NumberCommand.EvenNumberCommand.SubtractEvenNumber SubtractEvenNumber(this SubtractEvenNumberBuilder builder, Action block) + { + block(builder); + return builder.Build(); + } +} + diff --git a/test/Fraktalio.Tests/Examples/Numbers/Description.cs b/test/Fraktalio.Tests/Examples/Numbers/Description.cs new file mode 100644 index 0000000..5165bec --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/Description.cs @@ -0,0 +1,17 @@ +namespace Fraktalio.Tests.Examples.Numbers; + +public record Description(string Value) +{ + public static Description operator +(Description a, Description b) => + new($"{a.Value} + {b.Value}"); + + public static Description operator -(Description a, Description b) => + new($"{a.Value} - {b.Value}"); + + public static implicit operator string(Description value) => value.Value; +} + +public static class DescriptionFactory +{ + public static Description Description(Func block) => new(block()); +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberCommand.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberCommand.cs new file mode 100644 index 0000000..714ac85 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/NumberCommand.cs @@ -0,0 +1,37 @@ +namespace Fraktalio.Tests.Examples.Numbers; + +public abstract class NumberCommand +{ + public abstract Description Description { get; } + public abstract NumberValue Value { get; } + + public abstract class EvenNumberCommand : NumberCommand + { + public sealed class AddEvenNumber(Description description, NumberValue value) : EvenNumberCommand + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + + public sealed class SubtractEvenNumber(Description description, NumberValue value) : EvenNumberCommand + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + } + + public abstract class OddNumberCommand : NumberCommand + { + public sealed class AddOddNumber(Description description, NumberValue value) : OddNumberCommand + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + + public sealed class SubtractOddNumber(Description description, NumberValue value) : OddNumberCommand + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberEvent.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberEvent.cs new file mode 100644 index 0000000..73566b3 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/NumberEvent.cs @@ -0,0 +1,37 @@ +namespace Fraktalio.Tests.Examples.Numbers; + +public abstract class NumberEvent +{ + public abstract Description Description { get; } + public abstract NumberValue Value { get; } + + public abstract class EvenNumberEvent : NumberEvent + { + public sealed class EvenNumberAdded(Description description, NumberValue value) : EvenNumberEvent + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + + public sealed class EvenNumberSubtracted(Description description, NumberValue value) : EvenNumberEvent + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + } + + public abstract class OddNumberEvent : NumberEvent + { + public sealed class OddNumberAdded(Description description, NumberValue value) : OddNumberEvent + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + + public sealed class OddNumberSubtracted(Description description, NumberValue value) : OddNumberEvent + { + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; + } + } +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberState.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberState.cs new file mode 100644 index 0000000..9db2e09 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/NumberState.cs @@ -0,0 +1,19 @@ +namespace Fraktalio.Tests.Examples.Numbers; + +public abstract class NumberState +{ + public abstract Description Description { get; } + public abstract NumberValue Value { get; } +} + +public sealed class EvenNumberState(Description description, NumberValue value) : NumberState +{ + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; +} + +public sealed class OddNumberState(Description description, NumberValue value) : NumberState +{ + public override Description Description { get; } = description; + public override NumberValue Value { get; } = value; +} diff --git a/test/Fraktalio.Tests/Examples/Numbers/NumberValue.cs b/test/Fraktalio.Tests/Examples/Numbers/NumberValue.cs new file mode 100644 index 0000000..6a7a2d7 --- /dev/null +++ b/test/Fraktalio.Tests/Examples/Numbers/NumberValue.cs @@ -0,0 +1,17 @@ +namespace Fraktalio.Tests.Examples.Numbers; + +public record NumberValue(int Value) +{ + public static Description operator +(NumberValue a, NumberValue b) => + new($"{a.Value} + {b.Value}"); + + public static Description operator -(NumberValue a, NumberValue b) => + new($"{a.Value} - {b.Value}"); + + public static implicit operator int(NumberValue value) => value.Value; +} + +public static class NumberValueFactory +{ + public static NumberValue Value(Func block) => new(block()); +} diff --git a/test/Fraktalio.Tests/Fraktalio.Tests.csproj b/test/Fraktalio.Tests/Fraktalio.Tests.csproj new file mode 100644 index 0000000..d118379 --- /dev/null +++ b/test/Fraktalio.Tests/Fraktalio.Tests.csproj @@ -0,0 +1,29 @@ + + + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/test/Fraktalio.Tests/GlobalUsings.cs b/test/Fraktalio.Tests/GlobalUsings.cs new file mode 100644 index 0000000..3244567 --- /dev/null +++ b/test/Fraktalio.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/test/Fraktalio.Tests/SagaExtensions.cs b/test/Fraktalio.Tests/SagaExtensions.cs new file mode 100644 index 0000000..ce6ba81 --- /dev/null +++ b/test/Fraktalio.Tests/SagaExtensions.cs @@ -0,0 +1,11 @@ +using Fraktalio.Contracts; + +namespace Fraktalio.Tests; + +public static class SagaExtensions +{ + public static IEnumerable WhenActionResult(this ISaga saga, AR actionResults) + { + return saga.React(actionResults); + } +} diff --git a/test/Fraktalio.Tests/SagaTest.cs b/test/Fraktalio.Tests/SagaTest.cs new file mode 100644 index 0000000..3c13da4 --- /dev/null +++ b/test/Fraktalio.Tests/SagaTest.cs @@ -0,0 +1,22 @@ +using Fraktalio.Tests.Examples; +using Fraktalio.Tests.Examples.Numbers; + +namespace Fraktalio.Tests; + +public class SagaTest +{ + [Test] + public void EvenSagaTest() + { + var evenSaga = NumberSagaFactory.CreateEvenNumberSaga(); + + evenSaga.WhenActionResult( + new NumberEvent.EvenNumberEvent.EvenNumberAdded(new Description("2"), new NumberValue(2))) + .ExpectActions( + new NumberCommand.OddNumberCommand.AddOddNumber( + new Description("1"), + new NumberValue(1) + ) + ); + } +} diff --git a/test/Fraktalio.Tests/packages.lock.json b/test/Fraktalio.Tests/packages.lock.json new file mode 100644 index 0000000..7188ea2 --- /dev/null +++ b/test/Fraktalio.Tests/packages.lock.json @@ -0,0 +1,200 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[6.12.0, )", + "resolved": "6.12.0", + "contentHash": "ZXhHT2YwP9lajrwSKbLlFqsmCCvFJMoRSK9t7sImfnCyd0OB3MhgxdoMcVqxbq1iyxD6mD2fiackWmBb7ayiXQ==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.4.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.8.0, )", + "resolved": "17.8.0", + "contentHash": "BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==", + "dependencies": { + "Microsoft.CodeCoverage": "17.8.0", + "Microsoft.TestPlatform.TestHost": "17.8.0" + } + }, + "NUnit": { + "type": "Direct", + "requested": "[3.14.0, )", + "resolved": "3.14.0", + "contentHash": "R7iPwD7kbOaP3o2zldWJbWeMQAvDKD0uld27QvA3PAALl1unl7x0v2J7eGiJOYjimV/BuGT4VJmr45RjS7z4LA==", + "dependencies": { + "NETStandard.Library": "2.0.0" + } + }, + "NUnit.Analyzers": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "Odd1RusSMnfswIiCPbokAqmlcCCXjQ20poaXWrw+CWDnBY1vQ/x6ZGqgyJXpebPq5Uf8uEBe5iOAySsCdSrWdQ==" + }, + "NUnit3TestAdapter": { + "type": "Direct", + "requested": "[4.5.0, )", + "resolved": "4.5.0", + "contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A==" + }, + "Verify.NUnit": { + "type": "Direct", + "requested": "[22.5.0, )", + "resolved": "22.5.0", + "contentHash": "zCf6FaOdyJaOp01wUja9JgREVLICt0XwzbtnQPrrwzXaooyQyrAJrhAOc+hY1nO4eisDfJ15kDqv3HUfzIfdJg==", + "dependencies": { + "EmptyFiles": "5.0.0", + "NUnit": "3.14.0", + "Verify": "22.5.0" + } + }, + "Argon": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "KTbzEEvCC6QX0eEO6C49WBeHdkGT0Hq1EeCk7gd0GhTK+fMpRr8DvmDaqDFK58Fvvwo0dx7K5NhFUexXv+aVuQ==" + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "13.0.0", + "contentHash": "FVGXLxBFCULWVA3Esqi6IwE1HRTMpNmRyAH7KRobY+wo8BwwSNmE9a8znoY0UXoHwN1FTQFtgW+NiGYIZZ8FgQ==", + "dependencies": { + "EmptyFiles": "4.6.0", + "System.Management": "6.0.2" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "HGmJfvJ+8htlyvHKdaN7olLch/U3nrYa0rJ0xK7mKNja4YANdhcQEHfHzT93ehgG9TAUSj1MJaXZIB62HHEuiQ==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==", + "dependencies": { + "NuGet.Frameworks": "6.5.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.8.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" + }, + "SimpleInfoName": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "jZwJajqdF56n0HcwvM8wqrwhr8tBqp7E3dWVoa5XWMC4tqSXP4+rIfYipeVIavRUI5MSj5vYPCdwtF3h4RJvxA==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.2", + "contentHash": "s6c9x2Kghd+ncEDnT6ApYVOacDXr/Y57oSUSx6wjegMOfKxhtrXn3PdASPNU59y3kB9OJ1yb3l5k6uKr3bhqew==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" + }, + "Verify": { + "type": "Transitive", + "resolved": "22.5.0", + "contentHash": "E8KnPsAqtNdB8Sr++0/zGw5vyk7j9nBYLsJflAZnA6xG5ndaiq5+EUtjcM+JQetirLnE9f4wEU7/RnIDCnW//Q==", + "dependencies": { + "Argon": "0.13.0", + "DiffEngine": "13.0.0", + "EmptyFiles": "5.0.0", + "SimpleInfoName": "2.2.0", + "System.IO.Hashing": "8.0.0" + } + }, + "fraktalio": { + "type": "Project", + "dependencies": { + "Fraktalio.Contracts": "[1.0.0, )" + } + }, + "fraktalio.contracts": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2023.3.0, )" + } + }, + "JetBrains.Annotations": { + "type": "CentralTransitive", + "requested": "[2023.3.0, )", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + } + } + } +} \ No newline at end of file