├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── AiurVersionControl.sln ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── demos ├── Aiursoft.SnakeGame │ ├── ActionType.cs │ ├── Aiursoft.SnakeGame.csproj │ ├── Constants.cs │ ├── Launcher │ │ ├── Input.cs │ │ └── Render.cs │ ├── Models │ │ ├── Action.cs │ │ └── Position.cs │ ├── Program.cs │ └── Services │ │ ├── Game.cs │ │ ├── GameObject.cs │ │ ├── IDrawable.cs │ │ ├── IRecurrent.cs │ │ └── Implements │ │ ├── Food.cs │ │ ├── Grid.cs │ │ ├── Snake.cs │ │ └── SnakeRecurrent.cs ├── Aiursoft.SnakeGameServer │ ├── Aiursoft.SnakeGameServer.csproj │ ├── Controllers │ │ └── HomeController.cs │ ├── Program.cs │ └── Startup.cs └── SampleWPF │ ├── App.xaml │ ├── App.xaml.cs │ ├── Components │ ├── BookListItem.xaml │ ├── BookListItem.xaml.cs │ ├── BookListItemPresenter.cs │ ├── BooksCRUD.xaml │ ├── BooksCRUD.xaml.cs │ ├── BooksCRUDPresenter.cs │ ├── CommitsManagement.xaml │ ├── CommitsManagement.xaml.cs │ ├── CommitsManagementPresenter.cs │ ├── RemoteControl.xaml │ ├── RemoteControl.xaml.cs │ ├── RemoteControlPresenter.cs │ ├── RemoteManagement.xaml │ ├── RemoteManagement.xaml.cs │ └── RemoteManagementPresenter.cs │ ├── Libraries │ ├── AsyncRelayCommand.cs │ ├── Presenter.cs │ └── RelayCommand.cs │ ├── Models │ └── Book.cs │ ├── SampleWPF.csproj │ ├── Services │ ├── Network.cs │ └── ServerProgram.cs │ └── Windows │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ └── MainWindowPresenter.cs ├── ninja.yaml ├── nuget.config ├── src ├── Aiursoft.AiurEventSyncer.Abstract │ ├── Aiursoft.AiurEventSyncer.Abstract.csproj │ ├── Commit.cs │ ├── IConnectionProvider.cs │ ├── IRemote.cs │ └── IRepository.cs ├── Aiursoft.AiurEventSyncer.WebExtends │ ├── Aiursoft.AiurEventSyncer.WebExtends.csproj │ └── WebExtends.cs ├── Aiursoft.AiurEventSyncer │ ├── Aiursoft.AiurEventSyncer.csproj │ ├── ConnectionProviders │ │ ├── FakeConnection.cs │ │ ├── Models │ │ │ └── PushModel.cs │ │ ├── RetryableWebSocketConnection.cs │ │ └── WebSocketConnection.cs │ ├── Models │ │ ├── InsertMode.cs │ │ ├── Remote.cs │ │ └── Repository.cs │ ├── Remotes │ │ ├── ObjectRemote.cs │ │ └── WebSocketRemote.cs │ └── Tools │ │ ├── CommitsDatabaseExtends.cs │ │ ├── SafeQueue.cs │ │ ├── TaskQueue.cs │ │ └── WebSocketExtends.cs ├── Aiursoft.AiurStore │ ├── Aiursoft.AiurStore.csproj │ ├── Models │ │ ├── IOutOnlyDatabase.cs │ │ └── InOutDatabase.cs │ ├── Providers │ │ └── MemoryAiurStoreDb.cs │ └── Tools │ │ ├── JsonTools.cs │ │ └── ListExtends.cs ├── Aiursoft.AiurVersionControl.Crud │ ├── Aiursoft.AiurVersionControl.Crud.csproj │ ├── CollectionRepository.cs │ ├── CollectionWorkSpace.cs │ └── Modifications │ │ ├── Add.cs │ │ ├── Drop.cs │ │ └── Patch.cs └── Aiursoft.AiurVersionControl │ ├── Aiursoft.AiurVersionControl.csproj │ ├── Models │ ├── ControlledRepository.cs │ ├── IModification.cs │ ├── RemoteWithWorkSpace.cs │ └── WorkSpace.cs │ └── Remotes │ ├── ObjectRemoteWithWorkSpace.cs │ └── WebSocketRemoteWithWorkSpace.cs └── tests ├── Aiursoft.AiurEventSyncer.Tests ├── Aiursoft.AiurEventSyncer.Tests.csproj ├── AutoTest.cs ├── ConnectionRetryTests.cs ├── DbRepoTest.cs ├── MergeTest.cs ├── Models │ └── Book.cs ├── PerformanceTest.cs ├── PointerTest.cs ├── PullTest.cs ├── PushTest.cs └── Tools │ └── TestExtends.cs ├── Aiursoft.AiurStore.Tests ├── Aiursoft.AiurStore.Tests.csproj ├── DbOperationsTest.cs └── Tools │ └── TestExtends.cs ├── Aiursoft.AiurVersionControl.Crud.Tests ├── Aiursoft.AiurVersionControl.Crud.Tests.csproj ├── ModelTest.cs └── Models │ └── Book.cs ├── Aiursoft.AiurVersionControl.Tests ├── Aiursoft.AiurVersionControl.Tests.csproj ├── BasicModelTests.cs └── Models │ ├── AddModification.cs │ └── NumberWorkSpace.cs ├── Aiursoft.EventSyncerWithArrayDbServer.Tests ├── Aiursoft.EventSyncerWithArrayDbServer.Tests.csproj └── KahlaTest.cs └── FunctionalTest ├── SampleWebApp.Test ├── IntegrationTests │ └── BasicTests.cs └── SampleWebApp.Test.csproj └── SampleWebApp ├── Controllers └── HomeController.cs ├── Models └── LogItem.cs ├── Program.cs ├── SampleWebApp.csproj ├── SampleWebApp.csproj.user ├── Services ├── RepositoryContainer.cs └── RepositoryFactory.cs ├── Startup.cs └── appsettings.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.scss] 12 | indent_size = 2 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | 18 | [*.json] 19 | indent_size = 2 20 | 21 | [*.cshtml] 22 | # Some CSS Resharper doesn't know how to load. 23 | resharper_unknown_css_class_highlighting=none 24 | 25 | # Some JS Resharper doesn't know how to load. 26 | resharper_undeclared_global_variable_using_highlighting=suggesting 27 | 28 | # Some HTML reference Resharper doesn't know how to load. 29 | resharper_html_path_error_highlighting=none 30 | 31 | # Allow JS global var. 32 | resharper_use_of_implicit_global_in_function_scope_highlighting=none 33 | 34 | # Stop suggesting IE compatibility. 35 | resharper_css_browser_compatibility_highlighting=none 36 | 37 | # Localization might not be finished. 38 | resharper_not_overridden_in_specific_culture_highlighting=suggestion 39 | 40 | # Allow view render global var. 41 | resharper_access_to_modified_closure_highlighting=suggestion 42 | 43 | # Suppress id not resolved because resharper can't understand 44 | resharper_html_id_not_resolved_highlighting=suggestion 45 | 46 | [*.cs] 47 | # Allow names like `IPAddress`. 48 | resharper_inconsistent_naming_highlighting=suggestion 49 | 50 | # Allow unused auto property get. 51 | resharper_unused_auto_property_accessor_global_highlighting=suggestion 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | .idea/ 4 | lib 5 | *.user 6 | *.min.css 7 | *.min.js 8 | bin/ 9 | obj/ 10 | dist/ 11 | node_modules/ 12 | Properties/ 13 | npm-debug.log 14 | bundle.js 15 | appsettings.Production.json 16 | appsettings.Development.json 17 | *.log 18 | TestResults/ 19 | app.db* 20 | .angular/ 21 | .yarn/ 22 | *.tsbuildinfo 23 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - lint 4 | - test 5 | - publish 6 | - deploy 7 | 8 | before_script: 9 | - 'export DOTNET_CLI_TELEMETRY_OPTOUT=1' 10 | - 'export PATH=$PATH:$HOME/.dotnet/tools' 11 | - 'which jb || dotnet tool install JetBrains.ReSharper.GlobalTools --global --add-source https://nuget.aiursoft.cn/v3/index.json --configfile ./nuget.config -v d' 12 | - 'which reportgenerator || dotnet tool install dotnet-reportgenerator-globaltool --global --add-source https://nuget.aiursoft.cn/v3/index.json --configfile ./nuget.config -v d' 13 | - 'echo "Hostname: $(hostname)"' 14 | - 'dotnet --info' 15 | 16 | variables: 17 | GIT_CLONE_PATH: '$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_PIPELINE_ID' 18 | 19 | restore: 20 | stage: build 21 | script: 22 | - dotnet restore --no-cache --configfile nuget.config 23 | 24 | build: 25 | stage: build 26 | needs: 27 | - restore 28 | script: 29 | - dotnet build -maxcpucount:1 --no-self-contained 30 | 31 | lint: 32 | stage: lint 33 | needs: 34 | - build 35 | script: 36 | # 3 times retry because sometimes the first time will fail 37 | - jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml || jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml || jb inspectcode ./*.sln --output=analyze_output.xml --build -f=xml 38 | # Remove the warning of UnusedAutoPropertyAccessor InconsistentNaming 39 | - sed -i '/InconsistentNaming/d' analyze_output.xml 40 | - sed -i '/AssignNullToNotNullAttribute/d' analyze_output.xml # This is because jetbrains is not smart enough to understand the nullability of C# 8.0 41 | - sed -i '/UnusedAutoPropertyAccessor/d' analyze_output.xml 42 | - sed -i '/DuplicateResource/d' analyze_output.xml 43 | - grep 'WARNING' analyze_output.xml && cat analyze_output.xml && exit 1 || echo "No warning found" 44 | artifacts: 45 | when: always 46 | expire_in: 1 day 47 | paths: 48 | - ./analyze_output.xml 49 | 50 | test: 51 | stage: test 52 | needs: 53 | - build 54 | coverage: '/TOTAL_COVERAGE=(\d+.\d+)/' 55 | script: 56 | - dotnet test *.sln --collect:"XPlat Code Coverage" --logger "junit;MethodFormat=Class;FailureBodyFormat=Verbose" 57 | - reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"." -reporttypes:"cobertura" 58 | - COVERAGE_VALUE=$(grep -oPm 1 'line-rate="\K([0-9.]+)' "./Cobertura.xml") 59 | - COVERAGE_PERCENTAGE=$(echo "scale=2; $COVERAGE_VALUE * 100" | bc) 60 | - 'echo "TOTAL_COVERAGE=$COVERAGE_PERCENTAGE%"' 61 | artifacts: 62 | when: always 63 | expire_in: 1 day 64 | paths: 65 | - ./**/TestResults.xml 66 | - ./Cobertura.xml 67 | reports: 68 | junit: 69 | - ./**/TestResults.xml 70 | coverage_report: 71 | coverage_format: cobertura 72 | path: ./Cobertura.xml 73 | 74 | pack: 75 | stage: publish 76 | needs: 77 | - lint 78 | - test 79 | script: 80 | - dotnet build -maxcpucount:1 --configuration Release --no-self-contained *.sln 81 | - dotnet pack -maxcpucount:1 --configuration Release *.sln || echo "Some packaging failed!" 82 | artifacts: 83 | expire_in: 1 week 84 | paths: 85 | - '**/*.nupkg' 86 | 87 | deploy_local_nuget: 88 | stage: deploy 89 | environment: production 90 | needs: 91 | - pack 92 | dependencies: 93 | - pack 94 | script: 95 | - | 96 | for file in $(find . -name "*.nupkg"); do 97 | dotnet nuget push "$file" --api-key "$LOCAL_NUGET_API_KEY" --source "https://nuget.aiursoft.cn/v3/index.json" --skip-duplicate || exit 1; 98 | done 99 | only: 100 | - master 101 | 102 | deploy_public_nuget: 103 | stage: deploy 104 | environment: production 105 | needs: 106 | - pack 107 | - deploy_local_nuget 108 | dependencies: 109 | - pack 110 | script: 111 | - | 112 | for file in $(find . -name "*.nupkg"); do 113 | dotnet nuget push "$file" --api-key "$NUGET_API_KEY" --source "https://api.nuget.org/v3/index.json" --skip-duplicate || exit 1; 114 | done 115 | only: 116 | - master 117 | 118 | deploy_docker_registry: 119 | stage: deploy 120 | environment: production 121 | needs: 122 | - lint 123 | - test 124 | script: 125 | - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then TAG="latest"; else TAG="$CI_COMMIT_REF_NAME"; fi 126 | - echo building image hub.aiursoft.cn/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$TAG 127 | - docker build . -t hub.aiursoft.cn/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest 128 | - docker push hub.aiursoft.cn/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest 129 | rules: 130 | - if: '$CI_COMMIT_BRANCH == "master"' 131 | exists: 132 | - Dockerfile 133 | 134 | deploy_docker_hub: 135 | stage: deploy 136 | environment: production 137 | needs: 138 | - deploy_docker_registry 139 | script: 140 | - if [ "$CI_PROJECT_NAMESPACE" = "anduin" ]; then NAMESPACE="anduin2019"; else NAMESPACE="$CI_PROJECT_NAMESPACE"; fi 141 | - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then TAG="latest"; else TAG="$CI_COMMIT_REF_NAME"; fi 142 | - echo building image $NAMESPACE/$CI_PROJECT_NAME:$TAG 143 | - docker build . -t $NAMESPACE/$CI_PROJECT_NAME:$TAG 144 | - echo "Logging in to Docker Hub..." 145 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 146 | - docker push $NAMESPACE/$CI_PROJECT_NAME:$TAG 147 | rules: 148 | - if: '$CI_COMMIT_BRANCH == "master"' 149 | exists: 150 | - Dockerfile 151 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Aiursoft Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We are committed to creating a positive and inclusive environment where all participants feel respected and valued. 8 | 9 | ## Our Principles 10 | 11 | To ensure that the software we develop aligns with the Aiursoft Principles and serves all users equally well, we uphold the following: 12 | 13 | 1. **Offline Functionality** 14 | Any compiled artifacts of the software must be capable of performing their full functions without requiring an internet connection. 15 | 16 | 2. **Local Compilation in Aiursoft** 17 | The source code of the software must be capable of being compiled locally within Aiursoft's infrastructure without requiring an internet connection. 18 | 19 | 3. **Third-Party Local Compilation** 20 | The source code of the software must be capable of being compiled locally by third-party developers with an internet connection. 21 | 22 | These principles ensure the software’s reliability, accessibility, and functionality across varied environments and user conditions. 23 | 24 | ## Our Standards 25 | 26 | Examples of behavior that contributes to creating a positive environment include: 27 | 28 | - Using welcoming and inclusive language 29 | - Being respectful of differing viewpoints and experiences 30 | - Gracefully accepting constructive criticism 31 | - Focusing on what is best for the community 32 | - Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 37 | - Trolling, insulting/derogatory comments, and personal or political attacks 38 | - Public or private harassment 39 | - Publishing others’ private information, such as a physical or electronic address, without explicit permission 40 | - Other conduct which could reasonably be considered inappropriate in a professional setting 41 | 42 | ## Our Responsibilities 43 | 44 | Maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 45 | 46 | Maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies within all project spaces, and it also applies in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [anduin@aiursoft.com]. All complaints will be reviewed and investigated promptly and fairly. Maintainers are obligated to respect the privacy and security of the reporter of any incident. 55 | 56 | ## Acknowledgement 57 | 58 | This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 59 | 60 | By participating in this project, you agree to uphold this Code of Conduct and to adhere to the principles and standards we strive to maintain for a respectful, inclusive, and productive community. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Aiursoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | # Developer Certificate of Origin 24 | 25 | In addition to the permissions and conditions stated above, the maintenance and development of the Software shall adhere to the following Aiursoft Principles: 26 | 27 | 1. Any compiled artifacts of the Software must be capable of performing their full functions without requiring an internet connection. 28 | 2. The source code of the Software must be capable of being compiled locally within Aiursoft's infrastructure without requiring an internet connection. 29 | 3. The source code of the Software must be capable of being compiled locally by third-party developers with an internet connection. 30 | 31 | # Our Pledge 32 | 33 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 34 | 35 | # Our Standards 36 | 37 | Examples of behavior that contributes to creating a positive environment include: 38 | 39 | - Using welcoming and inclusive language 40 | - Being respectful of differing viewpoints and experiences 41 | - Gracefully accepting constructive criticism 42 | - Focusing on what is best for the community 43 | - Showing empathy towards other community members 44 | 45 | Examples of unacceptable behavior by participants include: 46 | 47 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 48 | - Trolling, insulting/derogatory comments, and personal or political attacks 49 | - Public or private harassment 50 | - Publishing others’ private information, such as a physical or electronic address, without explicit permission 51 | - Other conduct which could reasonably be considered inappropriate in a professional setting 52 | 53 | # Our Responsibilities 54 | 55 | Maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 56 | 57 | Maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 58 | 59 | # Scope 60 | 61 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AiurVersionControl 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitlab.aiursoft.cn/aiursoft/aiurversioncontrol/-/blob/master/LICENSE) 4 | [![Pipeline stat](https://gitlab.aiursoft.cn/aiursoft/aiurversioncontrol/badges/master/pipeline.svg)](https://gitlab.aiursoft.cn/aiursoft/aiurversioncontrol/-/pipelines) 5 | [![Test Coverage](https://gitlab.aiursoft.cn/aiursoft/aiurversioncontrol/badges/master/coverage.svg)](https://gitlab.aiursoft.cn/aiursoft/aiurversioncontrol/-/pipelines) 6 | [![NuGet version](https://img.shields.io/nuget/v/Aiursoft.AiurVersionControl.svg?style=flat-square)](https://www.nuget.org/packages/Aiursoft.AiurVersionControl/) 7 | [![ManHours](https://manhours.aiursoft.cn/r/gitlab.aiursoft.cn/aiursoft/aiurversioncontrol.svg)](https://gitlab.aiursoft.cn/aiursoft/aiurversioncontrol/-/commits/master?ref_type=heads) 8 | 9 | A powerful collection sync framework that powers Kahla. 10 | 11 | ## How to install 12 | 13 | | Name | Download | Description | 14 | |----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| 15 | | Aiursoft.AiurVersionControl | [![NuGet version (Aiursoft.AiurVersionControl)](https://img.shields.io/nuget/v/Aiursoft.AiurVersionControl.svg?style=flat-square)](https://www.nuget.org/packages/Aiursoft.AiurVersionControl/) | An event repro engine which helps generate final workspace from commit history. | 16 | | Aiursoft.AiurEventSyncer.WebExtends | [![NuGet version (Aiursoft.AiurEventSyncer.WebExtends)](https://img.shields.io/nuget/v/Aiursoft.AiurEventSyncer.WebExtends.svg?style=flat-square)](https://www.nuget.org/packages/Aiursoft.AiurEventSyncer.WebExtends/) | WebSocket protocol server side support for AiurEventSyncer. | 17 | | Aiursoft.AiurEventSyncer | [![NuGet version (Aiursoft.AiurEventSyncer)](https://img.shields.io/nuget/v/Aiursoft.AiurEventSyncer.svg?style=flat-square)](https://www.nuget.org/packages/Aiursoft.AiurEventSyncer/) | A commits sync framework which achieves final consistency and always availability. | 18 | | Aiursoft.AiurStore | [![NuGet version (Aiursoft.AiurStore)](https://img.shields.io/nuget/v/Aiursoft.AiurStore.svg?style=flat-square)](https://www.nuget.org/packages/Aiursoft.AiurStore/) | An abstract database layer which describes a immutable data storage. | 19 | 20 | ## Run locally 21 | 22 | Requirements about how to run 23 | 24 | 1. [.NET 9 SDK](http://dot.net/) 25 | 2. Execute `dotnet run` to run the app 26 | 27 | ## Run in Microsoft Visual Studio 28 | 29 | 1. Open the `.sln` file in the project path. 30 | 2. Press `F5`. 31 | 32 | ## How to contribute 33 | 34 | There are many ways to contribute to the project: logging bugs, submitting pull requests, reporting issues, and creating suggestions. 35 | 36 | Even if you with push rights on the repository, you should create a personal fork and create feature branches there when you need them. This keeps the main repository clean and your workflow cruft out of sight. 37 | 38 | We're also interested in your feedback on the future of this project. You can submit a suggestion or feature request through the issue tracker. To make this process more effective, we're asking that these include more information to help define them more clearly. 39 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/ActionType.cs: -------------------------------------------------------------------------------- 1 | namespace Aiursoft.SnakeGame 2 | { 3 | public enum ActionType 4 | { 5 | /// 6 | /// Action for snake move 7 | /// 8 | Move = 1, 9 | /// 10 | /// Action for snake eat 11 | /// 12 | Eat = 2 13 | } 14 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Aiursoft.SnakeGame.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | Aiursoft.SnakeGame 6 | Aiursoft.SnakeGame 7 | false 8 | false 9 | enable 10 | false 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Aiursoft.SnakeGame 2 | { 3 | public sealed class Constants 4 | { 5 | /// 6 | /// WebSocketRemote repository port. 7 | /// 8 | private const int Port = 15000; 9 | 10 | /// 11 | /// WebSocketRemote endpoint for connecting. 12 | /// 13 | public static readonly string EndPointUrl = $"ws://localhost:{Port}/repo.ares"; 14 | 15 | /// 16 | /// Game initial refresh rate. (milliseconds) 17 | /// 18 | public static readonly int InitialSpeed = 150; 19 | 20 | /// 21 | /// Game initial grid size. 22 | /// 23 | public static readonly int GridSize = 40; 24 | } 25 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Launcher/Input.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Models; 2 | 3 | namespace Aiursoft.SnakeGame.Launcher 4 | { 5 | public static class Input 6 | { 7 | public static void ChangeDirection(ConsoleKey command, Position p) 8 | { 9 | switch (command) 10 | { 11 | case ConsoleKey.LeftArrow: 12 | if (p.X != 0) break; 13 | p.X = -1; 14 | p.Y = 0; 15 | break; 16 | case ConsoleKey.UpArrow: 17 | if (p.Y != 0) break; 18 | p.X = 0; 19 | p.Y = -1; 20 | break; 21 | case ConsoleKey.RightArrow: 22 | if (p.X != 0) break; 23 | p.X = 1; 24 | p.Y = 0; 25 | break; 26 | case ConsoleKey.DownArrow: 27 | if (p.Y != 0) break; 28 | p.X = 0; 29 | p.Y = 1; 30 | break; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Launcher/Render.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Services; 2 | 3 | namespace Aiursoft.SnakeGame.Launcher 4 | { 5 | public static class Render 6 | { 7 | private static decimal _gameSpeed = Constants.InitialSpeed; 8 | 9 | public static async Task StartGame(Game game) 10 | { 11 | // Add Remote Repository 12 | await game.AddRemote(Constants.EndPointUrl); 13 | 14 | Console.Clear(); 15 | 16 | // Draw Items 17 | game.Draw(); 18 | 19 | while (!game.IsGameEnd) 20 | { 21 | game.UpdateDirection(); 22 | game.UpdateFrame(); 23 | if (game.NeedSpeedUp()) 24 | { 25 | _gameSpeed *= 0.95m; 26 | } 27 | 28 | // Listening input command 29 | game.ListenInput(); 30 | 31 | await Task.Delay((int)_gameSpeed); 32 | } 33 | } 34 | 35 | public static async Task StartGameWithObserver(Game game, Game observer) 36 | { 37 | // Add Remote Repository 38 | await game.AddRemote(Constants.EndPointUrl); 39 | await observer.AddRemote(Constants.EndPointUrl); 40 | 41 | observer.GenerateRecurrent(); 42 | 43 | Console.Clear(); 44 | 45 | // Draw Items 46 | game.Draw(); 47 | observer.Draw(); 48 | 49 | // Wait the input to start the game. 50 | game.ListenInput(true); 51 | 52 | while (!game.IsGameEnd) 53 | { 54 | // Player's panel 55 | game.UpdateDirection(); 56 | game.UpdateFrame(); 57 | if (game.NeedSpeedUp()) 58 | { 59 | _gameSpeed *= 0.95m; 60 | } 61 | 62 | // Observer's panel 63 | observer.RecurrentFromRepo(); 64 | observer.UpdateFrame(); 65 | 66 | // Listening input command 67 | game.ListenInput(); 68 | 69 | await Task.Delay(Convert.ToInt32(_gameSpeed)); 70 | } 71 | 72 | while (!observer.IsGameEnd) 73 | { 74 | observer.RecurrentFromRepo(); 75 | observer.UpdateFrame(); 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Models/Action.cs: -------------------------------------------------------------------------------- 1 | namespace Aiursoft.SnakeGame.Models 2 | { 3 | public struct Action 4 | { 5 | public ActionType Type { get; set; } 6 | 7 | public Position Direction { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Models/Position.cs: -------------------------------------------------------------------------------- 1 | namespace Aiursoft.SnakeGame.Models 2 | { 3 | public class Position : ICloneable 4 | { 5 | private readonly Guid _hash; 6 | 7 | public Position() 8 | { 9 | this._hash = Guid.NewGuid(); 10 | } 11 | 12 | public int X { get; set; } 13 | public int Y { get; set; } 14 | 15 | public override bool Equals(object obj) 16 | { 17 | if (obj is Position p) 18 | { 19 | return X == p.X && Y == p.Y; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | public override int GetHashCode() 26 | { 27 | return _hash.GetHashCode(); 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return X + "," + Y; 33 | } 34 | 35 | public object Clone() 36 | { 37 | return MemberwiseClone(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Program.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Launcher; 2 | using Aiursoft.SnakeGame.Services; 3 | 4 | namespace Aiursoft.SnakeGame 5 | { 6 | class Program 7 | { 8 | public static async Task Main(string[] args) 9 | { 10 | // Game with Observer 11 | await Render.StartGameWithObserver( 12 | new Game(Constants.GridSize, 0), 13 | new Game(Constants.GridSize, Constants.GridSize + Constants.GridSize / 2)); 14 | 15 | // Game without observer. 16 | // await Render.StartGame(new Game(Constants.GridSize, 0)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/Game.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.AiurEventSyncer.Models; 2 | using Aiursoft.AiurEventSyncer.Remotes; 3 | using Aiursoft.SnakeGame.Launcher; 4 | using Aiursoft.SnakeGame.Models; 5 | using Aiursoft.SnakeGame.Services.Implements; 6 | 7 | namespace Aiursoft.SnakeGame.Services 8 | { 9 | public class Game : IDrawable 10 | { 11 | public bool IsGameEnd { get; set; } 12 | private readonly Repository _repo; 13 | private readonly Position _direction = new(); 14 | private readonly Grid _grid; 15 | private readonly Food _food; 16 | private readonly Position _originalSnakePosition; 17 | private readonly int _offset; 18 | private Snake _snake; 19 | private ConsoleKey _command; 20 | private IRecurrent _rec; 21 | 22 | public Game(int gridSize, int offset) 23 | { 24 | _repo = new Repository(); 25 | IsGameEnd = false; 26 | _offset = offset; 27 | _grid = new Grid(gridSize, offset); 28 | _originalSnakePosition = new Position{ X = gridSize / 2 + offset, Y = gridSize / 2 }; 29 | _snake = new Snake((Position)_originalSnakePosition.Clone()); 30 | _food = new Food(gridSize, offset); 31 | } 32 | 33 | public async Task AddRemote(string endpointUrl) 34 | { 35 | await new WebSocketRemote(endpointUrl).AttachAsync(_repo); 36 | } 37 | 38 | public void Draw() 39 | { 40 | _grid.Draw(); 41 | _snake.Draw(); 42 | _food.Draw(); 43 | } 44 | 45 | public void UpdateDirection() 46 | { 47 | Input.ChangeDirection(_command, _direction); 48 | 49 | _snake.Update(_direction); 50 | _repo.Commit(new Models.Action{Type = ActionType.Move, Direction = _direction}); 51 | } 52 | public void UpdateFrame() 53 | { 54 | CheckDeath(); 55 | _snake.Draw(); 56 | } 57 | 58 | public bool NeedSpeedUp() 59 | { 60 | return CheckEat(); 61 | } 62 | 63 | public void GenerateRecurrent() 64 | { 65 | _rec = new SnakeRecurrent(); 66 | } 67 | 68 | public void RecurrentFromRepo() 69 | { 70 | _snake = _rec.Recurrent(new Snake((Position)_originalSnakePosition.Clone()), _repo, _offset); 71 | } 72 | 73 | public void ListenInput(bool listenNow = false) 74 | { 75 | if (Console.KeyAvailable || listenNow) 76 | { 77 | _command = Console.ReadKey().Key; 78 | } 79 | } 80 | 81 | private void CheckDeath() 82 | { 83 | // Detect if snake hits the boundary or itself 84 | if (_grid.OutsideGrid(_snake.Head) || _snake.SnakeIntersection()) 85 | { 86 | IsGameEnd = true; 87 | Console.SetCursorPosition(21, 20); 88 | Console.WriteLine("Oops, the snake died..."); 89 | } 90 | } 91 | 92 | private bool CheckEat() 93 | { 94 | if (_snake.CanEat(_food.GetFoodPosition())) 95 | { 96 | _food.RandomFoodPosition(_grid, _snake); 97 | _snake.AddBody(); 98 | _repo.Commit(new Models.Action{Type = ActionType.Eat, Direction = _food.GetFoodPosition()}); 99 | return true; 100 | } 101 | 102 | return false; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/GameObject.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Models; 2 | 3 | namespace Aiursoft.SnakeGame.Services 4 | { 5 | public abstract class GameObject : IDrawable 6 | { 7 | public void Draw() 8 | { 9 | DrawObject(); 10 | } 11 | 12 | protected abstract void DrawObject(); 13 | 14 | public static void Draw(Position p, ConsoleColor color) 15 | { 16 | Console.SetCursorPosition(p.X, p.Y); 17 | Console.ForegroundColor = color; 18 | Console.Write("█"); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/IDrawable.cs: -------------------------------------------------------------------------------- 1 | namespace Aiursoft.SnakeGame.Services 2 | { 3 | public interface IDrawable 4 | { 5 | void Draw(); 6 | } 7 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/IRecurrent.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.AiurEventSyncer.Models; 2 | 3 | namespace Aiursoft.SnakeGame.Services 4 | { 5 | public interface IRecurrent 6 | { 7 | T Recurrent(T t, Repository r, int offset = 0); 8 | 9 | T RecurrentFromId(T t, Repository r, string id = null); 10 | } 11 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/Implements/Food.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Models; 2 | 3 | namespace Aiursoft.SnakeGame.Services.Implements 4 | { 5 | public class Food : IDrawable 6 | { 7 | private Position _foodPosition; 8 | 9 | public Food(int gridSize, int offset = 0) 10 | { 11 | _foodPosition = new Position{ X = gridSize / 4 + offset, Y = gridSize / 4}; 12 | Draw(); 13 | } 14 | 15 | public Position GetFoodPosition() 16 | { 17 | return _foodPosition; 18 | } 19 | 20 | public void RandomFoodPosition(Grid grid,Snake snake) 21 | { 22 | while (_foodPosition == null || snake.OnSnake(_foodPosition)) 23 | { 24 | _foodPosition = grid.RandomGridPosition(); 25 | } 26 | 27 | Draw(); 28 | } 29 | 30 | public void Draw() 31 | { 32 | Console.SetCursorPosition(_foodPosition.X, _foodPosition.Y); 33 | Console.ForegroundColor = ConsoleColor.DarkRed; 34 | Console.Write("█"); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/Implements/Grid.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Models; 2 | 3 | namespace Aiursoft.SnakeGame.Services.Implements 4 | { 5 | public class Grid : IDrawable 6 | { 7 | private readonly int _gridSize; 8 | // Distance between left boundary and left of console 9 | private readonly int _offset; 10 | private readonly Random _random; 11 | 12 | public Grid(int gridSize, int offset = 0, int seed = 0) 13 | { 14 | _gridSize = gridSize; 15 | _offset = offset; 16 | _random = seed != 0 ? new Random(seed) : new Random(); 17 | Draw(); 18 | } 19 | 20 | public Position RandomGridPosition() 21 | { 22 | return new Position{ X = _random.Next(_offset + 2, _offset + _gridSize - 2), Y = _random.Next(2, _gridSize - 2)}; 23 | } 24 | 25 | public bool OutsideGrid(Position p) 26 | { 27 | return p.X <= _offset + 1 || p.X >= _offset + _gridSize || p.Y <= 1 || p.Y >= _gridSize; 28 | } 29 | 30 | public void Draw() 31 | { 32 | for (var i = 1; i <= _gridSize; i++) 33 | { 34 | Console.ForegroundColor = ConsoleColor.White; 35 | Console.SetCursorPosition(_offset + 1, i); 36 | Console.Write("█"); 37 | Console.SetCursorPosition(_offset + _gridSize, i); 38 | Console.Write("█"); 39 | } 40 | 41 | for (var i = 1; i <= _gridSize; i++) 42 | { 43 | Console.ForegroundColor = ConsoleColor.White; 44 | Console.SetCursorPosition(_offset + i, 1); 45 | Console.Write("█"); 46 | Console.SetCursorPosition(_offset + i, _gridSize); 47 | Console.Write("█"); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/Implements/Snake.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGame.Models; 2 | 3 | namespace Aiursoft.SnakeGame.Services.Implements 4 | { 5 | public class Snake : GameObject 6 | { 7 | private readonly List _body = new(); 8 | public Position Head => _body[0]; 9 | // Use for erase tail. 10 | private Position _lastPosition; 11 | public int Count => _body.Count; 12 | 13 | public Snake(Position p, int count = 1) 14 | { 15 | for (var i = 0; i < count; i++) 16 | { 17 | _body.Add(p); 18 | } 19 | } 20 | 21 | public void AddBody() 22 | { 23 | _body.Add(new Position{ X = _body[^1].X, Y = _body[^1].Y}); 24 | } 25 | 26 | public void Update(Position inputDirection) 27 | { 28 | _lastPosition = (Position)_body[^1].Clone(); 29 | 30 | for (var i = _body.Count - 2; i >= 0; i--) 31 | { 32 | _body[i + 1] = (Position)_body[i].Clone(); 33 | } 34 | Head.X += inputDirection.X; 35 | Head.Y += inputDirection.Y; 36 | } 37 | 38 | protected override void DrawObject() 39 | { 40 | foreach (Position p in _body) 41 | { 42 | Console.SetCursorPosition(p.X, p.Y); 43 | Console.ForegroundColor = ConsoleColor.DarkGreen; 44 | Console.WriteLine("█"); 45 | } 46 | // Erase tail 47 | if (_lastPosition != null && !_lastPosition.Equals(_body[^1])) 48 | { 49 | Console.SetCursorPosition(_lastPosition.X, _lastPosition.Y); 50 | Console.WriteLine(" "); 51 | } 52 | } 53 | 54 | public bool OnSnake(Position p, bool ignoreHead = false) 55 | { 56 | if (ignoreHead) 57 | { 58 | if (_body.Count <= 4) return false; 59 | for (var i = 4; i < _body.Count; i++) 60 | { 61 | if (p.Equals(_body[i])) 62 | { 63 | return true; 64 | } 65 | } 66 | 67 | return false; 68 | } 69 | return _body.Contains(p); 70 | } 71 | 72 | public bool CanEat(Position food) 73 | { 74 | return Head.X == food.X && Head.Y == food.Y; 75 | } 76 | 77 | public bool SnakeIntersection() 78 | { 79 | return OnSnake(Head, true); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGame/Services/Implements/SnakeRecurrent.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.AiurEventSyncer.Abstract; 2 | using Aiursoft.AiurEventSyncer.Models; 3 | using Aiursoft.AiurEventSyncer.Tools; 4 | using Aiursoft.SnakeGame.Models; 5 | 6 | namespace Aiursoft.SnakeGame.Services.Implements 7 | { 8 | public class SnakeRecurrent : IRecurrent 9 | { 10 | public Snake Recurrent(Snake snake, Repository repo, int offset = 0) 11 | { 12 | var commits = repo.Commits; 13 | return DoRecurrent(snake, commits, offset); 14 | } 15 | 16 | public Snake RecurrentFromId(Snake snake, Repository repo, string position = null) 17 | { 18 | var commits = repo.Commits.GetCommitsAfterId, Models.Action>(position); 19 | return DoRecurrent(snake, commits); 20 | } 21 | 22 | private static Snake DoRecurrent(Snake snake, IEnumerable> commits, int offset = 0) 23 | { 24 | Position foodPosition = new Position{ X = 0, Y = 0 }; 25 | foreach (var commit in commits) 26 | { 27 | switch (commit.Item.Type) 28 | { 29 | case ActionType.Move: 30 | snake.Update(commit.Item.Direction); 31 | break; 32 | case ActionType.Eat: 33 | snake.AddBody(); 34 | foodPosition.X = commit.Item.Direction.X + offset; 35 | foodPosition.Y = commit.Item.Direction.Y; 36 | break; 37 | default: 38 | throw new ArgumentOutOfRangeException(); 39 | } 40 | } 41 | 42 | if (foodPosition.X != 0 && foodPosition.Y != 0) 43 | { 44 | GameObject.Draw(foodPosition, ConsoleColor.DarkRed); 45 | } 46 | 47 | return snake; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGameServer/Aiursoft.SnakeGameServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | Aiursoft.SnakeGameServer 6 | Aiursoft.SnakeGameServer 7 | false 8 | false 9 | enable 10 | false 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGameServer/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.AiurEventSyncer.Models; 2 | using Aiursoft.AiurEventSyncer.WebExtends; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Action = Aiursoft.SnakeGame.Models.Action; 5 | 6 | namespace Aiursoft.SnakeGameServer.Controllers 7 | { 8 | public class HomeController : Controller 9 | { 10 | private readonly RepositoryContainer _repositoryContainer; 11 | 12 | public HomeController(RepositoryContainer repositoryContainer) 13 | { 14 | _repositoryContainer = repositoryContainer; 15 | } 16 | 17 | public IActionResult Index() 18 | { 19 | return Ok(); 20 | } 21 | 22 | [Route("repo.ares")] 23 | public Task ReturnRepoDemo(string start) 24 | { 25 | var repo = _repositoryContainer.GetLogItemRepository(); 26 | return HttpContext.RepositoryAsync(repo, start); 27 | } 28 | } 29 | 30 | public class RepositoryContainer 31 | { 32 | private readonly object _obj = new(); 33 | private Repository _logItemRepository; 34 | 35 | public Repository GetLogItemRepository() 36 | { 37 | lock (_obj) 38 | { 39 | if (_logItemRepository == null) 40 | { 41 | _logItemRepository = new Repository(); 42 | } 43 | } 44 | return _logItemRepository; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGameServer/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Aiursoft.SnakeGameServer 2 | { 3 | public class Program 4 | { 5 | public static void Main(string[] args) 6 | { 7 | BuildHost(args) 8 | .Run(); 9 | } 10 | 11 | public static IHost BuildHost(string[] args, int port = 15000) 12 | { 13 | return CreateHostBuilder(args, port) 14 | .Build(); 15 | } 16 | 17 | public static IHostBuilder CreateHostBuilder(string[] args, int port) 18 | { 19 | return Host.CreateDefaultBuilder(args) 20 | .ConfigureWebHostDefaults(webBuilder => 21 | { 22 | webBuilder.UseUrls($"http://localhost:{port}"); 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demos/Aiursoft.SnakeGameServer/Startup.cs: -------------------------------------------------------------------------------- 1 | using Aiursoft.SnakeGameServer.Controllers; 2 | 3 | namespace Aiursoft.SnakeGameServer 4 | { 5 | public class Startup 6 | { 7 | public void ConfigureServices(IServiceCollection services) 8 | { 9 | services.AddControllersWithViews(); 10 | services.AddSingleton(); 11 | } 12 | 13 | public void Configure(IApplicationBuilder app) 14 | { 15 | app.UseDeveloperExceptionPage(); 16 | app.UseWebSockets(); 17 | app.UseRouting(); 18 | app.UseEndpoints(endpoint => endpoint.MapDefaultControllerRoute()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demos/SampleWPF/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 |