diff --git a/.editorconfig b/.editorconfig index 510e1b7..beeadd0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,13 +10,10 @@ indent_style = space [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true -charset = utf-8-bom - ############################### # .NET Coding Conventions # ############################### -[*.{cs,vb}] # Organize usings dotnet_sort_system_directives_first = true @@ -63,16 +60,11 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Use PascalCase for constant fields dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_accessibilities = * dotnet_naming_symbols.constant_fields.required_modifiers = const -# SA1309: A field name in C# begins with an underscore -dotnet_diagnostic.SA1309.severity = warning - -# SA1200: Using directive should appear within a namespace declaration -dotnet_diagnostic.SA1200.severity = none ############################### # C# Coding Conventions # @@ -81,7 +73,16 @@ dotnet_diagnostic.SA1200.severity = none # IDE0060: Remove unused parameter dotnet_code_quality_unused_parameters = all:warning -[*.cs] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# IDE0059: Unnecessary assignment of a value +csharp_style_unused_value_assignment_preference = discard_variable:warning + # var preferences csharp_style_var_for_built_in_types = true:warning csharp_style_var_when_type_is_apparent = true:warning @@ -94,6 +95,8 @@ csharp_style_expression_bodied_operators = true:warning csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning # Pattern matching preferences csharp_style_pattern_matching_over_is_with_cast_check = true:warning @@ -113,9 +116,14 @@ csharp_prefer_simple_default_expression = true:warning csharp_style_pattern_local_over_anonymous_function = true:warning csharp_style_inlined_variable_declaration = true:warning +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent + # IDE0057: Use range operator csharp_style_prefer_range_operator = true:warning +csharp_style_namespace_declarations = file_scoped:warning + ############################### # C# Formatting Rules # ############################### @@ -149,7 +157,6 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_blocks = true - # IDE0051: Remove unused private members dotnet_diagnostic.IDE0051.severity = warning @@ -162,17 +169,31 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning # SA1503: Braces should not be omitted dotnet_diagnostic.SA1503.severity = warning -# SA1101: Prefix local calls with this +# Prefix local calls with this dotnet_diagnostic.SA1101.severity = none -# SA1116: Split parameters should start on line after declaration +# Split parameters should start on line after declaration dotnet_diagnostic.SA1116.severity = warning -# SA1516: Elements should be separated by blank line +# Elements should be separated by blank line dotnet_diagnostic.SA1516.severity = none -# SA1011: Closing square brackets should be spaced correctly +# Closing square brackets should be spaced correctly dotnet_diagnostic.SA1011.severity = none -# SA0001: XML comment analysis is disabled due to project configuration +# XML comment analysis is disabled due to project configuration dotnet_diagnostic.SA0001.severity = none + +# An element within a C# code file is out of order within regard to access level, in relation to other elements in the code +dotnet_diagnostic.SA1202.severity = warning + +dotnet_diagnostic.CA1852.severity = warning + +# Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)' +dotnet_diagnostic.CA1853.severity = warning + +csharp_prefer_simple_using_statement = true:suggestion + +# Using directives must be placed inside of a namespace declaration +# IDE0065: Misplaced using directive +csharp_using_directive_placement = inside_namespace diff --git a/.github/workflows/add-new-issue-to-project.yml b/.github/workflows/add-new-issue-to-project.yml new file mode 100644 index 0000000..59adce2 --- /dev/null +++ b/.github/workflows/add-new-issue-to-project.yml @@ -0,0 +1,18 @@ +name: 🤖Add New Issue To Project + + +on: + issues: + types: opened + + +jobs: + add_new_issue_to_project: + name: Add New Issue + uses: KinsonDigital/Infrastructure/.github/workflows/add-issue-to-project.yml@v9.1.0 + with: + org-name: "${{ vars.ORGANIZATION_NAME }}" + org-project-name: "${{ vars.ORG_PROJECT_NAME }}" + project-name: "${{ vars.PROJECT_NAME }}" + secrets: + cicd-pat: ${{ secrets.CICD_TOKEN }} diff --git a/.github/workflows/build-pr-status-check.yml b/.github/workflows/build-pr-status-check.yml new file mode 100644 index 0000000..17e8a4f --- /dev/null +++ b/.github/workflows/build-pr-status-check.yml @@ -0,0 +1,32 @@ +name: ✅Build Status Check +run-name: ✅Build Status Check ${{ github.base_ref == 'main' && '(Release Build)' || '(Debug Build)' }} + + +defaults: + run: + shell: pwsh + + +on: + workflow_dispatch: + pull_request: + branches: [main, preview] + + +jobs: + main_build_status_check: + name: ${{ vars.PROJECT_NAME }} Build Status Check + uses: KinsonDigital/Infrastructure/.github/workflows/build-csharp-project.yml@v9.1.0 + with: + project-name: "${{ vars.PROJECT_NAME }}" + build-config: Debug + net-sdk-version: "${{ vars.NET_SDK_VERSION }}" + + + perf_build_status_check: + name: ${{ vars.PROJECT_NAME }} Perf Tests Build Status Check + uses: KinsonDigital/Infrastructure/.github/workflows/build-csharp-project.yml@v9.1.0 + with: + project-name: "${{ vars.PROJECT_NAME }}PerfTests" + build-config: Debug + net-sdk-version: "${{ vars.NET_SDK_VERSION }}" diff --git a/.github/workflows/prev-release.yml b/.github/workflows/prev-release.yml new file mode 100644 index 0000000..8c58a66 --- /dev/null +++ b/.github/workflows/prev-release.yml @@ -0,0 +1,31 @@ +name: 🚀Preview Release + + +defaults: + run: + shell: pwsh + + +on: + workflow_dispatch: + + +jobs: + preview_release: + name: Preview Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Restore DotNet Tools + run: dotnet tool restore + + - name: Run Preview Release + run: dotnet cicd PreviewRelease --skip-twitter-announcement + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NugetOrgApiKey: ${{ secrets.NUGET_ORG_API_KEY }} + TwitterConsumerApiKey: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TwitterConsumerApiSecret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TwitterAccessToken: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TwitterAccessTokenSecret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/.github/workflows/sonar-scan-pr-status-check.yml b/.github/workflows/sonar-scan-pr-status-check.yml new file mode 100644 index 0000000..3368f6d --- /dev/null +++ b/.github/workflows/sonar-scan-pr-status-check.yml @@ -0,0 +1,66 @@ +name: ✅Sonar Scan Status Check + + +defaults: + run: + shell: pwsh + + +on: + workflow_dispatch: + pull_request: + branches: [main, preview] + + +jobs: + sonar_analyze_code: + name: Analyze Code + runs-on: ubuntu-latest + steps: + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: 'zulu' # Alternative distribution options are available. + + - uses: actions/setup-dotnet@v3 + name: Setup dotnet + with: + dotnet-version: '7.x.x' + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v3 + with: + path: ./.sonar/scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + + - name: Install SonarCloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + run: | + New-Item -Path ./.sonar/scanner -ItemType Directory + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + + - name: Build and Analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + ./.sonar/scanner/dotnet-sonarscanner begin /k:"KinsonDigital_Carbonate" /o:"kinsondigital" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + + dotnet clean "${{ github.workspace }}/Carbonate/Carbonate.csproj"; + dotnet build "${{ github.workspace }}/Carbonate/Carbonate.csproj" -c Debug; + + ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/.github/workflows/sync-bot.yml b/.github/workflows/sync-bot.yml new file mode 100644 index 0000000..760d998 --- /dev/null +++ b/.github/workflows/sync-bot.yml @@ -0,0 +1,51 @@ +name: 🤖Sync Bot + + +defaults: + run: + shell: pwsh + + +on: + issues: + types: [labeled, unlabeled, assigned, unassigned, milestoned, demilestoned] + + +jobs: + sync_bot: + name: Sync Bot + if: ${{ !github.event.issue.pull_request }} + runs-on: ubuntu-latest + steps: + - name: Set Up Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Run Sync Bot (Issue Change) + run: | + $scriptUrl = "${{ vars.SCRIPT_BASE_URL }}/${{ vars.CICD_SCRIPTS_VERSION }}/${{ vars.SCRIPT_RELATIVE_DIR_PATH}}/sync-bot-status-check.ts"; + + $issueNumber = "${{ github.event.issue.number }}"; + + Write-Host "::notice::Project Name: ${{ vars.PROJECT_NAME }}"; + Write-Host "::notice::Issue: $issueNumber"; + + if ($manuallyExecuted -and $issueNumber -eq "0") { + Write-Host "::error::The issue or PR number must be a value greater than 0."; + exit 1; + } + + <# Deno Args: + 1. Project name + 2. Issue number + 3. Event type - set to issue event type + 4. PAT + #> + deno run ` + --allow-net ` + "$scriptUrl" ` + "${{ vars.PROJECT_NAME }}" ` + "$issueNumber" ` + "issue" ` + "cicd-pat: ${{ secrets.CICD_TOKEN }}"; diff --git a/.github/workflows/sync-issue-to-pr.yml b/.github/workflows/sync-issue-to-pr.yml new file mode 100644 index 0000000..eaa148f --- /dev/null +++ b/.github/workflows/sync-issue-to-pr.yml @@ -0,0 +1,59 @@ +name: 🔄️Sync Issue To PR + + +defaults: + run: + shell: pwsh + + +on: + pull_request: + types: opened + issue_comment: # This event is triggered when creating issue and pr comments + types: created + + +jobs: + sync_issue_to_pr: + name: Start Sync Process + if: | + github.event_name == 'issue_comment' || + github.event_name == 'pull_request' && startsWith(github.head_ref, 'feature/') + runs-on: ubuntu-latest + steps: + - name: Set Up Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Sync + run: | + $eventName = "${{ github.event_name }}"; + $scriptUrl = "${{ vars.SCRIPT_BASE_URL }}/${{ vars.CICD_SCRIPTS_VERSION }}/${{ vars.SCRIPT_RELATIVE_DIR_PATH }}/sync-issue-to-pr.ts"; + $prNumber = $eventName -eq "pull_request" ? "${{ github.event.number }}" : "${{ github.event.issue.number }}"; + $command = $eventName -eq "issue_comment" ? "${{ github.event.comment.body }}" : "[initial-sync]"; + + Write-Host "::notice::Event Name: $eventName"; + Write-Host "::notice::Organization Name: ${{ vars.ORGANIZATION_NAME }}"; + Write-Host "::notice::Project Name: ${{ vars.PROJECT_NAME }}"; + Write-Host "::notice::Requested By: ${{ github.event.sender.login }}"; + Write-Host "::notice::PR Number: $prNumber"; + Write-Host "::notice::Comment: $command"; + + <# Deno Args: + 1. Organization name + 2. Project name + 3. Login name of the user making the issue change + 4. Pull request number + 5. The sync command - Either '[initial-sync]' or '[run-sync]' + 6. PAT + #> + deno run ` + --allow-net ` + "$scriptUrl" ` + "${{ vars.ORGANIZATION_NAME }}" ` + "${{ vars.PROJECT_NAME }}" ` + "${{ github.event.sender.login }}" ` + "$prNumber" ` + "$command" ` + "${{ secrets.CICD_TOKEN }}"; diff --git a/.github/workflows/sync-status-check.yml b/.github/workflows/sync-status-check.yml new file mode 100644 index 0000000..ac452db --- /dev/null +++ b/.github/workflows/sync-status-check.yml @@ -0,0 +1,51 @@ +name: ✅Sync Status Check + + +defaults: + run: + shell: pwsh + + +on: + pull_request: + branches: [main, preview] + + +jobs: + sync_status_check: + name: Sync Status Check + if: startsWith(github.head_ref, 'feature/') + runs-on: ubuntu-latest + steps: + - name: Set Up Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Run Sync Status Check + run: | + $scriptUrl = "${{ vars.SCRIPT_BASE_URL }}/${{ vars.CICD_SCRIPTS_VERSION }}/${{ vars.SCRIPT_RELATIVE_DIR_PATH}}/sync-bot-status-check.ts"; + $prNumber = "${{ github.event.number }}"; + + Write-Host "::notice::Project Name: ${{ vars.PROJECT_NAME }}"; + Write-Host "::notice::PR Number: $prNumber"; + Write-Host "::notice::Event Type: pr"; + + if ($manuallyExecuted -and $prNumber -eq "0") { + Write-Host "::error::The issue or PR number must be a value greater than 0."; + exit 1; + } + + <# Deno Args: + 1. Project Name + 2. Pull request number + 3. Event Type - set to pull request event type + 4. PAT + #> + deno run ` + --allow-net ` + "$scriptUrl" ` + "${{ vars.PROJECT_NAME }}" ` + "$prNumber" ` + "pr" ` + "${{ secrets.CICD_TOKEN }}"; diff --git a/.github/workflows/testing-pr-status-check.yml b/.github/workflows/testing-pr-status-check.yml new file mode 100644 index 0000000..94d31f1 --- /dev/null +++ b/.github/workflows/testing-pr-status-check.yml @@ -0,0 +1,31 @@ +name: ✅Testing Status Check +run-name: ✅Testing Status Check ${{ github.base_ref == 'main' && '(Release Build)' || '(Debug Build)' }} + + +defaults: + run: + shell: pwsh + + +on: + pull_request: + branches: [main, preview] + + +jobs: + run_unit_tests: + name: ${{ vars.PROJECT_NAME }} Unit Tests Status Check + uses: KinsonDigital/Infrastructure/.github/workflows/run-csharp-tests.yml@v9.1.0 + with: + project-name: "${{ vars.PROJECT_NAME }}Tests" + build-config: Debug + net-sdk-version: "${{ vars.NET_SDK_VERSION }}" + + + run_integration_tests: + name: ${{ vars.PROJECT_NAME }} Integration Tests Status Check + uses: KinsonDigital/Infrastructure/.github/workflows/run-csharp-tests.yml@v9.1.0 + with: + project-name: "${{ vars.PROJECT_NAME }}IntegrationTests" + build-config: Debug + net-sdk-version: "${{ vars.NET_SDK_VERSION }}" diff --git a/.run/CarbonatePerfTests.run.xml b/.run/CarbonatePerfTests.run.xml new file mode 100644 index 0000000..9482afc --- /dev/null +++ b/.run/CarbonatePerfTests.run.xml @@ -0,0 +1,20 @@ + + + + diff --git a/.vscode/release-notes.code-snippets b/.vscode/release-notes.code-snippets new file mode 100644 index 0000000..8265fc4 --- /dev/null +++ b/.vscode/release-notes.code-snippets @@ -0,0 +1,51 @@ +{ + // Place your Release Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + + "Issue Link": { + "scope": "markdown", + "prefix": "issue-link", + "body": [ + "[#${1:Issue #}](https://github.com/KinsonDigital/Carbonate/issues/${1:Issue #}) - " + ], + "description": "Link to the issue" + }, + "Issue Link (Outside Contributor)": { + "scope": "markdown", + "prefix": "issue-link-oc", + "body": [ + "[#${1:Issue #}](https://github.com/KinsonDigital/Carbonate/issues/${1:Issue #}) ([@${2:Repo Owner}🙏🏼](https://github.com/${2:Repo Owner})) - " + ], + "description": "Link to the issue with outside contributor" + }, + "Outside Contributor": { + "scope": "markdown", + "prefix": "outside-contributor", + "body": [ + "([${1:Repo Owner}🙏🏼](https://github.com/${1:Repo Owner}))" + ], + "description": "Outside contributor link" + }, + "Repo URL": { + "scope": "markdown", + "prefix": "repo-url", + "body": [ + "[${1|BranchValidator,CASL,CICD,GitHubData,GitHubReleaseChecker,GotNuget,KDActionUtils,TagVerifier,Carbonate,VersionMiner,VersionValidator|}](https://github.com/KinsonDigital/${1|BranchValidator,CASL,GitHubData,GitHubReleaseChecker,GotNuget,KDActionUtils,TagVerifier,Carbonate,VersionMiner,VersionValidator|})" + ], + "description": "Outside contributor link" + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 44359dc..c900237 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,31 @@ { "cSpell.words": [ - "[TODO: add project name here]", + "Carbonate", "Kinson", + "parameterless", + "Pullable", + "Pushable", "pwsh", - "runsettings" - ] -} \ No newline at end of file + "Reactable", + "runsettings", + "unsubscriptions" + ], + "dotnet.defaultSolution": "Carbonate.sln", + "[typescript]": { + "editor.insertSpaces": false, + "editor.tabSize": 4, + }, + "[yaml]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + }, + "[jsonc]": { + "editor.insertSpaces": false, + "editor.tabSize": 4, + }, + "editor.detectIndentation": false, + "[github-actions-workflow]": { + "editor.tabSize": 2, + "editor.insertSpaces": true + } +} diff --git a/CSharpLibTemplateRepo.sln b/CSharpLibTemplateRepo.sln deleted file mode 100644 index 0579734..0000000 --- a/CSharpLibTemplateRepo.sln +++ /dev/null @@ -1,47 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpLibTemplateRepo", "CSharpLibTemplateRepo\CSharpLibTemplateRepo.csproj", "{7858F8F1-497A-4261-94D7-339442B09494}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpLibTemplateRepoTests", "Testing\CSharpLibTemplateRepoTests\CSharpLibTemplateRepoTests.csproj", "{A2124FFE-C6A1-46A7-8440-F0F5709DD08C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{E204672C-F241-4BB7-AAE4-5147BC5B4FF5}" - ProjectSection(SolutionItems) = preProject - Testing\.editorconfig = Testing\.editorconfig - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67F82AED-1502-4803-855A-CDB26DC685B4}" - ProjectSection(SolutionItems) = preProject - README.md = README.md - .editorconfig = .editorconfig - .gitignore = .gitignore - LICENSE = LICENSE - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Issue Templates", "Issue Templates", "{EC84F317-C63E-4B7E-9619-12D291701201}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{8518113E-D0C3-4E63-8F2E-542EB6D1CCE9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pull Request Templates", "Pull Request Templates", "{74273D28-BDB5-4FF0-B4E2-20A171671A58}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7858F8F1-497A-4261-94D7-339442B09494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7858F8F1-497A-4261-94D7-339442B09494}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7858F8F1-497A-4261-94D7-339442B09494}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7858F8F1-497A-4261-94D7-339442B09494}.Release|Any CPU.Build.0 = Release|Any CPU - {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A2124FFE-C6A1-46A7-8440-F0F5709DD08C} = {E204672C-F241-4BB7-AAE4-5147BC5B4FF5} - {EC84F317-C63E-4B7E-9619-12D291701201} = {67F82AED-1502-4803-855A-CDB26DC685B4} - {8518113E-D0C3-4E63-8F2E-542EB6D1CCE9} = {67F82AED-1502-4803-855A-CDB26DC685B4} - {74273D28-BDB5-4FF0-B4E2-20A171671A58} = {67F82AED-1502-4803-855A-CDB26DC685B4} - EndGlobalSection -EndGlobal diff --git a/Carbonate.sln b/Carbonate.sln new file mode 100644 index 0000000..24cc5b1 --- /dev/null +++ b/Carbonate.sln @@ -0,0 +1,93 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Carbonate", "Carbonate\Carbonate.csproj", "{7858F8F1-497A-4261-94D7-339442B09494}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonateTests", "Testing\CarbonateTests\CarbonateTests.csproj", "{A2124FFE-C6A1-46A7-8440-F0F5709DD08C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{E204672C-F241-4BB7-AAE4-5147BC5B4FF5}" + ProjectSection(SolutionItems) = preProject + Testing\.editorconfig = Testing\.editorconfig + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67F82AED-1502-4803-855A-CDB26DC685B4}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + LICENSE.md = LICENSE.md + README.md = README.md + renovate.json = renovate.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{8518113E-D0C3-4E63-8F2E-542EB6D1CCE9}" + ProjectSection(SolutionItems) = preProject + .github\workflows\add-new-issue-to-project.yml = .github\workflows\add-new-issue-to-project.yml + .github\workflows\build-pr-status-check.yml = .github\workflows\build-pr-status-check.yml + .github\workflows\prev-release.yml = .github\workflows\prev-release.yml + .github\workflows\sonar-scan-pr-status-check.yml = .github\workflows\sonar-scan-pr-status-check.yml + .github\workflows\sync-bot.yml = .github\workflows\sync-bot.yml + .github\workflows\sync-issue-to-pr.yml = .github\workflows\sync-issue-to-pr.yml + .github\workflows\sync-status-check.yml = .github\workflows\sync-status-check.yml + .github\workflows\testing-pr-status-check.yml = .github\workflows\testing-pr-status-check.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonateIntegrationTests", "Testing\CarbonateIntegrationTests\CarbonateIntegrationTests.csproj", "{388E7191-CE3A-492C-9A19-AE67A659E466}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonatePerfTests", "Testing\CarbonatePerfTests\CarbonatePerfTests.csproj", "{FA8B11A4-48C9-4824-A90F-9D39F3678028}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release Benchmark|x64 = Release Benchmark|x64 + Release MemPerf|x64 = Release MemPerf|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7858F8F1-497A-4261-94D7-339442B09494}.Debug|x64.ActiveCfg = Debug|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Debug|x64.Build.0 = Debug|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Release Benchmark|x64.ActiveCfg = Release|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Release Benchmark|x64.Build.0 = Release|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Release MemPerf|x64.ActiveCfg = Release|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Release MemPerf|x64.Build.0 = Release|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Release|x64.ActiveCfg = Release|x64 + {7858F8F1-497A-4261-94D7-339442B09494}.Release|x64.Build.0 = Release|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Debug|x64.ActiveCfg = Debug|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Debug|x64.Build.0 = Debug|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release Benchmark|x64.ActiveCfg = Release|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release Benchmark|x64.Build.0 = Release|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release MemPerf|x64.ActiveCfg = Release|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release MemPerf|x64.Build.0 = Release|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release|x64.ActiveCfg = Release|x64 + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C}.Release|x64.Build.0 = Release|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Debug|x64.ActiveCfg = Debug|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Debug|x64.Build.0 = Debug|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Release Benchmark|x64.ActiveCfg = Release|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Release Benchmark|x64.Build.0 = Release|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Release MemPerf|x64.ActiveCfg = Release|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Release MemPerf|x64.Build.0 = Release|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Release|x64.ActiveCfg = Release|x64 + {388E7191-CE3A-492C-9A19-AE67A659E466}.Release|x64.Build.0 = Release|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Debug|x64.ActiveCfg = Debug|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Debug|x64.Build.0 = Debug|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Release Benchmark|x64.ActiveCfg = Release Benchmark|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Release Benchmark|x64.Build.0 = Release Benchmark|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Release MemPerf|x64.ActiveCfg = Release MemPerf|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Release MemPerf|x64.Build.0 = Release MemPerf|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Release|x64.ActiveCfg = Release|x64 + {FA8B11A4-48C9-4824-A90F-9D39F3678028}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2124FFE-C6A1-46A7-8440-F0F5709DD08C} = {E204672C-F241-4BB7-AAE4-5147BC5B4FF5} + {8518113E-D0C3-4E63-8F2E-542EB6D1CCE9} = {67F82AED-1502-4803-855A-CDB26DC685B4} + {388E7191-CE3A-492C-9A19-AE67A659E466} = {E204672C-F241-4BB7-AAE4-5147BC5B4FF5} + {FA8B11A4-48C9-4824-A90F-9D39F3678028} = {E204672C-F241-4BB7-AAE4-5147BC5B4FF5} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DF1E613C-8039-4941-A938-2A7FA0741F5A} + EndGlobalSection +EndGlobal diff --git a/Carbonate.sln.DotSettings b/Carbonate.sln.DotSettings new file mode 100644 index 0000000..19a1d5d --- /dev/null +++ b/Carbonate.sln.DotSettings @@ -0,0 +1,86 @@ + + 145 + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + False + False + True + False + True + True + True + True + True + True + True + XUnit Unit Test Method + True + 2 + True + 0 + True + 1 + True + True + 2.0 + InCSharpFile + xu-test-method + True + [Fact] +public void $MEMBER_UNDER_TEST$_$SCENARIO_UNDER_TEST$_$EXPECTED_RESULT$() +{ + // Arrange + + // Act + + // Assert +} + True + True + XUnit test method for testing null constructor parameters + True + 0 + True + True + 2.0 + InCSharpFile + xu-null-ctor-param + True + [Fact] +public void Ctor_WithNull$PARAM_1$Param_ThrowsException() +{ + // Arrange & Act + var act = () => + { + }; + + // Assert + act.Should() + .Throw<ArgumentNullException>() + .WithMessage("The parameter must not be null. (Parameter '$END$$PARAM_1$')"); +} + True + True + Template for creating an object creation method for unit testing + False + True + 0 + True + True + 2.0 + InCSharpFile + xu-create-test-obj-method + True + /// <summary> +/// Creates a new instance of <see cref="$OBJECT_TYPE$"/> for the purpose of testing. +/// </summary> +/// <returns>The instance to test.</returns> +private $OBJECT_TYPE$ CreateSystemUnderTest() + => new ($END$); + True + True + True + True + True + True + True diff --git a/Carbonate/BiDirectional/IPullReactable.cs b/Carbonate/BiDirectional/IPullReactable.cs new file mode 100644 index 0000000..04f48eb --- /dev/null +++ b/Carbonate/BiDirectional/IPullReactable.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.BiDirectional; + +using Core; +using Core.BiDirectional; + +/// +/// Defines a provider for pull-based responses. +/// +/// The type of data coming in. +/// The type of data going out. +public interface IPullReactable : IReactable>, IPullable +{ +} diff --git a/Carbonate/BiDirectional/IPullable.cs b/Carbonate/BiDirectional/IPullable.cs new file mode 100644 index 0000000..f39ab6f --- /dev/null +++ b/Carbonate/BiDirectional/IPullable.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.BiDirectional; + +using System.Diagnostics.CodeAnalysis; + +/// +/// Gives the ability to pull data from a source using a messaging mechanism. +/// +/// The type of data coming in. +/// The type of data going out. +public interface IPullable +{ + /// + /// Requests to pull data from a source that matches the given , + /// with the given additional . + /// + /// The data to send to the responder. + /// The ID of the response. + /// The data result going out. + [SuppressMessage("ReSharper", "UnusedParameter.Global", Justification = "Public API.")] + [SuppressMessage("ReSharper", "UnusedMemberInSuper.Global", Justification = "Public API.")] + TDataOut? Pull(in TDataIn data, Guid respondId); +} diff --git a/Carbonate/BiDirectional/PullReactable.cs b/Carbonate/BiDirectional/PullReactable.cs new file mode 100644 index 0000000..eb7829d --- /dev/null +++ b/Carbonate/BiDirectional/PullReactable.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// NOTE: Leave the loops as 'for loops'. This is a small performance improvement. +// ReSharper disable ForCanBeConvertedToForeach +// ReSharper disable LoopCanBeConvertedToQuery +namespace Carbonate.BiDirectional; + +using Core.BiDirectional; + +/// +public class PullReactable + : ReactableBase>, IPullReactable +{ + /// + public TDataOut? Pull(in TDataIn data, Guid respondId) + { + for (var i = 0; i < Reactors.Count; i++) + { + if (Reactors[i].Id != respondId) + { + continue; + } + + return Reactors[i].OnRespond(data); + } + + return default; + } +} diff --git a/Carbonate/BiDirectional/RespondReactor.cs b/Carbonate/BiDirectional/RespondReactor.cs new file mode 100644 index 0000000..c119a30 --- /dev/null +++ b/Carbonate/BiDirectional/RespondReactor.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.BiDirectional; + +using System.Diagnostics.CodeAnalysis; +using Core.BiDirectional; + +/// +[SuppressMessage( + "ReSharper", + "ClassWithVirtualMembersNeverInherited.Global", + Justification = "Left unsealed to give users more control")] +public class RespondReactor : ReactorBase, IRespondReactor +{ + private readonly Func? onRespondData; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the requiring a response. + /// The name of the . + /// Executed when requesting a response with data. + /// + /// Executed when the provider has finished sending push-based notifications and is unsubscribed. + /// + /// Executed when the provider experiences an error. + /// + /// Note: The is not used for unique identification purposes. + ///
+ /// It is only metadata for debugging or miscellaneous purposes. + ///
+ public RespondReactor( + Guid respondId, + string name = "", + Func? onRespondData = null, + Action? onUnsubscribe = null, + Action? onError = null) + : base(respondId, name, onUnsubscribe, onError) => this.onRespondData = onRespondData; + + /// + public virtual TDataOut? OnRespond(TDataIn data) + { + if (Unsubscribed) + { + return default; + } + + if (data is null) + { + throw new ArgumentNullException(nameof(data), "The parameter must not be null."); + } + + return this.onRespondData is null ? default : this.onRespondData.Invoke(data); + } + + /// + public override string ToString() => $"{Name}{(string.IsNullOrEmpty(Name) ? string.Empty : " - ")}{Id}"; +} diff --git a/CSharpLibTemplateRepo/CSharpLibTemplateRepo.csproj b/Carbonate/Carbonate.csproj similarity index 50% rename from CSharpLibTemplateRepo/CSharpLibTemplateRepo.csproj rename to Carbonate/Carbonate.csproj index be793fe..0d15109 100644 --- a/CSharpLibTemplateRepo/CSharpLibTemplateRepo.csproj +++ b/Carbonate/Carbonate.csproj @@ -7,10 +7,10 @@ enable - 1.0.0-preview.1 + 1.0.0-preview.14 - 1.0.0-preview.1 + 1.0.0-preview.14 1.0.0 - KinsonDigital.[TODO: add project name here] + KinsonDigital.Carbonate Calvin Wilkinson Kinson Digital - [TODO: add project name here] - [TODO: Add description here] - Copyright ©2022 Kinson Digital - [TODO: Add tags here] - [TODO: add project name here] - velaptor-logo.ico - https://github.com/KinsonDigital/[TODO: add project name here]/blob/release/v1.0.0/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.1.md - https://github.com/KinsonDigital/[TODO: add project name here] - https://github.com/KinsonDigital/[TODO: add project name here] + Carbonate + Library for doing internal messaging using the observable pattern. + Copyright ©2023 Kinson Digital + messaging observable subscribe + Carbonate + carbonate-logo.ico + https://github.com/KinsonDigital/Carbonate/blob/release/v1.0.0/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.1.md + https://github.com/KinsonDigital/Carbonate + https://github.com/KinsonDigital/Carbonate git - [TODO: Add logo image here] + carbonate-logo-light-mode.png x64 LICENSE.md README.md CS7035 + Debug;Release + + bin\x64\Carbonate.xml + + + + + + + + + + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Carbonate/Core/BiDirectional/IRespondReactor.cs b/Carbonate/Core/BiDirectional/IRespondReactor.cs new file mode 100644 index 0000000..faa2448 --- /dev/null +++ b/Carbonate/Core/BiDirectional/IRespondReactor.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.BiDirectional; + +/// +/// Provides a mechanism for receiving responses. +/// +/// The type of data coming in. +/// The type of data going out. +public interface IRespondReactor : IReactor, IResponder +{ +} diff --git a/Carbonate/Core/BiDirectional/IResponder.cs b/Carbonate/Core/BiDirectional/IResponder.cs new file mode 100644 index 0000000..0cc3e9f --- /dev/null +++ b/Carbonate/Core/BiDirectional/IResponder.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.BiDirectional; + +/// +/// Gives the ability to respond to a pull request from an . +/// +/// The type of data coming in. +/// The type of data going out. +public interface IResponder +{ + /// + /// Returns a response. + /// + /// The data for the . + /// The data result going out.> + TDataOut? OnRespond(TDataIn data); +} diff --git a/Carbonate/Core/IReactable.cs b/Carbonate/Core/IReactable.cs new file mode 100644 index 0000000..d4c9bbe --- /dev/null +++ b/Carbonate/Core/IReactable.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core; + +using System.Collections.ObjectModel; + +/// +/// Defines a provider for pushing notifications or receiving responses. +/// +/// The reactor that can subscribed to events. +public interface IReactable : IDisposable + where TReactor : class, IReactor +{ + /// + /// Gets the list of reactors that are subscribed to this . + /// + ReadOnlyCollection Reactors { get; } + + /// + /// Gets the list of subscription IDs. + /// + ReadOnlyCollection SubscriptionIds { get; } + + /// + /// Notifies the provider that an reactor is to receive notifications. + /// + /// The object that is to receive notifications. + /// + /// A reference to an interface that allows reactors to stop receiving + /// notifications before the provider has finished sending them. + /// + IDisposable Subscribe(TReactor reactor); + + /// + /// Unsubscribes notifications to all s that match the given . + /// + /// The ID of the event to end. + /// + /// Will not invoke the . more than once. + /// + void Unsubscribe(Guid id); + + /// + /// Unsubscribes all of the currently subscribed reactors. + /// + void UnsubscribeAll(); +} diff --git a/Carbonate/Core/IReactor.cs b/Carbonate/Core/IReactor.cs new file mode 100644 index 0000000..104c471 --- /dev/null +++ b/Carbonate/Core/IReactor.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core; + +/// +/// Provides a mechanism for pushing notifications or receiving responses. +/// +public interface IReactor +{ + /// + /// Gets the ID of the subscription where this should respond. + /// + Guid Id { get; } + + /// + /// Gets the name of the . + /// + /// + /// Note: This is not used for unique identification purposes. + ///
+ /// It is only metadata for debugging or miscellaneous purposes. + ///
+ string Name { get; } + + /// + /// Gets a value indicating whether or not the has been unsubscribed. + /// + /// + /// This means that the will not receive. + /// + bool Unsubscribed { get; } + + /// + /// Notifies the subscriber that the provider has finished sending push-based notifications and has been unsubscribed. + /// + /// + /// Will not be invoked more than once. + /// + void OnUnsubscribe(); + + /// + /// Notifies the subscriber that the provider has experienced an error condition. + /// + /// An object that provides additional information about the error. + void OnError(Exception error); +} diff --git a/Carbonate/Core/NonDirectional/IReceive.cs b/Carbonate/Core/NonDirectional/IReceive.cs new file mode 100644 index 0000000..a068eb7 --- /dev/null +++ b/Carbonate/Core/NonDirectional/IReceive.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.NonDirectional; + +/// +/// Gives the ability to receive push notifications. +/// +public interface IReceiver +{ + /// + /// Gets a notification of an event. + /// + void OnReceive(); +} diff --git a/Carbonate/Core/NonDirectional/IReceiveReactor.cs b/Carbonate/Core/NonDirectional/IReceiveReactor.cs new file mode 100644 index 0000000..b438ccd --- /dev/null +++ b/Carbonate/Core/NonDirectional/IReceiveReactor.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.NonDirectional; + +/// +/// A reactor capable of standard functionality that can receive push notifications. +/// +public interface IReceiveReactor : IReactor, IReceiver +{ +} diff --git a/Carbonate/Core/ReactorUnsubscriber.cs b/Carbonate/Core/ReactorUnsubscriber.cs new file mode 100644 index 0000000..6c57b3b --- /dev/null +++ b/Carbonate/Core/ReactorUnsubscriber.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core; + +/// +/// A reactor unsubscriber for unsubscribing from a . +/// +internal sealed class ReactorUnsubscriber : IDisposable +{ + private readonly List reactors; + private readonly IReactor reactor; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The list of reactor subscriptions. + /// The reactor that has been subscribed. + internal ReactorUnsubscriber(List reactors, IReactor reactor) + { + this.reactors = reactors ?? throw new ArgumentNullException(nameof(reactors), "The parameter must not be null."); + this.reactor = reactor ?? throw new ArgumentNullException(nameof(reactor), "The parameter must not be null."); + } + + /// + /// Gets the total number of subscriptions. + /// + public int TotalReactors => this.reactors.Count; + + /// + public void Dispose() => Dispose(true); + + /// + /// + /// + /// Disposes managed resources when true. + private void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + if (this.reactors.Contains(this.reactor)) + { + this.reactors.Remove(this.reactor); + } + } + + this.isDisposed = true; + } +} diff --git a/Carbonate/Core/UniDirectional/IReceiveReactor.cs b/Carbonate/Core/UniDirectional/IReceiveReactor.cs new file mode 100644 index 0000000..fbcc95d --- /dev/null +++ b/Carbonate/Core/UniDirectional/IReceiveReactor.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.UniDirectional; + +/// +/// A reactor capable of standard functionality that can receive push notifications. +/// +/// The type of data coming in. +public interface IReceiveReactor : IReactor, IReceiver +{ +} diff --git a/Carbonate/Core/UniDirectional/IReceiver.cs b/Carbonate/Core/UniDirectional/IReceiver.cs new file mode 100644 index 0000000..bc4c38b --- /dev/null +++ b/Carbonate/Core/UniDirectional/IReceiver.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.UniDirectional; + +/// +/// Gives the ability to receive push notifications. +/// +/// The type of data coming in. +public interface IReceiver +{ + /// + /// Gets a notification of an event with the given . + /// + /// The notification data. + void OnReceive(TDataIn data); +} diff --git a/Carbonate/Core/UniDirectional/IRespondReactor.cs b/Carbonate/Core/UniDirectional/IRespondReactor.cs new file mode 100644 index 0000000..205ef4d --- /dev/null +++ b/Carbonate/Core/UniDirectional/IRespondReactor.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.UniDirectional; + +/// +/// Provides a mechanism for receiving responses. +/// +/// The type of data going out. +public interface IRespondReactor : IReactor, IResponder +{ +} diff --git a/Carbonate/Core/UniDirectional/IResponder.cs b/Carbonate/Core/UniDirectional/IResponder.cs new file mode 100644 index 0000000..e779f00 --- /dev/null +++ b/Carbonate/Core/UniDirectional/IResponder.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.Core.UniDirectional; + +/// +/// Gives the ability to respond to a pull request from an . +/// +/// The type of data going out. +public interface IResponder +{ + /// + /// Returns a response. + /// + /// The data result going out.> + TDataOut? OnRespond(); +} diff --git a/Carbonate/NonDirectional/IPushReactable.cs b/Carbonate/NonDirectional/IPushReactable.cs new file mode 100644 index 0000000..74550c4 --- /dev/null +++ b/Carbonate/NonDirectional/IPushReactable.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.NonDirectional; + +using Core; +using Core.NonDirectional; + +/// +/// Defines a provider for push-based notifications. +/// +public interface IPushReactable : IReactable, IPushable +{ +} diff --git a/Carbonate/NonDirectional/IPushable.cs b/Carbonate/NonDirectional/IPushable.cs new file mode 100644 index 0000000..ce06a0a --- /dev/null +++ b/Carbonate/NonDirectional/IPushable.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.NonDirectional; + +/// +/// Pushes out notifications. +/// +public interface IPushable +{ + /// + /// Pushes a single notification for an event that matches the given . + /// + /// The ID of the event where the notification will be pushed. + void Push(Guid eventId); +} diff --git a/Carbonate/NonDirectional/PushReactable.cs b/Carbonate/NonDirectional/PushReactable.cs new file mode 100644 index 0000000..1df822f --- /dev/null +++ b/Carbonate/NonDirectional/PushReactable.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.NonDirectional; + +using Core.NonDirectional; + +/// +public class PushReactable : ReactableBase, IPushReactable +{ + /// + public void Push(Guid eventId) + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(PushReactable), $"{nameof(PushReactable)} disposed."); + } + + try + { + /* Work from the end to the beginning of the list + * just in case the reactable is disposed(removed) + * in the OnReceive() method. + */ + for (var i = Reactors.Count - 1; i >= 0; i--) + { + /*NOTE: + * The purpose of this logic is to prevent array index errors + * if an OnReceive() implementation ends up unsubscribing a single + * subscription or unsubscribing from a single event id + * + * If the current index is not less than or equal to + * the total number of items, reset the index to the last item + */ + i = i > Reactors.Count - 1 + ? Reactors.Count - 1 + : i; + + if (Reactors[i].Id != eventId) + { + continue; + } + + Reactors[i].OnReceive(); + } + } + catch (Exception e) + { + SendError(e, eventId); + } + } + + /// + /// Sends an error to all of the subscribers that matches the given . + /// + /// The exception that occured. + /// The ID of the event where the notification will be pushed. + private void SendError(Exception exception, Guid eventId) + { + /* Work from the end to the beginning of the list + * just in case the reactable is disposed(removed) + * in the OnReceive() method. + */ + for (var i = Reactors.Count - 1; i >= 0; i--) + { + /*NOTE: + * The purpose of this logic is to prevent array index errors + * if an OnReceive() implementation ends up unsubscribing a single + * subscription or unsubscribing from a single event id + * + * If the current index is not less than or equal to + * the total number of items, reset the index to the last item + */ + i = i > Reactors.Count - 1 + ? Reactors.Count - 1 + : i; + + if (Reactors[i].Id != eventId) + { + continue; + } + + Reactors[i].OnError(exception); + } + } +} diff --git a/Carbonate/NonDirectional/ReceiveReactor.cs b/Carbonate/NonDirectional/ReceiveReactor.cs new file mode 100644 index 0000000..d84b13a --- /dev/null +++ b/Carbonate/NonDirectional/ReceiveReactor.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.NonDirectional; + +using System.Diagnostics.CodeAnalysis; +using Core; +using Core.NonDirectional; + +/// +[SuppressMessage( + "ReSharper", + "ClassWithVirtualMembersNeverInherited.Global", + Justification = "Left unsealed to give users more control")] +public class ReceiveReactor : ReactorBase, IReceiveReactor +{ + private readonly Action? onReceive; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the event that was pushed by an . + /// The name of the . + /// Executed when a push notification occurs with no data. + /// + /// Executed when the provider has finished sending push-based notifications and is unsubscribed. + /// + /// Executed when the provider experiences an error. + /// + /// Note: The is not used for unique identification purposes. + ///
+ /// It is only metadata for debugging or miscellaneous purposes. + ///
+ public ReceiveReactor( + Guid eventId, + string name = "", + Action? onReceive = null, + Action? onUnsubscribe = null, + Action? onError = null) + : base(eventId, name, onUnsubscribe, onError) => this.onReceive = onReceive; + + /// + public virtual void OnReceive() + { + if (Unsubscribed) + { + return; + } + + this.onReceive?.Invoke(); + } + + /// + public override string ToString() => $"{Name}{(string.IsNullOrEmpty(Name) ? string.Empty : " - ")}{Id}"; +} diff --git a/Carbonate/ReactableBase.cs b/Carbonate/ReactableBase.cs new file mode 100644 index 0000000..024431a --- /dev/null +++ b/Carbonate/ReactableBase.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate; + +using System.Collections.ObjectModel; +using Core; +using UniDirectional; + +/// +/// Defines a provider for pushing notifications or receiving responses with default behavior. +/// +/// The type of reactor to use. +public abstract class ReactableBase : IReactable + where T : class, IReactor +{ + private readonly List reactors = new (); + private bool notificationsEnded; + + /// + public ReadOnlyCollection Reactors => this.reactors.AsReadOnly(); + + /// + public ReadOnlyCollection SubscriptionIds => this.reactors + .Select(r => r.Id) + .Distinct() + .ToList().AsReadOnly(); + + /// + /// Gets a value indicating whether or not if the has been disposed. + /// + protected bool IsDisposed { get; private set; } + + /// + /// Thrown if this method is invoked after disposal. + /// Thrown if the given is null. + public virtual IDisposable Subscribe(T reactor) + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(PushReactable), $"{nameof(PushReactable)} disposed."); + } + + if (reactor is null) + { + throw new ArgumentNullException(nameof(reactor), "The parameter must not be null."); + } + + this.reactors.Add(reactor); + + return new ReactorUnsubscriber(this.reactors.Cast().ToList(), reactor); + } + + /// + /// Thrown if this method is invoked after disposal. + public virtual void Unsubscribe(Guid id) + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(PushReactable), $"{nameof(PushReactable)} disposed."); + } + + if (this.notificationsEnded) + { + return; + } + + /* Keep this loop as a for-loop. Do not convert to for-each. + * This is due to the Dispose() method possibly being called during + * iteration of the reactors list which will cause an exception. + */ + for (var i = this.reactors.Count - 1; i >= 0; i--) + { + /*NOTE: + * The purpose of this logic is to prevent array index errors + * if an OnReceive() implementation ends up unsubscribing a single + * subscription or unsubscribing from a single event id + * + * If the current index is not less than or equal to + * the total number of items, reset the index to the last item + */ + i = i > this.reactors.Count - 1 + ? this.reactors.Count - 1 + : i; + + if (this.reactors[i].Id != id) + { + continue; + } + + var beforeTotal = this.reactors.Count; + + this.reactors[i].OnUnsubscribe(); + + var nothingRemoved = Math.Abs(beforeTotal - this.reactors.Count) <= 0; + + // Make sure that the OnUnsubscribe implementation did not remove + // the reactor before attempting to remove it + if (nothingRemoved) + { + this.reactors.Remove(this.reactors[i]); + } + } + + this.notificationsEnded = this.reactors.All(r => r.Unsubscribed); + } + + /// + /// Thrown if this method is invoked after disposal. + public virtual void UnsubscribeAll() + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(PushReactable), $"{nameof(PushReactable)} disposed."); + } + + /* Keep this loop as a for-loop. Do not convert to for-each. + * This is due to the Dispose() method possibly being called during + * iteration of the reactors list which will cause an exception. + */ + for (var i = this.reactors.Count - 1; i >= 0; i--) + { + /*NOTE: + * The purpose of this logic is to prevent array index errors + * if an OnReceive() implementation ends up unsubscribing a single + * subscription or unsubscribing from a single event id + * + * If the current index is not less than or equal to + * the total number of items, reset the index to the last item + */ + i = i > this.reactors.Count - 1 + ? this.reactors.Count - 1 + : i; + + this.reactors[i].OnUnsubscribe(); + } + + this.reactors.Clear(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// + /// + /// Disposes managed resources when true. + /// + /// All s that are still subscribed will have its + /// method invoked and the s will be unsubscribed. + /// + private void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + if (disposing) + { + UnsubscribeAll(); + } + + IsDisposed = true; + } +} diff --git a/Carbonate/ReactorBase.cs b/Carbonate/ReactorBase.cs new file mode 100644 index 0000000..2b61cf6 --- /dev/null +++ b/Carbonate/ReactorBase.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate; + +using Core; + +/// +/// Provides a mechanism for push or pull based messaging. +/// +public abstract class ReactorBase : IReactor +{ + private readonly Action? onUnsubscribe; + private readonly Action? onError; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the event that was pushed by an . + /// The name of the . + /// + /// Executed when the provider has finished sending push-based notifications and is unsubscribed. + /// + /// Executed when the provider experiences an error. + /// + /// Note: The is not used for unique identification. + ///
+ /// It is only metadata for debugging or miscellaneous purposes. + ///
+ protected ReactorBase( + Guid eventId, + string name = "", + Action? onUnsubscribe = null, + Action? onError = null) + { + Id = eventId; + Name = string.IsNullOrEmpty(name) ? string.Empty : name; + this.onUnsubscribe = onUnsubscribe; + this.onError = onError; + } + + /// + public Guid Id { get; } + + /// + public string Name { get; } + + /// + public virtual bool Unsubscribed { get; private set; } + + /// + public virtual void OnUnsubscribe() + { + if (Unsubscribed) + { + return; + } + + this.onUnsubscribe?.Invoke(); + Unsubscribed = true; + } + + /// + public virtual void OnError(Exception error) + { + if (Unsubscribed) + { + return; + } + + if (error is null) + { + throw new ArgumentNullException(nameof(error), "The parameter must not be null."); + } + + this.onError?.Invoke(error); + } +} diff --git a/Carbonate/UniDirectional/IPullReactable.cs b/Carbonate/UniDirectional/IPullReactable.cs new file mode 100644 index 0000000..67960e9 --- /dev/null +++ b/Carbonate/UniDirectional/IPullReactable.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +using Core; +using Core.UniDirectional; + +/// +/// Defines a provider for pull-based responses. +/// +/// The type of data going out. +public interface IPullReactable : IReactable>, IPullable +{ +} diff --git a/Carbonate/UniDirectional/IPullable.cs b/Carbonate/UniDirectional/IPullable.cs new file mode 100644 index 0000000..e2f8b0f --- /dev/null +++ b/Carbonate/UniDirectional/IPullable.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +using System.Diagnostics.CodeAnalysis; + +/// +/// Gives the ability to pull data from a source using a messaging mechanism. +/// +/// The type of data going out. +public interface IPullable +{ + /// + /// Requests to pull data from a source that matches the given . + /// + /// The ID of the response. + /// The data result going out.> + [SuppressMessage("ReSharper", "UnusedParameter.Global", Justification = "Public API.")] + [SuppressMessage("ReSharper", "UnusedMemberInSuper.Global", Justification = "Public API.")] + TDataOut? Pull(Guid respondId); +} diff --git a/Carbonate/UniDirectional/IPushReactable.cs b/Carbonate/UniDirectional/IPushReactable.cs new file mode 100644 index 0000000..15acad0 --- /dev/null +++ b/Carbonate/UniDirectional/IPushReactable.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +using Core; +using Core.UniDirectional; + +/// +/// Defines a provider for push-based notifications. +/// +/// The type of data coming in. +public interface IPushReactable : IReactable>, IPushable +{ +} diff --git a/Carbonate/UniDirectional/IPushable.cs b/Carbonate/UniDirectional/IPushable.cs new file mode 100644 index 0000000..9a88df5 --- /dev/null +++ b/Carbonate/UniDirectional/IPushable.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +/// +/// Pushes out notifications. +/// +/// The type of data coming in. +public interface IPushable +{ + /// + /// Pushes a single notification with the given for an event that matches the given . + /// + /// The data that contains the data to push. + /// The ID of the event where the notification will be pushed. + void Push(in TDataIn data, Guid eventId); +} diff --git a/Carbonate/UniDirectional/PullReactable.cs b/Carbonate/UniDirectional/PullReactable.cs new file mode 100644 index 0000000..508193d --- /dev/null +++ b/Carbonate/UniDirectional/PullReactable.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// NOTE: Leave the loops as 'for loops'. This is a small performance improvement. +// ReSharper disable ForCanBeConvertedToForeach +// ReSharper disable LoopCanBeConvertedToQuery +namespace Carbonate.UniDirectional; + +using Core.UniDirectional; + +/// +public class PullReactable + : ReactableBase>, IPullReactable +{ + /// + public TDataOut? Pull(Guid respondId) + { + for (var i = 0; i < Reactors.Count; i++) + { + if (Reactors[i].Id != respondId) + { + continue; + } + + return Reactors[i].OnRespond() ?? default(TDataOut); + } + + return default; + } +} diff --git a/Carbonate/UniDirectional/PushReactable.cs b/Carbonate/UniDirectional/PushReactable.cs new file mode 100644 index 0000000..5f94d1b --- /dev/null +++ b/Carbonate/UniDirectional/PushReactable.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +using Core.UniDirectional; + +/// +public class PushReactable : ReactableBase>, IPushReactable +{ + /// + /// Thrown if this method is invoked after disposal. + public void Push(in TDataIn data, Guid eventId) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data), "The parameter must not be null."); + } + + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(PushReactable), $"{nameof(PushReactable)} disposed."); + } + + try + { + /* Work from the end to the beginning of the list + * just in case the reactable is disposed(removed) + * in the OnReceive() method. + */ + for (var i = Reactors.Count - 1; i >= 0; i--) + { + /*NOTE: + * The purpose of this logic is to prevent array index errors + * if an OnReceive() implementation ends up unsubscribing a single + * subscription or unsubscribing from a single event id + * + * If the current index is not less than or equal to + * the total number of items, reset the index to the last item + */ + i = i > Reactors.Count - 1 + ? Reactors.Count - 1 + : i; + + if (Reactors[i].Id != eventId) + { + continue; + } + + Reactors[i].OnReceive(data); + } + } + catch (Exception e) + { + SendError(e, eventId); + } + } + + /// + /// Sends an error to all of the subscribers that matches the given . + /// + /// The exception that occured. + /// The ID of the event where the notification will be pushed. + private void SendError(Exception exception, Guid eventId) + { + /* Work from the end to the beginning of the list + * just in case the reactable is disposed(removed) + * in the OnReceive() method. + */ + for (var i = Reactors.Count - 1; i >= 0; i--) + { + /*NOTE: + * The purpose of this logic is to prevent array index errors + * if an OnReceive() implementation ends up unsubscribing a single + * subscription or unsubscribing from a single event id + * + * If the current index is not less than or equal to + * the total number of items, reset the index to the last item + */ + i = i > Reactors.Count - 1 + ? Reactors.Count - 1 + : i; + + if (Reactors[i].Id != eventId) + { + continue; + } + + Reactors[i].OnError(exception); + } + } +} diff --git a/Carbonate/UniDirectional/ReceiveReactor.cs b/Carbonate/UniDirectional/ReceiveReactor.cs new file mode 100644 index 0000000..0298752 --- /dev/null +++ b/Carbonate/UniDirectional/ReceiveReactor.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +using System.Diagnostics.CodeAnalysis; +using Core; +using Core.UniDirectional; + +/// +[SuppressMessage( + "ReSharper", + "ClassWithVirtualMembersNeverInherited.Global", + Justification = "Left unsealed to give users more control")] +public class ReceiveReactor : ReactorBase, IReceiveReactor +{ + private readonly Action? onReceiveData; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the event that was pushed by an . + /// The name of the . + /// Executed when a push notification occurs with some data. + /// + /// Executed when the provider has finished sending push-based notifications and is unsubscribed. + /// + /// Executed when the provider experiences an error. + /// + /// Note: The is not used for unique identification purposes. + ///
+ /// It is only metadata for debugging or miscellaneous purposes. + ///
+ public ReceiveReactor( + Guid eventId, + string name = "", + Action? onReceiveData = null, + Action? onUnsubscribe = null, + Action? onError = null) + : base(eventId, name, onUnsubscribe, onError) => this.onReceiveData = onReceiveData; + + /// + public virtual void OnReceive(TDataIn data) + { + if (Unsubscribed) + { + return; + } + + if (data is null) + { + throw new ArgumentNullException(nameof(data), "The parameter must not be null."); + } + + this.onReceiveData?.Invoke(data); + } + + /// + public override string ToString() => $"{Name}{(string.IsNullOrEmpty(Name) ? string.Empty : " - ")}{Id}"; +} diff --git a/Carbonate/UniDirectional/RespondReactor.cs b/Carbonate/UniDirectional/RespondReactor.cs new file mode 100644 index 0000000..72a8174 --- /dev/null +++ b/Carbonate/UniDirectional/RespondReactor.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace Carbonate.UniDirectional; + +using System.Diagnostics.CodeAnalysis; +using Core.UniDirectional; + +/// +[SuppressMessage( + "ReSharper", + "ClassWithVirtualMembersNeverInherited.Global", + Justification = "Left unsealed to give users more control")] +public class RespondReactor : ReactorBase, IRespondReactor +{ + private readonly Func? onRespond; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the requiring a response. + /// The name of the . + /// Executed when requesting a response with no data. + /// + /// Executed when the provider has finished sending push-based notifications and is unsubscribed. + /// + /// Executed when the provider experiences an error. + /// + /// Note: The is not used for unique identification purposes. + ///
+ /// It is only metadata for debugging or miscellaneous purposes. + ///
+ public RespondReactor( + Guid respondId, + string name = "", + Func? onRespond = null, + Action? onUnsubscribe = null, + Action? onError = null) + : base(respondId, name, onUnsubscribe, onError) => this.onRespond = onRespond; + + /// + public virtual TDataOut? OnRespond() + { + if (Unsubscribed) + { + return default; + } + + return this.onRespond is null ? default : this.onRespond.Invoke(); + } + + /// + public override string ToString() => $"{Name}{(string.IsNullOrEmpty(Name) ? string.Empty : " - ")}{Id}"; +} diff --git a/Carbonate/carbonate-logo.ico b/Carbonate/carbonate-logo.ico new file mode 100644 index 0000000..c25b693 Binary files /dev/null and b/Carbonate/carbonate-logo.ico differ diff --git a/CSharpLibTemplateRepo/stylecop.json b/Carbonate/stylecop.json similarity index 100% rename from CSharpLibTemplateRepo/stylecop.json rename to Carbonate/stylecop.json diff --git a/Documentation/Branching.md b/Documentation/Branching.md deleted file mode 100644 index c78600b..0000000 --- a/Documentation/Branching.md +++ /dev/null @@ -1,132 +0,0 @@ -

Branching

- -**CSharpLibTemplateRepo** uses a more complicated branching model, but it gives you more control of the SDLC (Software Development Life Cycle). This branching model allows a clear purpose for adding features, bug fixes, preview releases, QA releases and standard releases. - -As a standard contributor, all you have to worry about is creating feature branches and creating pull requests to merge those branches into the develop branch. The rest is taken care of by a solid CI/CD system as well as the maintainers of the project. Only the organization owner and designated team members will manage the release process. So, contributing is very easy!!🥳 - -**_NOTE_:** As you know, everything in software is subject to change, including the branching model. If too many issues or complications occur with the current branching model and/or release process, it will be changed accordingly. - ---- - -

Branches Used

- - -

Master Branch

- -Long living branch that represents stable production versions of **CSharpLibTemplateRepo**: -- **Branch Syntax:** master -- **Branches That Can Merge Into Master:** - - Release branches via pull request -- **Created From:** none -- **Merges Into:** none -- **Environment:** Production -- **CI/CD:** - - Upon pull request completion, the release branches are merged into the master branch and are automatically built, tested, and released to production as a nuget package. - - The testing application is attached as an artifact to the release branch for the purpose of testing. - - -

Develop Branch

- -Long living branch that represents the most current development in progress: -- **Branch Syntax:** develop -- **Branches That Can Merge Into Develop Branch:** - - Feature branches via pull requests -- **Created From:** none -- **Merges Into:** none -- **Environment:** QA -- **CI/CD:** - - Automatically built, tested, and deployed as a QA release upon pull request completion. - - The testing application is attached as an artifact to the QA release for the purpose of testing. - - -

Feature Branches

- -Short living branch where a developer's work will be performed and merged back into the develop branch via a pull request: -- **Branch Syntax:** feature/\-\ - - Example: feature/123-my-branch -- **Branches That Can Merge Into Feature Branches:** None -- **Created From:** develop -- **Merges Into:** develop -- **Environment:** none -- **CI/CD:** - - Build and unit test status checks are automatically run for each change to the pull request. - - All status checks must pass for a pull request to be completed. - - -

Hotfix Branches

- -Short lived branch where urgent bug fixes or changes will be performed: - -**_NOTE_:** Hotfix branches should be carefully reviewed and only performed when the software is considered **broken** and/or **unusable**. Changes to this branch should be absolutely minimal and merged directly into the master branch via a pull request. -- **Branch Syntax:** hotfix/\-\ - - Example: hotfix/123-my-hotfix -- **Branches That Can Merge Into Hotfix Branches:** none -- **Created From:** master -- **Merges Into:** master -- **Environment:** none -- **CI/CD:** - - Build and unit test status checks are automatically run for each change to the pull request. - - All status checks must pass for a pull request to be completed. - - -

Release Branches

- -Represents features and/or bug fixes to be released as a production or preview release: -- **Branch Syntax:** release/v\.\.\ - - Example: release/v1.2.3 -- **Branches That Can Merge Into Release Branches:** - - Preview branches via pull request -- **Created From:** develop branch -- **Merged Into:** develop and master branches -- **Environment:** none -- **CI/CD:** - - Can be a major, minor, or patch release. - - Can be used for preview releases. - - Preview releases are only done manually. - - Build, unit test, and version validation status checks are automatically run for each change to the pull request. - - All status checks must pass for a pull request to be completed. - - When a release is performed, 2 pull requests are created. One for a merge into the develop branch and one for a merge into the master branch. - - Upon merging into the develop (QA) branch, a QA release will be automatically performed. - - Upon merging into the master (Production) branch, a production release will be automatically performed. - - -

Preview Branches

- -Holds minimal changes for the purpose of upcoming production release stability. - -**_NOTE_:** Used for refactoring, bug fixes, and changes related to making an upcoming release more stable and to give users the chance to utilize the software and provide feedback before a major release. Introducing major features outside of the changes in the upcoming release are not allowed. These kinds of changes are performed on the preview branch by using preview feature branches. -- **Branch Syntax:** preview/v\.\.\-preview.\ - - Example: preview/v1.2.3-preview.4 -- **Branches That Can Merge Into Preview Branches:** preview feature branches -- **Created From:** release branches -- **Merged Into:** release branches -- **Environment:** none -- **CI/CD:** - - The major, minor, and patch numbers of the preview branch and the release branch it was created from, must match. - - Build, unit test, and version validation status checks are automatically run for each change to the pull request. - - All status checks must pass for a pull request to be completed. - - -

Preview Feature Branches

- -Where a developer's work will be performed when implementing features/changes for a preview branch via a pull request. -- **Branch Syntax:** preview/feature/\-\ - - Example: preview/feature/123-my-branch -- **Branches That Can Merge Into Preview Feature Branches:** none -- **Created From:** preview branches -- **Merged Into:** preview branches -- **Environment:** none -- **CI/CD:** - - Build and unit test status checks are automatically run for each change to the pull request. - - All status checks must pass for a pull request to be completed. - ---- - -

-
- Branching Diagram -
- -![BranchingDiagram](./Images/BranchingDiagram-DarkMode-v1.1.png#gh-dark-mode-only) -![BranchingDiagram](./Images/BranchingDiagram-LightMode-v1.1.png#gh-light-mode-only) -

diff --git a/Documentation/Images/BranchingDiagram-DarkMode-v1.1.png b/Documentation/Images/BranchingDiagram-DarkMode-v1.1.png deleted file mode 100644 index 5fb7ca8..0000000 Binary files a/Documentation/Images/BranchingDiagram-DarkMode-v1.1.png and /dev/null differ diff --git a/Documentation/Images/BranchingDiagram-LightMode-v1.1.png b/Documentation/Images/BranchingDiagram-LightMode-v1.1.png deleted file mode 100644 index 2b8c223..0000000 Binary files a/Documentation/Images/BranchingDiagram-LightMode-v1.1.png and /dev/null differ diff --git a/Documentation/Images/carbonate-logo-dark-mode.png b/Documentation/Images/carbonate-logo-dark-mode.png new file mode 100644 index 0000000..b32da7f Binary files /dev/null and b/Documentation/Images/carbonate-logo-dark-mode.png differ diff --git a/Documentation/Images/carbonate-logo-light-mode.png b/Documentation/Images/carbonate-logo-light-mode.png new file mode 100644 index 0000000..43d4ffb Binary files /dev/null and b/Documentation/Images/carbonate-logo-light-mode.png differ diff --git a/Documentation/ReleaseNotes/Preview-Release-Notes-TEMPLATE.md b/Documentation/ReleaseNotes/Preview-Release-Notes-TEMPLATE.md index 9ed083a..929819e 100644 --- a/Documentation/ReleaseNotes/Preview-Release-Notes-TEMPLATE.md +++ b/Documentation/ReleaseNotes/Preview-Release-Notes-TEMPLATE.md @@ -1,5 +1,5 @@

- [TODO: Add project name here] Preview Release Notes - [add-prev-release-here] + Carbonate Preview Release Notes - [add-prev-release-here]

Quick Reminder

diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.1.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.1.md new file mode 100644 index 0000000..ac4fa3b --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.1.md @@ -0,0 +1,16 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.1 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#1](https://github.com/KinsonDigital/Carbonate/issues/1) - This is the initial implementation and release of this library. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.10.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.10.md new file mode 100644 index 0000000..6062334 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.10.md @@ -0,0 +1,20 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.10 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#51](https://github.com/KinsonDigital/Carbonate/issues/51) - Added the 2 interfaces below: + >💡The classes `PushReactable` and `PullReactable` now inherit these interfaces respectively which + > give the ability to use the interface types instead of the class types and still get access to the push and pull methods. This also increases testability and mocking. + - `IPushReactable` + - `IPullReactable` diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.11.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.11.md new file mode 100644 index 0000000..c667fd9 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.11.md @@ -0,0 +1,23 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.11 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

Bug Fixes 🐛

+ +1. [#55](https://github.com/KinsonDigital/Carbonate/issues/55) - Fixed a bug where all subscriptions with any response ID were being invoked for pull reactables. + +--- + +

Other 🪧

+
(Includes anything that does not fit into the categories above)
+ +1. [#55](https://github.com/KinsonDigital/Carbonate/issues/55) - Added a small performance improvement to the `PullReactable.Pull()` methods. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.12.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.12.md new file mode 100644 index 0000000..c67bfc9 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.12.md @@ -0,0 +1,36 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.12 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +
+ +## ⚠️WARNING!!⚠️ +
+ +
+ +This release contains _**MANY**_ breaking changes and the list below is not exhaustive. The purpose of these changes was to enable better usability, flexibility, testability, and type safety. +
+ +--- + +

Breaking Changes 🧨

+ +1. [#59](https://github.com/KinsonDigital/Carbonate/issues/59) - The following breaking changes were introduced: + >💡This list is not exhaustive + - Created the following 6 namespaces for better organization and to help increase understanding of the data flow of the various classes and interfaces. The majority of the classes and interfaces in the library have been moved to these namespaces. Each class and interfaces was moved into the correct namespace that matches the direction that data may or may not flow. + - `Carbonate.Core.NonDirectional` + - `Carbonate.Core.UniDirectional` + - `Carbonate.Core.BiDirectional` + - `Carbonate.NonDirectional` + - `Carbonate.UniDirectional` + - `Carbonate.BiDirectional` + - Moved the majority of the interfaces and classes in the library to one of the new interfaces mentioned above. + - Added generic parameters to various interfaces and the associated class implementations. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.13.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.13.md new file mode 100644 index 0000000..6ebfa50 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.13.md @@ -0,0 +1,36 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.13 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

Breaking Changes 🧨

+ +1. [#71](https://github.com/KinsonDigital/Carbonate/issues/71) - Removed the following types from the library: + >💡This was removed due to the types not being required anymore. Changes to the library have made these types not unnecessary. + - `IMessage` + - `IResult` + - `MessageFactory` + - `ResultFactory` +2. [#71](https://github.com/KinsonDigital/Carbonate/issues/71) - Changed all of the return types for the following interfaces and classes to nullable. + - `PullReactable` + - `IPullable` + - `RespondReactor` + - `IPullable` + - `PullReactable` + +--- + +

Other 🪧

+
(Includes anything that does not fit into the categories above)
+ +1. [#67](https://github.com/KinsonDigital/Carbonate/issues/67) - Added XML code documentation to the NuGet package. + >💡This will provide documentation in the IDE during development. +2. [#66](https://github.com/KinsonDigital/Carbonate/issues/66) - Updated badges in the project's README file. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.14.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.14.md new file mode 100644 index 0000000..d06cb03 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.14.md @@ -0,0 +1,27 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.14 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#77](https://github.com/KinsonDigital/Carbonate/issues/77) - Changed the following classes: + >💡These changes were done to give users more control. + - Removed the `sealed` keyword from the following classes: + - `NonDirectional.ReceiveReactor` + - `UniDirectional.ReceiveReactor` + - `UniDirectional.RespondReactor` + - `BiDirectional.RespondReactor` + - Changed the following methods to `virtual`: + - `NonDirectional.ReceiveReactor.OnReceive()` + - `UniDirectional.ReceiveReactor.OnReceive()` + - `UniDirectional.RespondReactor.OnRespond()` + - `BiDirectional.RespondReactor.OnRespond()` diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.2.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.2.md new file mode 100644 index 0000000..e28a439 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.2.md @@ -0,0 +1,33 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.2 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#9](https://github.com/KinsonDigital/Carbonate/issues/9) - Added a parameterless constructor to the `Reactable` class. + >💡This was added to use the default internal `ISerializer` implementation for passing messages. + +--- + +

Internal Changes ⚙️

+
(Changes that do not affect users. Not breaking changes, new features, or bug fixes.)
+ +1. [#10](https://github.com/KinsonDigital/Carbonate/issues/10) - Updated the [CICD](https://github.com/KinsonDigital/CICD) build system from version _**v1.0.0-preview.14**_ to _**v1.0.0-preview.16**_ + +--- + +

Other 🪧

+
(Includes anything that does not fit into the categories above)
+ +1. [#7](https://github.com/KinsonDigital/Carbonate/issues/7) - Fixed various grammar issues throughout the code base's code documentation. +2. [#7](https://github.com/KinsonDigital/Carbonate/issues/7) - Changed release builds to use non-indented JSON serialization for a small performance improvement. +3. [#6](https://github.com/KinsonDigital/Carbonate/issues/6) - Changed the project logo icon in the README file for the NuGet package to use the light mode version for better visibility. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.3.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.3.md new file mode 100644 index 0000000..a7c71f2 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.3.md @@ -0,0 +1,16 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.3 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

Bug Fixes 🐛

+ +1. [#16](https://github.com/KinsonDigital/Carbonate/issues/16) - Fixed a bug where the `IMessage.GetData()` would always return `null`. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.4.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.4.md new file mode 100644 index 0000000..78a50e5 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.4.md @@ -0,0 +1,16 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.4 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

Bug Fixes 🐛

+ +1. [#20](https://github.com/KinsonDigital/Carbonate/issues/20) - Fixed a bug where only half of the total unsubscriptions were being unsubscribed. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.5.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.5.md new file mode 100644 index 0000000..01447d8 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.5.md @@ -0,0 +1,17 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.5 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

Breaking Changes 🧨

+ +1. [#24](https://github.com/KinsonDigital/Carbonate/issues/24) - Changed the name of the `Reactable.Push()` method overloads. This was done to improve overloaded method resolution. + >💡These methods are not overloaded anymore. One of them has bee named to `PushData()` and the other to `PushMessage()`. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.6.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.6.md new file mode 100644 index 0000000..936711e --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.6.md @@ -0,0 +1,26 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.6 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#29](https://github.com/KinsonDigital/Carbonate/issues/29) - Added a new method with the name `Push()` to the `IReactable` interface and `Reactable` class. + >💡This new method is for pushing a notification without any data. +2. [#29](https://github.com/KinsonDigital/Carbonate/issues/29) - Added a new parameter to the `Reactor` class constructor. + >💡This is the implementation to invoke when the new `IReactable.Push()` method is invoked. + +--- + +

Internal Changes ⚙️

+
(Changes that do not affect users. Not breaking changes, new features, or bug fixes.)
+ +1. [#29](https://github.com/KinsonDigital/Carbonate/issues/29) - Added integration and unit tests diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.7.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.7.md new file mode 100644 index 0000000..ea868e2 --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.7.md @@ -0,0 +1,16 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.7 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

Bug Fixes 🐛

+ +1. [#33](https://github.com/KinsonDigital/Carbonate/issues/33) - Fixed a bug where the `Reactable.UnsubscribeAll()` method was not unsubscribing all of the `IReactor` subscriptions. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.8.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.8.md new file mode 100644 index 0000000..18bd19d --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.8.md @@ -0,0 +1,55 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.8 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#43](https://github.com/KinsonDigital/Carbonate/issues/43) - Added an exception to the `Reactable` class when invoking the methods below after disposal. + >💡This gives the benefit of letting the user know what is expected after using the object after invoking `Reactable.Dispose()`. + - `Reactable.Subscribe()` + - `Reactable.Push()` + - `Reactable.PushData()` + - `Reactable.PushMessage()` + - `Reactable.Unsubscribe()` + - `Reactable.UnsubscribeAll()` +2. [#39](https://github.com/KinsonDigital/Carbonate/issues/39) - Added a new property named `Name` to the `IReactable` interface and `Reactable` class. + >💡This is used to help with debugging and personal identification. + + >⚠️This is not used in anyway to identify unique `Reactors` subscribed to the `Reactable`. That is the purpose of the `EventId` property. +3. [#39](https://github.com/KinsonDigital/Carbonate/issues/39) - Added a default implementation of the `Reactor.ToString()` method that shows the name of the `Reactor` combined with the `EventId` + >💡If a name is used, the format will be _**'\ - \'**_ and _**'\'**_ if no name is used. + +--- + +

Bug Fixes 🐛

+ +1. [#41](https://github.com/KinsonDigital/Carbonate/issues/41) - Fixed the following bugs: + - Fixed a bug where the `Reactable.Unsubscribe()` method was not removing the subscription from the internal subscription list. + - Fixed a bug where a premature unsubscription in `OnNext()` implementation was throwing an exception when invoking the `Reactable.Push()` method. + - Fixed a bug where a premature unsubscription in `OnNext()` implementation was throwing an exception when invoking the `Reactable.PushData()` method. + - Fixed a bug where a premature unsubscription in `OnNext()` implementation was throwing an exception when invoking the `Reactable.PushMessage()` method. + - Fixed a bug where a premature unsubscription in `OnComplete()` implementation was throwing an exception when invoking the `Reactable.Unsubscribe()` method. + - Fixed a bug where a premature unsubscription in `OnComplete()` implementation was throwing an exception when invoking the `Reactable.UnsubscribeAll()` method. +2. [#40](https://github.com/KinsonDigital/Carbonate/issues/40) - Fixed a bug where invoking the `Reactable.Dispose()` method was throwing an exception. + +--- + +

Breaking Changes 🧨

+ +1. [#41](https://github.com/KinsonDigital/Carbonate/issues/41) - Renamed the `ISerialize` interface to `ISerializeService`. + +--- + +

Internal Changes ⚙️

+
(Changes that do not affect users. Not breaking changes, new features, or bug fixes.)
+ +1. [#37](https://github.com/KinsonDigital/Carbonate/issues/37) - Updated the [checkout](https://github.com/marketplace/actions/checkout) GitHub actions in all of the workflows from _**v2**_ to _**v3**_. diff --git a/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.9.md b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.9.md new file mode 100644 index 0000000..910276b --- /dev/null +++ b/Documentation/ReleaseNotes/PreviewReleases/Release-Notes-v1.0.0-preview.9.md @@ -0,0 +1,77 @@ +

+ Carbonate Preview Release Notes - v1.0.0-preview.9 +

+ +

Quick Reminder

+ +
+ +As with all software, there is always a chance for issues and bugs, especially for preview releases, which is why your input is greatly appreciated. 🙏🏼 +
+ +--- + +

New Features ✨

+ +1. [#47](https://github.com/KinsonDigital/Carbonate/issues/47) - Added the ability to _**Pull**_ a message from a source. This is the opposite of the old functionality of doing _**Push**_. Think of this as requesting data from a source. The following new types have been added to the library to not only add this functionality but improve upon the original functionality. + - `IReceiver` + - `IReceiveReactor` + - `IResponder` + - `IRespondReactor` + - `IResult` + - `IPullable` + - `IPushable` + - `PullReactable` + - `PushReactable` + - `ReactableBase` + - `RespondReactor` + - `ResultFactory` + +--- + +

Breaking Changes 🧨

+ +1. [#47](https://github.com/KinsonDigital/Carbonate/issues/47) - Made various breaking changes throughout the library. + - Renamed the following `Reactor` class constructor parameters: + - Renamed the parameter `onNext` to `onReceive`. + - Renamed the parameter `onNextMsg` to `onReceiveMsg`. + - Renamed the parameter `onCompleted` to `onUnsubscribe`. + - Renamed the interface `IReactor.OnComplete()` method (now the `IReceiveReactor` class), to `OnUnsubscribe()`. + - Renamed the class `Reactor.OnComplete()` method (now the `ReceiveReactor` class), to `OnUnsubscribe()`. + - Renamed the class `Reactor` to `ReceiveReactor`. + - Renamed the class `IReactor` to `IReceiveReactor`. + - Added a class-level generic to the `IReactable` interface. + >💡This new generic is constrained to the interface `IReactor`. + - Moved the following methods in the `IReactable` interface to the new `IPushable` interface: + - `Push()` + - `PushData()` + - `PushMessage()` + - Renamed the `IReactable.EventIds` property to `SubscriptionIds`. + - The interface `IReactor` methods `OnNext()` and `OnComplete()` have been removed. + - The interface `IReactable` + - Renamed the `Reactable` class to `PushReactable`. + - Moved the following types to the namespace `Carbonate.Core`: + - `IReactable` + - `Reactable` + >💡Now named `PushReactable` + - `IReactor` + - `Reactor` + >💡Now named `ReceiveReactor` + - `IMessage` + +--- + +

Nuget/Library Updates 📦

+ +1. [#47](https://github.com/KinsonDigital/Carbonate/issues/47) - Updated, replaced, and removed various NuGet packages. + - Replaced the NuGet package **NSubstitute** with **Moq** version _**4.18.4**_ + >💡Also updated all of the mocking code in the unit tests project to use **Moq**. + - Updated the NuGet package **Microsoft.NET.Test.Sdk** from version _**v17.3.2**_ to _**v17.4.1**_ for the integration tests project. + - Updated the NuGet package **Microsoft.NET.Test.Sdk** from version _**v17.4.0**_ to _**v17.4.1**_ for the unit tests project. + +--- + +

Other 🪧

+
(Includes anything that does not fit into the categories above)
+ +1. [#47](https://github.com/KinsonDigital/Carbonate/issues/47) - Added a new word to the dictionary for _**JetBrains Rider**_ users. diff --git a/Documentation/ReleaseNotes/Production-Release-Notes-TEMPLATE.md b/Documentation/ReleaseNotes/Production-Release-Notes-TEMPLATE.md index 8c4c64d..ab34316 100644 --- a/Documentation/ReleaseNotes/Production-Release-Notes-TEMPLATE.md +++ b/Documentation/ReleaseNotes/Production-Release-Notes-TEMPLATE.md @@ -1,5 +1,5 @@

- [TODO: Add project name here] Production Release Notes - [add-prod-release-here] + Carbonate Production Release Notes - [add-prod-release-here]

New Features✨

diff --git a/LICENSE b/LICENSE.md similarity index 96% rename from LICENSE rename to LICENSE.md index 51b6230..3120b4a 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Calvin Wilkinson +Copyright (c) 2023 Calvin Wilkinson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c1d7181..a203448 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,51 @@ -

[TODO: add project name here]

+
+ +![logo](./Documentation/Images/carbonate-logo-light-mode.png#gh-light-mode-only) +![logo](./Documentation/Images/carbonate-logo-dark-mode.png#gh-dark-mode-only) +
+ +

Carbonate

- +[![Prod Release PR Status Check](https://img.shields.io/github/actions/workflow/status/KinsonDigital/Carbonate/prod-release-pr-status-check.yml?color=2F8840&label=Prod%20CI%20Build&logo=GitHub)](https://github.com/KinsonDigital/Carbonate/actions/workflows/prod-release-pr-status-check.yml) +[![Prev Release PR Status Check](https://img.shields.io/github/actions/workflow/status/KinsonDigital/Carbonate/prev-release-pr-status-check.yml?color=2F8840&label=Preview%20CI%20Build&logo=GitHub)](https://github.com/KinsonDigital/Carbonate/actions/workflows/prev-release-pr-status-check.yml) +[![Code Coverage](https://img.shields.io/codecov/c/github/KinsonDigital/Carbonate/preview?label=Code%20Coverage&logo=CodeCov&style=flat)](https://app.codecov.io/gh/KinsonDigital/Carbonate) -
+[![Latest NuGet Release](https://img.shields.io/nuget/vpre/kinsondigital.Carbonate?label=Latest%20Release&logo=nuget)](https://www.nuget.org/packages/KinsonDigital.Carbonate) +[![Nuget Downloads](https://img.shields.io/nuget/dt/KinsonDigital.Carbonate?color=0094FF&label=nuget%20downloads&logo=nuget)](https://www.nuget.org/stats/packages/KinsonDigital.Carbonate?groupby=Version) +[![Good First Issues](https://img.shields.io/github/issues/kinsondigital/Carbonate/good%20first%20issue?color=7057ff&label=Good%20First%20Issues)](https://github.com/KinsonDigital/Carbonate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +[![Discord](https://img.shields.io/discord/481597721199902720?color=%23575CCB&label=chat%20on%20discord&logo=discord&logoColor=white)](https://discord.gg/qewu6fNgv7) +

!! NOTICE !!

-We encourage you to use it and report back any issues and improvements you may have. That is what open source is all about. 🥳 +This library is still under development and is not at v1.0.0 yet!! However, all of the major features are available, so we encourage you to use the library and provide feedback. That is what open source is all about. 🥳 -

📖 About [TODO: add project name here]

+

📖 About Carbonate 📖

- +**Carbonate** is a messaging library for various applications, using the observable pattern. It adds the ability to easily and reliably send messages to different parts and/or systems of an application. -

🔧Maintainers

+

🙏🏼 Contributing 🙏🏼

-We currently have the following maintainers: -- [Calvin Wilkinson](https://twitter.com/KDCoder) [Follow Calvin Wilkinson on Twitter](https://twitter.com/KDCoder) (GitHub Organization / Owner) -- [Kristen Wilkinson](https://twitter.com/kswilky) [Follow Calvin Wilkinson on Twitter](https://twitter.com/KDCoder) (GitHub Organization / Documentation Maintainer / Tester) +Interested in contributing? If so, click [here](https://github.com/KinsonDigital/.github/blob/master/docs/CONTRIBUTING.md) to learn how to contribute your time or [here](https://github.com/sponsors/KinsonDigital) if you are interested in contributing your funds via a one-time or recurring donation. -

🙏🏼Contributing

+

🔧 Maintainers 🔧

-**[TODO: add project name here]** encourages and uses [Early Pull Requests](https://medium.com/practical-blend/pull-request-first-f6bb667a9b6). Please don't wait until you're finished with your work before creating a PR. +[![twitter-logo](https://raw.githubusercontent.com/KinsonDigital/.github/master/Images/twitter-logo-16x16.svg)Calvin Wilkinson](https://twitter.com/KDCoder) (KinsonDigital GitHub Organization - Owner) -1. Fork the repository. -2. Create a feature branch following the feature branch section in the documentation [here](./Documentation/Branching.md). -3. Add an empty commit to the new feature branch to start your work off. - * Use this git command: `git commit --allow-empty -m "start work for issue #"`. - * Example: `git commit --allow-empty -m "start work for issue #123"`. -4. Once you've pushed a commit, open a [**draft pull request**](https://github.blog/2019-02-14-introducing-draft-pull-requests/). Do this **before** you actually start working. -5. Make your commits in small, incremental steps with clear descriptions. -6. All unit tests must pass before a PR will be completed. -7. Make sure that the code follows the the coding standards. - * Pay attention to the warnings in **Visual Studio**!! - * Refer to the *.editorconfig* files in the code base for rules. -8. Tag a maintainer when you're done and ask for a review! +[![twitter-logo](https://raw.githubusercontent.com/KinsonDigital/.github/master/Images/twitter-logo-16x16.svg)Kristen Wilkinson](https://twitter.com/kswilky) (KinsonDigital GitHub Organization - Documentation Maintainer & Tester) -If you have any questions, please reach out to a project maintainer. -

Practices

+

🚔 Licensing and Governance 🚔

-- The code base is highly tested by using unit tests while maintaining a high level of code coverage. Manual testing is performed using the included testing application built specifically for manually testing the library. When contributing, make sure to add or adjust the unit tests appropriately regarding your changes and perform manual testing. -- We use a combination of [StyleCop](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) and [Microsoft.CodeAnalysis.NetAnalyzers](https://github.com/dotnet/roslyn-analyzers) libraries for maintaining coding standards. - - We understand that there are some exceptions to the rule and not all coding standards fit every situation. In these scenarios, contact a maintainer and lets discuss it!! Warnings can always be suppressed if need be. -- We use [semantic versioning 2.0](https://semver.org/) for versioning. -- Branching model below. - - [Branching Diagram (GitHub Dark Mode)](./Documentation/Images/BranchingDiagram-DarkMode-v1.1.png) - - [Branching Diagram (GitHub Light Mode)](./Documentation/Images/BranchingDiagram-LightMode-v1.1.png) +
-

Licensing And Governance

-
+[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=flat)](https://github.com/KinsonDigital/.github/blob/master/docs/code_of_conduct.md) +[![GitHub](https://img.shields.io/github/license/kinsondigital/CASL)](https://github.com/KinsonDigital/Carbonate/blob/release/v1.0.0/LICENSE.md) -[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg?style=flat)](code_of_conduct.md) -![GitHub](https://img.shields.io/github/license/kinsondigital/[TODO: add project name here]) -
+
-**[TODO: add project name here]** is distributed under the very permissive **MIT license** and all dependencies are distributed under MIT-compatible licenses. +This software is distributed under the very permissive MIT license and all dependencies are distributed under MIT-compatible licenses. This project has adopted the code of conduct defined by the **Contributor Covenant** to clarify expected behavior in our community. diff --git a/Testing/.editorconfig b/Testing/.editorconfig index f8a738c..e191cf1 100644 --- a/Testing/.editorconfig +++ b/Testing/.editorconfig @@ -9,15 +9,14 @@ indent_style = space [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true -charset = utf-8-bom ############################### # .NET Coding Conventions # ############################### [*.cs] -# Expression-bodied members -csharp_style_expression_bodied_methods = true:none + +csharp_style_expression_bodied_methods = true:silent ############################### # C# Formatting Rules # @@ -39,7 +38,7 @@ dotnet_diagnostic.SA1124.severity = none dotnet_diagnostic.CA2000.severity = none # IDE0017: Simplify object initialization -dotnet_style_object_initializer = false:none +dotnet_style_object_initializer = true:none # CA1063: Implement IDisposable Correctly dotnet_diagnostic.CA1063.severity = none @@ -70,5 +69,3 @@ dotnet_diagnostic.CS8625.severity = none # CS8604: Possible null reference argument. dotnet_diagnostic.CS8604.severity = none - -resharper_redundant_name_qualifier_highlighting=none diff --git a/Testing/CarbonateIntegrationTests/CarbonateIntegrationTests.csproj b/Testing/CarbonateIntegrationTests/CarbonateIntegrationTests.csproj new file mode 100644 index 0000000..b5fb182 --- /dev/null +++ b/Testing/CarbonateIntegrationTests/CarbonateIntegrationTests.csproj @@ -0,0 +1,46 @@ + + + + net7.0 + enable + enable + + false + + Release;Debug + + x64 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/Testing/CarbonateIntegrationTests/NonDirectionalPushReactable_IntegrationTests.cs b/Testing/CarbonateIntegrationTests/NonDirectionalPushReactable_IntegrationTests.cs new file mode 100644 index 0000000..8afeec5 --- /dev/null +++ b/Testing/CarbonateIntegrationTests/NonDirectionalPushReactable_IntegrationTests.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// ReSharper disable AccessToModifiedClosure +namespace CarbonateIntegrationTests; + +using System.Diagnostics.CodeAnalysis; +using Carbonate.NonDirectional; +using FluentAssertions; +using Xunit; + +/// +/// Tests all of the components integrated together related to the . +/// +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Integrations Tests Are Named In This Way")] +public class NonDirectionalPushReactable_IntegrationTests +{ + [Fact] + public void WhenPushing_And_WithSingleEventID_And_WithSingleSubscription_And_WithSingleUnsubscribe_ReturnsCorrectResults() + { + // Arrange + var eventId = new Guid("98a879d4-e819-41da-80e4-a1b459b3e43f"); + + IDisposable? unsubscriber = null; + + var reactable = new PushReactable(); + + unsubscriber = reactable.Subscribe(new ReceiveReactor( + eventId: eventId, + onReceive: () => { }, + onUnsubscribe: () => unsubscriber?.Dispose())); + + // Act + reactable.Push(eventId); + reactable.Unsubscribe(eventId); + + // Assert + reactable.Reactors.Should().HaveCount(0); + } +} diff --git a/Testing/CarbonateIntegrationTests/PullReactableWithData_IntegrationTests.cs b/Testing/CarbonateIntegrationTests/PullReactableWithData_IntegrationTests.cs new file mode 100644 index 0000000..e14fb39 --- /dev/null +++ b/Testing/CarbonateIntegrationTests/PullReactableWithData_IntegrationTests.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateIntegrationTests; + +using System.Diagnostics.CodeAnalysis; +using Carbonate.BiDirectional; +using FluentAssertions; +using Xunit; + +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Integrations Tests Are Named In This Way")] +public class PullReactableWithData_IntegrationTests +{ + #region Method Tests + [Fact] + public void WhenUsingPull_WithOutgoingData_ReturnsCorrectResult() + { + // Arrange + var respondId = Guid.NewGuid(); + + var sut = new PullReactable(); + + sut.Subscribe(new RespondReactor( + respondId: respondId, + name: "test-name", + onRespondData: _ => new SampleData { IntValue = 123, StringValue = "test-str" })); + + // Act + var actual = sut.Pull(123, respondId); + + // Assert + actual.Should().NotBeNull(); + actual.Should().BeOfType(); + actual.IntValue.Should().Be(123); + actual.StringValue.Should().Be("test-str"); + } + #endregion +} diff --git a/Testing/CarbonateIntegrationTests/PullReactableWithoutDataGoingIn_IntegrationTests.cs b/Testing/CarbonateIntegrationTests/PullReactableWithoutDataGoingIn_IntegrationTests.cs new file mode 100644 index 0000000..6f06950 --- /dev/null +++ b/Testing/CarbonateIntegrationTests/PullReactableWithoutDataGoingIn_IntegrationTests.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateIntegrationTests; + +using System.Diagnostics.CodeAnalysis; +using Carbonate.UniDirectional; +using FluentAssertions; +using Xunit; + +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Integrations Tests Are Named In This Way")] +public class PullReactableWithoutDataGoingIn_IntegrationTests +{ + #region Method Tests + [Fact] + public void WhenUsingPull_WithNoOutgoingData_ReturnsCorrectResult() + { + // Arrange + var respondId = Guid.NewGuid(); + + var sut = new PullReactable(); + + sut.Subscribe(new RespondReactor( + respondId: respondId, + name: "test-name", + onRespond: () => new SampleData { IntValue = 123, StringValue = "test-str" })); + + // Act + var actual = sut.Pull(respondId); + + // Assert + actual.Should().NotBeNull(); + actual.Should().BeOfType(); + actual.IntValue.Should().Be(123); + actual.StringValue.Should().Be("test-str"); + } + #endregion +} diff --git a/Testing/CarbonateIntegrationTests/SampleData.cs b/Testing/CarbonateIntegrationTests/SampleData.cs new file mode 100644 index 0000000..c7d0696 --- /dev/null +++ b/Testing/CarbonateIntegrationTests/SampleData.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateIntegrationTests; + +/// +/// Holds sample data for the purpose of integration testing. +/// +public class SampleData +{ + /// + /// Gets the sample string value. + /// + public string StringValue { get; init; } = string.Empty; + + /// + /// Gets the sample int value. + /// + public int IntValue { get; init; } +} diff --git a/Testing/CarbonateIntegrationTests/stylecop.json b/Testing/CarbonateIntegrationTests/stylecop.json new file mode 100644 index 0000000..f946ff0 --- /dev/null +++ b/Testing/CarbonateIntegrationTests/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "KinsonDigital" + } + } +} diff --git a/Testing/CarbonatePerfTests/Benchmarks/PullReactable_Class.cs b/Testing/CarbonatePerfTests/Benchmarks/PullReactable_Class.cs new file mode 100644 index 0000000..f84b22a --- /dev/null +++ b/Testing/CarbonatePerfTests/Benchmarks/PullReactable_Class.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests.Benchmarks; + +using System.Diagnostics.CodeAnalysis; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Carbonate.UniDirectional; + +[MemoryDiagnoser] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Only used for testing.")] +public class PullReactable_Class +{ + // ReSharper disable NotAccessedField.Local + private PullReactable? pullReactableA; + private PullReactable? pullReactableB; + private StructDataStore? structDataStore; + private StructDataPuller? structDataPuller; + private PtrDataStore? ptrDataStore; + private PtrDataPuller? ptrDataPuller; + + // ReSharper restore NotAccessedField.Local + [GlobalSetup] + public void GlobalSetup() + { + this.pullReactableA = new PullReactable(); + this.pullReactableB = new PullReactable(); + + this.structDataStore = new StructDataStore(this.pullReactableA); + this.structDataPuller = new StructDataPuller(this.pullReactableA); + + this.ptrDataStore = new PtrDataStore(this.pullReactableB); + this.ptrDataPuller = new PtrDataPuller(this.pullReactableB); + } + + [Benchmark(Description = "PullReactable.Pull() Method | Setup A")] + public void PullReactable_Pull_Method_Setup_A() + { + _ = this.structDataPuller.Pull(); + } + + [Benchmark(Description = "PullReactable.Pull() Method | Setup B")] + public void PullReactable_Pull_Method_Setup_B() + { + _ = this.ptrDataPuller.Pull(); + } +} diff --git a/Testing/CarbonatePerfTests/CarbonatePerfTests.csproj b/Testing/CarbonatePerfTests/CarbonatePerfTests.csproj new file mode 100644 index 0000000..287d493 --- /dev/null +++ b/Testing/CarbonatePerfTests/CarbonatePerfTests.csproj @@ -0,0 +1,41 @@ + + + + Exe + net7.0 + enable + enable + Release;Debug;Release Benchmark;Release MemPerf + x64 + true + + + + true + + + + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Testing/CarbonatePerfTests/DataItemResult.cs b/Testing/CarbonatePerfTests/DataItemResult.cs new file mode 100644 index 0000000..2d9be2e --- /dev/null +++ b/Testing/CarbonatePerfTests/DataItemResult.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +public class DataItemResult +{ + private readonly StructItem[] dataItems; + + public DataItemResult(Memory dataItems) => this.dataItems = dataItems.ToArray(); + + public bool IsEmpty => false; + + public StructItem[] GetValue(Action? onError = null) => this.dataItems; +} diff --git a/Testing/CarbonatePerfTests/DataResult.cs b/Testing/CarbonatePerfTests/DataResult.cs new file mode 100644 index 0000000..2a78eaa --- /dev/null +++ b/Testing/CarbonatePerfTests/DataResult.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +public class DataResult +{ + private readonly nint dataItems; + + public DataResult(nint dataItems) => this.dataItems = dataItems; + + public bool IsEmpty => false; + + public nint GetValue(Action? onError = null) => this.dataItems; +} diff --git a/Testing/CarbonatePerfTests/Ids.cs b/Testing/CarbonatePerfTests/Ids.cs new file mode 100644 index 0000000..54fc316 --- /dev/null +++ b/Testing/CarbonatePerfTests/Ids.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +/// +/// Used for testing. +/// +public static class Ids +{ + /// + /// Gets a used for testing. + /// + public static Guid GetDatId { get; } = new ("daa5ed5a-dd38-49a3-bec9-881af21100ec"); +} diff --git a/Testing/CarbonatePerfTests/MemPerfs/MemPerfRunner.cs b/Testing/CarbonatePerfTests/MemPerfs/MemPerfRunner.cs new file mode 100644 index 0000000..fed3eb8 --- /dev/null +++ b/Testing/CarbonatePerfTests/MemPerfs/MemPerfRunner.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests.MemPerfs; + +using System.Diagnostics.CodeAnalysis; +using Carbonate.UniDirectional; + +public static class MemPerfRunner +{ + // ReSharper disable NotAccessedField.Local + private static readonly StructDataStore StructStructDataStore; + private static readonly StructDataPuller StructStructDataPuller; + private static readonly PtrDataStore PointerDataStore; + private static readonly PtrDataPuller PointerDataPuller; + + // ReSharper restore NotAccessedField.Local + + /// + /// Initializes static members of the class. + /// + static MemPerfRunner() + { + var pullStructReactable = new PullReactable(); + StructStructDataStore = new StructDataStore(pullStructReactable); + StructStructDataPuller = new StructDataPuller(pullStructReactable); + + var pullPtrReactable = new PullReactable(); + PointerDataStore = new PtrDataStore(pullPtrReactable); + PointerDataPuller = new PtrDataPuller(pullPtrReactable); + } + + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used for testing.")] + public static void Run(PerfScenarios perfScenarioTypes, int iterationTime = -1) + { + while (true) + { + switch (perfScenarioTypes) + { + case PerfScenarios.PullReactable_Pull_Method_With_Struct: + _ = StructStructDataPuller.Pull(); + break; + case PerfScenarios.PullReactable_Pull_Method_With_Ptr: + _ = PointerDataPuller.Pull(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(perfScenarioTypes), perfScenarioTypes, null); + } + + if (iterationTime != -1) + { + Thread.Sleep(iterationTime); + } + } + } +} diff --git a/Testing/CarbonatePerfTests/MemPerfs/PerfScenarios.cs b/Testing/CarbonatePerfTests/MemPerfs/PerfScenarios.cs new file mode 100644 index 0000000..075d965 --- /dev/null +++ b/Testing/CarbonatePerfTests/MemPerfs/PerfScenarios.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// ReSharper disable InconsistentNaming +namespace CarbonatePerfTests.MemPerfs; + +using Carbonate.UniDirectional; + +/// +/// Used for testing different scenarios. +/// +public enum PerfScenarios +{ + /// + /// For testing the . + /// method for struct types. + /// + PullReactable_Pull_Method_With_Struct, + + /// + /// For testing the . + /// method for types. + /// + PullReactable_Pull_Method_With_Ptr, +} diff --git a/Testing/CarbonatePerfTests/Program.cs b/Testing/CarbonatePerfTests/Program.cs new file mode 100644 index 0000000..796e4ce --- /dev/null +++ b/Testing/CarbonatePerfTests/Program.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +// ReSharper disable RedundantUsingDirective +using BenchmarkDotNet.Running; +using CarbonatePerfTests; +using Benchmarks; +using MemPerfs; + +// ReSharper restore RedundantUsingDirective +internal static class Program +{ + public static void Main() + { +#if DEBUG + Console.WriteLine("Add simple debugging code and manual testing here."); + +#elif RELEASE + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("WARNING!!"); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("This application is for testing purposes only."); + Console.WriteLine("This can only be run with the following solution configuartions:"); + Console.WriteLine("\t- Release Benchmark"); + Console.WriteLine("\t- Release MemPerf"); + +#elif RELEASE_BENCHMARK + + var summary = BenchmarkRunner.Run(); + Console.WriteLine(summary); + +#elif RELEASE_MEMPERF + + // MemPerfRunner.Run(PerfScenarios.PullReactable_Pull_Method_With_Struct, -1); + MemPerfRunner.Run(PerfScenarios.PullReactable_Pull_Method_With_Ptr, -1); + +#endif + Console.ReadLine(); + } +} diff --git a/Testing/CarbonatePerfTests/PtrDataPuller.cs b/Testing/CarbonatePerfTests/PtrDataPuller.cs new file mode 100644 index 0000000..afa69b7 --- /dev/null +++ b/Testing/CarbonatePerfTests/PtrDataPuller.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +using System.Runtime.CompilerServices; +using Carbonate.UniDirectional; + +/// +/// Used for perf testing. +/// +public class PtrDataPuller +{ + private readonly IPullReactable pullReactable; + + public PtrDataPuller(IPullReactable pullReactable) => this.pullReactable = pullReactable; + + public Span Pull() + { + var result = this.pullReactable.Pull(Ids.GetDatId); + + unsafe + { + return Unsafe.AsRef>(result.ToPointer()).Span; + } + } +} diff --git a/Testing/CarbonatePerfTests/PtrDataStore.cs b/Testing/CarbonatePerfTests/PtrDataStore.cs new file mode 100644 index 0000000..8d459e2 --- /dev/null +++ b/Testing/CarbonatePerfTests/PtrDataStore.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +using System.Runtime.CompilerServices; +using Carbonate.UniDirectional; + +public class PtrDataStore +{ + private Memory dataItems; + + /// + /// Initializes a new instance of the class. + /// + /// Pulls data from a source. + public PtrDataStore(IPullReactable pullReactable) + { + var newDataItems = new StructItem[4]; + newDataItems[0] = new StructItem { NumberValue = 10, StringValue = "ten" }; + newDataItems[1] = new StructItem { NumberValue = 20, StringValue = "twenty" }; + newDataItems[2] = new StructItem { NumberValue = 30, StringValue = "thirty" }; + newDataItems[3] = new StructItem { NumberValue = 40, StringValue = "forty" }; + + this.dataItems = new Memory(newDataItems); + + pullReactable.Subscribe(new RespondReactor( + respondId: Ids.GetDatId, + onRespond: GetDataPtr)); + } + + /// + /// Gets the pointer data result. + /// + /// The result. + private nint GetDataPtr() + { + unsafe + { + return new nint(Unsafe.AsPointer(ref this.dataItems)); + } + } +} diff --git a/Testing/CarbonatePerfTests/StructDataPuller.cs b/Testing/CarbonatePerfTests/StructDataPuller.cs new file mode 100644 index 0000000..ce7aebb --- /dev/null +++ b/Testing/CarbonatePerfTests/StructDataPuller.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +using Carbonate.UniDirectional; + +/// +/// Used for per testing. +/// +public class StructDataPuller +{ + private readonly IPullReactable pullReactable; + + public StructDataPuller(IPullReactable pullReactable) => this.pullReactable = pullReactable; + + public StructItem[]? Pull() + { + return this.pullReactable.Pull(Ids.GetDatId); + } +} diff --git a/Testing/CarbonatePerfTests/StructDataStore.cs b/Testing/CarbonatePerfTests/StructDataStore.cs new file mode 100644 index 0000000..a41d4da --- /dev/null +++ b/Testing/CarbonatePerfTests/StructDataStore.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonatePerfTests; + +using Carbonate.UniDirectional; + +/// +/// Used for performance testing. +/// +public class StructDataStore +{ + // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable + private readonly Memory dataItems; + + // ReSharper restore PrivateFieldCanBeConvertedToLocalVariable + + /// + /// Initializes a new instance of the class. + /// + /// Pulls data from a source. + public StructDataStore(IPullReactable pullReactable) + { + var newDataItems = new StructItem[4]; + newDataItems[0] = new StructItem { NumberValue = 10, StringValue = "ten" }; + newDataItems[1] = new StructItem { NumberValue = 20, StringValue = "twenty" }; + newDataItems[2] = new StructItem { NumberValue = 30, StringValue = "thirty" }; + newDataItems[3] = new StructItem { NumberValue = 40, StringValue = "forty" }; + + this.dataItems = new Memory(newDataItems); + + pullReactable.Subscribe(new RespondReactor( + respondId: Ids.GetDatId, + onRespond: () => this.dataItems.Span.ToArray())); + } +} diff --git a/Testing/CarbonatePerfTests/StructItem.cs b/Testing/CarbonatePerfTests/StructItem.cs new file mode 100644 index 0000000..33ae1af --- /dev/null +++ b/Testing/CarbonatePerfTests/StructItem.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// ReSharper disable UnusedAutoPropertyAccessor.Global +namespace CarbonatePerfTests; + +/// +/// Used for perf testing. +/// +public readonly struct StructItem +{ + public int NumberValue { get; init; } + + public string StringValue { get; init; } +} diff --git a/Testing/CSharpLibTemplateRepoTests/stylecop.json b/Testing/CarbonatePerfTests/stylecop.json similarity index 100% rename from Testing/CSharpLibTemplateRepoTests/stylecop.json rename to Testing/CarbonatePerfTests/stylecop.json diff --git a/Testing/CarbonateTests/BiDirectional/PullReactableTests.cs b/Testing/CarbonateTests/BiDirectional/PullReactableTests.cs new file mode 100644 index 0000000..c84726a --- /dev/null +++ b/Testing/CarbonateTests/BiDirectional/PullReactableTests.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.BiDirectional; + +using Carbonate.BiDirectional; +using Carbonate.Core.BiDirectional; +using FluentAssertions; +using Moq; +using Xunit; + +/// +/// Tests the class. +/// +public class PullReactableTests +{ + #region Method Tests + [Fact] + public void Pull_WithMatchingSubscription_ReturnsCorrectResult() + { + // Arrange + var respondIdA = Guid.NewGuid(); + var respondIdB = Guid.NewGuid(); + + const string returnData = "return-value"; + + var mockReactorA = new Mock>(); + mockReactorA.Name = nameof(mockReactorA); + mockReactorA.SetupGet(p => p.Id).Returns(respondIdA); + mockReactorA.Setup(m => m.OnRespond(It.IsAny())) + .Returns(returnData); + + var mockReactorB = new Mock>(); + mockReactorB.Name = nameof(mockReactorB); + mockReactorB.SetupGet(p => p.Id).Returns(respondIdB); + mockReactorB.Setup(m => m.OnRespond(It.IsAny())) + .Returns(returnData); + + const int data = 123; + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + + // Act + var actual = sut.Pull(data, respondIdB); + + // Assert + mockReactorA.Verify(m => m.OnRespond(It.IsAny()), Times.Never); + mockReactorB.Verify(m => m.OnRespond(It.IsAny()), Times.Once); + actual.Should().NotBeNull(); + actual.Should().NotBeNull(); + actual.Should().Be("return-value"); + } + + [Fact] + public void Pull_WithNoMatchingSubscription_ReturnsCorrectResult() + { + // Arrange + var sut = new PullReactable(); + + // Act + var actual = sut.Pull(123, Guid.NewGuid()); + + // Assert + actual.Should().Be(0); + } + #endregion + + /// + /// Creates a new instance of for the purpose of testing. + /// + /// The instance to test. + private static PullReactable CreateSystemUnderTest() => new (); +} diff --git a/Testing/CarbonateTests/BiDirectional/RespondReactorTests.cs b/Testing/CarbonateTests/BiDirectional/RespondReactorTests.cs new file mode 100644 index 0000000..9116f0d --- /dev/null +++ b/Testing/CarbonateTests/BiDirectional/RespondReactorTests.cs @@ -0,0 +1,210 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.BiDirectional; + +using Carbonate.BiDirectional; +using FluentAssertions; +using Moq; +using Xunit; + +/// +/// Tests the class. +/// +public class RespondReactorTests +{ + #region Constructor Tests + [Fact] + public void Ctor_WhenInvoked_SetsIdProperty() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var sut = new RespondReactor(id); + + // Assert + sut.Id.Should().Be(id); + } + + [Fact] + public void Ctor_WhenInvoked_SetsName() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var sut = new RespondReactor(id, "test-name"); + + // Assert + sut.Name.Should().Be("test-name"); + } + #endregion + + #region Method Tests + [Fact] + public void OnRespond_WhenUnsubscribed_ReturnsCorrectDefaultResult() + { + // Arrange + var sut = new RespondReactor(Guid.NewGuid(), + onRespondData: _ => 456); + sut.OnUnsubscribe(); + + // Act + var actual = sut.OnRespond(123); + + // Assert + actual.Should().Be(0); + } + + [Fact] + public void OnRespond_WhenDataIsNull_ThrowsException() + { + // Arrange + var sut = new RespondReactor(Guid.NewGuid()); + + // Act + var act = () => sut.OnRespond(null); + + // Assert + act.Should().Throw() + .WithMessage("The parameter must not be null. (Parameter 'data')"); + } + + [Fact] + public void OnRespond_WhenOnRespondDataIsNull_ReturnsCorrectDefaultResult() + { + // Arrange + var sut = new RespondReactor(Guid.NewGuid(), + onRespondData: _ => null); + + // Act + var actual = sut.OnRespond(123); + + // Assert + actual.Should().BeNull(); + } + + [Fact] + public void OnRespond_WhenOnRespondDataIsNotNull_ReturnsCorrectResult() + { + // Arrange + var obj = new object(); + var sut = new RespondReactor(Guid.NewGuid(), + onRespondData: _ => obj); + + // Act + var actual = sut.OnRespond(123); + + // Assert + actual.Should().NotBeNull(); + actual.Should().BeSameAs(obj); + } + + [Fact] + public void OnUnsubscribe_WhenInvoked_UnsubscribesReactor() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onUnsubscribe: () => totalActionInvokes++); + + // Act + sut.OnUnsubscribe(); + sut.OnUnsubscribe(); + + // Assert + sut.Unsubscribed.Should().BeTrue(); + totalActionInvokes.Should().Be(1); + } + + [Fact] + public void OnError_WhenUnsubscribed_DoesNotInvokedOnErrorAction() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onError: _ => totalActionInvokes++); + + sut.OnUnsubscribe(); + + // Act + sut.OnError(It.IsAny()); + + // Assert + totalActionInvokes.Should().Be(0); + } + + [Fact] + public void OnError_WhenNotUnsubscribedAndWithNullParam_ThrowsException() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onError: _ => totalActionInvokes++); + + // Act + var act = () => sut.OnError(null); + + // Assert + act.Should().Throw() + .WithMessage("The parameter must not be null. (Parameter 'error')"); + totalActionInvokes.Should().Be(0); + } + + [Fact] + public void OnError_WhenNotUnsubscribed_InvokesOnErrorAction() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onError: e => + { + e.Should().BeOfType(); + e.Message.Should().Be("test-exception"); + + totalActionInvokes++; + }); + + // Act + sut.OnError(new InvalidOperationException("test-exception")); + + // Assert + totalActionInvokes.Should().Be(1); + } + + [Theory] + [InlineData("test-value", "87e99bdc-a972-427a-90be-f2c07c4f9aef", "test-value - 87e99bdc-a972-427a-90be-f2c07c4f9aef")] + [InlineData(null, "87e99bdc-a972-427a-90be-f2c07c4f9aef", "87e99bdc-a972-427a-90be-f2c07c4f9aef")] + [InlineData("", "87e99bdc-a972-427a-90be-f2c07c4f9aef", "87e99bdc-a972-427a-90be-f2c07c4f9aef")] + public void ToString_WhenInvoked_ReturnsCorrectResult( + string name, + string guid, + string expected) + { + // Arrange + var id = new Guid(guid); + + var sut = new RespondReactor(id, name); + + // Act + var actual = sut.ToString(); + + // Assert + actual.Should().Be(expected); + } + #endregion +} diff --git a/Testing/CSharpLibTemplateRepoTests/CSharpLibTemplateRepoTests.csproj b/Testing/CarbonateTests/CarbonateTests.csproj similarity index 86% rename from Testing/CSharpLibTemplateRepoTests/CSharpLibTemplateRepoTests.csproj rename to Testing/CarbonateTests/CarbonateTests.csproj index ba46509..8b0b79e 100644 --- a/Testing/CSharpLibTemplateRepoTests/CSharpLibTemplateRepoTests.csproj +++ b/Testing/CarbonateTests/CarbonateTests.csproj @@ -6,7 +6,9 @@ enable enable false - [TODO: add project name here]Tests + CarbonateTests + Release;Debug + x64 @@ -15,12 +17,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -37,7 +39,7 @@ - + diff --git a/Testing/CarbonateTests/Core/ReactorUnsubscriberTests.cs b/Testing/CarbonateTests/Core/ReactorUnsubscriberTests.cs new file mode 100644 index 0000000..0d1e799 --- /dev/null +++ b/Testing/CarbonateTests/Core/ReactorUnsubscriberTests.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.Core; + +using Carbonate.Core; +using FluentAssertions; +using Moq; +using Xunit; + +/// +/// Tests the class. +/// +public class ReactorUnsubscriberTests +{ + #region Constructor Tests + [Fact] + public void Ctor_WithNullReactorsParam_ThrowsException() + { + // Arrange & Act + var act = () => + { + _ = new ReactorUnsubscriber(null, null); + }; + + // Assert + act.Should() + .Throw() + .WithMessage("The parameter must not be null. (Parameter 'reactors')"); + } + + [Fact] + public void Ctor_WithNullReactorParam_ThrowsException() + { + // Arrange & Act + var act = () => + { + _ = new ReactorUnsubscriber(Array.Empty().ToList(), null); + }; + + // Assert + act.Should() + .Throw() + .WithMessage("The parameter must not be null. (Parameter 'reactor')"); + } + #endregion + + #region Prop Tests + [Fact] + public void TotalReactors_WhenInvoked_ReturnsCorrectResult() + { + // Arrange + var reactors = new[] { new Mock().Object, new Mock().Object }; + + var sut = new ReactorUnsubscriber(reactors.ToList(), new Mock().Object); + + // Act + var actual = sut.TotalReactors; + + // Assert + actual.Should().Be(2); + } + + [Fact] + public void Dispose_WhenInvoked_RemovesFromReactorsList() + { + // Arrange + var reactorA = new Mock(); + var reactorB = new Mock(); + var reactorC = new Mock(); + + var reactors = new[] { reactorA.Object, reactorB.Object, reactorC.Object }; + + var sut = new ReactorUnsubscriber(reactors.ToList(), reactorB.Object); + + // Act + sut.Dispose(); + sut.Dispose(); + + var actual = sut.TotalReactors; + + // Assert + actual.Should().Be(2); + } + #endregion +} diff --git a/Testing/CarbonateTests/Helpers/Fakes/ReactableBaseFake.cs b/Testing/CarbonateTests/Helpers/Fakes/ReactableBaseFake.cs new file mode 100644 index 0000000..257713c --- /dev/null +++ b/Testing/CarbonateTests/Helpers/Fakes/ReactableBaseFake.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.Helpers.Fakes; + +using Carbonate; +using Carbonate.Core; + +/// +/// Used for the purpose of testing the class. +/// +public class ReactableBaseFake : ReactableBase +{ +} diff --git a/Testing/CarbonateTests/Helpers/Fakes/ReactorBaseFake.cs b/Testing/CarbonateTests/Helpers/Fakes/ReactorBaseFake.cs new file mode 100644 index 0000000..91d0768 --- /dev/null +++ b/Testing/CarbonateTests/Helpers/Fakes/ReactorBaseFake.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.Helpers.Fakes; + +using Carbonate; + +/// +/// Used for the purpose of testing the . +/// +public class ReactorBaseFake : ReactorBase +{ + /// + /// Initializes a new instance of the class. + /// + /// Test ID. + /// Test name. + /// Test action for unsubscribing. + /// Test action for errors. + public ReactorBaseFake( + Guid eventId, + string name = "", + Action? onUnsubscribe = null, + Action? onError = null) + : base(eventId, name, onUnsubscribe, onError) + { + } +} diff --git a/Testing/CarbonateTests/Helpers/PullTestData.cs b/Testing/CarbonateTests/Helpers/PullTestData.cs new file mode 100644 index 0000000..62bfb43 --- /dev/null +++ b/Testing/CarbonateTests/Helpers/PullTestData.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.Helpers; + +/// +/// Used for testing. +/// +public class PullTestData +{ + /// + /// Gets a number. + /// + public int Number { get; init; } +} diff --git a/Testing/CarbonateTests/NonDirectional/PushReactableTests.cs b/Testing/CarbonateTests/NonDirectional/PushReactableTests.cs new file mode 100644 index 0000000..21aa55e --- /dev/null +++ b/Testing/CarbonateTests/NonDirectional/PushReactableTests.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// ReSharper disable AccessToModifiedClosure +namespace CarbonateTests.NonDirectional; + +using Carbonate.Core.NonDirectional; +using Carbonate.NonDirectional; +using FluentAssertions; +using Moq; +using Xunit; + +public class PushReactableTests +{ + #region Method Tests + [Fact] + public void Push_WhenInvokedAfterDisposal_ThrowsException() + { + // Arrange + var sut = CreateSystemUnderTest(); + sut.Dispose(); + + // Act + var act = () => sut.Push(Guid.Empty); + + // Assert + act.Should().Throw() + .WithMessage($"{nameof(PushReactable)} disposed.{Environment.NewLine}Object name: 'PushReactable'."); + } + + [Fact] + public void Push_WhenInvoking_NotifiesCorrectSubscriptionsThatMatchEventId() + { + // Arrange + var invokedEventId = Guid.NewGuid(); + var notInvokedEventId = Guid.NewGuid(); + + var mockReactorA = new Mock(); + mockReactorA.SetupGet(p => p.Id).Returns(invokedEventId); + + var mockReactorB = new Mock(); + mockReactorB.SetupGet(p => p.Id).Returns(notInvokedEventId); + + var mockReactorC = new Mock(); + mockReactorC.SetupGet(p => p.Id).Returns(invokedEventId); + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + sut.Subscribe(mockReactorC.Object); + + // Act + sut.Push(invokedEventId); + + // Assert + mockReactorA.Verify(m => m.OnReceive(), Times.Once); + mockReactorB.Verify(m => m.OnReceive(), Times.Never); + mockReactorC.Verify(m => m.OnReceive(), Times.Once); + } + + [Fact] + public void Push_WhenUnsubscribingInsideOnReceiveReactorAction_DoesNotThrowException() + { + // Arrange + var mainId = new Guid("aaaaaaaa-a683-410a-b03e-8f8fe105b5af"); + var otherId = new Guid("bbbbbbbb-258d-4988-a169-4c23abf51c02"); + + IDisposable? otherUnsubscriberA = null; + IDisposable? otherUnsubscriberB = null; + + var initReactorA = new ReceiveReactor( + eventId: mainId); + + var otherReactorA = new ReceiveReactor(eventId: otherId); + var otherReactorB = new ReceiveReactor(eventId: otherId); + + var sut = CreateSystemUnderTest(); + + var initReactorC = new ReceiveReactor( + eventId: mainId, + onReceive: () => + { + otherUnsubscriberA?.Dispose(); + otherUnsubscriberB?.Dispose(); + }); + + sut.Subscribe(initReactorA); + otherUnsubscriberA = sut.Subscribe(otherReactorA); + otherUnsubscriberB = sut.Subscribe(otherReactorB); + sut.Subscribe(initReactorC); + + // Act + var act = () => sut.Push(mainId); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void Push_WhenExceptionOccursInOnReceiveSubscription_InvokesOnErrorForReactor() + { + // Arrange + var idA = Guid.NewGuid(); + var idB = Guid.NewGuid(); + + var reactorA = new ReceiveReactor( + eventId: idA, + onReceive: () => throw new Exception("test-exception"), + onError: e => + { + e.Should().BeOfType(); + e.Message.Should().Be("test-exception"); + }); + + var reactorB = new ReceiveReactor(eventId: idB); + + var sut = CreateSystemUnderTest(); + + sut.Subscribe(reactorA); + sut.Subscribe(reactorB); + + // Act + var act = () => sut.Push(idA); + + // Assert + act.Should().NotThrow(); + } + #endregion + + /// + /// Creates a new instance of for the purpose of testing. + /// + /// The instance to test. + private static PushReactable CreateSystemUnderTest() => new (); +} diff --git a/Testing/CarbonateTests/NonDirectional/ReceiveReactorTests.cs b/Testing/CarbonateTests/NonDirectional/ReceiveReactorTests.cs new file mode 100644 index 0000000..8f7ba2d --- /dev/null +++ b/Testing/CarbonateTests/NonDirectional/ReceiveReactorTests.cs @@ -0,0 +1,158 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.NonDirectional; + +using Carbonate.NonDirectional; +using FluentAssertions; +using Xunit; + +/// +/// Tests the class. +/// +public class ReceiveReactorTests +{ + #region Constructor Tests + [Fact] + public void Ctor_WhenInvoked_SetsEventId() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var sut = new ReceiveReactor(guid); + var actual = sut.Id; + + // Assert + actual.Should().Be(guid); + } + #endregion + + #region Method Tests + [Fact] + public void OnReceive_WhenSendingNothingAndSubscribed_InvokesAction() + { + // Arrange + var onReceiveInvoked = false; + void OnReceive() => onReceiveInvoked = true; + + var sut = new ReceiveReactor(Guid.NewGuid(), onReceive: OnReceive); + + // Act + sut.OnReceive(); + + // Assert + onReceiveInvoked.Should().BeTrue(); + } + + [Fact] + public void OnReceive_WhenSendingNothingAndNotSubscribed_DoesNotInvokeAction() + { + // Arrange + var onReceiveInvoked = false; + void OnReceive() => onReceiveInvoked = true; + + var sut = new ReceiveReactor(Guid.NewGuid(), onReceive: OnReceive); + + sut.OnUnsubscribe(); + + // Act + sut.OnReceive(); + + // Assert + onReceiveInvoked.Should().BeFalse(); + } + + [Fact] + public void OnUnsubscribe_WhenNotUnsubscribed_InvokesAction() + { + // Arrange + var onReceiveInvoked = false; + void OnUnsubscribe() => onReceiveInvoked = true; + + var sut = new ReceiveReactor(Guid.NewGuid(), onUnsubscribe: OnUnsubscribe); + + // Act + sut.OnUnsubscribe(); + + // Assert + onReceiveInvoked.Should().BeTrue(); + } + + [Fact] + public void OnUnsubscribe_WhenUnsubscribed_DoesNotInvokeActionAgain() + { + // Arrange + var totalInvokes = 0; + void OnUnsubscribe() => totalInvokes++; + + var sut = new ReceiveReactor(Guid.NewGuid(), onUnsubscribe: OnUnsubscribe); + sut.OnUnsubscribe(); + + // Act + sut.OnUnsubscribe(); + + // Assert + totalInvokes.Should().Be(1); + } + + [Fact] + public void OnError_WhenNotUnsubscribed_InvokesAction() + { + // Arrange + var onErrorInvoked = false; + void OnError(Exception ex) => onErrorInvoked = true; + + var exception = new Exception("test-exception"); + + var sut = new ReceiveReactor(Guid.NewGuid(), onError: OnError); + + // Act + sut.OnError(exception); + + // Assert + onErrorInvoked.Should().BeTrue(); + } + + [Fact] + public void OnError_WhenNotSubscribed_DoesNotInvokeAction() + { + // Arrange + var onReceiveInvoked = false; + void OnError(Exception ex) => onReceiveInvoked = true; + + var exception = new Exception("test-exception"); + + var sut = new ReceiveReactor(Guid.NewGuid(), onError: OnError); + + sut.OnUnsubscribe(); + + // Act + sut.OnError(exception); + + // Assert + onReceiveInvoked.Should().BeFalse(); + } + + [Theory] + [InlineData(null, "5739afd9-be4c-4402-a12d-6bcde35cc8c3")] + [InlineData("", "5739afd9-be4c-4402-a12d-6bcde35cc8c3")] + [InlineData("test-value", "test-value - 5739afd9-be4c-4402-a12d-6bcde35cc8c3")] + public void ToString_WhenInvoked_ReturnsCorrectResult(string name, string expected) + { + // Arrange + var id = new Guid("5739afd9-be4c-4402-a12d-6bcde35cc8c3"); + + var sut = new ReceiveReactor( + eventId: id, + name: name); + + // Act + var actual = sut.ToString(); + + // Assert + actual.Should().Be(expected); + } + #endregion +} diff --git a/Testing/CarbonateTests/ReactableBaseTests.cs b/Testing/CarbonateTests/ReactableBaseTests.cs new file mode 100644 index 0000000..0329cc1 --- /dev/null +++ b/Testing/CarbonateTests/ReactableBaseTests.cs @@ -0,0 +1,334 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests; + +using Carbonate.Core; +using Carbonate.Core.UniDirectional; +using Carbonate.UniDirectional; +using FluentAssertions; +using Helpers.Fakes; +using Moq; +using Xunit; + +public class ReactableBaseTests +{ + /// + /// Initializes a new instance of the class. + /// + public ReactableBaseTests() + { + } + + #region Prop Tests + [Fact] + public void EventIds_WhenGettingValue_ReturnsCorrectResult() + { + // Arrange + var eventIdA = Guid.NewGuid(); + var eventIdB = Guid.NewGuid(); + var eventIdC = eventIdA; + + var expected = new[] { eventIdA, eventIdB }; + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(eventIdA); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(eventIdB); + + var mockReactorC = new Mock>(); + mockReactorC.SetupGet(p => p.Id).Returns(eventIdC); + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + sut.Subscribe(mockReactorC.Object); + + // Act + var actual = sut.SubscriptionIds; + + // Assert + actual.Should().BeEquivalentTo(expected); + } + #endregion + + #region Method Tests + [Fact] + public void Subscribe_WhenInvokedAfterDisposal_ThrowsException() + { + // Arrange + var sut = CreateSystemUnderTest(); + sut.Dispose(); + + // Act + var act = () => sut.Subscribe(null); + + // Assert + act.Should().Throw() + .WithMessage($"{nameof(PushReactable)} disposed.{Environment.NewLine}Object name: 'PushReactable'."); + } + + [Fact] + public void Subscribe_WithNullReactor_ThrowsException() + { + // Arrange + var sut = CreateSystemUnderTest(); + + // Act + var act = () => sut.Subscribe(null); + + // Assert + act.Should().Throw() + .WithMessage("The parameter must not be null. (Parameter 'reactor')"); + } + + [Fact] + public void Subscribe_WhenInvoked_ReactorsPropReturnsReactors() + { + // Arrange + var mockReactorA = new Mock>(); + var mockReactorB = new Mock>(); + + var expected = new[] { mockReactorA.Object, mockReactorB.Object }; + + var sut = CreateSystemUnderTest(); + + // Act + var reactorUnsubscriberA = sut.Subscribe(mockReactorA.Object); + var reactorUnsubscriberB = sut.Subscribe(mockReactorB.Object); + + var actual = sut.Reactors; + + // Assert + actual.Should().BeEquivalentTo(expected); + actual[0].Should().BeSameAs(mockReactorA.Object); + actual[1].Should().BeSameAs(mockReactorB.Object); + reactorUnsubscriberA.Should().NotBeNull(); + reactorUnsubscriberB.Should().NotBeNull(); + } + + [Fact] + public void Unsubscribe_WhenInvokedAfterDisposal_ThrowsException() + { + // Arrange + var sut = CreateSystemUnderTest(); + sut.Dispose(); + + // Act + var act = () => sut.Unsubscribe(Guid.Empty); + + // Assert + act.Should().Throw() + .WithMessage($"{nameof(PushReactable)} disposed.{Environment.NewLine}Object name: 'PushReactable'."); + } + + [Fact] + public void Unsubscribe_WhenUnsubscribingSomeEvents_UnsubscribesCorrectReactors() + { + // Arrange + var eventToUnsubscribeFrom = Guid.NewGuid(); + var eventNotToUnsubscribeFrom = Guid.NewGuid(); + + var mockReactorA = new Mock(); + mockReactorA.SetupGet(p => p.Id).Returns(eventToUnsubscribeFrom); + + var mockReactorB = new Mock(); + mockReactorB.SetupGet(p => p.Id).Returns(eventNotToUnsubscribeFrom); + + var mockReactorC = new Mock(); + mockReactorC.SetupGet(p => p.Id).Returns(eventToUnsubscribeFrom); + + // Act + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + sut.Subscribe(mockReactorC.Object); + + sut.Unsubscribe(eventToUnsubscribeFrom); + + // Assert + mockReactorA.Verify(m => m.OnUnsubscribe(), Times.Once); + mockReactorB.Verify(m => m.OnUnsubscribe(), Times.Never); + mockReactorC.Verify(m => m.OnUnsubscribe(), Times.Once); + sut.Reactors.Should().HaveCount(1); + } + + [Fact] + public void Unsubscribe_WhenUnsubscribingAllEventsOneAtATime_UnsubscribesCorrectReactors() + { + // Arrange + var eventToUnsubscribeFrom = Guid.NewGuid(); + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(eventToUnsubscribeFrom); + mockReactorA.Setup(m => m.Unsubscribed).Returns(true); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(eventToUnsubscribeFrom); + mockReactorB.Setup(m => m.Unsubscribed).Returns(true); + + // Act + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + + sut.Unsubscribe(eventToUnsubscribeFrom); + sut.Unsubscribe(eventToUnsubscribeFrom); + + // Assert + mockReactorA.Verify(m => m.OnUnsubscribe(), Times.Once); + mockReactorB.Verify(m => m.OnUnsubscribe(), Times.Once); + sut.Reactors.Should().BeEmpty(); + } + + [Fact] + public void Unsubscribe_WhenUnsubscribingInsideOnUnsubscribeReactorAction_DoesNotThrowException() + { + // Arrange + var mainId = new Guid("aaaaaaaa-a683-410a-b03e-8f8fe105b5af"); + var otherId = new Guid("bbbbbbbb-258d-4988-a169-4c23abf51c02"); + + var initReactorA = new ReceiveReactor( + eventId: mainId); + + var otherReactorA = new ReceiveReactor(eventId: otherId); + var otherReactorB = new ReceiveReactor(eventId: otherId); + + var sut = CreateSystemUnderTest(); + + var initReactorC = new ReceiveReactor( + eventId: mainId, + onUnsubscribe: () => + { + sut.Unsubscribe(otherId); + }); + + sut.Subscribe(initReactorA); + sut.Subscribe(otherReactorA); + sut.Subscribe(otherReactorB); + sut.Subscribe(initReactorC); + + // Act + var act = () => sut.Unsubscribe(mainId); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void UnsubscribeAll_WhenInvokedAfterDisposal_ThrowsException() + { + // Arrange + var sut = CreateSystemUnderTest(); + sut.Dispose(); + + // Act + var act = () => sut.UnsubscribeAll(); + + // Assert + act.Should().Throw() + .WithMessage($"{nameof(PushReactable)} disposed.{Environment.NewLine}Object name: 'PushReactable'."); + } + + [Fact] + public void UnsubscribeAll_WhenInvoked_UnsubscribesFromAll() + { + // Arrange + var eventToUnsubscribeFrom = Guid.NewGuid(); + var eventNotToUnsubscribeFrom = Guid.NewGuid(); + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(eventToUnsubscribeFrom); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(eventNotToUnsubscribeFrom); + + var mockReactorC = new Mock>(); + mockReactorC.SetupGet(p => p.Id).Returns(eventToUnsubscribeFrom); + + // Act + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorC.Object); + sut.Subscribe(mockReactorB.Object); + sut.Subscribe(mockReactorA.Object); + + sut.UnsubscribeAll(); + + // Assert + mockReactorA.Verify(m => m.OnUnsubscribe(), Times.Once); + mockReactorB.Verify(m => m.OnUnsubscribe(), Times.Once); + mockReactorC.Verify(m => m.OnUnsubscribe(), Times.Once); + sut.Reactors.Should().BeEmpty(); + } + + [Fact] + public void UnsubscribeAll_WhenUnsubscribingInsideOnUnsubscribeReactorAction_DoesNotThrowException() + { + // Arrange + var mainId = new Guid("aaaaaaaa-a683-410a-b03e-8f8fe105b5af"); + var otherId = new Guid("bbbbbbbb-258d-4988-a169-4c23abf51c02"); + + var initReactorA = new ReceiveReactor( + eventId: mainId); + + var otherReactorA = new ReceiveReactor(eventId: otherId); + var otherReactorB = new ReceiveReactor(eventId: otherId); + + var sut = CreateSystemUnderTest(); + + var initReactorC = new ReceiveReactor( + eventId: mainId, + onUnsubscribe: () => + { + sut.Unsubscribe(otherId); + }); + + sut.Subscribe(initReactorA); + sut.Subscribe(otherReactorA); + sut.Subscribe(otherReactorB); + sut.Subscribe(initReactorC); + + // Act + var act = () => sut.UnsubscribeAll(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void Dispose_WhenInvoked_DisposesOfReactable() + { + // Arrange + var eventIdA = Guid.NewGuid(); + var eventIdB = Guid.NewGuid(); + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(eventIdA); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(eventIdB); + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + + // Act + sut.Dispose(); + sut.Dispose(); + + // Assert + mockReactorA.Verify(m => m.OnUnsubscribe(), Times.Once); + mockReactorB.Verify(m => m.OnUnsubscribe(), Times.Once); + + sut.Reactors.Should().BeEmpty(); + } + #endregion + + /// + /// Creates a new instance of for the purpose of testing. + /// + /// The instance to test. + private static ReactableBaseFake CreateSystemUnderTest() => new (); +} diff --git a/Testing/CarbonateTests/ReactorBaseTests.cs b/Testing/CarbonateTests/ReactorBaseTests.cs new file mode 100644 index 0000000..55af15f --- /dev/null +++ b/Testing/CarbonateTests/ReactorBaseTests.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests; + +using Carbonate.UniDirectional; +using FluentAssertions; +using Helpers.Fakes; +using Xunit; + +public class ReactorBaseTests +{ + #region Method Tests + [Fact] + public void OnUnsubscribe_WhenNotUnsubscribed_InvokesAction() + { + // Arrange + var onReceiveInvoked = false; + void OnUnsubscribe() => onReceiveInvoked = true; + + var sut = new ReactorBaseFake(Guid.NewGuid(), onUnsubscribe: OnUnsubscribe); + + // Act + sut.OnUnsubscribe(); + + // Assert + onReceiveInvoked.Should().BeTrue(); + } + + [Fact] + public void OnUnsubscribe_WhenUnsubscribed_DoesNotInvokeActionAgain() + { + // Arrange + var totalInvokes = 0; + void OnUnsubscribe() => totalInvokes++; + + var sut = new ReceiveReactor(Guid.NewGuid(), onUnsubscribe: OnUnsubscribe); + sut.OnUnsubscribe(); + + // Act + sut.OnUnsubscribe(); + + // Assert + totalInvokes.Should().Be(1); + } + + [Fact] + public void OnError_WhenNotUnsubscribed_InvokesAction() + { + // Arrange + var onErrorInvoked = false; + void OnError(Exception ex) => onErrorInvoked = true; + + var exception = new Exception("test-exception"); + + var sut = new ReceiveReactor(Guid.NewGuid(), onError: OnError); + + // Act + sut.OnError(exception); + + // Assert + onErrorInvoked.Should().BeTrue(); + } + + [Fact] + public void OnError_WhenNotSubscribed_DoesNotInvokeAction() + { + // Arrange + var onReceiveInvoked = false; + void OnError(Exception ex) => onReceiveInvoked = true; + + var exception = new Exception("test-exception"); + + var sut = new ReceiveReactor(Guid.NewGuid(), onError: OnError); + + sut.OnUnsubscribe(); + + // Act + sut.OnError(exception); + + // Assert + onReceiveInvoked.Should().BeFalse(); + } + #endregion +} diff --git a/Testing/CarbonateTests/UniDirectional/PullReactableTests.cs b/Testing/CarbonateTests/UniDirectional/PullReactableTests.cs new file mode 100644 index 0000000..97a736b --- /dev/null +++ b/Testing/CarbonateTests/UniDirectional/PullReactableTests.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.UniDirectional; + +using Carbonate.Core.UniDirectional; +using Carbonate.UniDirectional; +using FluentAssertions; +using Moq; +using Xunit; + +/// +/// Tests the class. +/// +public class PullReactableTests +{ + #region Method Tests + [Fact] + public void Pull_WhenResponseIsNotNull_ReturnsCorrectResult() + { + // Arrange + var respondId = Guid.NewGuid(); + + const string returnData = "return-value"; + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(respondId); + mockReactorA.Setup(m => m.OnRespond()).Returns(returnData); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(respondId); + mockReactorB.Setup(m => m.OnRespond()).Returns(returnData); + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + + // Act + var actual = sut.Pull(respondId); + + // Assert + mockReactorA.Verify(m => m.OnRespond(), Times.Once); + mockReactorB.Verify(m => m.OnRespond(), Times.Never); + actual.Should().NotBeNull(); + actual.Should().NotBeNull(); + actual.Should().Be("return-value"); + } + + [Fact] + public void Pull_WhenSubscriptionExists_InvokesCorrectSubscriptions() + { + // Arrange + var respondIdA = Guid.NewGuid(); + var respondIdB = Guid.NewGuid(); + var respondIdC = Guid.NewGuid(); + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(respondIdA); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(respondIdB); + + var mockReactorC = new Mock>(); + mockReactorC.SetupGet(p => p.Id).Returns(respondIdC); + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + sut.Subscribe(mockReactorC.Object); + + // Act + sut.Pull(respondIdB); + + // Assert + mockReactorA.Verify(m => m.OnRespond(), Times.Never); + mockReactorB.Verify(m => m.OnRespond(), Times.Once); + mockReactorC.Verify(m => m.OnRespond(), Times.Never); + } + + [Fact] + public void Pull_WhenSubscriptionDoesNotExist_ReturnsCorrectDefaultResult() + { + // Arrange + var sut = CreateSystemUnderTest(); + + // Act + var actual = sut.Pull(It.IsAny()); + + // Assert + actual.Should().BeNull(); + } + #endregion + + /// + /// Creates a new instance of for the purpose of testing. + /// + /// The instance to test. + private static PullReactable CreateSystemUnderTest() => new (); +} diff --git a/Testing/CarbonateTests/UniDirectional/PushReactableTests.cs b/Testing/CarbonateTests/UniDirectional/PushReactableTests.cs new file mode 100644 index 0000000..ded929c --- /dev/null +++ b/Testing/CarbonateTests/UniDirectional/PushReactableTests.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +// ReSharper disable AccessToModifiedClosure +namespace CarbonateTests.UniDirectional; + +using Carbonate.Core.UniDirectional; +using Carbonate.UniDirectional; +using FluentAssertions; +using Moq; +using Xunit; + +/// +/// Tests the class. +/// +public class PushReactableTests +{ + #region Method Tests + [Fact] + public void Push_WhenDataParamIsNull_ThrowsException() + { + // Arrange + var sut = new PushReactable(); + + // Act + var act = () => sut.Push(null, It.IsAny()); + + // Assert + act.Should().Throw() + .WithMessage("The parameter must not be null. (Parameter 'data')"); + } + + [Fact] + public void Push_WhenInvokedAfterDisposal_ThrowsException() + { + // Arrange + var sut = CreateSystemUnderTest(); + sut.Dispose(); + + // Act + var act = () => sut.Push(123, Guid.Empty); + + // Assert + act.Should().Throw() + .WithMessage($"{nameof(PushReactable)} disposed.{Environment.NewLine}Object name: 'PushReactable'."); + } + + [Fact] + public void Push_WhenInvoking_NotifiesCorrectSubscriptionsThatMatchEventId() + { + // Arrange + var invokedEventId = Guid.NewGuid(); + var notInvokedEventId = Guid.NewGuid(); + + var mockReactorA = new Mock>(); + mockReactorA.SetupGet(p => p.Id).Returns(invokedEventId); + + var mockReactorB = new Mock>(); + mockReactorB.SetupGet(p => p.Id).Returns(notInvokedEventId); + + var mockReactorC = new Mock>(); + mockReactorC.SetupGet(p => p.Id).Returns(invokedEventId); + + var sut = CreateSystemUnderTest(); + sut.Subscribe(mockReactorA.Object); + sut.Subscribe(mockReactorB.Object); + sut.Subscribe(mockReactorC.Object); + + // Act + sut.Push(123, invokedEventId); + + // Assert + mockReactorA.Verify(m => m.OnReceive(It.IsAny()), Times.Once); + mockReactorB.Verify(m => m.OnReceive(It.IsAny()), Times.Never); + mockReactorC.Verify(m => m.OnReceive(It.IsAny()), Times.Once); + } + + [Fact] + public void Push_WhenUnsubscribingInsideOnReceiveReactorAction_DoesNotThrowException() + { + // Arrange + var mainId = new Guid("aaaaaaaa-a683-410a-b03e-8f8fe105b5af"); + var otherId = new Guid("bbbbbbbb-258d-4988-a169-4c23abf51c02"); + + IDisposable? otherUnsubscriberA = null; + IDisposable? otherUnsubscriberB = null; + + var initReactorA = new ReceiveReactor( + eventId: mainId); + + var otherReactorA = new ReceiveReactor(eventId: otherId); + var otherReactorB = new ReceiveReactor(eventId: otherId); + + const int data = 123; + + var sut = CreateSystemUnderTest(); + + var initReactorC = new ReceiveReactor( + eventId: mainId, + onReceiveData: _ => + { + otherUnsubscriberA?.Dispose(); + otherUnsubscriberB?.Dispose(); + }); + + sut.Subscribe(initReactorA); + otherUnsubscriberA = sut.Subscribe(otherReactorA); + otherUnsubscriberB = sut.Subscribe(otherReactorB); + sut.Subscribe(initReactorC); + + // Act + var act = () => sut.Push(data, mainId); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void Push_WhenSubscriptionThrowsException_InvokesSubscriptionOnError() + { + // Arrange + var idA = Guid.NewGuid(); + var idB = Guid.NewGuid(); + + var sut = CreateSystemUnderTest(); + var reactorA = new ReceiveReactor( + eventId: idA, + onReceiveData: _ => throw new Exception("test-exception"), + onError: e => + { + // Assert + e.Should().BeOfType(); + e.Message.Should().Be("test-exception"); + }); + var reactorB = new ReceiveReactor(idB); + + sut.Subscribe(reactorA); + sut.Subscribe(reactorB); + + // Act + sut.Push(It.IsAny(), idA); + } + #endregion + + /// + /// Creates a new instance of for the purpose of testing. + /// + /// The instance to test. + private static PushReactable CreateSystemUnderTest() => new (); +} diff --git a/Testing/CarbonateTests/UniDirectional/ReceiveReactorTests.cs b/Testing/CarbonateTests/UniDirectional/ReceiveReactorTests.cs new file mode 100644 index 0000000..a6aca24 --- /dev/null +++ b/Testing/CarbonateTests/UniDirectional/ReceiveReactorTests.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.UniDirectional; + +using Carbonate.UniDirectional; +using FluentAssertions; +using Xunit; + +/// +/// Tests the class. +/// +public class ReceiveReactorTests +{ + #region Constructor Tests + [Fact] + public void Ctor_WhenInvoked_SetsEventId() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var sut = new ReceiveReactor(guid); + var actual = sut.Id; + + // Assert + actual.Should().Be(guid); + } + #endregion + + #region Method Tests + [Fact] + public void OnReceive_WhenSendingDataAndSubscribed_InvokesAction() + { + // Arrange + var onReceiveInvoked = false; + void OnReceive(int incomingData) => onReceiveInvoked = true; + + const int data = 123; + + var sut = new ReceiveReactor(Guid.NewGuid(), onReceiveData: OnReceive); + + // Act + sut.OnReceive(data); + + // Assert + onReceiveInvoked.Should().BeTrue(); + } + + [Fact] + public void OnReceive_WhenSendingDataAndNotSubscribed_DoesNotInvokeAction() + { + // Arrange + var onReceiveInvoked = false; + void OnReceive(int incomingData) => onReceiveInvoked = true; + + const int data = 123; + + var sut = new ReceiveReactor(Guid.NewGuid(), onReceiveData: OnReceive); + + sut.OnUnsubscribe(); + + // Act + sut.OnReceive(data); + + // Assert + onReceiveInvoked.Should().BeFalse(); + } + + [Fact] + public void OnReceive_WhenSendingNullData_ThrowsException() + { + // Arrange + var sut = new ReceiveReactor(Guid.NewGuid()); + + // Act + var act = () => sut.OnReceive(null); + + // Assert + act.Should().Throw() + .WithMessage("The parameter must not be null. (Parameter 'data')"); + } + + [Theory] + [InlineData(null, "5739afd9-be4c-4402-a12d-6bcde35cc8c3")] + [InlineData("", "5739afd9-be4c-4402-a12d-6bcde35cc8c3")] + [InlineData("test-value", "test-value - 5739afd9-be4c-4402-a12d-6bcde35cc8c3")] + public void ToString_WhenInvoked_ReturnsCorrectResult(string name, string expected) + { + // Arrange + var id = new Guid("5739afd9-be4c-4402-a12d-6bcde35cc8c3"); + + var sut = new ReceiveReactor( + eventId: id, + name: name); + + // Act + var actual = sut.ToString(); + + // Assert + actual.Should().Be(expected); + } + #endregion +} diff --git a/Testing/CarbonateTests/UniDirectional/RespondReactorTests.cs b/Testing/CarbonateTests/UniDirectional/RespondReactorTests.cs new file mode 100644 index 0000000..85d61d9 --- /dev/null +++ b/Testing/CarbonateTests/UniDirectional/RespondReactorTests.cs @@ -0,0 +1,197 @@ +// +// Copyright (c) KinsonDigital. All rights reserved. +// + +namespace CarbonateTests.UniDirectional; + +using Carbonate.UniDirectional; +using FluentAssertions; +using Moq; +using Xunit; + +/// +/// Tests the class. +/// +public class RespondReactorTests +{ + #region Constructor Tests + [Fact] + public void Ctor_WhenInvoked_SetsIdProperty() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var sut = new RespondReactor(id); + + // Assert + sut.Id.Should().Be(id); + } + + [Fact] + public void Ctor_WhenInvoked_SetsName() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var sut = new RespondReactor(id, "test-name"); + + // Assert + sut.Name.Should().Be("test-name"); + } + #endregion + + #region Method Tests + [Fact] + public void OnRespond_WhenUnsubscribed_DoesNotInvokeOnRespondAction() + { + // Arrange + var totalActionInvokes = 0; + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onRespond: () => + { + totalActionInvokes++; + return It.IsAny(); + }); + + sut.OnUnsubscribe(); + + // Act + _ = sut.OnRespond(); + + // Assert + totalActionInvokes.Should().Be(0); + } + + [Fact] + public void OnRespond_WhenNotUnsubscribed_InvokesOnRespondAction() + { + // Arrange + var totalActionInvokes = 0; + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onRespond: () => + { + totalActionInvokes++; + return "return-value"; + }); + + // Act + _ = sut.OnRespond(); + + // Assert + totalActionInvokes.Should().Be(1); + } + + [Fact] + public void OnUnsubscribe_WhenInvoked_UnsubscribesReactor() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onUnsubscribe: () => totalActionInvokes++); + + // Act + sut.OnUnsubscribe(); + sut.OnUnsubscribe(); + + // Assert + sut.Unsubscribed.Should().BeTrue(); + totalActionInvokes.Should().Be(1); + } + + [Fact] + public void OnError_WhenUnsubscribed_DoesNotInvokedOnErrorAction() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onError: _ => totalActionInvokes++); + + sut.OnUnsubscribe(); + + // Act + sut.OnError(It.IsAny()); + + // Assert + totalActionInvokes.Should().Be(0); + } + + [Fact] + public void OnError_WhenNotUnsubscribedAndWithNullParam_ThrowsException() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onError: _ => totalActionInvokes++); + + // Act + var act = () => sut.OnError(null); + + // Assert + act.Should().Throw() + .WithMessage("The parameter must not be null. (Parameter 'error')"); + totalActionInvokes.Should().Be(0); + } + + [Fact] + public void OnError_WhenNotUnsubscribed_InvokesOnErrorAction() + { + // Arrange + var totalActionInvokes = 0; + + var sut = new RespondReactor( + It.IsAny(), + It.IsAny(), + onError: e => + { + e.Should().BeOfType(); + e.Message.Should().Be("test-exception"); + + totalActionInvokes++; + }); + + // Act + sut.OnError(new InvalidOperationException("test-exception")); + + // Assert + totalActionInvokes.Should().Be(1); + } + + [Theory] + [InlineData("test-value", "4ff67e7b-bdda-4e0c-b34c-0b32270c336d", "test-value - 4ff67e7b-bdda-4e0c-b34c-0b32270c336d")] + [InlineData(null, "4ff67e7b-bdda-4e0c-b34c-0b32270c336d", "4ff67e7b-bdda-4e0c-b34c-0b32270c336d")] + [InlineData("", "4ff67e7b-bdda-4e0c-b34c-0b32270c336d", "4ff67e7b-bdda-4e0c-b34c-0b32270c336d")] + public void ToString_WhenInvoked_ReturnsCorrectResult( + string name, + string guid, + string expected) + { + // Arrange + var id = new Guid(guid); + + var sut = new RespondReactor( + respondId: id, + name: name); + + // Act + var actual = sut.ToString(); + + // Assert + actual.Should().Be(expected); + } + #endregion +} diff --git a/Testing/CarbonateTests/stylecop.json b/Testing/CarbonateTests/stylecop.json new file mode 100644 index 0000000..6e1021a --- /dev/null +++ b/Testing/CarbonateTests/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "KinsonDigital" + } + } +} diff --git a/code_of_conduct.md b/code_of_conduct.md deleted file mode 100644 index dec444a..0000000 --- a/code_of_conduct.md +++ /dev/null @@ -1,134 +0,0 @@ - -# 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, caste, color, 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 -kinsondigital@gmail.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][v2.0]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available -at [https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..24eda6e --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>KinsonDigital/.github//config/renovate-config.json"], + "baseBranches": ["release/v1.0.0"] + }