├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── ReleaseNotes.md ├── azure-devops ├── azure-pipelines-cd.yml ├── azure-pipelines-ci.yml ├── stage-build.yml └── stage-deploy.yml ├── clean.sh ├── docker-compose.yml ├── init-local-env.sh ├── nats_config ├── auth.config ├── ca-key.pem ├── ca.pem ├── client-cert.pem ├── client-key.pem ├── client.pfx ├── server-cert.pem ├── server-key.pem ├── tls.config └── tlsVerify.config ├── resources ├── .env.sample ├── integrationtests.local.json.sample └── nuget-usage.md └── src ├── Directory.Build.props ├── MyNatsClient.sln ├── main ├── MyNatsClient.Encodings.Json │ ├── JsonEncoding.cs │ ├── JsonSettings.cs │ ├── MyNatsClient.Encodings.Json.csproj │ ├── NatsClientExtensions.cs │ └── NatsMsgOpJsonExtensions.cs ├── MyNatsClient.Encodings.Protobuf │ ├── MyNatsClient.Encodings.Protobuf.csproj │ ├── NatsClientProtobufExtensions.cs │ ├── NatsMsgOpProtobufExtensions.cs │ └── ProtobufEncoding.cs └── MyNatsClient │ ├── ConnectionInfo.cs │ ├── Credentials.cs │ ├── DisconnectReason.cs │ ├── Events │ ├── ClientAutoReconnectFailed.cs │ ├── ClientConnected.cs │ ├── ClientDisconnected.cs │ └── ClientWorkerFailed.cs │ ├── Host.cs │ ├── IAsyncPublisher.cs │ ├── IClientEvent.cs │ ├── IConsumerFactory.cs │ ├── IDecoder.cs │ ├── IEncoder.cs │ ├── IEncoding.cs │ ├── INatsClient.cs │ ├── INatsConnection.cs │ ├── INatsConnectionManager.cs │ ├── INatsObservable.cs │ ├── INatsServerInfo.cs │ ├── INatsStreamWriter.cs │ ├── IOp.cs │ ├── IPublisher.cs │ ├── ISocketFactory.cs │ ├── ISubscription.cs │ ├── Internals │ ├── AsyncPublisher.cs │ ├── Commands │ │ ├── ConnectCmd.cs │ │ ├── PingCmd.cs │ │ ├── PongCmd.cs │ │ ├── PubCmd.cs │ │ ├── SubCmd.cs │ │ └── UnSubCmd.cs │ ├── DefaultTaskSchedulerConsumerFactory.cs │ ├── DelegatingObserver.cs │ ├── Disposable.cs │ ├── Extensions │ │ ├── ArrayExtensions.cs │ │ └── SocketExtensions.cs │ ├── NatsConnection.cs │ ├── NatsConnectionManager.cs │ ├── NatsEncoder.cs │ ├── NatsServerInfo.cs │ ├── NatsStreamWriter.cs │ ├── Observables.cs │ ├── Publisher.cs │ ├── SafeObserver.cs │ ├── SocketFactory.cs │ ├── Subscription.cs │ ├── Swallow.cs │ └── UniqueId.cs │ ├── LoggerManager.cs │ ├── MsgHeaders.cs │ ├── MyNatsClient.csproj │ ├── NatsClient.cs │ ├── NatsException.cs │ ├── NatsExceptionCodes.cs │ ├── NatsObservable.cs │ ├── NatsObserver.cs │ ├── NatsOpMediator.cs │ ├── NatsOpStreamReader.cs │ ├── Ops │ ├── ErrOp.cs │ ├── InfoOp.cs │ ├── MsgOp.cs │ ├── NullOp.cs │ ├── OkOp.cs │ ├── PingOp.cs │ └── PongOp.cs │ ├── PubFlushMode.cs │ ├── ReadOnlyMsgHeaders.cs │ ├── Rx │ └── NatsObservableExtensions.cs │ ├── SocketAddressType.cs │ ├── SocketOptions.cs │ └── SubscriptionInfo.cs ├── samples ├── Benchmarks │ ├── Benchmarks.csproj │ └── Program.cs └── RequestResponseSample │ ├── Program.cs │ └── RequestResponseSample.csproj └── testing ├── IntegrationTests ├── AutoReconnectOnFailureTests.cs ├── BasicAuthTests.cs ├── ConnectTests.cs ├── Encodings │ ├── ClientJsonEncodingTests.cs │ ├── ClientProtobufEncodingTests.cs │ └── EncodingTestItem.cs ├── Extension │ └── ConnectionInfoExtensions.cs ├── IntegrationTests.csproj ├── PubSubTests.cs ├── RequestTests.cs ├── Resources │ └── client.pfx ├── Should.cs ├── SubTests.cs ├── Sync.cs ├── TestContext.cs ├── TestSettings.cs ├── TlsTests.cs ├── TlsVerifyTests.cs ├── UnSubTests.cs └── integrationtests.json └── UnitTests ├── ConnectionInfoTests.cs ├── CredentialsTests.cs ├── Encodings ├── EncodingTestItem.cs ├── EncodingTestOf.cs ├── JsonEncodingTests.cs └── ProtobufEncodingTests.cs ├── FakeLogger.cs ├── HostTests.cs ├── LoggerManagerTests.cs ├── MsgHeadersTests.cs ├── NatsObservableTests.cs ├── NatsOpMediatorTests.cs ├── NatsOpStreamReaderTests.cs ├── NatsServerInfoParseTests.cs ├── Ops ├── ErrOpTests.cs ├── InfoOpTests.cs ├── MsgOpTests.cs ├── OkOpTests.cs ├── PingOpTests.cs └── PongOpTests.cs ├── ReadOnlyMsgHeadersTests.cs ├── SubscriptionInfoTests.cs ├── UnitTests.cs └── UnitTests.csproj /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/compose* 19 | **/Dockerfile* 20 | **/node_modules 21 | **/npm-debug.log 22 | **/obj 23 | **/secrets.dev.yaml 24 | **/values.dev.yaml 25 | README.md 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Baseline 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | max_line_length = 160 12 | 13 | [*.{yml,json}] 14 | indent_size = 2 15 | 16 | # Xml project files 17 | [*.{csproj}] 18 | indent_size = 2 19 | 20 | # Xml files (data, config, projects) 21 | [*.{xml,props,targets,config,csproj}] 22 | indent_size = 2 23 | 24 | # Shell scripts 25 | [*.sh] 26 | end_of_line = lf 27 | 28 | # C# 29 | [*.cs] 30 | dotnet_sort_system_directives_first = true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vs 2 | .vs/ 3 | [Bb]in/ 4 | [D]ebug/ 5 | [Aa]rtifacts/ 6 | [Oo]bj/ 7 | [Tt]est[Rr]esult/ 8 | [Tt]emp/ 9 | *.user 10 | *.vspscc 11 | *.vssscc 12 | *.suo 13 | *.cache 14 | 15 | .env 16 | *.local 17 | *.local.json 18 | *.local.ini 19 | 20 | # misc 21 | *~ 22 | *.swp 23 | *.sdf 24 | *.orig 25 | *.pfx 26 | !client.pfx 27 | 28 | # rider 29 | .idea 30 | 31 | # vs-code 32 | .vscode 33 | 34 | # resharper 35 | _ReSharper.* 36 | *.resharper* 37 | *.[Rr]e[Ss]harper.user 38 | *.DotSettings.user 39 | 40 | #windows stuff 41 | Thumbs.db -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 2 | WORKDIR /src 3 | COPY . . 4 | RUN dotnet build src/MyNatsClient.sln -c Release --no-incremental 5 | RUN dotnet test src/testing/UnitTests/UnitTests.csproj -c Release --no-build 6 | RUN dotnet test src/testing/IntegrationTests/IntegrationTests.csproj -c Release --no-build 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daniel Wertheim 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. -------------------------------------------------------------------------------- /azure-devops/azure-pipelines-cd.yml: -------------------------------------------------------------------------------- 1 | name: $(SemVer) 2 | 3 | variables: 4 | BuildConfiguration: Release 5 | SemVer: $[ variables['Build.SourceBranchName'] ] 6 | CommitId: $(Build.SourceVersion) 7 | 8 | trigger: 9 | batch: true 10 | branches: 11 | include: 12 | - refs/tags/* 13 | 14 | pr: none 15 | 16 | pool: 17 | vmImage: ubuntu-latest 18 | 19 | stages: 20 | - template: stage-build.yml 21 | - template: stage-deploy.yml 22 | -------------------------------------------------------------------------------- /azure-devops/azure-pipelines-ci.yml: -------------------------------------------------------------------------------- 1 | name: $(SemVer) 2 | 3 | variables: 4 | BuildConfiguration: Release 5 | BuildRev: $[counter(format('{0:yyyyMMdd}', pipeline.startTime), 1)] 6 | SemVer: $[format('{0:yyyy}.{0:MM}.{0:dd}-pre{1}', pipeline.startTime, variables.BuildRev)] 7 | CommitId: $(Build.SourceVersion) 8 | 9 | trigger: 10 | batch: true 11 | branches: 12 | include: 13 | - master 14 | 15 | pr: 16 | autoCancel: true 17 | branches: 18 | include: 19 | - master 20 | 21 | pool: 22 | vmImage: ubuntu-latest 23 | 24 | stages: 25 | - template: stage-build.yml 26 | -------------------------------------------------------------------------------- /azure-devops/stage-build.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - stage: Build 3 | jobs: 4 | - job: BuildTestPack 5 | displayName: 'Build, test & pack' 6 | timeoutInMinutes: 5 7 | cancelTimeoutInMinutes: 2 8 | steps: 9 | 10 | - task: UseDotNet@2 11 | displayName: 'Use .NET Core 5.x' 12 | inputs: 13 | version: '5.x' 14 | packageType: sdk 15 | 16 | - task: DotNetCoreCLI@2 17 | displayName: 'Build Solution' 18 | inputs: 19 | command: build 20 | projects: 'src/*.sln' 21 | arguments: '-c $(BuildConfiguration) --no-incremental --nologo -p:TreatWarningsAsErrors=true -p:Version=$(SemVer) -p:InformationalVersion=$(CommitId)' 22 | 23 | - task: DotNetCoreCLI@2 24 | displayName: 'UnitTests' 25 | inputs: 26 | command: test 27 | projects: 'src/**/UnitTests.csproj' 28 | arguments: '-c $(BuildConfiguration) --no-build' 29 | testRunTitle: 'UnitTests' 30 | 31 | - task: DockerCompose@0 32 | displayName: 'Start test dependencies' 33 | env: 34 | mynats_credentials__user: $(mynats_credentials__user) 35 | mynats_credentials__pass: $(mynats_credentials__pass) 36 | inputs: 37 | containerregistrytype: 'Container Registry' 38 | dockerComposeFile: 'docker-compose.yml' 39 | dockerComposeFileArgs: | 40 | mynats_credentials__user=$(mynats_credentials__user) 41 | mynats_credentials__pass=$(mynats_credentials__pass) 42 | action: 'Run a Docker Compose command' 43 | dockerComposeCommand: 'up -d' 44 | 45 | - task: CmdLine@2 46 | displayName: 'Wait for test dependencies' 47 | inputs: 48 | script: 'sleep 15' 49 | 50 | - task: DotNetCoreCLI@2 51 | displayName: 'IntegrationTests' 52 | env: 53 | mynats_credentials__user: $(mynats_credentials__user) 54 | mynats_credentials__pass: $(mynats_credentials__pass) 55 | inputs: 56 | command: test 57 | projects: 'src/**/IntegrationTests.csproj' 58 | arguments: '-c $(BuildConfiguration) --no-build' 59 | testRunTitle: 'IntegrationTests' 60 | 61 | - task: DotNetCoreCLI@2 62 | displayName: 'Pack Nupkg' 63 | inputs: 64 | command: custom 65 | custom: pack 66 | projects: 'src/*.sln' 67 | arguments: '-c $(BuildConfiguration) --no-build -o $(Build.ArtifactStagingDirectory) -p:Version=$(SemVer) -p:InformationalVersion=$(CommitId)' 68 | 69 | - task: PublishPipelineArtifact@1 70 | displayName: 'Publish Artifacts' 71 | inputs: 72 | path: '$(Build.ArtifactStagingDirectory)' 73 | artifact: Artifacts 74 | 75 | - task: DockerCompose@0 76 | displayName: 'Stop test dependencies' 77 | condition: always() 78 | inputs: 79 | containerregistrytype: 'Container Registry' 80 | dockerComposeFile: 'docker-compose.yml' 81 | action: 'Run a Docker Compose command' 82 | dockerComposeCommand: 'down' -------------------------------------------------------------------------------- /azure-devops/stage-deploy.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - stage: Deploy 3 | condition: and (succeeded(), startsWith( variables['Build.SourceBranch'], 'refs/tags' )) 4 | dependsOn: Build 5 | jobs: 6 | - deployment: DeployArtifacts 7 | environment: 'Prod' 8 | displayName: 'Deploys artifacts' 9 | timeoutInMinutes: 4 10 | cancelTimeoutInMinutes: 2 11 | strategy: 12 | runOnce: 13 | deploy: 14 | steps: 15 | - checkout: none 16 | - task: NuGetCommand@2 17 | displayName: 'Push Nupkg to NuGet' 18 | inputs: 19 | command: push 20 | nugetFeedType: external 21 | publishFeedCredentials: nuget_push 22 | verbosityPush: Normal 23 | packagesToPush: '$(Pipeline.Workspace)/**/*.nupkg;!$(Pipeline.Workspace)/**/*.symbols.nupkg' 24 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | git clean -dfx -e clean.sh -e src/testing/IntegrationTests/integrationtests.local.json -e .env -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nats: 4 | image: "nats" 5 | ports: 6 | - "4222:4222" 7 | nats-auth: 8 | image: "nats" 9 | ports: 10 | - "4223:4222" 11 | environment: 12 | - mynats_credentials__user 13 | - mynats_credentials__pass 14 | command: "-c /nats_config/auth.config" 15 | volumes: 16 | - ./nats_config:/nats_config 17 | nats-tls: 18 | image: "nats" 19 | ports: 20 | - "4224:4222" 21 | command: "-c /nats_config/tls.config" 22 | volumes: 23 | - ./nats_config:/nats_config 24 | nats-tlsverify: 25 | image: "nats" 26 | ports: 27 | - "4225:4222" 28 | command: "-c /nats_config/tlsVerify.config" 29 | volumes: 30 | - ./nats_config:/nats_config 31 | -------------------------------------------------------------------------------- /init-local-env.sh: -------------------------------------------------------------------------------- 1 | cp ./resources/.env.sample .env 2 | cp ./resources/integrationtests.local.json.sample ./src/testing/IntegrationTests/integrationtests.local.json -------------------------------------------------------------------------------- /nats_config/auth.config: -------------------------------------------------------------------------------- 1 | authorization { 2 | user: $mynats_credentials__user 3 | password: $mynats_credentials__pass 4 | timeout: 5 5 | } 6 | -------------------------------------------------------------------------------- /nats_config/ca-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAuDEebqKbenN6off0rIOJuQF12V13jU/pc75drH8T0L9HOtKZ 3 | GBHPaDYF0bnw3JquTKH0m2nNHIxMvft3kS2ZXenv1A5/W6csmdh9LKj6ww6Nr0Ql 4 | 9GPET7k0hUnncOhoaoQpLpltxyHjWxq8WGYphXgkAqzCynOLNtppiRKkb3bcJawC 5 | iC1G0XVcJu0FtrbEnTPvIqtFMUbHvnddB9stb9r08MsRWZtUZs17tN133JOkiof0 6 | /1ArKahVEKK5VSS37iyVKFNepIjG8Ch3kWLxCzTcuRc6JrsJWO/vLedixdR01b8F 7 | i+0IHD/7mSDe8+gqM21tFrGH8ebG9gFqsqweOQIDAQABAoIBAC11ugwWSAdl3vuA 8 | xjuZz3EA4kQ+pFYVRgoSDu6XAp3/x6XF+pENPEJ9KtyDZuYbqy8wvb1p1HzaR8qP 9 | +Qe02GhsZ4vP9DD7xq9F1kcigZmIywFZ8YAq63W9wS+weNXOFo9LhRqCNazzOTjw 10 | FVJADLlrWH/sgbsT32UvbOKu3jTAFsj9wLSopnIsmnF5FgiDeug5Abxq4SnGuGFh 11 | FlPlaZ/WOwEElLZcZEGhF+rnSojQ0j8bkzEQW4LXgOQZV4CKYYhr85rJ19CiQCQb 12 | xfUFi2yYHjZncZsXLy8WU0A1SXd46dmgYNKJL8mFJb2hb7vPvupRtf2OkSf8OA/u 13 | r0jJ+s0CgYEA6UDmWL6GJn6tsV8ugE1JCfWJ5C2PhtTk7oEp3Q9j5KpqRMcVXFHK 14 | h8G55X2V7VEIvCoxL3TvcWs0Zt3y1VWouPbSGOpWsmotYVCHUkUMIcpi2HmRHDJ8 15 | 5jFYzsgJ3jRKMgVXlaJ38P8NkBjDqh7lW3MkqGZfWh7r4Rqir9lKHk8CgYEAyidp 16 | INC0vFj/x41l3d/qnMtW45LMPGVXkw+6zhemJ8Cdgy3QxoEcfVoNLx6eK7Fc/i/Z 17 | Ig3wJ9UqCfbz46bS7pKbjJx7d+GA+Tw86baTvG7OA/R6a7gObkPJKnSTZ9i1JZLB 18 | Mz+yrvY/fSLXFWFPDNK6ftcmwSntVV7tW8DxIPcCgYBD2OvnzmuJEPoHCO+mxFRF 19 | gFV+uL3x80UwFwQNWCevYN6OaPZOpz/pIzGmjlQo9pQ63271JedsL0nLzi2PXl44 20 | hxrvwmI7fWT71Ie4J3ty7MK5wwqxkAICl0IEy+0K5Fzcle83CE4pHJdeXcW0W05N 21 | vn1gOn7r4OitzqiZy/OiQwKBgBjKbn0EuzVh4LQzJevoutfRo+0yonNBplS1mB4Q 22 | DmNPnsFoi/LcAIdq1LUTlygajyHo8kLX3Ly1ynsAWvZvDe0N859xzT5tXCi0d2/d 23 | 4Q45tHANujmdj7979p6LP0x76fMapTDIvfvw3OZWsHPBxDT5Xr+RWQJVRvKyOn+H 24 | EMsPAoGBAOYdTtWWI/BBxtbZlOA/+T6foPRLCIr6iTOUjTaRvI9jcv7knIlAH8um 25 | t0QhE7q3hZTN+eyI1HNSGrxPGLvuRGnYJ8TyEuCzBccwG5iaJNjLahHhRlTIpVyz 26 | HPzia8ocoltGDG195cAB+9YjjRqj1swGp9r2JRVFeQ4ehTcMUunx 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /nats_config/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDNDCCAhygAwIBAgIUXBhZKEVruReD57q07p4z45ZzU0cwDQYJKoZIhvcNAQEL 3 | BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yMTAzMDkxOTE2MDlaFw0yMjAzMDkx 4 | OTE2MDlaMBIxEDAOBgNVBAMMB3Rlc3QtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQC4MR5uopt6c3qh9/Ssg4m5AXXZXXeNT+lzvl2sfxPQv0c60pkY 6 | Ec9oNgXRufDcmq5MofSbac0cjEy9+3eRLZld6e/UDn9bpyyZ2H0sqPrDDo2vRCX0 7 | Y8RPuTSFSedw6GhqhCkumW3HIeNbGrxYZimFeCQCrMLKc4s22mmJEqRvdtwlrAKI 8 | LUbRdVwm7QW2tsSdM+8iq0UxRse+d10H2y1v2vTwyxFZm1RmzXu03Xfck6SKh/T/ 9 | UCspqFUQorlVJLfuLJUoU16kiMbwKHeRYvELNNy5FzomuwlY7+8t52LF1HTVvwWL 10 | 7QgcP/uZIN7z6CozbW0WsYfx5sb2AWqyrB45AgMBAAGjgYEwfzAPBgNVHRMBAf8E 11 | BTADAQH/MB0GA1UdDgQWBBSlq86qaHMWwzlkQTNdITc6czgF5jBNBgNVHSMERjBE 12 | gBSlq86qaHMWwzlkQTNdITc6czgF5qEWpBQwEjEQMA4GA1UEAwwHdGVzdC1jYYIU 13 | XBhZKEVruReD57q07p4z45ZzU0cwDQYJKoZIhvcNAQELBQADggEBADa1u/VvC20I 14 | UTaUTep5v8Ut3lE6nBsUAV64/GXYMZPSd/ErtINrpR47cyoqDmRq2mdp9p+F4ALh 15 | 5+rZssG+tCwl3xuICva2f6nkK+qRZk0MF5YEXYVtqGcrmPofmkUFviSKxMHyROez 16 | ihQmkOyuIuHzR5sE2aNfZcA24+Kq8iW+FKSbmTItuzA+5HcRQchvKyJ05eMrH7Pn 17 | mbTXNvw/9955+bY4nnjk4rr97SaVrqo1Etljdy52DJFXqf8x/eG/Ppx4V/YVNC4d 18 | 7yH5V/b+FYXIJqw3u3d4xNELeiBIpWfT3N64D2Ny7+KQdcBP6DDTVOdLyLKeu284 19 | g0TfWXpVZ0M= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /nats_config/client-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC7TCCAdWgAwIBAgIUMOYVxThjztQAsuOdRQdiPX1mb/YwDQYJKoZIhvcNAQEL 3 | BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yMTAzMDkxOTE2MjBaFw0yMjAzMDkx 4 | OTE2MjBaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBAMI0i+4XXi5UtRHo0ZN9B0/pAr3HjSpOWdCsQRw+mX7pBK4s 6 | 7JQa4xbw2lvvHpydWa93gEQBgbvsZ67mvq9Mmsg2+uaG0wo/wTBOCHirgBgLkBWC 7 | 589js94aGFpvu/Iqb/uysodfr+20+HFHuWfpZdAByzOemFq21svQih64VSx0rJHL 8 | HYzwQrY4yD1/Yx3+e5VPGtK0i5U47b84jeyMMrSIKOY2dJWDNkMyVKPA1jsk/8Ps 9 | sRmp9HF/s4eoRJODn6t1bg8T4w1X4hYTDBTsvCKEOajgJp5ljjv2JPHzPrGx1Vpw 10 | ZjQdvUX7xG2zhdRKzx7ho1yMR6VefNHOPCQ4QUcCAwEAAaM5MDcwCQYDVR0TBAIw 11 | ADALBgNVHQ8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0G 12 | CSqGSIb3DQEBCwUAA4IBAQBTd3nBJoGAUQDexVvU0R/iIyV9vdTh93OgBzG26ne4 13 | GDB6h3tnkkPxHCQYKDbgWyZm2QyEJpuhdYCt4tPqikEj5iq9dCFMLLzwwb8CUBuz 14 | IiJh+F+PxICp+jY1Zxm604eIRqEwqlW2LmWjfafpdaUURJihlPd87naL02MsgUJt 15 | S876BEgzQWkLIX8u6vYjgffGpcB0gaGCzjD+g5dEzEObhbS5LULap0kalKx5H9EU 16 | NdjLrDud50mFGkq/JF6PlEDQqbIndtJapMqqRFEwrjWsi8ege7RT5Jfv2q1mK362 17 | kjeFbY3fNT6KP9cLUT4LVDhLNngSr+cGoRXXVAofTQRa 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /nats_config/client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAwjSL7hdeLlS1EejRk30HT+kCvceNKk5Z0KxBHD6ZfukErizs 3 | lBrjFvDaW+8enJ1Zr3eARAGBu+xnrua+r0yayDb65obTCj/BME4IeKuAGAuQFYLn 4 | z2Oz3hoYWm+78ipv+7Kyh1+v7bT4cUe5Z+ll0AHLM56YWrbWy9CKHrhVLHSskcsd 5 | jPBCtjjIPX9jHf57lU8a0rSLlTjtvziN7IwytIgo5jZ0lYM2QzJUo8DWOyT/w+yx 6 | Gan0cX+zh6hEk4Ofq3VuDxPjDVfiFhMMFOy8IoQ5qOAmnmWOO/Yk8fM+sbHVWnBm 7 | NB29RfvEbbOF1ErPHuGjXIxHpV580c48JDhBRwIDAQABAoIBAQCcQ2W4jUr14qjT 8 | oU9I6piAnw/ann+i4+p28rNtQ52EtYBTmcg+n6fMJL6+rJgoEqBAhNpLXh8ClAUV 9 | gYs7SFte8IZQCXo0S/ppoNBGMFDgiCVGec+GW0Y2Vy7zTeafw6/Zif8G2GKZ1Tz3 10 | AWxZBT8r8NWNUnZqv6Xz13fbohy+qDaGaQF7rojlHZm61LMkTjFIDLYof5ph08Dl 11 | iYEkZSmK0429B1aH10nPcYv+qHugZEQeEns/Zg1bUOhgw6lOFp/UbkFytR9VyI9+ 12 | Xkqie/6AZfI7L/0o8DxS2prrkpj7+1Bdl/iAGwV1ZzaTrJLtERs9fzuSwDC0xGUG 13 | K5WDCy1pAoGBAPQ81z4yvGJVA/icQGa97VBsCX78N3kGk1LMNmyz7iYJbDNkOU3x 14 | WZ8Kyxf6huFxpiatfyZWjfaHJm+n1CLAjxtce9gYyJ6U9h/QdqjxbroZw8/8wuUN 15 | 26CMqNnRfEQ+JtnWnt1I/DrD7Do75qs6Anvd8FYHzWAjdzG1I/IdfuxLAoGBAMuO 16 | 3a/wRPPLjfohVDNl1S/FnuJ4z0ReXyQ1RHXZCuGX2vAhf2EB/nG+Z4B4QBsMEpd/ 17 | 7RdoGks+R4SeEoEXwqe5DapJ5d+F8P+qAsbM/TWc5g16I7YK3nsEgtEoaMfX/Sl7 18 | 517d/x3jfD2C18ncWWcBGgl8ybkd3QMk21yIsul1AoGAM6jixi8pFYtx6ZXzMUge 19 | +BHB+HAUs66mXq7/HAL67mSz1DLwxAG2uSMpTgsZmHnXcPrlmBBaurhfcuKGxksA 20 | egMX1t93H8PWQ2hukNFwYECu4EFQX+6Mc+sMk/XxWOXkhdMViyyy3SMvximBYtpE 21 | SDrraOcBk8IspgDwd3exrv0CgYEAoXmPekXLLOpIkidmN++MEf1ecKENKlsCdiKb 22 | UDEAZLylvN+VwEpooao9SLgbglktVmjwlyTJ1u5spVMPssA6dfpiULeWVp5V8znW 23 | QgXSx3jTNdWyBnLQ0h1d+LVpiT5ZYVMc0zCfdAJFSDZHH6ZgdUl2chg/nU2HSTsv 24 | mFQIORECgYBoamA/U+RdYyBqFUv1WjoCGLbxfFJAY4ENNFo5o6CBPTKVXSMTzLcS 25 | xWqu/6VQCPvNVRLFdNBTLn1nRPvYUM0pjmIYLRBYRWkDrfsSuppfUORA941gfpA/ 26 | tAK10mNoghOnJt7ybCmdcSRVIeX1K70+/MmWLOjCqYsLPghbeK+RQw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /nats_config/client.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielwertheim/mynatsclient/1b1eca2ae6646d4d25b207f956abe456fa5bed1d/nats_config/client.pfx -------------------------------------------------------------------------------- /nats_config/server-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC7TCCAdWgAwIBAgIUMOYVxThjztQAsuOdRQdiPX1mb/UwDQYJKoZIhvcNAQEL 3 | BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yMTAzMDkxOTE2MDlaFw0yMjAzMDkx 4 | OTE2MDlaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBAMpNQctMgvBps3ch0l5j787VIpdAl+RVCTEtmwSqIysGWYly 6 | KADPqJr/+owXFqkqV+hfEtv6vkHTSTm13hFqOqFLBpX3JTjkZxeSF326wlaAzdF7 7 | FkiDRLKsR/NIf6wpSFVGlCd/cS5DlGUwsvbgJRat/0M+SU32BlMNN2sbO0JTpBpD 8 | TmSM+YmHxfOvfXf2blNtdolaSThAcn4hmHpQUDIn+FWEZB480YNgAnXiWWOGBQZF 9 | 8lfpVVF85TGmp9HA7/UIQwTsgubNRzx+Ha9yathZl5yPlq5Wersh7yORA5KVbVhb 10 | 62t3aB26cXBzL1xD7k2sbmZrmhgqzHMh+LOC1AMCAwEAAaM5MDcwCQYDVR0TBAIw 11 | ADALBgNVHQ8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0G 12 | CSqGSIb3DQEBCwUAA4IBAQAKlAu11VwHnBGf/HTNLsaQc6iVkUYs5UtqZcvN5MOA 13 | W6KlBHWH8Bi74WQz9uA3NAeKlNn3SiQ2rRFkC+Mbg5ytq/BwVFtGTqQKwPA8VEed 14 | T0QnX1AhBspjGkZiA2VEEhSA2ZxPfPkEkWmZXsFF2gVAl90ZW1a7MZvsVERaPF2C 15 | a/2SWNQrryq8T2slY/kLVqgasPGv7jTgW5tD74xDdt+j4/tbiLVzvUzk9vDvdr/M 16 | J2ZCJJ0eD1GYE+vFZyfU+II0SNL/x2Q01eOW/YVMsD4uoRj/LGyawVxiQ0oZzVH2 17 | k/1J9B/f9LUKBnbsPvhjP9vlFAEMvNUpRuMm4XNUitOe 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /nats_config/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyk1By0yC8GmzdyHSXmPvztUil0CX5FUJMS2bBKojKwZZiXIo 3 | AM+omv/6jBcWqSpX6F8S2/q+QdNJObXeEWo6oUsGlfclOORnF5IXfbrCVoDN0XsW 4 | SINEsqxH80h/rClIVUaUJ39xLkOUZTCy9uAlFq3/Qz5JTfYGUw03axs7QlOkGkNO 5 | ZIz5iYfF8699d/ZuU212iVpJOEByfiGYelBQMif4VYRkHjzRg2ACdeJZY4YFBkXy 6 | V+lVUXzlMaan0cDv9QhDBOyC5s1HPH4dr3Jq2FmXnI+WrlZ6uyHvI5EDkpVtWFvr 7 | a3doHbpxcHMvXEPuTaxuZmuaGCrMcyH4s4LUAwIDAQABAoIBACnh1L1CGdmsCHuh 8 | hOwFxKANgBvQQ6IB+OU1i+JrVtyRc/gAqkeAtmdgl35pSUe8fHafOEhSoRD/rSlF 9 | fbQdSgYagkKJ6CrfHnjCxhpBj/pYsEOuN+7LPm2/PSYg3E3IPjFpa0LCH6x0UUSh 10 | 0GtL/GG1pCNHweViCe0nz1FdUedpCtbEmEif+Uj/xhp0HeLhJz7oGazkp9mgFlO9 11 | qqKf7wOJc49vH7ku9UNx9xedoD1IXzUqsuwVARNFeoC9IiufER+KemxY6D7kSgMu 12 | SZglTXYNT/iyxcG5dcxp3SVL7QVhuTAfLslKqGXVbY/8KI/c9A8Ey+hnP9bfjLNp 13 | zy++yAECgYEA/CWomO02rY0NX3D58ydnpWBu85pyFZfu1EY82mysMSQcmAMdSC/s 14 | gfC5jIdAZTmdsI4TMKBPgdDvUrrBuK7fmnBKDabOdHxErWQ/ky1w1luJOlXhIsQd 15 | E0QSmUmqkaUGUgFJTFyC3ovNxWOwOoonENBydBVdE9+LEoUMOvKMQ08CgYEAzWSd 16 | cZgWQsKlllFPCxadFaCXZ/iZUdjsfKL87InRrOBMwHTEaHQTDp8xboGP/oYfVu5Z 17 | 2XA0v2vmq7SjEWLbW5UgvMk5cPxpCWbhUaG5oAwBwpi+LhggNKMkSX1aBiE0kVfS 18 | CcQ2A9MsTlzhSR9KrZCumhFMjrWd2PfsorSfxw0CgYB5PzqVXuv+BGe7zz3B+N7U 19 | HUz9AvV9ALQtiyDT/5HFh5HgXw8DmVfnG/R9aMREaLI1JfyAU3Yn/Go4o+lnf1+y 20 | ifByJBX+2YrEvgH8KcuqqBWS4Z0C9L7udrtizpOguXQSlt/CQnIDxcJK+qbRX4Ft 21 | im6e927skX/0hdndtqhP7QKBgQCP3ccOdzo4fwDffoqAONHFAVysPgyesPRNBRlm 22 | Uc7laaM5N1EP3SYwGr/UKLqEzMuM551V2euInXrQkNGb7wO5bYal1cj/ZD3qkP6I 23 | YGCWQ3LELJshzFJarF9RfpUdMAsTN1Fu99nh6fvh09sVje6QDoTOdAmM+JfdDyPY 24 | f+5ezQKBgE4ATTud8Nt74KW9wEPRkUsbwC7GbqnZzgWyNIrYy1aq35bLMu7LYHYP 25 | 2O7oHL6w95C5AyI5ox0exkwgngoo6CKIaV+pCSpDo7ZisHOTUOYYZglnv7Y3NQLP 26 | yqblSY8m+yf1Tbp7OB+Y/RImpclD5Molz9kcP3koWcow7YoQ6Yp3 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /nats_config/tls.config: -------------------------------------------------------------------------------- 1 | tls { 2 | cert_file: "/nats_config/server-cert.pem" 3 | key_file: "/nats_config/server-key.pem" 4 | timeout: 5 5 | } 6 | -------------------------------------------------------------------------------- /nats_config/tlsVerify.config: -------------------------------------------------------------------------------- 1 | tls { 2 | ca_file: "/nats_config/ca.pem" 3 | cert_file: "/nats_config/server-cert.pem" 4 | key_file: "/nats_config/server-key.pem" 5 | verify: true 6 | timeout: 5 7 | } 8 | -------------------------------------------------------------------------------- /resources/.env.sample: -------------------------------------------------------------------------------- 1 | mynats_credentials__user=test 2 | mynats_credentials__pass=p@ssword123 3 | -------------------------------------------------------------------------------- /resources/integrationtests.local.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "credentials": { 3 | "user": "test", 4 | "pass": "p@ssword123" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /resources/nuget-usage.md: -------------------------------------------------------------------------------- 1 | ## Pub-Sub sample 2 | Simple pub-sub sample showing one client that publishes and one that subscribes. This can of course be the same client and you can also have more clients subscribing etc. 3 | 4 | **Publisher** 5 | 6 | ```csharp 7 | var cnInfo = new ConnectionInfo("192.168.1.10"); 8 | var client = new NatsClient(cnInfo); 9 | 10 | await client.ConnectAsync(); 11 | 12 | await client.PubAsync("tick", GetNextTick()); 13 | 14 | //or using an encoding package e.g. Json 15 | await client.PubAsJsonAsync("tickItem", new Tick { Value = GetNextTick() }); 16 | ``` 17 | 18 | **Subscriber** 19 | 20 | ```csharp 21 | var cnInfo = new ConnectionInfo("192.168.1.10"); 22 | var client = new NatsClient(cnInfo); 23 | 24 | await _client.ConnectAsync(); 25 | 26 | await client.SubAsync("tick", stream => stream.Subscribe(msg => { 27 | Console.WriteLine($"Clock ticked. Tick is {msg.GetPayloadAsString()}"); 28 | })); 29 | 30 | //or using an encoding package e.g Json 31 | await client.SubAsync("tickItem", stream => stream.Subscribe(msg => { 32 | Console.WriteLine($"Clock ticked. Tick is {msg.FromJson().Value}"); 33 | })) 34 | ``` -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.0.0 4 | danielwertheim 5 | danielwertheim 6 | Copyright © danielwertheim 7 | latest 8 | https://github.com/danielwertheim/mynatsclient 9 | Git 10 | MIT 11 | https://github.com/danielwertheim/mynatsclient 12 | https://github.com/danielwertheim/mynatsclient/releases 13 | true 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/MyNatsClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29418.71 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyNatsClient", "main\MyNatsClient\MyNatsClient.csproj", "{509E85D1-89DC-4682-B375-32A6C77E4BEF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "testing\UnitTests\UnitTests.csproj", "{BDB39882-DB23-418E-9074-2EA0BD743691}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyNatsClient.Encodings.Protobuf", "main\MyNatsClient.Encodings.Protobuf\MyNatsClient.Encodings.Protobuf.csproj", "{3862761E-12EC-4087-817F-ED3D765C3693}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8537E787-3FC8-462D-8D49-6A8AB590336A}" 13 | ProjectSection(SolutionItems) = preProject 14 | Directory.Build.props = Directory.Build.props 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyNatsClient.Encodings.Json", "main\MyNatsClient.Encodings.Json\MyNatsClient.Encodings.Json.csproj", "{331262C9-913C-47EA-A265-8F7EA9A83850}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "testing\IntegrationTests\IntegrationTests.csproj", "{DB63AC57-292E-494F-90B4-734E6D2DAA85}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BDD5D742-B315-4003-93E3-623CF365FAA5}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{67EF08EB-6EBF-4BD7-A100-407EDF47C24E}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RequestResponseSample", "samples\RequestResponseSample\RequestResponseSample.csproj", "{AEC8AC0D-03A8-452F-BC49-A5999F18320C}" 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "samples\Benchmarks\Benchmarks.csproj", "{1AA4BBBA-21F9-4D33-8224-45EE41B82369}" 28 | EndProject 29 | Global 30 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 31 | Debug|Any CPU = Debug|Any CPU 32 | Release|Any CPU = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {509E85D1-89DC-4682-B375-32A6C77E4BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {509E85D1-89DC-4682-B375-32A6C77E4BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {509E85D1-89DC-4682-B375-32A6C77E4BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {509E85D1-89DC-4682-B375-32A6C77E4BEF}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {BDB39882-DB23-418E-9074-2EA0BD743691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {BDB39882-DB23-418E-9074-2EA0BD743691}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {BDB39882-DB23-418E-9074-2EA0BD743691}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {BDB39882-DB23-418E-9074-2EA0BD743691}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {3862761E-12EC-4087-817F-ED3D765C3693}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {3862761E-12EC-4087-817F-ED3D765C3693}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {3862761E-12EC-4087-817F-ED3D765C3693}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {3862761E-12EC-4087-817F-ED3D765C3693}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {331262C9-913C-47EA-A265-8F7EA9A83850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {331262C9-913C-47EA-A265-8F7EA9A83850}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {331262C9-913C-47EA-A265-8F7EA9A83850}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {331262C9-913C-47EA-A265-8F7EA9A83850}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {DB63AC57-292E-494F-90B4-734E6D2DAA85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {DB63AC57-292E-494F-90B4-734E6D2DAA85}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {DB63AC57-292E-494F-90B4-734E6D2DAA85}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {DB63AC57-292E-494F-90B4-734E6D2DAA85}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {AEC8AC0D-03A8-452F-BC49-A5999F18320C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {AEC8AC0D-03A8-452F-BC49-A5999F18320C}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {AEC8AC0D-03A8-452F-BC49-A5999F18320C}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {AEC8AC0D-03A8-452F-BC49-A5999F18320C}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {1AA4BBBA-21F9-4D33-8224-45EE41B82369}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {1AA4BBBA-21F9-4D33-8224-45EE41B82369}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {1AA4BBBA-21F9-4D33-8224-45EE41B82369}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {1AA4BBBA-21F9-4D33-8224-45EE41B82369}.Release|Any CPU.Build.0 = Release|Any CPU 63 | EndGlobalSection 64 | GlobalSection(SolutionProperties) = preSolution 65 | HideSolutionNode = FALSE 66 | EndGlobalSection 67 | GlobalSection(NestedProjects) = preSolution 68 | {BDB39882-DB23-418E-9074-2EA0BD743691} = {BDD5D742-B315-4003-93E3-623CF365FAA5} 69 | {DB63AC57-292E-494F-90B4-734E6D2DAA85} = {BDD5D742-B315-4003-93E3-623CF365FAA5} 70 | {AEC8AC0D-03A8-452F-BC49-A5999F18320C} = {67EF08EB-6EBF-4BD7-A100-407EDF47C24E} 71 | {1AA4BBBA-21F9-4D33-8224-45EE41B82369} = {67EF08EB-6EBF-4BD7-A100-407EDF47C24E} 72 | EndGlobalSection 73 | GlobalSection(ExtensibilityGlobals) = postSolution 74 | SolutionGuid = {8EC07F8D-0B36-4E4D-80ED-A6A95214AA0D} 75 | EndGlobalSection 76 | EndGlobal 77 | -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Json/JsonEncoding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Newtonsoft.Json; 5 | 6 | namespace MyNatsClient.Encodings.Json 7 | { 8 | public class JsonEncoding : IEncoding 9 | { 10 | private readonly JsonSerializer _serializer; 11 | 12 | public static JsonEncoding Default { get; set; } = new JsonEncoding(); 13 | 14 | public JsonEncoding(JsonSerializerSettings settings = null) 15 | { 16 | _serializer = JsonSerializer.Create(settings ?? JsonSettings.Create()); 17 | } 18 | 19 | public ReadOnlyMemory Encode(TItem item) where TItem : class 20 | { 21 | if(item == null) 22 | return ReadOnlyMemory.Empty; 23 | 24 | using (var stream = new MemoryStream()) 25 | { 26 | using (var sw = new StreamWriter(stream, Encoding.UTF8)) 27 | { 28 | var jw = new JsonTextWriter(sw); 29 | 30 | _serializer.Serialize(jw, item, typeof(TItem)); 31 | } 32 | 33 | return stream.ToArray(); 34 | } 35 | } 36 | 37 | public object Decode(ReadOnlySpan payload, Type objectType) 38 | { 39 | if(objectType == null) 40 | throw new ArgumentNullException(nameof(objectType)); 41 | 42 | if (payload == null || payload.Length == 0) 43 | return null; 44 | 45 | using (var stream = new MemoryStream(payload.ToArray(), false)) 46 | { 47 | using (var sr = new StreamReader(stream, Encoding.UTF8)) 48 | { 49 | var jr = new JsonTextReader(sr); 50 | 51 | return _serializer.Deserialize(jr, objectType); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Json/JsonSettings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace MyNatsClient.Encodings.Json 6 | { 7 | public class JsonSettings 8 | { 9 | public static JsonSerializerSettings Create() 10 | { 11 | var settings = new JsonSerializerSettings 12 | { 13 | DateFormatHandling = DateFormatHandling.IsoDateFormat, 14 | DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind, 15 | ContractResolver = new DefaultContractResolver 16 | { 17 | NamingStrategy = new DefaultNamingStrategy 18 | { 19 | ProcessDictionaryKeys = false 20 | } 21 | } 22 | }; 23 | settings.Converters.Add(new StringEnumConverter()); 24 | 25 | return settings; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Json/MyNatsClient.Encodings.Json.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0;netcoreapp3.1 5 | NATS NATS.io messaging JSON JSONEncoding PubSub pub-sub request-response rx reactivex reactiveextensions 6 | JSON Encoding for assisting with sending and consuming JSON messages using MyNatsClient which provides a simple, effective sync and async library for interacting with NATS Server. It uses IObservable so it is ReactiveX (RX) friendly. 7 | 8 | 9 | 10 | 1701;1702;1705;1591 11 | bin\Release\$(TargetFramework)\MyNatsClient.Encodings.Json.xml 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Json/NatsClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace MyNatsClient.Encodings.Json 4 | { 5 | public static class NatsClientJsonExtensions 6 | { 7 | public static void PubAsJson(this INatsClient client, string subject, TItem item, string replyTo = null) where TItem : class 8 | { 9 | var payload = JsonEncoding.Default.Encode(item); 10 | 11 | client.Pub(subject, payload, replyTo); 12 | } 13 | 14 | public static async Task PubAsJsonAsync(this INatsClient client, string subject, TItem item, string replyTo = null) where TItem : class 15 | { 16 | var payload = JsonEncoding.Default.Encode(item); 17 | 18 | await client.PubAsync(subject, payload, replyTo).ConfigureAwait(false); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Json/NatsMsgOpJsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Ops; 3 | 4 | namespace MyNatsClient.Encodings.Json 5 | { 6 | public static class NatsMsgOpJsonExtensions 7 | { 8 | public static T FromJson(this MsgOp msgOp) where T : class 9 | => FromJson(msgOp, typeof(T)) as T; 10 | 11 | public static object FromJson(this MsgOp msgOp, Type objectType) 12 | { 13 | return JsonEncoding.Default.Decode(msgOp.Payload.Span, objectType); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Protobuf/MyNatsClient.Encodings.Protobuf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0;netcoreapp3.1 5 | NATS NATS.io messaging Protobuf ProtobufEncoding PubSub pub-sub request-response rx reactivex reactiveextensions 6 | Protobuf Encoding for assisting with sending and consuming Protobuf messages using MyNatsClient which provides a simple, effective sync and async library for interacting with NATS Server using. It uses IObservable so it is ReactiveX (RX) friendly. 7 | 8 | 9 | 10 | 1701;1702;1705;1591 11 | bin\Release\$(TargetFramework)\MyNatsClient.Encodings.Protobuf.xml 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Protobuf/NatsClientProtobufExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace MyNatsClient.Encodings.Protobuf 4 | { 5 | public static class NatsClientProtobufExtensions 6 | { 7 | public static void PubAsProtobuf(this INatsClient client, string subject, TItem item, string replyTo = null) where TItem : class 8 | { 9 | var payload = ProtobufEncoding.Default.Encode(item); 10 | 11 | client.Pub(subject, payload, replyTo); 12 | } 13 | 14 | public static async Task PubAsProtobufAsync(this INatsClient client, string subject, TItem item, string replyTo = null) where TItem : class 15 | { 16 | var payload = ProtobufEncoding.Default.Encode(item); 17 | 18 | await client.PubAsync(subject, payload, replyTo).ConfigureAwait(false); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Protobuf/NatsMsgOpProtobufExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Ops; 3 | 4 | namespace MyNatsClient.Encodings.Protobuf 5 | { 6 | public static class NatsMsgOpProtobufExtensions 7 | { 8 | public static T FromProtobuf(this MsgOp msgOp) where T : class 9 | => FromProtobuf(msgOp, typeof(T)) as T; 10 | 11 | public static object FromProtobuf(this MsgOp msgOp, Type objectType) 12 | { 13 | return ProtobufEncoding.Default.Decode(msgOp.Payload.Span, objectType); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient.Encodings.Protobuf/ProtobufEncoding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using ProtoBuf; 4 | 5 | namespace MyNatsClient.Encodings.Protobuf 6 | { 7 | public class ProtobufEncoding : IEncoding 8 | { 9 | public static ProtobufEncoding Default { get; set; } = new ProtobufEncoding(); 10 | 11 | public ReadOnlyMemory Encode(TItem item) where TItem : class 12 | { 13 | if(item == null) 14 | return ReadOnlyMemory.Empty; 15 | 16 | using (var stream = new MemoryStream()) 17 | { 18 | Serializer.Serialize(stream, item); 19 | 20 | return stream.ToArray(); 21 | } 22 | } 23 | 24 | public object Decode(ReadOnlySpan payload, Type objectType) 25 | { 26 | if(objectType == null) 27 | throw new ArgumentNullException(nameof(objectType)); 28 | 29 | if (payload == null || payload.Length == 0) 30 | return null; 31 | 32 | using(var stream = new MemoryStream(payload.ToArray(), false)) 33 | return Serializer.Deserialize(objectType, stream); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/ConnectionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Security; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | namespace MyNatsClient 7 | { 8 | public class ConnectionInfo 9 | { 10 | /// 11 | /// Gets the hosts that a client can randomly connect to. 12 | /// 13 | public Host[] Hosts { get; } 14 | 15 | /// 16 | /// Gets the connection name which is used when a connection 17 | /// is established against the NATS server. 18 | /// 19 | public string Name { get; set; } = "mynatsclient"; 20 | 21 | /// 22 | /// When enabled (default), one single global Inbox-subsription is 23 | /// initiated against the NATS-Server upon first request. After that, 24 | /// all responses are reported back to that inbox. 25 | /// If disabled, one subsription is initated and disposed per request. 26 | /// 27 | public bool UseInboxRequests { get; set; } = true; 28 | 29 | /// 30 | /// Gets or sets value indicating if client should 31 | /// respond to server pings automatically. 32 | /// Default is true. 33 | /// 34 | public bool AutoRespondToPing { get; set; } = true; 35 | 36 | /// 37 | /// Gets or sets value indicating if client should 38 | /// try and auto reconnect on failure. 39 | /// Default is false. 40 | /// 41 | public bool AutoReconnectOnFailure { get; set; } = true; 42 | 43 | /// 44 | /// Gets or sets the credentials used when connecting against the hosts. 45 | /// 46 | /// You can specify host specific credentials on each host. 47 | public Credentials Credentials { get; set; } = Credentials.Empty; 48 | 49 | /// 50 | /// Gets or sets value if verbose output should be used. 51 | /// Default is false. 52 | /// 53 | public bool Verbose { get; set; } 54 | 55 | /// 56 | /// Gets or sets value determining how the clients flush behavior 57 | /// should be when sending messages. E.g. when Pub or PubAsync is called. 58 | /// Default is Auto (will Flush after each Pub or PubAsync). 59 | /// 60 | public PubFlushMode PubFlushMode { get; set; } = PubFlushMode.Auto; 61 | 62 | /// 63 | /// Gets or sets the default value to use for request timeout. 64 | /// 65 | public int RequestTimeoutMs { get; set; } = 5000; 66 | 67 | /// 68 | /// Gets or sets certificate collection used when authenticating the client against the server 69 | /// when the server is configured to use TLS and to verify the clients. 70 | /// If the server is onyl configured to use TLS but not configured to verfify the 71 | /// clients, no client certificates need to be provided. 72 | /// 73 | public X509Certificate2Collection ClientCertificates { get; set; } = new X509Certificate2Collection(); 74 | 75 | /// 76 | /// Gets or sets custom handler to use for verifying the server certificate. 77 | /// This is used if the server is configured to use TLS. 78 | /// 79 | public Func ServerCertificateValidation { get; set; } 80 | 81 | /// 82 | /// Gets or sets to use when creating the clients 83 | /// underlying socket via . 84 | /// 85 | public SocketOptions SocketOptions { get; set; } = new SocketOptions(); 86 | 87 | public ConnectionInfo(string host, int? port = null) 88 | : this(new Host(host, port)) { } 89 | 90 | public ConnectionInfo(Host host) 91 | : this(new[] { host }) { } 92 | 93 | public ConnectionInfo(Host[] hosts) 94 | { 95 | if (hosts?.Any() == false) 96 | throw new ArgumentException("At least one host need to be specified.", nameof(hosts)); 97 | 98 | Hosts = hosts; 99 | } 100 | 101 | public ConnectionInfo Clone() 102 | { 103 | var hosts = Hosts 104 | .Select(i => new Host(i.Address, i.Port) 105 | { 106 | Credentials = i.Credentials != null 107 | ? new Credentials(i.Credentials.User, i.Credentials.Pass) 108 | : Credentials.Empty 109 | }) 110 | .ToArray(); 111 | 112 | return new ConnectionInfo(hosts) 113 | { 114 | Name = Name, 115 | UseInboxRequests = UseInboxRequests, 116 | AutoRespondToPing = AutoRespondToPing, 117 | AutoReconnectOnFailure = AutoReconnectOnFailure, 118 | Credentials = new Credentials(Credentials.User, Credentials.Pass), 119 | Verbose = Verbose, 120 | RequestTimeoutMs = RequestTimeoutMs, 121 | PubFlushMode = PubFlushMode, 122 | ClientCertificates = new X509Certificate2Collection(ClientCertificates), 123 | ServerCertificateValidation = ServerCertificateValidation, 124 | SocketOptions = new SocketOptions 125 | { 126 | AddressType = SocketOptions.AddressType, 127 | ReceiveBufferSize = SocketOptions.ReceiveBufferSize, 128 | SendBufferSize = SocketOptions.SendBufferSize, 129 | ReceiveTimeoutMs = SocketOptions.ReceiveTimeoutMs, 130 | SendTimeoutMs = SocketOptions.SendTimeoutMs, 131 | ConnectTimeoutMs = SocketOptions.ConnectTimeoutMs, 132 | UseNagleAlgorithm = SocketOptions.UseNagleAlgorithm 133 | } 134 | }; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Credentials.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | public class Credentials : IEquatable 6 | { 7 | public static readonly Credentials Empty = new Credentials(); 8 | public string User { get; } 9 | public string Pass { get; } 10 | 11 | private Credentials() { } 12 | 13 | public Credentials(string user, string pass) 14 | { 15 | User = user; 16 | Pass = pass; 17 | } 18 | 19 | public static bool operator ==(Credentials left, Credentials right) 20 | { 21 | return Equals(left, right); 22 | } 23 | 24 | public static bool operator !=(Credentials left, Credentials right) 25 | { 26 | return !Equals(left, right); 27 | } 28 | 29 | public bool Equals(Credentials other) 30 | { 31 | if (ReferenceEquals(null, other)) return false; 32 | if (ReferenceEquals(this, other)) return true; 33 | return string.Equals(User, other.User) && string.Equals(Pass, other.Pass); 34 | } 35 | 36 | public override bool Equals(object obj) 37 | { 38 | return Equals(obj as Credentials); 39 | } 40 | 41 | public override int GetHashCode() 42 | { 43 | unchecked 44 | { 45 | return (User.GetHashCode() * 397) ^ Pass.GetHashCode(); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/DisconnectReason.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public enum DisconnectReason 4 | { 5 | /// 6 | /// Disconnect was caused by invoke by user. 7 | /// 8 | ByUser, 9 | /// 10 | /// Disconnect was caused by client failure. 11 | /// 12 | DueToFailure 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Events/ClientAutoReconnectFailed.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Events 2 | { 3 | public class ClientAutoReconnectFailed : IClientEvent 4 | { 5 | public INatsClient Client { get; } 6 | 7 | public ClientAutoReconnectFailed(INatsClient client) 8 | { 9 | Client = client; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Events/ClientConnected.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Events 2 | { 3 | public class ClientConnected : IClientEvent 4 | { 5 | public INatsClient Client { get; } 6 | 7 | public ClientConnected(INatsClient client) 8 | { 9 | Client = client; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Events/ClientDisconnected.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Events 2 | { 3 | public class ClientDisconnected : IClientEvent 4 | { 5 | public INatsClient Client { get; } 6 | public DisconnectReason Reason { get; } 7 | 8 | public ClientDisconnected(INatsClient client, DisconnectReason reason) 9 | { 10 | Client = client; 11 | Reason = reason; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Events/ClientWorkerFailed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Events 4 | { 5 | public class ClientWorkerFailed : IClientEvent 6 | { 7 | public INatsClient Client { get; } 8 | public Exception Exception { get; } 9 | 10 | public ClientWorkerFailed(INatsClient client, Exception exception) 11 | { 12 | Client = client; 13 | Exception = exception; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Host.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | public class Host 6 | { 7 | public const int DefaultPort = 4222; 8 | 9 | public string Address { get; } 10 | public int Port { get; } 11 | public Credentials Credentials { get; set; } = Credentials.Empty; 12 | 13 | public Host(string address, int? port = null) 14 | { 15 | Address = address ?? throw new ArgumentException("Address must be specified.", nameof(address)); 16 | Port = port ?? DefaultPort; 17 | } 18 | 19 | internal bool HasNonEmptyCredentials() 20 | => Credentials != null && Credentials != Credentials.Empty; 21 | 22 | public override string ToString() => $"{Address}:{Port}"; 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/IAsyncPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient 5 | { 6 | public interface IAsyncPublisher 7 | { 8 | Task PubAsync(string subject, string body, string replyTo = null); 9 | Task PubAsync(string subject, ReadOnlyMemory body, string replyTo = null); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/IClientEvent.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public interface IClientEvent 4 | { 5 | INatsClient Client { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/IConsumerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace MyNatsClient 6 | { 7 | /// 8 | /// Responsible for returning a Task that continiously runs the consumer. 9 | /// 10 | public interface IConsumerFactory 11 | { 12 | Task Run(Action consumer, CancellationToken cancellationToken); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/IDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | public interface IDecoder 6 | { 7 | object Decode(ReadOnlySpan payload, Type objectType); 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/IEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | public interface IEncoder 6 | { 7 | ReadOnlyMemory Encode(TItem item) where TItem : class; 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/IEncoding.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public interface IEncoding : IEncoder, IDecoder { } 4 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/INatsConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace MyNatsClient 6 | { 7 | public interface INatsConnection : IDisposable 8 | { 9 | INatsServerInfo ServerInfo { get; } 10 | bool IsConnected { get; } 11 | 12 | IEnumerable ReadOps(); 13 | void WithWriteLock(Action a); 14 | void WithWriteLock(Action a, TArg arg); 15 | Task WithWriteLockAsync(Func a); 16 | Task WithWriteLockAsync(Func a, TArg arg); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/INatsConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace MyNatsClient 6 | { 7 | public interface INatsConnectionManager 8 | { 9 | /// 10 | /// Tries to establish a connection to any of the specified hosts in the 11 | /// sent . 12 | /// 13 | /// 14 | /// 15 | /// Connection and any received during the connection phase. 16 | Task<(INatsConnection connection, IList consumedOps)> OpenConnectionAsync(ConnectionInfo connectionInfo, CancellationToken cancellationToken); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/INatsObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | public interface INatsObservable : IObservable, IDisposable { } 6 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/INatsServerInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MyNatsClient 4 | { 5 | public interface INatsServerInfo 6 | { 7 | string ServerId { get; } 8 | string Version { get; } 9 | string Go { get; } 10 | string Host { get; } 11 | int Port { get; } 12 | bool AuthRequired { get; } 13 | bool TlsRequired { get; } 14 | bool TlsVerify { get; } 15 | int MaxPayload { get; } 16 | List ConnectUrls { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/INatsStreamWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient 5 | { 6 | public interface INatsStreamWriter 7 | { 8 | void Flush(); 9 | Task FlushAsync(); 10 | void Write(ReadOnlySpan data, bool flush); 11 | Task WriteAsync(ReadOnlyMemory data, bool flush); 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/IOp.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public interface IOp 4 | { 5 | string Marker { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/IPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | public interface IPublisher 6 | { 7 | void Pub(string subject, string body, string replyTo = null); 8 | void Pub(string subject, ReadOnlyMemory body, string replyTo = null); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/ISocketFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | 3 | namespace MyNatsClient 4 | { 5 | public interface ISocketFactory 6 | { 7 | Socket Create(SocketOptions options); 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/ISubscription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient 4 | { 5 | /// 6 | /// 7 | /// Represents a subscription against a NATS broker as well 8 | /// as an associated message handler against the in-process 9 | /// observable message stream. 10 | /// 11 | /// 12 | /// If you forget to explicitly dispose, the 13 | /// that created this subscribtion, will clean the subscription when it is disposed. 14 | /// 15 | public interface ISubscription : IDisposable 16 | { 17 | SubscriptionInfo SubscriptionInfo { get; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/AsyncPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using MyNatsClient.Internals.Commands; 4 | 5 | namespace MyNatsClient.Internals 6 | { 7 | internal class AsyncPublisher : IAsyncPublisher 8 | { 9 | private readonly INatsStreamWriter _writer; 10 | private readonly int _maxPayload; 11 | 12 | internal AsyncPublisher(INatsStreamWriter writer, int maxPayload) 13 | { 14 | _writer = writer; 15 | _maxPayload = maxPayload; 16 | } 17 | 18 | public Task PubAsync(string subject, string body, string replyTo = null) 19 | { 20 | var payload = NatsEncoder.GetBytes(body); 21 | if (payload.Length > _maxPayload) 22 | throw NatsException.ExceededMaxPayload(_maxPayload, payload.Length); 23 | 24 | return PubCmd.WriteAsync(_writer, subject.AsMemory(), replyTo.AsMemory(), payload); 25 | } 26 | 27 | public Task PubAsync(string subject, ReadOnlyMemory body, string replyTo = null) 28 | { 29 | if (body.Length > _maxPayload) 30 | throw NatsException.ExceededMaxPayload(_maxPayload, body.Length); 31 | 32 | return PubCmd.WriteAsync(_writer, subject.AsMemory(), replyTo.AsMemory(), body); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Commands/ConnectCmd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace MyNatsClient.Internals.Commands 5 | { 6 | internal static class ConnectCmd 7 | { 8 | internal static ReadOnlySpan Generate(bool verbose, Credentials credentials, string name) 9 | { 10 | var opString = GenerateConnectionOpString( 11 | verbose, 12 | credentials ?? Credentials.Empty, 13 | name); 14 | 15 | return NatsEncoder.GetBytes(opString).Span; 16 | } 17 | 18 | private static string GenerateConnectionOpString(bool verbose, Credentials credentials, string name) 19 | { 20 | var sb = new StringBuilder(); 21 | sb.Append("CONNECT {\"name\":\""); 22 | sb.Append(name); 23 | sb.Append("\",\"lang\":\"csharp\",\"protocol\":1,\"pedantic\":false,\"verbose\":"); 24 | sb.Append(verbose ? "true" : "false"); 25 | 26 | if (credentials != Credentials.Empty) 27 | { 28 | sb.Append(",\"user\":\""); 29 | sb.Append(credentials.User); 30 | sb.Append("\",\"pass\":\""); 31 | sb.Append(credentials.Pass); 32 | sb.Append("\""); 33 | } 34 | sb.Append("}"); 35 | sb.Append(NatsEncoder.Crlf); 36 | 37 | return sb.ToString(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Commands/PingCmd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient.Internals.Commands 5 | { 6 | internal static class PingCmd 7 | { 8 | internal static readonly ReadOnlyMemory Bytes = NatsEncoder.GetBytes($"PING{NatsEncoder.Crlf}"); 9 | 10 | internal static void Write(INatsStreamWriter writer) 11 | => writer.Write(Bytes.Span, true); 12 | 13 | internal static Task WriteAsync(INatsStreamWriter writer) 14 | => writer.WriteAsync(Bytes, true); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Commands/PongCmd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient.Internals.Commands 5 | { 6 | internal static class PongCmd 7 | { 8 | private static readonly ReadOnlyMemory Bytes = NatsEncoder.GetBytes($"PONG{NatsEncoder.Crlf}"); 9 | 10 | internal static void Write(INatsStreamWriter writer) 11 | => writer.Write(Bytes.Span, true); 12 | 13 | internal static Task WriteAsync(INatsStreamWriter writer) 14 | => writer.WriteAsync(Bytes, true); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Commands/PubCmd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient.Internals.Commands 5 | { 6 | internal static class PubCmd 7 | { 8 | private static readonly byte[] Cmd = { (byte)'P', (byte)'U', (byte)'B' }; 9 | 10 | internal static void Write(INatsStreamWriter writer, ReadOnlySpan subject, ReadOnlySpan replyTo, ReadOnlyMemory body) 11 | { 12 | var bodySize = body.Length.ToString().AsSpan(); 13 | var preBodySize = 3 + 1 + subject.Length + 1 + (replyTo.IsEmpty ? 0 : replyTo.Length + 1) + bodySize.Length + NatsEncoder.CrlfBytesLen; 14 | var preBody = new Span(new byte[preBodySize]); 15 | 16 | FillPreBody(preBody, subject, replyTo, bodySize); 17 | 18 | writer.Write(preBody, false); 19 | writer.Write(body.Span, false); 20 | writer.Write(NatsEncoder.CrlfBytes, false); 21 | } 22 | 23 | internal static async Task WriteAsync(INatsStreamWriter writer, ReadOnlyMemory subject, ReadOnlyMemory replyTo, ReadOnlyMemory body) 24 | { 25 | var bodySize = body.Length.ToString().AsMemory(); 26 | var preBodySize = 3 + 1 + subject.Length + 1 + (replyTo.Length > 0 ? replyTo.Length + 1 : 0) + bodySize.Length + NatsEncoder.CrlfBytesLen; 27 | var preBody = new Memory(new byte[preBodySize]); 28 | 29 | FillPreBody(preBody.Span, subject.Span, replyTo.Span, bodySize.Span); 30 | 31 | await writer.WriteAsync(preBody, false).ConfigureAwait(false); 32 | await writer.WriteAsync(body, false).ConfigureAwait(false); 33 | await writer.WriteAsync(NatsEncoder.CrlfBytes, false).ConfigureAwait(false); 34 | } 35 | 36 | private static void FillPreBody(Span trg, ReadOnlySpan subject, ReadOnlySpan replyTo, ReadOnlySpan bodySize) 37 | { 38 | trg[0] = Cmd[0]; 39 | trg[1] = Cmd[1]; 40 | trg[2] = Cmd[2]; 41 | trg[3] = NatsEncoder.SpaceByte; 42 | 43 | var nextSlot = 4; 44 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, subject); 45 | trg[nextSlot++] = NatsEncoder.SpaceByte; 46 | 47 | if (!replyTo.IsEmpty) 48 | { 49 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, replyTo); 50 | trg[nextSlot++] = NatsEncoder.SpaceByte; 51 | } 52 | 53 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, bodySize); 54 | 55 | NatsEncoder.WriteCrlf(trg, nextSlot); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Commands/SubCmd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient.Internals.Commands 5 | { 6 | internal static class SubCmd 7 | { 8 | private static readonly byte[] Cmd = { (byte)'S', (byte)'U', (byte)'B' }; 9 | 10 | internal static void Write(INatsStreamWriter writer, ReadOnlySpan subject, ReadOnlySpan subscriptionId, ReadOnlySpan queueGroup) 11 | { 12 | var trg = new Span(new byte[3 + 1 + subject.Length + 1 + (queueGroup.IsEmpty ? 0 : queueGroup.Length + 1) + subscriptionId.Length + NatsEncoder.CrlfBytesLen]); 13 | 14 | Fill(trg, subject, subscriptionId, queueGroup); 15 | 16 | writer.Write(trg, false); 17 | } 18 | 19 | internal static async Task WriteAsync(INatsStreamWriter writer, ReadOnlyMemory subject, ReadOnlyMemory subscriptionId, ReadOnlyMemory queueGroup) 20 | { 21 | var trg = new Memory(new byte[3 + 1 + subject.Length + 1 + (queueGroup.IsEmpty ? 0 : queueGroup.Length + 1) + subscriptionId.Length + NatsEncoder.CrlfBytesLen]); 22 | 23 | Fill(trg.Span, subject.Span, subscriptionId.Span, queueGroup.Span); 24 | 25 | await writer.WriteAsync(trg, false).ConfigureAwait(false); 26 | } 27 | 28 | private static void Fill(Span trg, ReadOnlySpan subject, ReadOnlySpan subscriptionId, ReadOnlySpan queueGroup) 29 | { 30 | trg[0] = Cmd[0]; 31 | trg[1] = Cmd[1]; 32 | trg[2] = Cmd[2]; 33 | trg[3] = NatsEncoder.SpaceByte; 34 | 35 | var nextSlot = 4; 36 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, subject); 37 | trg[nextSlot++] = NatsEncoder.SpaceByte; 38 | 39 | if (!queueGroup.IsEmpty) 40 | { 41 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, queueGroup); 42 | trg[nextSlot++] = NatsEncoder.SpaceByte; 43 | } 44 | 45 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, subscriptionId); 46 | NatsEncoder.WriteCrlf(trg, nextSlot); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Commands/UnSubCmd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace MyNatsClient.Internals.Commands 5 | { 6 | internal static class UnsubCmd 7 | { 8 | private static readonly byte[] Cmd = { (byte)'U', (byte)'N', (byte)'S', (byte)'U', (byte)'B' }; 9 | 10 | internal static void Write(INatsStreamWriter writer, ReadOnlySpan subscriptionId, int? maxMessages = null) 11 | { 12 | var maxMessagesString = maxMessages.ToString().AsSpan(); 13 | var trg = new Span(new byte[5 + 1 + subscriptionId.Length + (maxMessagesString.IsEmpty ? 0 : maxMessagesString.Length + 1) + NatsEncoder.CrlfBytesLen]); 14 | 15 | Fill(trg, subscriptionId, maxMessagesString); 16 | 17 | writer.Write(trg, false); 18 | } 19 | 20 | internal static async Task WriteAsync(INatsStreamWriter writer, ReadOnlyMemory subscriptionId, int? maxMessages = null) 21 | { 22 | var maxMessagesString = maxMessages.ToString().AsMemory(); 23 | var trg = new Memory(new byte[5 + 1 + subscriptionId.Length + (maxMessagesString.IsEmpty ? 0 : maxMessagesString.Length + 1) + NatsEncoder.CrlfBytesLen]); 24 | 25 | Fill(trg.Span, subscriptionId.Span, maxMessagesString.Span); 26 | 27 | await writer.WriteAsync(trg, false).ConfigureAwait(false); 28 | } 29 | 30 | private static void Fill(Span trg, ReadOnlySpan subscriptionId, ReadOnlySpan maxMessagesString) 31 | { 32 | trg[0] = Cmd[0]; 33 | trg[1] = Cmd[1]; 34 | trg[2] = Cmd[2]; 35 | trg[3] = Cmd[3]; 36 | trg[4] = Cmd[4]; 37 | trg[5] = NatsEncoder.SpaceByte; 38 | 39 | var nextSlot = 6; 40 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, subscriptionId); 41 | 42 | if (!maxMessagesString.IsEmpty) 43 | { 44 | trg[nextSlot++] = NatsEncoder.SpaceByte; 45 | nextSlot = NatsEncoder.WriteSingleByteChars(trg, nextSlot, maxMessagesString); 46 | } 47 | 48 | NatsEncoder.WriteCrlf(trg, nextSlot); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/DefaultTaskSchedulerConsumerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace MyNatsClient.Internals 6 | { 7 | internal class DefaultTaskSchedulerConsumerFactory : IConsumerFactory 8 | { 9 | public Task Run(Action consumer, CancellationToken cancellationToken) 10 | => Task.Factory.StartNew( 11 | consumer, 12 | cancellationToken, 13 | TaskCreationOptions.LongRunning, 14 | TaskScheduler.Default); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/DelegatingObserver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal class DelegatingObserver : IObserver 6 | { 7 | private readonly Action _onNext; 8 | private readonly Action _onError; 9 | private readonly Action _onCompleted; 10 | 11 | internal DelegatingObserver( 12 | Action onNext, 13 | Action onError = null, 14 | Action onCompleted = null) 15 | { 16 | _onNext = onNext ?? throw new ArgumentNullException(nameof(onNext)); 17 | _onError = onError; 18 | _onCompleted = onCompleted; 19 | } 20 | 21 | public void OnNext(T value) 22 | => _onNext(value); 23 | 24 | public void OnError(Exception error) 25 | => _onError?.Invoke(error); 26 | 27 | public void OnCompleted() 28 | => _onCompleted?.Invoke(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Disposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal static class Disposable 6 | { 7 | internal static IDisposable Empty { get; } 8 | 9 | static Disposable() 10 | { 11 | Empty = new Impl(); 12 | } 13 | 14 | private class Impl : IDisposable 15 | { 16 | public void Dispose() { } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Extensions/ArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MyNatsClient.Internals.Extensions 5 | { 6 | internal static class ArrayExtensions 7 | { 8 | private static readonly Random Rnd = new Random(); 9 | 10 | internal static T[] GetRandomized(this T[] src) 11 | { 12 | var result = new T[src.Length]; 13 | var range = new List(src); 14 | 15 | for (var i = 0; i < src.Length; i++) 16 | { 17 | var rndIndex = Rnd.Next(0, range.Count - 1); 18 | result[i] = range[rndIndex]; 19 | range.RemoveAt(rndIndex); 20 | } 21 | 22 | return result; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Extensions/SocketExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace MyNatsClient.Internals.Extensions 8 | { 9 | internal static class SocketExtensions 10 | { 11 | internal static async Task ConnectAsync(this Socket socket, Host host, int timeoutMs, CancellationToken cancellationToken) 12 | { 13 | var endPoint = new DnsEndPoint(host.Address, host.Port); 14 | 15 | var connectTask = socket.ConnectAsync(endPoint); 16 | 17 | await Task.WhenAny(Task.Delay(timeoutMs, cancellationToken), connectTask).ConfigureAwait(false); 18 | 19 | //var connectedOk = socket.Connected && connectTask.IsCompletedSuccessfully; 20 | //if (connectedOk) 21 | // return; 22 | 23 | if (socket.Connected) 24 | return; 25 | 26 | socket.Close(); 27 | 28 | throw NatsException.FailedToConnectToHost( 29 | host, $"Socket could not connect against {host}, within specified timeout {timeoutMs.ToString()}ms."); 30 | } 31 | 32 | internal static NetworkStream CreateReadStream(this Socket socket) 33 | { 34 | var ns = new NetworkStream(socket, FileAccess.Read, false); 35 | 36 | if (socket.ReceiveTimeout > 0) 37 | ns.ReadTimeout = socket.ReceiveTimeout; 38 | 39 | return ns; 40 | } 41 | 42 | internal static NetworkStream CreateWriteStream(this Socket socket) 43 | { 44 | var ns = new NetworkStream(socket, FileAccess.Write, false); 45 | 46 | if (socket.SendTimeout > 0) 47 | ns.WriteTimeout = socket.SendTimeout; 48 | 49 | return ns; 50 | } 51 | 52 | internal static NetworkStream CreateReadWriteStream(this Socket socket) 53 | { 54 | var ns = new NetworkStream(socket, FileAccess.ReadWrite, false); 55 | 56 | if (socket.SendTimeout > 0) 57 | ns.WriteTimeout = socket.SendTimeout; 58 | 59 | if (socket.ReceiveTimeout > 0) 60 | ns.ReadTimeout = socket.ReceiveTimeout; 61 | 62 | return ns; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/NatsConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace MyNatsClient.Internals 11 | { 12 | internal sealed class NatsConnection : INatsConnection 13 | { 14 | private readonly ILogger _logger = LoggerManager.CreateLogger(); 15 | private readonly CancellationToken _cancellationToken; 16 | 17 | private Socket _socket; 18 | private Stream _stream; 19 | private BufferedStream _writeStream; 20 | private BufferedStream _readStream; 21 | private SemaphoreSlim _writeStreamSync; 22 | private NatsOpStreamReader _reader; 23 | private NatsStreamWriter _writer; 24 | private bool _isDisposed; 25 | 26 | public INatsServerInfo ServerInfo { get; } 27 | public bool IsConnected => _socket.Connected; 28 | 29 | private bool CanRead => 30 | _socket.Connected && 31 | _stream.CanRead && 32 | !_cancellationToken.IsCancellationRequested; 33 | 34 | internal NatsConnection( 35 | NatsServerInfo serverInfo, 36 | Socket socket, 37 | Stream stream, 38 | CancellationToken cancellationToken) 39 | { 40 | ServerInfo = serverInfo ?? throw new ArgumentNullException(nameof(serverInfo)); 41 | 42 | _socket = socket ?? throw new ArgumentNullException(nameof(socket)); 43 | if (!socket.Connected) 44 | throw new ArgumentException("Socket is not connected.", nameof(socket)); 45 | 46 | _stream = stream ?? throw new ArgumentNullException(nameof(stream)); 47 | _writeStream = new BufferedStream(_stream, socket.SendBufferSize); 48 | _readStream = new BufferedStream(_stream, socket.ReceiveBufferSize); 49 | _cancellationToken = cancellationToken; 50 | _writeStreamSync = new SemaphoreSlim(1, 1); 51 | _writer = new NatsStreamWriter(_writeStream, _cancellationToken); 52 | _reader = NatsOpStreamReader.Use(_readStream); 53 | } 54 | 55 | public void Dispose() 56 | { 57 | ThrowIfDisposed(); 58 | 59 | _isDisposed = true; 60 | 61 | var exs = new List(); 62 | 63 | void TryDispose(IDisposable disposable) 64 | { 65 | try 66 | { 67 | disposable.Dispose(); 68 | } 69 | catch (Exception ex) 70 | { 71 | exs.Add(ex); 72 | } 73 | } 74 | 75 | TryDispose(_reader); 76 | TryDispose(_writeStream); 77 | TryDispose(_readStream); 78 | TryDispose(_stream); 79 | 80 | try 81 | { 82 | _socket.Shutdown(SocketShutdown.Both); 83 | } 84 | catch (Exception ex) 85 | { 86 | exs.Add(ex); 87 | } 88 | 89 | TryDispose(_socket); 90 | TryDispose(_writeStreamSync); 91 | 92 | _reader = null; 93 | _writeStream = null; 94 | _readStream = null; 95 | _stream = null; 96 | _socket = null; 97 | _writeStreamSync = null; 98 | _reader = null; 99 | _writer = null; 100 | 101 | if (exs.Any()) 102 | throw new AggregateException("Failed while disposing connection. See inner exception(s) for more details.", exs); 103 | } 104 | 105 | private void ThrowIfDisposed() 106 | { 107 | if (_isDisposed) 108 | throw new ObjectDisposedException(GetType().Name); 109 | } 110 | 111 | private void ThrowIfNotConnected() 112 | { 113 | if (!IsConnected) 114 | throw NatsException.NotConnected(); 115 | } 116 | 117 | public IEnumerable ReadOps() 118 | { 119 | ThrowIfDisposed(); 120 | 121 | ThrowIfNotConnected(); 122 | 123 | _logger.LogDebug("Starting OPs read loop"); 124 | 125 | while (CanRead) 126 | { 127 | #if Debug 128 | _logger.LogDebug("Reading OP"); 129 | #endif 130 | yield return _reader.ReadOp(); 131 | } 132 | } 133 | 134 | public void WithWriteLock(Action a) 135 | { 136 | ThrowIfDisposed(); 137 | 138 | ThrowIfNotConnected(); 139 | 140 | _writeStreamSync.Wait(_cancellationToken); 141 | 142 | try 143 | { 144 | a(_writer); 145 | } 146 | finally 147 | { 148 | _writeStreamSync.Release(); 149 | } 150 | } 151 | 152 | public void WithWriteLock(Action a, TArg arg) 153 | { 154 | ThrowIfDisposed(); 155 | 156 | ThrowIfNotConnected(); 157 | 158 | _writeStreamSync.Wait(_cancellationToken); 159 | 160 | try 161 | { 162 | a(_writer, arg); 163 | } 164 | finally 165 | { 166 | _writeStreamSync.Release(); 167 | } 168 | } 169 | 170 | public async Task WithWriteLockAsync(Func a) 171 | { 172 | ThrowIfDisposed(); 173 | 174 | ThrowIfNotConnected(); 175 | 176 | await _writeStreamSync.WaitAsync(_cancellationToken).ConfigureAwait(false); 177 | 178 | try 179 | { 180 | await a(_writer).ConfigureAwait(false); 181 | } 182 | finally 183 | { 184 | _writeStreamSync.Release(); 185 | } 186 | } 187 | 188 | public async Task WithWriteLockAsync(Func a, TArg arg) 189 | { 190 | ThrowIfDisposed(); 191 | 192 | ThrowIfNotConnected(); 193 | 194 | await _writeStreamSync.WaitAsync(_cancellationToken).ConfigureAwait(false); 195 | 196 | try 197 | { 198 | await a(_writer, arg).ConfigureAwait(false); 199 | } 200 | finally 201 | { 202 | _writeStreamSync.Release(); 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/NatsEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace MyNatsClient.Internals 5 | { 6 | internal static class NatsEncoder 7 | { 8 | private static readonly Encoding Encoding = Encoding.UTF8; 9 | 10 | internal const string Crlf = "\r\n"; 11 | internal static readonly byte[] CrlfBytes = { (byte)'\r', (byte)'\n' }; 12 | internal static readonly int CrlfBytesLen = CrlfBytes.Length; 13 | internal static readonly byte SpaceByte = (byte)' '; 14 | 15 | internal static ReadOnlyMemory GetBytes(ReadOnlySpan src) 16 | => Encoding.GetBytes(src.ToArray()); 17 | 18 | internal static string GetString(ReadOnlySpan src) 19 | => Encoding.GetString(src); 20 | 21 | internal static string GetSingleByteCharString(ReadOnlySpan src) 22 | => string.Create(src.Length, src.ToArray(), (trg, v) => 23 | { 24 | for (var i = 0; i < v.Length; i++) 25 | trg[i] = (char)v[i]; 26 | }); 27 | 28 | internal static int WriteCrlf(Span trg, int trgOffset) 29 | { 30 | trg[trgOffset++] = CrlfBytes[0]; 31 | trg[trgOffset++] = CrlfBytes[1]; 32 | 33 | return trgOffset; 34 | } 35 | 36 | internal static int WriteSingleByteChars(Span trg, int trgOffset, ReadOnlySpan src) 37 | { 38 | for (var i = 0; i < src.Length; i++) 39 | trg[trgOffset++] = (byte)src[i]; 40 | 41 | return trgOffset; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/NatsServerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | 6 | namespace MyNatsClient.Internals 7 | { 8 | public class NatsServerInfo : INatsServerInfo 9 | { 10 | public string ServerId { get; private set; } 11 | public string Version { get; private set; } 12 | public string Go { get; private set; } 13 | public string Host { get; private set; } 14 | public int Port { get; private set; } 15 | public bool AuthRequired { get; private set; } 16 | public bool TlsRequired { get; private set; } 17 | public bool TlsVerify { get; private set; } 18 | public int MaxPayload { get; private set; } 19 | public bool Headers { get; set; } 20 | public List ConnectUrls { get; } = new List(); 21 | public string Ip { get; set; } 22 | 23 | private NatsServerInfo() { } 24 | 25 | public static NatsServerInfo Parse(ReadOnlyMemory data) 26 | { 27 | var result = new NatsServerInfo(); 28 | 29 | using var doc = JsonDocument.Parse(data, new JsonDocumentOptions { AllowTrailingCommas = true }); 30 | 31 | var root = doc.RootElement; 32 | 33 | if (root.TryGetProperty("server_id", out var el)) 34 | result.ServerId = el.GetString(); 35 | 36 | if (root.TryGetProperty("version", out el)) 37 | result.Version = el.GetString(); 38 | 39 | if (root.TryGetProperty("go", out el)) 40 | result.Go = el.GetString(); 41 | 42 | if (root.TryGetProperty("host", out el)) 43 | result.Host = el.GetString(); 44 | 45 | if (root.TryGetProperty("port", out el)) 46 | result.Port = el.GetInt32(); 47 | 48 | if (root.TryGetProperty("auth_required", out el)) 49 | result.AuthRequired = el.GetBoolean(); 50 | 51 | if (root.TryGetProperty("tls_required", out el)) 52 | result.TlsRequired = el.GetBoolean(); 53 | 54 | if (root.TryGetProperty("tls_verify", out el)) 55 | result.TlsVerify = el.GetBoolean(); 56 | 57 | if (root.TryGetProperty("headers", out el)) 58 | result.Headers = el.GetBoolean(); 59 | 60 | if (root.TryGetProperty("max_payload", out el)) 61 | result.MaxPayload = el.GetInt32(); 62 | 63 | if (root.TryGetProperty("connect_urls", out el)) 64 | result.ConnectUrls.AddRange(el.EnumerateArray().Select(i => i.GetString())); 65 | 66 | if (root.TryGetProperty("ip", out el)) 67 | result.Ip = el.GetString(); 68 | 69 | return result; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/NatsStreamWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace MyNatsClient.Internals 7 | { 8 | internal class NatsStreamWriter : INatsStreamWriter 9 | { 10 | private readonly Stream _stream; 11 | private readonly CancellationToken _cancellationToken; 12 | 13 | internal NatsStreamWriter(Stream stream, CancellationToken cancellationToken) 14 | { 15 | _stream = stream; 16 | _cancellationToken = cancellationToken; 17 | } 18 | 19 | public void Flush() 20 | => _stream.Flush(); 21 | 22 | public async Task FlushAsync() 23 | => await _stream.FlushAsync(_cancellationToken).ConfigureAwait(false); 24 | 25 | public void Write(ReadOnlySpan data, bool flush) 26 | { 27 | _stream.Write(data); 28 | if(flush) 29 | _stream.Flush(); 30 | } 31 | 32 | public async Task WriteAsync(ReadOnlyMemory data, bool flush) 33 | { 34 | await _stream.WriteAsync(data, _cancellationToken).ConfigureAwait(false); 35 | if(flush) 36 | await _stream.FlushAsync(_cancellationToken).ConfigureAwait(false); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Observables.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Ops; 3 | using MyNatsClient.Rx; 4 | 5 | namespace MyNatsClient.Internals 6 | { 7 | internal sealed class OfTypeObservable : INatsObservable where TResult : class 8 | { 9 | private readonly INatsObservable _src; 10 | 11 | public OfTypeObservable(INatsObservable src) 12 | => _src = src ?? throw new ArgumentNullException(nameof(src)); 13 | 14 | public void Dispose() => _src.Dispose(); 15 | 16 | public IDisposable Subscribe(IObserver observer) 17 | => _src.SubscribeSafe(new OfTypeObserver(observer)); 18 | 19 | private sealed class OfTypeObserver : IObserver 20 | { 21 | private readonly IObserver _observer; 22 | 23 | public OfTypeObserver(IObserver observer) 24 | => _observer = observer; 25 | 26 | public void OnNext(object value) 27 | { 28 | if (value is TResult result) 29 | _observer.OnNext(result); 30 | } 31 | 32 | public void OnError(Exception error) 33 | => _observer.OnError(error); 34 | 35 | public void OnCompleted() 36 | => _observer.OnCompleted(); 37 | } 38 | } 39 | 40 | internal sealed class CastObservable : INatsObservable where TFrom : class where TTo : class 41 | { 42 | private readonly INatsObservable _src; 43 | 44 | public CastObservable(INatsObservable src) 45 | => _src = src ?? throw new ArgumentNullException(nameof(src)); 46 | 47 | public void Dispose() => _src.Dispose(); 48 | 49 | public IDisposable Subscribe(IObserver observer) 50 | => _src.SubscribeSafe(new CastObserver(observer)); 51 | 52 | private sealed class CastObserver : IObserver 53 | { 54 | private readonly IObserver _observer; 55 | 56 | public CastObserver(IObserver observer) 57 | => _observer = observer; 58 | 59 | public void OnNext(TFrom value) 60 | => _observer.OnNext(value as TTo); 61 | 62 | public void OnError(Exception error) 63 | => _observer.OnError(error); 64 | 65 | public void OnCompleted() 66 | => _observer.OnCompleted(); 67 | } 68 | } 69 | 70 | internal sealed class WhereObservable : INatsObservable where T : class 71 | { 72 | private readonly INatsObservable _src; 73 | private readonly Func _predicate; 74 | 75 | public WhereObservable(INatsObservable src, Func predicate) 76 | { 77 | _src = src ?? throw new ArgumentNullException(nameof(src)); 78 | _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); 79 | } 80 | 81 | public void Dispose() => _src.Dispose(); 82 | 83 | public IDisposable Subscribe(IObserver observer) 84 | => _src.SubscribeSafe(new WhereObserver(observer, _predicate)); 85 | 86 | private sealed class WhereObserver : IObserver 87 | { 88 | private readonly IObserver _observer; 89 | private readonly Func _predicate; 90 | 91 | public WhereObserver(IObserver observer, Func predicate) 92 | { 93 | _observer = observer ?? throw new ArgumentNullException(nameof(observer)); 94 | _predicate = predicate; 95 | } 96 | 97 | public void OnNext(T value) 98 | { 99 | if (_predicate(value)) 100 | _observer.OnNext(value); 101 | } 102 | 103 | public void OnError(Exception error) 104 | => _observer.OnError(error); 105 | 106 | public void OnCompleted() 107 | => _observer.OnCompleted(); 108 | } 109 | } 110 | 111 | internal sealed class CatchObservable : INatsObservable 112 | where T : class 113 | where TException : Exception 114 | { 115 | private readonly INatsObservable _src; 116 | private readonly Action _handler; 117 | 118 | public CatchObservable(INatsObservable src, Action handler) 119 | { 120 | _src = src ?? throw new ArgumentNullException(nameof(src)); 121 | _handler = handler ?? throw new ArgumentNullException(nameof(handler)); 122 | } 123 | 124 | public void Dispose() => _src.Dispose(); 125 | 126 | public IDisposable Subscribe(IObserver observer) 127 | => _src.SubscribeSafe(new CatchObserver(observer, _handler)); 128 | 129 | private sealed class CatchObserver : IObserver 130 | { 131 | private readonly IObserver _observer; 132 | private readonly Action _handler; 133 | 134 | public CatchObserver(IObserver observer, Action handler) 135 | { 136 | _observer = observer ?? throw new ArgumentNullException(nameof(observer)); 137 | _handler = handler; 138 | } 139 | 140 | public void OnNext(T value) 141 | { 142 | try 143 | { 144 | _observer.OnNext(value); 145 | } 146 | catch (TException ex) 147 | { 148 | _handler(ex); 149 | } 150 | } 151 | 152 | public void OnError(Exception error) 153 | => _observer.OnError(error); 154 | 155 | public void OnCompleted() 156 | => _observer.OnCompleted(); 157 | } 158 | } 159 | 160 | internal sealed class WhereSubjectMatchObservable : INatsObservable 161 | { 162 | private readonly INatsObservable _src; 163 | private readonly string _subject; 164 | 165 | public WhereSubjectMatchObservable(INatsObservable src, string subject) 166 | { 167 | _src = src ?? throw new ArgumentNullException(nameof(src)); 168 | _subject = subject ?? throw new ArgumentNullException(nameof(subject)); 169 | } 170 | 171 | public void Dispose() => _src.Dispose(); 172 | 173 | public IDisposable Subscribe(IObserver observer) 174 | => _src.SubscribeSafe(new WhereSubjectMatchObserver(observer, _subject)); 175 | 176 | private sealed class WhereSubjectMatchObserver : IObserver 177 | { 178 | private readonly IObserver _observer; 179 | private readonly string _subject; 180 | 181 | public WhereSubjectMatchObserver(IObserver observer, string subject) 182 | { 183 | _observer = observer ?? throw new ArgumentNullException(nameof(observer)); 184 | _subject = subject; 185 | } 186 | 187 | public void OnNext(MsgOp value) 188 | { 189 | //TODO: Wildcards etc 190 | if (_subject.Equals(value.Subject, StringComparison.Ordinal)) 191 | _observer.OnNext(value); 192 | } 193 | 194 | public void OnError(Exception error) 195 | => _observer.OnError(error); 196 | 197 | public void OnCompleted() 198 | => _observer.OnCompleted(); 199 | } 200 | } 201 | 202 | internal sealed class SelectObservable : INatsObservable where TFrom : class where TTo : class 203 | { 204 | private readonly INatsObservable _src; 205 | private readonly Func _map; 206 | 207 | public SelectObservable(INatsObservable src, Func map) 208 | { 209 | _src = src ?? throw new ArgumentNullException(nameof(src)); 210 | _map = map ?? throw new ArgumentNullException(nameof(src)); 211 | } 212 | 213 | public void Dispose() => _src.Dispose(); 214 | 215 | public IDisposable Subscribe(IObserver observer) 216 | => _src.SubscribeSafe(new SelectObserver(observer, _map)); 217 | 218 | private sealed class SelectObserver : IObserver 219 | { 220 | private readonly IObserver _observer; 221 | private readonly Func _map; 222 | 223 | public SelectObserver(IObserver observer, Func map) 224 | { 225 | _observer = observer ?? throw new ArgumentNullException(nameof(observer)); 226 | _map = map; 227 | } 228 | 229 | public void OnNext(TFrom value) 230 | => _observer.OnNext(_map(value)); 231 | 232 | public void OnError(Exception error) 233 | => _observer.OnError(error); 234 | 235 | public void OnCompleted() 236 | => _observer.OnCompleted(); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Publisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using MyNatsClient.Internals.Commands; 4 | 5 | namespace MyNatsClient.Internals 6 | { 7 | internal class Publisher : IPublisher 8 | { 9 | private readonly INatsStreamWriter _writer; 10 | private readonly int _maxPayload; 11 | 12 | internal Publisher(INatsStreamWriter writer, int maxPayload) 13 | { 14 | _writer = writer; 15 | _maxPayload = maxPayload; 16 | } 17 | 18 | public void Pub(string subject, string body, string replyTo = null) 19 | { 20 | var payload = NatsEncoder.GetBytes(body); 21 | if (payload.Length > _maxPayload) 22 | throw NatsException.ExceededMaxPayload(_maxPayload, payload.Length); 23 | 24 | PubCmd.Write(_writer, subject, replyTo, payload); 25 | } 26 | 27 | public void Pub(string subject, ReadOnlyMemory body, string replyTo = null) 28 | { 29 | if (body.Length > _maxPayload) 30 | throw NatsException.ExceededMaxPayload(_maxPayload, body.Length); 31 | 32 | PubCmd.Write(_writer, subject, replyTo, body); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/SafeObserver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal class SafeObserver : IObserver 6 | { 7 | private readonly Action _onNext; 8 | private readonly Action _onError; 9 | private readonly Action _onCompleted; 10 | 11 | internal SafeObserver( 12 | Action onNext, 13 | Action onError = null, 14 | Action onCompleted = null) 15 | { 16 | _onNext = onNext ?? throw new ArgumentNullException(nameof(onNext)); 17 | _onError = onError; 18 | _onCompleted = onCompleted; 19 | } 20 | 21 | public void OnNext(T value) 22 | { 23 | try 24 | { 25 | _onNext(value); 26 | } 27 | catch 28 | { 29 | // ignored 30 | } 31 | } 32 | 33 | public void OnError(Exception error) 34 | => _onError?.Invoke(error); 35 | 36 | public void OnCompleted() 37 | => _onCompleted?.Invoke(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/SocketFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal class SocketFactory : ISocketFactory 6 | { 7 | private static AddressFamily GetAddressFamily(SocketAddressType addressType) 8 | => addressType == SocketAddressType.IpV6 9 | ? AddressFamily.InterNetworkV6 10 | : AddressFamily.InterNetwork; 11 | 12 | public Socket Create(SocketOptions options) 13 | { 14 | var socket = !options.AddressType.HasValue 15 | ? new Socket(SocketType.Stream, ProtocolType.Tcp) 16 | : new Socket(GetAddressFamily(options.AddressType.Value), SocketType.Stream, ProtocolType.Tcp); 17 | 18 | if (options.UseNagleAlgorithm.HasValue) 19 | socket.NoDelay = !options.UseNagleAlgorithm.Value; 20 | 21 | if (options.ReceiveBufferSize.HasValue) 22 | socket.ReceiveBufferSize = options.ReceiveBufferSize.Value; 23 | 24 | if (options.SendBufferSize.HasValue) 25 | socket.SendBufferSize = options.SendBufferSize.Value; 26 | 27 | if (options.ReceiveTimeoutMs.HasValue) 28 | socket.ReceiveTimeout = options.ReceiveTimeoutMs.Value; 29 | 30 | if (options.SendTimeoutMs.HasValue) 31 | socket.SendTimeout = options.SendTimeoutMs.Value; 32 | 33 | return socket; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Subscription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal sealed class Subscription : ISubscription 6 | { 7 | private const string DisposeExMessage = "Failed while disposing subscription. See inner exception(s) for more details."; 8 | 9 | private IDisposable _subscription; 10 | private bool _isDisposed; 11 | private Action _onDisposing; 12 | 13 | public SubscriptionInfo SubscriptionInfo { get; } 14 | 15 | private Subscription( 16 | SubscriptionInfo subscriptionInfo, 17 | IDisposable subscription, 18 | Action onDisposing) 19 | { 20 | SubscriptionInfo = subscriptionInfo ?? throw new ArgumentNullException(nameof(subscriptionInfo)); 21 | _subscription = subscription ?? throw new ArgumentNullException(nameof(subscription)); 22 | _onDisposing = onDisposing ?? throw new ArgumentNullException(nameof(onDisposing)); 23 | } 24 | 25 | internal static Subscription Create( 26 | SubscriptionInfo subscriptionInfo, 27 | IDisposable subscription, 28 | Action onDisposing) 29 | { 30 | return new Subscription( 31 | subscriptionInfo, 32 | subscription, 33 | onDisposing); 34 | } 35 | 36 | public void Dispose() 37 | { 38 | if(_isDisposed) 39 | return; 40 | 41 | _isDisposed = true; 42 | 43 | Exception ex1 = null, ex2 = null; 44 | 45 | try 46 | { 47 | _subscription.Dispose(); 48 | } 49 | catch (Exception ex) 50 | { 51 | ex1 = ex; 52 | } 53 | 54 | try 55 | { 56 | _onDisposing.Invoke(SubscriptionInfo); 57 | } 58 | catch (Exception ex) 59 | { 60 | ex2 = ex; 61 | } 62 | 63 | _subscription = null; 64 | _onDisposing = null; 65 | 66 | if(ex1 == null && ex2 == null) 67 | return; 68 | 69 | if(ex1 != null && ex2 != null) 70 | throw new AggregateException(DisposeExMessage, ex1, ex2); 71 | 72 | if(ex1 != null) 73 | throw new AggregateException(DisposeExMessage, ex1); 74 | 75 | if(ex2 != null) 76 | throw new AggregateException(DisposeExMessage, ex2); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/Swallow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal static class Swallow 6 | { 7 | internal static void Everything(params Action[] actions) 8 | { 9 | if (actions == null) 10 | return; 11 | 12 | // ReSharper disable once ForCanBeConvertedToForeach 13 | for (var i = 0; i < actions.Length; i++) 14 | try 15 | { 16 | actions[i](); 17 | } 18 | catch 19 | { 20 | // ignored 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Internals/UniqueId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Internals 4 | { 5 | internal static class UniqueId 6 | { 7 | internal static string Generate() => Guid.NewGuid().ToString("N"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/LoggerManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | 5 | namespace MyNatsClient 6 | { 7 | public static class LoggerManager 8 | { 9 | private static ILoggerFactory _factory = NullLoggerFactory.Instance; 10 | 11 | public static ILogger CreateLogger(Type type) 12 | => _factory.CreateLogger(type); 13 | 14 | public static ILogger CreateLogger() 15 | => _factory.CreateLogger(); 16 | 17 | public static void UseFactory(ILoggerFactory factory) 18 | => _factory = factory; 19 | 20 | public static void ResetToDefaults() 21 | => UseFactory(NullLoggerFactory.Instance); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/MsgHeaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | 7 | namespace MyNatsClient 8 | { 9 | public interface IMsgHeaders : IReadOnlyDictionary> 10 | { 11 | string Protocol { get; } 12 | bool IsEmpty { get; } 13 | 14 | void Add(string key, string value); 15 | 16 | void Set(string key, string[] values); 17 | } 18 | 19 | public sealed class MsgHeaders : IMsgHeaders 20 | { 21 | private readonly ConcurrentDictionary> _state; 22 | 23 | public string Protocol => "NATS/1.0"; 24 | public IReadOnlyList this[string key] => _state[key]; 25 | public IEnumerable Keys => _state.Keys; 26 | public IEnumerable> Values => _state.Values; 27 | public int Count => _state.Count; 28 | public bool IsEmpty => _state.IsEmpty; 29 | 30 | private MsgHeaders() => _state = new ConcurrentDictionary>(); 31 | 32 | public static IMsgHeaders Create() => new MsgHeaders(); 33 | 34 | private static void EnsureKeyIsValid(string key) 35 | { 36 | if (string.IsNullOrEmpty(key)) 37 | throw new ArgumentException("Key is missing.", nameof(key)); 38 | 39 | foreach (var c in key) 40 | { 41 | if (c < 33 || c > 126 || c == ':' || c == '"' || c == '\'' || c == '?') 42 | throw new ArgumentException($"Key contains invalid char '{c}'.", nameof(key)); 43 | } 44 | } 45 | 46 | private static void EnsureValueIsValid(string value) 47 | { 48 | if (value == null) 49 | throw new ArgumentException("Value can not be null.", nameof(value)); 50 | 51 | foreach (var c in value) 52 | { 53 | if (c < 32 || c > 126 || c == ':' || c == '"' || c == '\'') 54 | throw new ArgumentException($"Value contains invalid char '{c}'.", nameof(value)); 55 | } 56 | } 57 | 58 | public void Add(string key, string value) 59 | { 60 | EnsureKeyIsValid(key); 61 | EnsureValueIsValid(value); 62 | 63 | _state.AddOrUpdate( 64 | key, 65 | _ => ImmutableList.Create(value), 66 | (_, existingValues) => ((ImmutableList)existingValues).Add(value)); 67 | } 68 | 69 | public void Set(string key, string[] values) 70 | { 71 | EnsureKeyIsValid(key); 72 | 73 | foreach (var value in values) 74 | EnsureValueIsValid(value); 75 | 76 | _state[key] = ImmutableList.Create(values); 77 | } 78 | 79 | public bool ContainsKey(string key) 80 | => _state.ContainsKey(key); 81 | 82 | public bool TryGetValue(string key, out IReadOnlyList values) 83 | => _state.TryGetValue(key, out values); 84 | 85 | public IEnumerator>> GetEnumerator() 86 | => _state.GetEnumerator(); 87 | 88 | IEnumerator IEnumerable.GetEnumerator() 89 | => _state.GetEnumerator(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/MyNatsClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0;netcoreapp3.1 5 | NATS NATS.io messaging PubSub pub-sub request-response rx reactivex reactiveextensions 6 | Provides a simple, effective sync and async library for interacting with NATS Server. It uses IObservable so it is ReactiveX (RX) friendly. 7 | 8 | 9 | 10 | 1701;1702;1705;1591 11 | bin\Release\$(TargetFramework)\MyNatsClient.xml 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/NatsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Ops; 3 | 4 | namespace MyNatsClient 5 | { 6 | public class NatsException : Exception 7 | { 8 | public string ExceptionCode { get; private set; } 9 | 10 | private NatsException(string exceptionCode, string message) 11 | : base(message) 12 | { 13 | ExceptionCode = exceptionCode ?? NatsExceptionCodes.Unknown; 14 | } 15 | 16 | internal static NatsException MissingCredentials(Host host) 17 | => new NatsException(NatsExceptionCodes.MissingCredentials, 18 | $"Error while connecting to {host}. Host requires credentials to be passed. None was specified. Pass credentials for specific host or for all hosts."); 19 | 20 | internal static NatsException MissingClientCertificates(Host host) 21 | => new NatsException(NatsExceptionCodes.MissingClientCertificates, $"Error while connecting to {host}. Host requires client certificates. None was specified."); 22 | 23 | internal static NatsException FailedToConnectToHost(Host host, string message) 24 | => new NatsException(NatsExceptionCodes.FailedToConnectToHost, $"Error while connecting to {host}. {message}"); 25 | 26 | internal static NatsException CouldNotEstablishAnyConnection() 27 | => new NatsException(NatsExceptionCodes.CouldNotEstablishAnyConnection, "No connection could be established against any of the specified hosts (servers)."); 28 | 29 | internal static NatsException ExceededMaxPayload(long maxPayload, long bufferLength) 30 | => new NatsException(NatsExceptionCodes.ExceededMaxPayload, $"Server indicated max payload of {maxPayload} bytes. Current dispatch is {bufferLength} bytes."); 31 | 32 | internal static NatsException CouldNotCreateSubscription(SubscriptionInfo subscriptionInfo) 33 | => new NatsException(NatsExceptionCodes.CouldNotCreateSubscription, 34 | $"Could not create subscription. Id='{subscriptionInfo.Id}'. Subject='{subscriptionInfo.Subject}' QueueGroup='{subscriptionInfo.QueueGroup}'."); 35 | 36 | internal static NatsException ConnectionFoundIdling(string host, int port) 37 | => new NatsException(NatsExceptionCodes.ConnectionFoundIdling, 38 | $"The Connection against server {host}:{port.ToString()} has not received any data in a to long period."); 39 | 40 | internal static NatsException ClientReceivedErrOp(ErrOp errOp) 41 | => new NatsException(NatsExceptionCodes.ClientReceivedErrOp, $"Client received ErrOp with message='{errOp.Message}'."); 42 | 43 | internal static NatsException ClientCouldNotConsumeStream() 44 | => new(NatsExceptionCodes.ClientCouldNotConsumeStream, "Client could not consume stream."); 45 | 46 | internal static NatsException OpParserError(string message) 47 | => new NatsException(NatsExceptionCodes.OpParserError, message); 48 | 49 | internal static NatsException OpParserUnsupportedOp(string opMarker) 50 | => new NatsException(NatsExceptionCodes.OpParserUnsupportedOp, $"Unsupported OP, don't know how to parse OP '{opMarker}'."); 51 | 52 | internal static NatsException OpParserOpParsingError(string op, byte expected, byte got) 53 | => new NatsException(NatsExceptionCodes.OpParserOpParsingError, $"Error while parsing {op}. Expected char code '{expected}' got '{got}'."); 54 | 55 | internal static NatsException OpParserOpParsingError(string op, string message) 56 | => new NatsException(NatsExceptionCodes.OpParserOpParsingError, $"Error while parsing {op}. {message}"); 57 | 58 | internal static NatsException InitRequestError(string message) 59 | => new NatsException(NatsExceptionCodes.InitRequestError, message); 60 | 61 | internal static NatsException NotConnected() 62 | => new NatsException(NatsExceptionCodes.NotConnected, "No connection exists."); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/NatsExceptionCodes.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public static class NatsExceptionCodes 4 | { 5 | internal const string Unknown = "Unknown"; 6 | public const string CouldNotCreateSubscription = "CouldNotCreateSubscription"; 7 | public const string MissingCredentials = "MissingCredentials"; 8 | public const string MissingClientCertificates = "MissingClientCertificates"; 9 | public const string FailedToConnectToHost = "FailedToConnectToHost"; 10 | public const string CouldNotEstablishAnyConnection = "CouldNotEstablishAnyConnection"; 11 | public const string ExceededMaxPayload = "ExceededMaxPayload"; 12 | public const string ConnectionFoundIdling = "ConnectionFoundIdling"; 13 | public const string ClientReceivedErrOp = "ClientReceivedErrOp"; 14 | public const string ClientCouldNotConsumeStream = "ClientCouldNotConsumeStream"; 15 | public const string OpParserError = "OpParser.Error"; 16 | public const string OpParserOpParsingError = "OpParser.Error"; 17 | public const string OpParserUnsupportedOp = "OpParser.UnsupportedOp"; 18 | public const string InitRequestError = "Client.InitRequestError"; 19 | public const string NotConnected = "NotConnected"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/NatsObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Microsoft.Extensions.Logging; 6 | using MyNatsClient.Internals; 7 | 8 | namespace MyNatsClient 9 | { 10 | public abstract class NatsObservable 11 | { 12 | protected readonly ILogger Logger; 13 | 14 | protected NatsObservable() 15 | { 16 | Logger = LoggerManager.CreateLogger(GetType()); 17 | } 18 | } 19 | 20 | public sealed class NatsObservableOf : NatsObservable, INatsObservable where T : class 21 | { 22 | private readonly ConcurrentDictionary _subscriptions 23 | = new ConcurrentDictionary(); 24 | 25 | private bool _isDisposed; 26 | 27 | public void Dispose() 28 | { 29 | if (_isDisposed) 30 | return; 31 | 32 | _isDisposed = true; 33 | 34 | var exs = new List(); 35 | foreach (var s in _subscriptions.Values) 36 | { 37 | try 38 | { 39 | s.Dispose(); 40 | } 41 | catch (Exception e) 42 | { 43 | exs.Add(e); 44 | } 45 | } 46 | 47 | _subscriptions.Clear(); 48 | 49 | if (exs.Any()) 50 | throw new AggregateException("Failed while disposing observable. See inner exception(s) for more details.", exs); 51 | } 52 | 53 | private void ThrowIfDisposed() 54 | { 55 | if (_isDisposed) 56 | throw new ObjectDisposedException(GetType().Name); 57 | } 58 | 59 | public void Emit(T value) 60 | { 61 | ThrowIfDisposed(); 62 | 63 | foreach (var subscription in _subscriptions.Values) 64 | { 65 | try 66 | { 67 | subscription.Observer.OnNext(value); 68 | } 69 | catch (Exception ex) 70 | { 71 | Logger.LogError(ex, "Error in observer while emitting value. Observer subscription will be removed."); 72 | 73 | //No invoke of OnError as it's not the producer that is failing. 74 | _subscriptions.TryRemove(subscription.Id, out _); 75 | 76 | Swallow.Everything(() => subscription.Dispose()); 77 | } 78 | } 79 | } 80 | 81 | private void DisposeSubscription(ObSubscription sub) 82 | { 83 | if (!_subscriptions.TryRemove(sub.Id, out _)) 84 | return; //Most likely, previously removed due to failure 85 | 86 | Swallow.Everything(() => sub.Observer.OnCompleted()); 87 | } 88 | 89 | public IDisposable Subscribe(IObserver observer) 90 | { 91 | ThrowIfDisposed(); 92 | 93 | if (observer == null) 94 | throw new ArgumentNullException(nameof(observer)); 95 | 96 | var sub = new ObSubscription(observer, DisposeSubscription); 97 | 98 | if (!_subscriptions.TryAdd(sub.Id, sub)) 99 | throw new ArgumentException("Can not subscribe observer. Each observer can only be subscribed once.", nameof(observer)); 100 | 101 | return sub; 102 | } 103 | 104 | private sealed class ObSubscription : IDisposable 105 | { 106 | private readonly Action _onDispose; 107 | 108 | public readonly int Id; 109 | public readonly IObserver Observer; 110 | 111 | public ObSubscription(IObserver observer, Action onDispose) 112 | { 113 | Id = observer.GetHashCode(); 114 | Observer = observer; 115 | _onDispose = onDispose; 116 | } 117 | 118 | public void Dispose() => _onDispose(this); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/NatsObserver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Internals; 3 | 4 | namespace MyNatsClient 5 | { 6 | public static class NatsObserver 7 | { 8 | public static IObserver Delegating( 9 | Action onNext, 10 | Action onError = null, 11 | Action onCompleted = null) => new DelegatingObserver(onNext, onError, onCompleted); 12 | 13 | public static IObserver Safe( 14 | Action onNext, 15 | Action onError = null, 16 | Action onCompleted = null) => new SafeObserver(onNext, onError, onCompleted); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/NatsOpMediator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MyNatsClient.Ops; 4 | 5 | namespace MyNatsClient 6 | { 7 | public sealed class NatsOpMediator : IDisposable 8 | { 9 | private const string DisposeExMessage = "Failed while disposing Op-mediator. See inner exception(s) for more details."; 10 | 11 | private bool _isDisposed; 12 | private NatsObservableOf _opStream; 13 | private NatsObservableOf _msgOpStream; 14 | 15 | public INatsObservable AllOpsStream => _opStream; 16 | public INatsObservable MsgOpsStream => _msgOpStream; 17 | 18 | public NatsOpMediator() 19 | { 20 | _opStream = new NatsObservableOf(); 21 | _msgOpStream = new NatsObservableOf(); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | if (_isDisposed) 27 | return; 28 | 29 | _isDisposed = true; 30 | 31 | Exception ex1 = null, ex2 = null; 32 | 33 | try 34 | { 35 | _opStream.Dispose(); 36 | } 37 | catch (Exception ex) 38 | { 39 | ex1 = ex; 40 | } 41 | 42 | try 43 | { 44 | _msgOpStream.Dispose(); 45 | } 46 | catch (Exception ex) 47 | { 48 | ex2 = ex; 49 | } 50 | 51 | _opStream = null; 52 | _msgOpStream = null; 53 | 54 | if(ex1 == null && ex2 == null) 55 | return; 56 | 57 | if(ex1 != null && ex2 != null) 58 | throw new AggregateException(DisposeExMessage, ex1, ex2); 59 | 60 | if(ex1 != null) 61 | throw new AggregateException(DisposeExMessage, ex1); 62 | 63 | if(ex2 != null) 64 | throw new AggregateException(DisposeExMessage, ex2); 65 | } 66 | 67 | public void Emit(IEnumerable ops) 68 | { 69 | foreach (var op in ops) 70 | Emit(op); 71 | } 72 | 73 | public void Emit(IOp op) 74 | { 75 | if (op is MsgOp msgOp) 76 | _msgOpStream.Emit(msgOp); 77 | 78 | _opStream.Emit(op); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/ErrOp.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Ops 2 | { 3 | public sealed class ErrOp : IOp 4 | { 5 | internal const string OpMarker = "-ERR"; 6 | 7 | public string Marker => OpMarker; 8 | 9 | public readonly string Message; 10 | 11 | public ErrOp(string message) 12 | => Message = message; 13 | 14 | public override string ToString() 15 | => OpMarker; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/InfoOp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MyNatsClient.Ops 4 | { 5 | public sealed class InfoOp : IOp 6 | { 7 | internal const string OpMarker = "INFO"; 8 | 9 | public string Marker => OpMarker; 10 | 11 | public readonly ReadOnlyMemory Message; 12 | 13 | public InfoOp(ReadOnlySpan message) 14 | => Message = message.ToArray(); 15 | 16 | public override string ToString() 17 | => OpMarker; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/MsgOp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using MyNatsClient.Internals; 5 | 6 | namespace MyNatsClient.Ops 7 | { 8 | public sealed class MsgOp : IOp 9 | { 10 | private const string MarkerWithoutHeaders = "MSG"; 11 | private const string MarkerWithHeaders = "HMSG"; 12 | 13 | public string Marker { get; } 14 | 15 | public readonly string Subject; 16 | public readonly string ReplyTo; 17 | public readonly string SubscriptionId; 18 | public readonly ReadOnlyMsgHeaders Headers; 19 | public readonly ReadOnlyMemory Payload; 20 | 21 | private MsgOp( 22 | string marker, 23 | ReadOnlySpan subject, 24 | ReadOnlySpan subscriptionId, 25 | ReadOnlySpan replyTo, 26 | ReadOnlyMsgHeaders headers, 27 | ReadOnlySpan payload) 28 | { 29 | Marker = marker; 30 | Subject = subject.ToString(); 31 | SubscriptionId = subscriptionId.ToString(); 32 | ReplyTo = replyTo.ToString(); 33 | Headers = headers; 34 | Payload = payload.ToArray(); 35 | } 36 | 37 | public static MsgOp CreateMsg( 38 | ReadOnlySpan subject, 39 | ReadOnlySpan subscriptionId, 40 | ReadOnlySpan replyTo, 41 | ReadOnlySpan payload) => new(MarkerWithoutHeaders, subject, subscriptionId, replyTo, ReadOnlyMsgHeaders.Empty, payload); 42 | 43 | public static MsgOp CreateHMsg( 44 | ReadOnlySpan subject, 45 | ReadOnlySpan subscriptionId, 46 | ReadOnlySpan replyTo, 47 | ReadOnlyMsgHeaders headers, 48 | ReadOnlySpan payload) => new(MarkerWithHeaders, subject, subscriptionId, replyTo, headers, payload); 49 | 50 | internal static string GetMarker(bool hasHeaders) 51 | => hasHeaders ? MarkerWithHeaders : MarkerWithoutHeaders; 52 | 53 | public string GetPayloadAsString() 54 | => NatsEncoder.GetString(Payload.Span); 55 | 56 | public override string ToString() 57 | => Marker; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/NullOp.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Ops 2 | { 3 | public sealed class NullOp : IOp 4 | { 5 | private const string OpMarker = "NULL"; 6 | 7 | public string Marker => OpMarker; 8 | 9 | public static readonly NullOp Instance = new(); 10 | 11 | private NullOp() { } 12 | 13 | public override string ToString() 14 | => OpMarker; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/OkOp.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Ops 2 | { 3 | public sealed class OkOp : IOp 4 | { 5 | internal const string OpMarker = "+OK"; 6 | 7 | public string Marker => OpMarker; 8 | 9 | public static readonly OkOp Instance = new(); 10 | 11 | private OkOp() { } 12 | 13 | public override string ToString() 14 | => OpMarker; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/PingOp.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Ops 2 | { 3 | public sealed class PingOp : IOp 4 | { 5 | internal const string OpMarker = "PING"; 6 | 7 | public string Marker => OpMarker; 8 | 9 | public static readonly PingOp Instance = new(); 10 | 11 | private PingOp() { } 12 | 13 | public override string ToString() 14 | => OpMarker; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Ops/PongOp.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient.Ops 2 | { 3 | public sealed class PongOp : IOp 4 | { 5 | internal const string OpMarker = "PONG"; 6 | 7 | public string Marker => OpMarker; 8 | 9 | public static readonly PongOp Instance = new(); 10 | 11 | private PongOp() { } 12 | 13 | public override string ToString() 14 | => OpMarker; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/PubFlushMode.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | /// 4 | /// Determines the clients flush behavior when sending messages. 5 | /// E.g. when Pub or PubAsync is called. 6 | /// 7 | public enum PubFlushMode 8 | { 9 | /// 10 | /// Will Flush after each Pub or PubAsync 11 | /// 12 | Auto, 13 | /// 14 | /// Will Flush when you call Flush or FlushAsync 15 | /// 16 | Manual 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/ReadOnlyMsgHeaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace MyNatsClient 6 | { 7 | public class ReadOnlyMsgHeaders : IReadOnlyDictionary> 8 | { 9 | public static readonly ReadOnlyMsgHeaders Empty = new (string.Empty, new Dictionary>(0)); 10 | 11 | private readonly IReadOnlyDictionary> _keyValues; 12 | 13 | public string Protocol { get; } 14 | public IReadOnlyList this[string key] => _keyValues[key]; 15 | public IEnumerable Keys => _keyValues.Keys; 16 | public IEnumerable> Values => _keyValues.Values; 17 | public int Count => _keyValues.Count; 18 | 19 | private ReadOnlyMsgHeaders(string protocol, IReadOnlyDictionary> keyValues) 20 | { 21 | Protocol = protocol; 22 | _keyValues = keyValues; 23 | } 24 | 25 | public static ReadOnlyMsgHeaders Create(ReadOnlySpan protocol, IReadOnlyDictionary> keyValues) 26 | { 27 | if (!protocol.StartsWith("NATS/")) 28 | throw new ArgumentException("Protocol must start with 'NATS/'.", nameof(protocol)); 29 | 30 | return new ReadOnlyMsgHeaders(protocol.ToString(), keyValues); 31 | } 32 | 33 | public IEnumerator>> GetEnumerator() 34 | => _keyValues.GetEnumerator(); 35 | 36 | IEnumerator IEnumerable.GetEnumerator() 37 | => _keyValues.GetEnumerator(); 38 | 39 | public bool ContainsKey(string key) 40 | => _keyValues.ContainsKey(key); 41 | 42 | public bool TryGetValue(string key, out IReadOnlyList value) 43 | => _keyValues.TryGetValue(key, out value); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/Rx/NatsObservableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Internals; 3 | using MyNatsClient.Ops; 4 | 5 | namespace MyNatsClient.Rx 6 | { 7 | public static class NatsObservableExtensions 8 | { 9 | public static INatsObservable Catch(this INatsObservable ob, Action handler) 10 | where TException : Exception 11 | where T : class 12 | => new CatchObservable(ob, handler); 13 | 14 | public static INatsObservable CatchAny(this INatsObservable ob, Action handler) 15 | where T : class 16 | => ob.Catch(handler); 17 | 18 | public static INatsObservable OfType(this INatsObservable ob) where TResult : class 19 | => new OfTypeObservable(ob); 20 | 21 | public static INatsObservable Cast(this INatsObservable ob) where TSource : class where TResult : class 22 | => new CastObservable(ob); 23 | 24 | public static INatsObservable Select(this INatsObservable ob, Func map) where TSource : class where TResult : class 25 | => new SelectObservable(ob, map); 26 | 27 | public static INatsObservable Where(this INatsObservable ob, Func predicate) where T : class 28 | => new WhereObservable(ob, predicate); 29 | 30 | public static INatsObservable WhereSubjectMatches(this INatsObservable ob, string subject) 31 | => new WhereSubjectMatchObservable(ob, subject); 32 | 33 | public static IDisposable Subscribe(this INatsObservable ob, Action onNext, Action onError = null, Action onCompleted = null) 34 | => ob.Subscribe(NatsObserver.Delegating(onNext, onError, onCompleted)); 35 | 36 | public static IDisposable SubscribeSafe(this INatsObservable ob, Action onNext, Action onError = null, Action onCompleted = null) 37 | => ob.Subscribe(NatsObserver.Safe(onNext, onError, onCompleted)); 38 | 39 | public static IDisposable SubscribeSafe(this INatsObservable ob, IObserver observer) where T : class 40 | => ob.Subscribe(NatsObserver.Safe(observer.OnNext, observer.OnError, observer.OnCompleted)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/MyNatsClient/SocketAddressType.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public enum SocketAddressType 4 | { 5 | IpV4, 6 | IpV6 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/SocketOptions.cs: -------------------------------------------------------------------------------- 1 | namespace MyNatsClient 2 | { 3 | public class SocketOptions 4 | { 5 | /// 6 | /// Gets or sets the type of address to use for the Socket. 7 | /// 8 | public SocketAddressType? AddressType { get; set; } = SocketAddressType.IpV4; 9 | 10 | /// 11 | /// Gets or sets the ReceiveBufferSize of the Socket. 12 | /// Will also adjust the buffer size of the underlying 13 | /// that is used by the consumer. 14 | /// 15 | public int? ReceiveBufferSize { get; set; } 16 | 17 | /// 18 | /// Gets or sets the SendBufferSize of the Socket. 19 | /// Will also adjust the buffer size of the underlying 20 | /// that is used by the publisher. 21 | /// 22 | public int? SendBufferSize { get; set; } 23 | 24 | /// 25 | /// Gets or sets the Recieve timeout in milliseconds for the Socket. 26 | /// When it times out, the client will look at internal settings 27 | /// to determine if it should fail or first try and ping the server. 28 | /// 29 | public int? ReceiveTimeoutMs { get; set; } = 5000; 30 | 31 | /// 32 | /// Gets or sets the Send timeout in milliseconds for the Socket. 33 | /// 34 | public int? SendTimeoutMs { get; set; } = 5000; 35 | 36 | 37 | /// 38 | /// Gets or sets the Connect timeout in milliseconds for the Socket. 39 | /// 40 | public int ConnectTimeoutMs { get; set; } = 5000; 41 | 42 | /// 43 | /// Gets or sets value indicating if the Nagle algoritm should be used or not 44 | /// on the created Socket. 45 | /// 46 | public bool? UseNagleAlgorithm { get; set; } = false; 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/MyNatsClient/SubscriptionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyNatsClient.Internals; 3 | 4 | namespace MyNatsClient 5 | { 6 | public sealed class SubscriptionInfo : IEquatable 7 | { 8 | private const char WildCardChar = '*'; 9 | private const char FullWildCardChar = '>'; 10 | private static readonly string[] EmptySubjectParts = new string[0]; 11 | 12 | private readonly int _wildCardPos, _fullWildCardPos; 13 | private readonly bool _matchAllSubjects; 14 | private readonly string[] _subjectParts; 15 | 16 | public string Id { get; } 17 | 18 | /// 19 | /// Gets the subject name the subscriber is subscribed to. 20 | /// 21 | public string Subject { get; } 22 | 23 | /// 24 | /// Gets the optionally specified queue group that the subscriber will join. 25 | /// 26 | public string QueueGroup { get; } 27 | 28 | /// 29 | /// Gets the number of messages the subscriber will wait before automatically unsubscribing. 30 | /// 31 | public int? MaxMessages { get; } 32 | 33 | /// 34 | /// Gets a value indicating if this subscription is a wild card subscription. 35 | /// 36 | public bool HasWildcardSubject { get; } 37 | 38 | /// 39 | /// Creates a subscription info object. 40 | /// 41 | /// The subject name to subscribe to 42 | /// If specified, the subscriber will join this queue group 43 | /// Number of messages to wait for before automatically unsubscribing 44 | public SubscriptionInfo(string subject, string queueGroup = null, int? maxMessages = null) 45 | { 46 | if(subject == null) 47 | throw new ArgumentNullException(nameof(subject)); 48 | 49 | _wildCardPos = subject.IndexOf(WildCardChar); 50 | _fullWildCardPos = subject.IndexOf(FullWildCardChar); 51 | 52 | if (_wildCardPos > -1 && _fullWildCardPos > -1) 53 | throw new ArgumentException("Subject can not contain both the wildcard and full wildcard character.", nameof(subject)); 54 | 55 | Id = UniqueId.Generate(); 56 | Subject = subject; 57 | QueueGroup = queueGroup; 58 | MaxMessages = maxMessages; 59 | 60 | HasWildcardSubject = _wildCardPos > -1 || _fullWildCardPos > -1; 61 | _matchAllSubjects = Subject[0] == FullWildCardChar; 62 | _subjectParts = !_matchAllSubjects && _wildCardPos > -1 63 | ? subject.Split('.') 64 | : EmptySubjectParts; 65 | } 66 | 67 | public bool Matches(string testSubject) 68 | { 69 | //EnsureArg.IsNotNullOrWhiteSpace(testSubject, nameof(testSubject)); 70 | 71 | if (_matchAllSubjects) 72 | return true; 73 | 74 | if (!HasWildcardSubject) 75 | return Subject.Equals(testSubject, StringComparison.Ordinal); 76 | 77 | if (_fullWildCardPos > -1) 78 | { 79 | var prefix = Subject.Substring(0, _fullWildCardPos); 80 | return testSubject.StartsWith(prefix, StringComparison.Ordinal); 81 | } 82 | 83 | if (_wildCardPos > -1) 84 | { 85 | var testParts = testSubject.Split('.'); 86 | if (testParts.Length != _subjectParts.Length) 87 | return false; 88 | 89 | for (var i = 0; i < testParts.Length; i++) 90 | { 91 | if (!testParts[i].Equals(_subjectParts[i], StringComparison.Ordinal) && !string.Equals(_subjectParts[i], "*", StringComparison.Ordinal)) 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | throw new Exception("Should not have reached this point."); 99 | } 100 | 101 | public override int GetHashCode() => Id.GetHashCode(); 102 | 103 | public bool Equals(SubscriptionInfo other) 104 | { 105 | if (ReferenceEquals(null, other)) return false; 106 | if (ReferenceEquals(this, other)) return true; 107 | 108 | return Id == other.Id; 109 | } 110 | 111 | public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is SubscriptionInfo other && Equals(other); 112 | } 113 | } -------------------------------------------------------------------------------- /src/samples/Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/samples/RequestResponseSample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Console; 5 | using MyNatsClient; 6 | using MyNatsClient.Rx; 7 | 8 | namespace RequestResponseSample 9 | { 10 | public class Program 11 | { 12 | private static INatsClient _client; 13 | 14 | public static async Task Main(string[] args) 15 | { 16 | LoggerManager.UseFactory(LoggerFactory.Create(b => b 17 | .AddFilter("System", LogLevel.Information) 18 | .AddFilter("Microsoft", LogLevel.Information) 19 | .SetMinimumLevel(LogLevel.Debug) 20 | .AddConsole())); 21 | 22 | var cnInfo = new ConnectionInfo("localhost"); 23 | 24 | _client = new NatsClient(cnInfo); 25 | 26 | await _client.ConnectAsync(); 27 | 28 | _client.Sub("getTemp", stream => stream.Subscribe(msg => 29 | { 30 | var parts = msg.GetPayloadAsString().Split('@'); 31 | _client.Pub(msg.ReplyTo, $"Temp is {TempService.Get(parts[0], parts[1])}C"); 32 | })); 33 | 34 | while (true) 35 | { 36 | Console.WriteLine("Query? (y=yes;n=no)"); 37 | if (Console.ReadKey().KeyChar == 'n') 38 | break; 39 | 40 | Console.WriteLine(); 41 | 42 | Console.WriteLine($"Got reply: {_client.RequestAsync("getTemp", "STOCKHOLM@SWEDEN").Result.GetPayloadAsString()}"); 43 | } 44 | 45 | _client.Disconnect(); 46 | } 47 | } 48 | 49 | internal static class TempService 50 | { 51 | private static readonly Random Rnd = new Random(); 52 | 53 | internal static decimal Get(string city, string countryCode) 54 | { 55 | return Rnd.Next(-3000, 4200) / 100M; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/samples/RequestResponseSample/RequestResponseSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/AutoReconnectOnFailureTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using Microsoft.Extensions.Logging; 5 | using Moq; 6 | using MyNatsClient; 7 | using MyNatsClient.Events; 8 | using MyNatsClient.Rx; 9 | using Xunit; 10 | 11 | namespace IntegrationTests 12 | { 13 | public class AutoReconnectOnFailureTests : Tests, IDisposable 14 | { 15 | private NatsClient _client; 16 | private Sync _sync; 17 | 18 | public AutoReconnectOnFailureTests(DefaultContext context) 19 | : base(context) 20 | { } 21 | 22 | public void Dispose() 23 | { 24 | _sync?.Dispose(); 25 | _sync = null; 26 | 27 | _client?.Disconnect(); 28 | _client?.Dispose(); 29 | _client = null; 30 | } 31 | 32 | [Fact] 33 | public async Task Client_Should_not_reconnect_When_user_initiated_disconnect() 34 | { 35 | var wasDisconnectedDueToFailure = false; 36 | var wasDisconnected = false; 37 | var wasReconnected = false; 38 | 39 | _sync = Sync.MaxTwo(); 40 | 41 | var cnInfo = Context.GetConnectionInfo(); 42 | cnInfo.AutoReconnectOnFailure = true; 43 | _client = new NatsClient(cnInfo); 44 | await _client.ConnectAsync(); 45 | 46 | _client.Events 47 | .OfType() 48 | .Subscribe(ev => 49 | { 50 | wasDisconnectedDueToFailure = ev.Reason == DisconnectReason.DueToFailure; 51 | wasDisconnected = true; 52 | _sync.Release(); 53 | }); 54 | 55 | _client.Events 56 | .OfType() 57 | .Subscribe(ev => 58 | { 59 | wasReconnected = true; 60 | _sync.Release(); 61 | }); 62 | 63 | _client.Disconnect(); 64 | 65 | //Wait for the Disconnected release and the potential Connected release 66 | _sync.WaitForAny(); 67 | 68 | wasDisconnectedDueToFailure.Should().BeFalse(); 69 | wasDisconnected.Should().BeTrue(); 70 | wasReconnected.Should().BeFalse(); 71 | _client.IsConnected.Should().BeFalse(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/BasicAuthTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using MyNatsClient; 5 | using Xunit; 6 | 7 | namespace IntegrationTests 8 | { 9 | public class BasicAuthTests : Tests 10 | { 11 | public BasicAuthTests(BasicAuthContext context) 12 | : base(context) 13 | { } 14 | 15 | [Fact] 16 | public async Task Client_Should_be_able_to_connect_When_valid_credentials_are_used() 17 | { 18 | using var client = Context.CreateClient(); 19 | 20 | await client.ConnectAsync(); 21 | 22 | client.IsConnected.Should().BeTrue(); 23 | } 24 | 25 | [Fact] 26 | public void Client_Should_not_be_able_to_connect_When_empty_credentials_are_used() 27 | { 28 | var cnInfo = Context.GetConnectionInfo(); 29 | cnInfo.Credentials = Credentials.Empty; 30 | cnInfo.Hosts[0].Credentials = Credentials.Empty; 31 | 32 | Func a = async () => 33 | { 34 | using var client = Context.CreateClient(cnInfo); 35 | await client.ConnectAsync(); 36 | }; 37 | 38 | a.Should().Throw().And.ExceptionCode.Should().Be(NatsExceptionCodes.MissingCredentials); 39 | } 40 | 41 | [Fact] 42 | public void Client_Should_not_be_able_to_connect_When_invalid_credentials_are_used() 43 | { 44 | var invalidCredentials = new Credentials("wrong", "credentials"); 45 | var cnInfo = Context.GetConnectionInfo(); 46 | cnInfo.Credentials = invalidCredentials; 47 | cnInfo.Hosts[0].Credentials = invalidCredentials; 48 | 49 | Func a = async () => 50 | { 51 | using var client = Context.CreateClient(cnInfo); 52 | await client.ConnectAsync(); 53 | }; 54 | 55 | a.Should().Throw().And.ExceptionCode.Should().Be(NatsExceptionCodes.FailedToConnectToHost); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/testing/IntegrationTests/ConnectTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using MyNatsClient; 5 | using Xunit; 6 | 7 | namespace IntegrationTests 8 | { 9 | public class ConnectTests : Tests, IDisposable 10 | { 11 | private NatsClient _client; 12 | private Sync _sync; 13 | 14 | public ConnectTests(DefaultContext context) 15 | : base(context) 16 | { 17 | } 18 | 19 | public void Dispose() 20 | { 21 | _sync?.Dispose(); 22 | _sync = null; 23 | 24 | _client?.Disconnect(); 25 | _client?.Dispose(); 26 | _client = null; 27 | } 28 | 29 | [Fact] 30 | public async Task Given_not_connected_Should_be_able_to_connect_with_specific_name() 31 | { 32 | var connectionInfo = Context.GetConnectionInfo(); 33 | connectionInfo.Name = Guid.NewGuid().ToString("N"); 34 | 35 | _client = await Context.ConnectClientAsync(connectionInfo); 36 | 37 | await Context.DelayAsync(); 38 | 39 | _client.IsConnected.Should().BeTrue(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Encodings/ClientJsonEncodingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using MyNatsClient; 5 | using MyNatsClient.Encodings.Json; 6 | using MyNatsClient.Rx; 7 | using Xunit; 8 | 9 | namespace IntegrationTests.Encodings 10 | { 11 | public class ClientJsonEncodingTests : Tests, IDisposable 12 | { 13 | private NatsClient _client; 14 | private Sync _sync; 15 | 16 | public ClientJsonEncodingTests(DefaultContext context) 17 | : base(context) 18 | { } 19 | 20 | public void Dispose() 21 | { 22 | _sync?.Dispose(); 23 | _sync = null; 24 | 25 | _client?.Disconnect(); 26 | _client?.Dispose(); 27 | _client = null; 28 | } 29 | 30 | [Fact] 31 | public async Task Should_be_able_to_publish_and_consume_JSON_payloads_synchronously() 32 | { 33 | var subject = Context.GenerateSubject(); 34 | var orgItem = EncodingTestItem.Create(); 35 | EncodingTestItem decodedItem = null; 36 | 37 | _sync = Sync.MaxOne(); 38 | _client = await Context.ConnectClientAsync(); 39 | _client.Sub(subject, stream => stream.Subscribe(msg => 40 | { 41 | decodedItem = msg.FromJson(); 42 | _sync.Release(); 43 | })); 44 | 45 | _client.PubAsJson(subject, orgItem); 46 | _sync.WaitForAll(); 47 | 48 | orgItem.Should().BeEquivalentTo(decodedItem); 49 | } 50 | 51 | [Fact] 52 | public async Task Should_be_able_to_publish_and_consume_JSON_payloads_asynchronously() 53 | { 54 | var subject = Context.GenerateSubject(); 55 | var orgItem = EncodingTestItem.Create(); 56 | EncodingTestItem decodedItem = null; 57 | 58 | _sync = Sync.MaxOne(); 59 | _client = await Context.ConnectClientAsync(); 60 | await _client.SubAsync(subject, stream => stream.Subscribe(msg => 61 | { 62 | decodedItem = msg.FromJson(); 63 | _sync.Release(); 64 | })); 65 | 66 | await _client.PubAsJsonAsync(subject, orgItem); 67 | _sync.WaitForAll(); 68 | 69 | orgItem.Should().BeEquivalentTo(decodedItem); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Encodings/ClientProtobufEncodingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using MyNatsClient; 5 | using MyNatsClient.Encodings.Json; 6 | using MyNatsClient.Rx; 7 | using Xunit; 8 | 9 | namespace IntegrationTests.Encodings 10 | { 11 | public class ClientProtobufEncodingTests : Tests, IDisposable 12 | { 13 | private NatsClient _client; 14 | private Sync _sync; 15 | 16 | public ClientProtobufEncodingTests(DefaultContext context) 17 | : base(context) 18 | { } 19 | 20 | public void Dispose() 21 | { 22 | _sync?.Dispose(); 23 | _sync = null; 24 | 25 | _client?.Disconnect(); 26 | _client?.Dispose(); 27 | _client = null; 28 | } 29 | 30 | [Fact] 31 | public async Task Should_be_able_to_publish_and_consume_JSON_payloads_synchronously() 32 | { 33 | var subject = Context.GenerateSubject(); 34 | var orgItem = EncodingTestItem.Create(); 35 | EncodingTestItem decodedItem = null; 36 | 37 | _sync = Sync.MaxOne(); 38 | _client = await Context.ConnectClientAsync(); 39 | _client.Sub(subject, stream => stream.Subscribe(msg => 40 | { 41 | decodedItem = msg.FromJson(); 42 | _sync.Release(); 43 | })); 44 | 45 | _client.PubAsJson(subject, orgItem); 46 | _sync.WaitForAll(); 47 | 48 | orgItem.Should().BeEquivalentTo(decodedItem); 49 | } 50 | 51 | [Fact] 52 | public async Task Should_be_able_to_publish_and_consume_JSON_payloads_asynchronously() 53 | { 54 | var subject = Context.GenerateSubject(); 55 | var orgItem = EncodingTestItem.Create(); 56 | EncodingTestItem decodedItem = null; 57 | 58 | _sync = Sync.MaxOne(); 59 | _client = await Context.ConnectClientAsync(); 60 | await _client.SubAsync(subject, stream => stream.Subscribe(msg => 61 | { 62 | decodedItem = msg.FromJson(); 63 | _sync.Release(); 64 | })); 65 | 66 | await _client.PubAsJsonAsync(subject, orgItem); 67 | _sync.WaitForAll(); 68 | 69 | orgItem.Should().BeEquivalentTo(decodedItem); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Encodings/EncodingTestItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProtoBuf; 3 | 4 | namespace IntegrationTests.Encodings 5 | { 6 | [ProtoContract] 7 | public class EncodingTestItem 8 | { 9 | public static EncodingTestItem Create() => new EncodingTestItem 10 | { 11 | SomeInt = 42, 12 | SomeNullableInt = 42, 13 | SomeNullableIntBeingNull = null, 14 | 15 | SomeDecimal = 3.14M, 16 | SomeNullableDecimal = 3.14M, 17 | SomeNullableDecimalBeingNull = null, 18 | 19 | SomeDateTime = new DateTime(2018, 6, 29, 18, 34, 34), 20 | SomeNullableDateTime = new DateTime(2018, 6, 29, 18, 34, 34), 21 | SomeNullableDateTimeBeingNull = null, 22 | 23 | SomeString = $"This is a string with a random GUID being '{Guid.NewGuid():N}", 24 | SomeStringBeingNull = null 25 | }; 26 | 27 | [ProtoMember(1)] 28 | public int SomeInt { get; set; } 29 | [ProtoMember(2)] 30 | public int? SomeNullableInt { get; set; } 31 | [ProtoMember(3)] 32 | public int? SomeNullableIntBeingNull { get; set; } 33 | 34 | [ProtoMember(4)] 35 | public decimal SomeDecimal { get; set; } 36 | [ProtoMember(5)] 37 | public decimal? SomeNullableDecimal { get; set; } 38 | [ProtoMember(6)] 39 | public decimal? SomeNullableDecimalBeingNull { get; set; } 40 | 41 | [ProtoMember(7)] 42 | public DateTime SomeDateTime { get; set; } 43 | [ProtoMember(8)] 44 | public DateTime? SomeNullableDateTime { get; set; } 45 | [ProtoMember(9)] 46 | public DateTime? SomeNullableDateTimeBeingNull { get; set; } 47 | 48 | [ProtoMember(10)] 49 | public string SomeString { get; set; } 50 | [ProtoMember(11)] 51 | public string SomeStringBeingNull { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Extension/ConnectionInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Security; 2 | using System.Security.Cryptography.X509Certificates; 3 | using MyNatsClient; 4 | 5 | namespace IntegrationTests.Extension 6 | { 7 | internal static class ConnectionInfoExtensions 8 | { 9 | private static bool AllowAll(X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) => true; 10 | 11 | internal static ConnectionInfo AllowAllServerCertificates(this ConnectionInfo connectionInfo) 12 | { 13 | connectionInfo.ServerCertificateValidation = AllowAll; 14 | 15 | return connectionInfo; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Resources/client.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielwertheim/mynatsclient/1b1eca2ae6646d4d25b207f956abe456fa5bed1d/src/testing/IntegrationTests/Resources/client.pfx -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Should.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using MyNatsClient; 5 | 6 | namespace IntegrationTests 7 | { 8 | internal static class Should 9 | { 10 | internal static void ThrowNatsException(Action a) 11 | => a.Should().ThrowExactly().Where(ex => ex.ExceptionCode == NatsExceptionCodes.NotConnected); 12 | 13 | internal static async Task ThrowNatsExceptionAsync(Func a) => 14 | (await a.Should().ThrowExactlyAsync()).Where(ex => ex.ExceptionCode == NatsExceptionCodes.NotConnected); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/Sync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using MyNatsClient.Ops; 6 | 7 | namespace IntegrationTests 8 | { 9 | public class Sync : IDisposable 10 | { 11 | private static readonly TimeSpan MaxWaitTime = TimeSpan.FromMilliseconds(10000); 12 | 13 | private readonly int _maxNumOfRequests; 14 | private readonly SemaphoreSlim _semaphore; 15 | private readonly ConcurrentQueue _interceptedMsgOps; 16 | 17 | public IEnumerable Intercepted => _interceptedMsgOps; 18 | public int InterceptedCount => _interceptedMsgOps.Count; 19 | 20 | private Sync(int maxNumOfRequests) 21 | { 22 | _maxNumOfRequests = maxNumOfRequests; 23 | _semaphore = new SemaphoreSlim(0, maxNumOfRequests); 24 | _interceptedMsgOps = new ConcurrentQueue(); 25 | } 26 | 27 | public static Sync Max(int numOfRequests) => new Sync(numOfRequests); 28 | 29 | public static Sync MaxOne() => new Sync(1); 30 | 31 | public static Sync MaxTwo() => new Sync(2); 32 | 33 | public static Sync MaxThree() => new Sync(3); 34 | 35 | public void Release(MsgOp msgOp = null) 36 | { 37 | if(msgOp != null) 38 | _interceptedMsgOps.Enqueue(msgOp); 39 | 40 | _semaphore.Release(1); 41 | } 42 | 43 | private void Wait(int aquireCount, TimeSpan? ts = null) 44 | { 45 | ts ??= MaxWaitTime; 46 | 47 | if (System.Diagnostics.Debugger.IsAttached) 48 | ts = TimeSpan.FromMilliseconds(-1); 49 | 50 | var aquiredCount = 0; 51 | using (var cts = new CancellationTokenSource(ts.Value)) 52 | { 53 | for (var c = 0; c < aquireCount; c++) 54 | { 55 | _semaphore.Wait(cts.Token); 56 | aquiredCount++; 57 | } 58 | } 59 | 60 | if(aquiredCount != aquireCount) 61 | throw new Exception($"Aquired {aquiredCount} but should have aquired {aquireCount}."); 62 | } 63 | 64 | public void WaitForAny() => Wait(1); 65 | 66 | public void WaitForAll() => Wait(_maxNumOfRequests); 67 | 68 | public void Dispose() => _semaphore.Dispose(); 69 | } 70 | } -------------------------------------------------------------------------------- /src/testing/IntegrationTests/TestContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using MyNatsClient; 4 | using Xunit; 5 | 6 | namespace IntegrationTests 7 | { 8 | public abstract class Tests : IClassFixture where TContext : TestContext 9 | { 10 | protected TContext Context { get; } 11 | 12 | protected Tests(TContext context) 13 | { 14 | Context = context; 15 | } 16 | } 17 | 18 | public abstract class TestContext 19 | { 20 | private readonly Host[] _hosts; 21 | 22 | protected TestContext(Host[] hosts) 23 | { 24 | LoggerManager.ResetToDefaults(); 25 | 26 | _hosts = hosts; 27 | } 28 | 29 | public async Task DelayAsync() 30 | => await Task.Delay(250); 31 | 32 | public ConnectionInfo GetConnectionInfo() 33 | => new ConnectionInfo(_hosts) 34 | { 35 | AutoRespondToPing = false, 36 | Verbose = false, 37 | SocketOptions = new SocketOptions 38 | { 39 | ConnectTimeoutMs = 8000 40 | } 41 | }; 42 | 43 | public virtual NatsClient CreateClient(ConnectionInfo connectionInfo = null) 44 | => new NatsClient(connectionInfo ?? GetConnectionInfo()); 45 | 46 | public async Task ConnectClientAsync(ConnectionInfo connectionInfo = null) 47 | { 48 | var client = CreateClient(connectionInfo); 49 | 50 | await client.ConnectAsync(); 51 | 52 | return client; 53 | } 54 | 55 | public string GenerateSubject() 56 | => Guid.NewGuid().ToString("N"); 57 | } 58 | 59 | public sealed class DefaultContext : TestContext 60 | { 61 | public const string Name = nameof(DefaultContext); 62 | 63 | public DefaultContext() 64 | : base(TestSettings.GetHosts(Name)) 65 | { 66 | } 67 | } 68 | 69 | public sealed class BasicAuthContext : TestContext 70 | { 71 | public const string Name = nameof(BasicAuthContext); 72 | 73 | public Credentials ValidCredentials { get; } 74 | 75 | public BasicAuthContext() 76 | : base(TestSettings.GetHosts(Name)) 77 | { 78 | ValidCredentials = TestSettings.GetCredentials(); 79 | } 80 | 81 | public override NatsClient CreateClient(ConnectionInfo connectionInfo = null) 82 | { 83 | if(connectionInfo == null) 84 | { 85 | connectionInfo = GetConnectionInfo(); 86 | foreach (var host in connectionInfo.Hosts) 87 | { 88 | host.Credentials = ValidCredentials; 89 | } 90 | } 91 | 92 | return base.CreateClient(connectionInfo); 93 | } 94 | } 95 | 96 | public sealed class TlsContext : TestContext 97 | { 98 | public const string Name = nameof(TlsContext); 99 | 100 | public TlsContext() 101 | : base(TestSettings.GetHosts(Name)) 102 | {} 103 | } 104 | 105 | public sealed class TlsVerifyContext : TestContext 106 | { 107 | public const string Name = nameof(TlsVerifyContext); 108 | 109 | public TlsVerifyContext() 110 | : base(TestSettings.GetHosts(Name)) 111 | {} 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/TestSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.Extensions.Configuration; 4 | using MyNatsClient; 5 | 6 | namespace IntegrationTests 7 | { 8 | public static class TestSettings 9 | { 10 | public static IConfigurationRoot Config { get; set; } 11 | 12 | static TestSettings() 13 | { 14 | var builder = new ConfigurationBuilder() 15 | .SetBasePath(AppContext.BaseDirectory); 16 | 17 | builder 18 | .AddJsonFile("integrationtests.json", false, false) 19 | .AddJsonFile("integrationtests.local.json", true, false) 20 | .AddEnvironmentVariables("mynats_"); 21 | 22 | Config = builder.Build(); 23 | } 24 | 25 | public static Credentials GetCredentials() 26 | { 27 | var credentialsConfig = Config.GetSection("credentials"); 28 | if (!credentialsConfig.Exists()) 29 | throw new Exception("Test configuration is missing 'credentials' section."); 30 | 31 | return new Credentials( 32 | credentialsConfig["user"], 33 | credentialsConfig["pass"]); 34 | } 35 | 36 | public static Host[] GetHosts(string context) 37 | { 38 | if (string.IsNullOrWhiteSpace(context)) 39 | throw new ArgumentException("Context must be provided.", nameof(context)); 40 | 41 | return Config 42 | .GetSection($"{context}:hosts") 43 | .GetChildren() 44 | .Select(i => new Host(i["address"], int.Parse(i["port"]))) 45 | .ToArray(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/TlsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using IntegrationTests.Extension; 5 | using MyNatsClient; 6 | using MyNatsClient.Rx; 7 | using Xunit; 8 | 9 | namespace IntegrationTests 10 | { 11 | public class TlsTests : Tests, IDisposable 12 | { 13 | private NatsClient _requester; 14 | private NatsClient _responder; 15 | 16 | public TlsTests(TlsContext context) 17 | : base(context) 18 | { 19 | } 20 | 21 | public void Dispose() 22 | { 23 | _requester?.Disconnect(); 24 | _requester?.Dispose(); 25 | _requester = null; 26 | 27 | _responder?.Disconnect(); 28 | _responder?.Dispose(); 29 | _responder = null; 30 | } 31 | 32 | [Fact] 33 | public async Task Given_responder_exists_When_requesting_using_string_It_should_get_response() 34 | { 35 | var value = Guid.NewGuid().ToString("N"); 36 | 37 | var connectionInfo = Context 38 | .GetConnectionInfo() 39 | .AllowAllServerCertificates(); 40 | 41 | _responder = await Context.ConnectClientAsync(connectionInfo); 42 | _requester = await Context.ConnectClientAsync(connectionInfo); 43 | 44 | _responder.Sub("getValue", stream => stream.Subscribe(msg => _responder.Pub(msg.ReplyTo, msg.GetPayloadAsString()))); 45 | 46 | var response = await _requester.RequestAsync("getValue", value); 47 | 48 | response.GetPayloadAsString().Should().Be(value); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/TlsVerifyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography.X509Certificates; 4 | using System.Threading.Tasks; 5 | using FluentAssertions; 6 | using IntegrationTests.Extension; 7 | using MyNatsClient; 8 | using MyNatsClient.Rx; 9 | using Xunit; 10 | 11 | namespace IntegrationTests 12 | { 13 | public class TlsVerifyTests : Tests, IDisposable 14 | { 15 | private NatsClient _requester; 16 | private NatsClient _responder; 17 | 18 | public TlsVerifyTests(TlsVerifyContext context) 19 | : base(context) 20 | { 21 | } 22 | 23 | public void Dispose() 24 | { 25 | _requester?.Disconnect(); 26 | _requester?.Dispose(); 27 | _requester = null; 28 | 29 | _responder?.Disconnect(); 30 | _responder?.Dispose(); 31 | _responder = null; 32 | } 33 | 34 | [Fact] 35 | public async Task Given_responder_exists_When_requesting_using_string_It_should_get_response() 36 | { 37 | var value = Guid.NewGuid().ToString("N"); 38 | 39 | var connectionInfo = Context 40 | .GetConnectionInfo() 41 | .AllowAllServerCertificates(); 42 | 43 | var clientCert = new X509Certificate2(Path.Combine("Resources", "client.pfx")); 44 | connectionInfo.ClientCertificates.Add(clientCert); 45 | 46 | _responder = await Context.ConnectClientAsync(connectionInfo); 47 | _requester = await Context.ConnectClientAsync(connectionInfo); 48 | 49 | _responder.Sub("getValue", stream => stream.Subscribe(msg => _responder.Pub(msg.ReplyTo, msg.GetPayloadAsString()))); 50 | 51 | var response = await _requester.RequestAsync("getValue", value); 52 | 53 | response.GetPayloadAsString().Should().Be(value); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/testing/IntegrationTests/UnSubTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using MyNatsClient; 6 | using MyNatsClient.Ops; 7 | using MyNatsClient.Rx; 8 | using Xunit; 9 | 10 | namespace IntegrationTests 11 | { 12 | public class UnSubTests : Tests, IDisposable 13 | { 14 | private NatsClient _client1; 15 | private NatsClient _client2; 16 | private NatsClient _client3; 17 | private Sync _sync; 18 | 19 | public UnSubTests(DefaultContext context) 20 | : base(context) 21 | { 22 | } 23 | 24 | public void Dispose() 25 | { 26 | _sync?.Dispose(); 27 | _sync = null; 28 | 29 | _client1?.Disconnect(); 30 | _client1?.Dispose(); 31 | _client1 = null; 32 | 33 | _client2?.Disconnect(); 34 | _client2?.Dispose(); 35 | _client2 = null; 36 | 37 | _client3?.Disconnect(); 38 | _client3?.Dispose(); 39 | _client3 = null; 40 | } 41 | 42 | private async Task ConnectAllClients() 43 | { 44 | _client1 = await Context.ConnectClientAsync(); 45 | _client2 = await Context.ConnectClientAsync(); 46 | _client3 = await Context.ConnectClientAsync(); 47 | } 48 | 49 | [Fact] 50 | public async Task Client_Should_be_able_to_unsub_from_a_subject() 51 | { 52 | var subject = Context.GenerateSubject(); 53 | var nr1Received = new ConcurrentQueue(); 54 | var nr2Received = new ConcurrentQueue(); 55 | var nr3Received = new ConcurrentQueue(); 56 | var subInfo1 = new SubscriptionInfo(subject); 57 | var subInfo2 = new SubscriptionInfo(subject); 58 | var subInfo3 = new SubscriptionInfo(subject); 59 | 60 | _sync = Sync.MaxThree(); 61 | await ConnectAllClients(); 62 | 63 | _client1.OpStream.OfType().Subscribe(msg => 64 | { 65 | _client1.Unsub(subInfo1); 66 | nr1Received.Enqueue(msg); 67 | _sync.Release(msg); 68 | }); 69 | _client1.Sub(subInfo1); 70 | 71 | _client2.OpStream.OfType().Subscribe(async msg => 72 | { 73 | await _client2.UnsubAsync(subInfo2); 74 | nr2Received.Enqueue(msg); 75 | _sync.Release(msg); 76 | }); 77 | _client2.Sub(subInfo2); 78 | 79 | _client3.OpStream.OfType().Subscribe(msg => 80 | { 81 | nr3Received.Enqueue(msg); 82 | _sync.Release(msg); 83 | }); 84 | _client3.Sub(subInfo3); 85 | 86 | await Context.DelayAsync(); 87 | 88 | _client1.Pub(subject, "mess1"); 89 | _sync.WaitForAll(); 90 | 91 | _client3.Unsub(subInfo3); 92 | await Context.DelayAsync(); 93 | 94 | _client1.Pub(subject, "mess2"); 95 | await Context.DelayAsync(); 96 | 97 | _sync.InterceptedCount.Should().Be(3); 98 | nr1Received.Should().HaveCount(1); 99 | nr2Received.Should().HaveCount(1); 100 | nr3Received.Should().HaveCount(1); 101 | } 102 | 103 | [Fact] 104 | public async Task Client_Should_be_able_to_unsub_from_a_subject_by_passing_a_subscription_object() 105 | { 106 | var subject = Context.GenerateSubject(); 107 | var nr1Received = new ConcurrentQueue(); 108 | var nr2Received = new ConcurrentQueue(); 109 | var nr3Received = new ConcurrentQueue(); 110 | var subInfo1 = new SubscriptionInfo(subject); 111 | var subInfo2 = new SubscriptionInfo(subject); 112 | var subInfo3 = new SubscriptionInfo(subject); 113 | 114 | _sync = Sync.MaxThree(); 115 | await ConnectAllClients(); 116 | 117 | _client1.OpStream.OfType().Subscribe(msg => 118 | { 119 | nr1Received.Enqueue(msg); 120 | _sync.Release(msg); 121 | }); 122 | var sub1 = _client1.Sub(subInfo1); 123 | 124 | _client2.OpStream.OfType().Subscribe(msg => 125 | { 126 | nr2Received.Enqueue(msg); 127 | _sync.Release(msg); 128 | }); 129 | var sub2 = _client2.Sub(subInfo2); 130 | 131 | _client3.OpStream.OfType().Subscribe(msg => 132 | { 133 | nr3Received.Enqueue(msg); 134 | _sync.Release(msg); 135 | }); 136 | var sub3 = _client3.Sub(subInfo3); 137 | 138 | await Context.DelayAsync(); 139 | 140 | _client1.Pub(subject, "mess1"); 141 | _sync.WaitForAll(); 142 | 143 | _client1.Unsub(sub1); 144 | _client2.Unsub(sub2); 145 | _client3.Unsub(sub3); 146 | await Context.DelayAsync(); 147 | 148 | _client1.Pub(subject, "mess2"); 149 | await Context.DelayAsync(); 150 | 151 | _sync.InterceptedCount.Should().Be(3); 152 | nr1Received.Should().HaveCount(1); 153 | nr2Received.Should().HaveCount(1); 154 | nr3Received.Should().HaveCount(1); 155 | } 156 | 157 | [Fact] 158 | public async Task Client_Should_be_able_to_unsub_from_a_subject_by_disposing_a_subscription_object() 159 | { 160 | var subject = Context.GenerateSubject(); 161 | var nr1Received = new ConcurrentQueue(); 162 | var nr2Received = new ConcurrentQueue(); 163 | var nr3Received = new ConcurrentQueue(); 164 | var subInfo1 = new SubscriptionInfo(subject); 165 | var subInfo2 = new SubscriptionInfo(subject); 166 | var subInfo3 = new SubscriptionInfo(subject); 167 | 168 | _sync = Sync.MaxThree(); 169 | await ConnectAllClients(); 170 | 171 | _client1.OpStream.OfType().Subscribe(msg => 172 | { 173 | nr1Received.Enqueue(msg); 174 | _sync.Release(msg); 175 | }); 176 | var sub1 = _client1.Sub(subInfo1); 177 | 178 | _client2.OpStream.OfType().Subscribe(msg => 179 | { 180 | nr2Received.Enqueue(msg); 181 | _sync.Release(msg); 182 | }); 183 | var sub2 = _client2.Sub(subInfo2); 184 | 185 | _client3.OpStream.OfType().Subscribe(msg => 186 | { 187 | nr3Received.Enqueue(msg); 188 | _sync.Release(msg); 189 | }); 190 | var sub3 = _client3.Sub(subInfo3); 191 | 192 | await Context.DelayAsync(); 193 | 194 | _client1.Pub(subject, "mess1"); 195 | _sync.WaitForAll(); 196 | 197 | sub1.Dispose(); 198 | sub2.Dispose(); 199 | sub3.Dispose(); 200 | await Context.DelayAsync(); 201 | 202 | _client1.Pub(subject, "mess2"); 203 | await Context.DelayAsync(); 204 | 205 | _sync.InterceptedCount.Should().Be(3); 206 | nr1Received.Should().HaveCount(1); 207 | nr2Received.Should().HaveCount(1); 208 | nr3Received.Should().HaveCount(1); 209 | } 210 | 211 | [Fact] 212 | public async Task Client_Should_be_able_to_auto_unsub_after_n_messages_to_subject() 213 | { 214 | var subject = Context.GenerateSubject(); 215 | var nr2Received = new ConcurrentQueue(); 216 | var nr3Received = new ConcurrentQueue(); 217 | var subInfo2 = new SubscriptionInfo(subject, maxMessages: 2); 218 | var subInfo3 = new SubscriptionInfo(subject, maxMessages: 2); 219 | 220 | _sync = Sync.MaxTwo(); 221 | await ConnectAllClients(); 222 | 223 | _client2.OpStream.OfType().Subscribe(msg => 224 | { 225 | nr2Received.Enqueue(msg); 226 | _sync.Release(msg); 227 | }); 228 | _client2.Sub(subInfo2); 229 | 230 | _client3.OpStream.OfType().Subscribe(msg => 231 | { 232 | nr3Received.Enqueue(msg); 233 | _sync.Release(msg); 234 | }); 235 | _client3.Sub(subInfo3); 236 | 237 | await Context.DelayAsync(); 238 | 239 | _client1.Pub(subject, "mess1"); 240 | _sync.WaitForAll(); 241 | 242 | _client1.Pub(subject, "mess2"); 243 | _sync.WaitForAll(); 244 | 245 | _client1.Pub(subject, "mess3"); 246 | await Context.DelayAsync(); 247 | 248 | _sync.InterceptedCount.Should().Be(4); 249 | nr2Received.Should().HaveCount(2); 250 | nr3Received.Should().HaveCount(2); 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /src/testing/IntegrationTests/integrationtests.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultContext": { 3 | "hosts": [ 4 | { 5 | "address": "localhost", 6 | "port": 4222 7 | } 8 | ] 9 | }, 10 | "basicAuthContext": { 11 | "hosts": [ 12 | { 13 | "address": "localhost", 14 | "port": 4223 15 | } 16 | ] 17 | }, 18 | "tlsContext": { 19 | "hosts": [ 20 | { 21 | "address": "localhost", 22 | "port": 4224 23 | } 24 | ] 25 | }, 26 | "tlsVerifyContext": { 27 | "hosts": [ 28 | { 29 | "address": "localhost", 30 | "port": 4225 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/testing/UnitTests/ConnectionInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using MyNatsClient; 4 | using Xunit; 5 | 6 | namespace UnitTests 7 | { 8 | public class ConnectionInfoTests : UnitTestsOf 9 | { 10 | public ConnectionInfoTests() 11 | { 12 | UnitUnderTest = new ConnectionInfo(new Host("localhost")); 13 | } 14 | 15 | [Fact] 16 | public void Defaults_Should_have_name_assigned() 17 | => UnitUnderTest.Name.Should().Be("mynatsclient"); 18 | 19 | [Fact] 20 | public void Defaults_Should_use_inbox_requests() 21 | { 22 | UnitUnderTest.UseInboxRequests.Should().BeTrue(); 23 | } 24 | 25 | [Fact] 26 | public void Defaults_Should_have_empty_credentials() 27 | { 28 | UnitUnderTest.Credentials.Should().Be(Credentials.Empty); 29 | } 30 | 31 | [Fact] 32 | public void Defaults_Should_not_be_verbose() 33 | { 34 | UnitUnderTest.Verbose.Should().BeFalse(); 35 | } 36 | 37 | [Fact] 38 | public void Defaults_Should_have_auto_reconnect_on_failure() 39 | { 40 | UnitUnderTest.AutoReconnectOnFailure.Should().BeTrue(); 41 | } 42 | 43 | [Fact] 44 | public void Defaults_Should_auto_respond_to_pings() 45 | { 46 | UnitUnderTest.AutoRespondToPing.Should().BeTrue(); 47 | } 48 | 49 | [Fact] 50 | public void Defaults_Should_have_auto_pub_flush_mode() 51 | { 52 | UnitUnderTest.PubFlushMode.Should().Be(PubFlushMode.Auto); 53 | } 54 | 55 | [Fact] 56 | public void Defaults_Should_have_a_request_timeout_of_5s() 57 | { 58 | UnitUnderTest.RequestTimeoutMs.Should().Be(5000); 59 | } 60 | 61 | [Fact] 62 | public void Defaults_Should_have_a_socket_recieve_timeout_of_5s() 63 | { 64 | UnitUnderTest.SocketOptions.ReceiveTimeoutMs.Should().Be(5000); 65 | } 66 | 67 | [Fact] 68 | public void Defaults_Should_have_a_socket_send_timeout_of_5s() 69 | { 70 | UnitUnderTest.SocketOptions.SendTimeoutMs.Should().Be(5000); 71 | } 72 | 73 | [Fact] 74 | public void Defaults_Should_have_a_socket_connect_timeout_of_5s() 75 | { 76 | UnitUnderTest.SocketOptions.ConnectTimeoutMs.Should().Be(5000); 77 | } 78 | 79 | [Fact] 80 | public void Defaults_Should_disable_Nagles_algorithm() 81 | { 82 | UnitUnderTest.SocketOptions.UseNagleAlgorithm.Should().BeFalse(); 83 | } 84 | 85 | [Fact] 86 | public void Clone_Should_clone_all_properties() 87 | { 88 | var other = new ConnectionInfo(new[] { new Host("192.168.1.20", 4223) }) 89 | { 90 | Name = Guid.NewGuid().ToString("N"), 91 | UseInboxRequests = false, 92 | Credentials = new Credentials("tester", "p@ssword"), 93 | Verbose = !UnitUnderTest.Verbose, 94 | AutoReconnectOnFailure = !UnitUnderTest.AutoReconnectOnFailure, 95 | AutoRespondToPing = !UnitUnderTest.AutoRespondToPing, 96 | RequestTimeoutMs = 100, 97 | PubFlushMode = UnitUnderTest.PubFlushMode, 98 | SocketOptions = new SocketOptions 99 | { 100 | ReceiveBufferSize = 1000, 101 | ReceiveTimeoutMs = 100, 102 | SendBufferSize = 1000, 103 | SendTimeoutMs = 100, 104 | ConnectTimeoutMs = 100, 105 | UseNagleAlgorithm = true 106 | } 107 | }; 108 | 109 | var cloned = other.Clone(); 110 | 111 | cloned.Should().BeEquivalentTo(other); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/testing/UnitTests/CredentialsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient; 3 | using Xunit; 4 | 5 | namespace UnitTests 6 | { 7 | public class CredentialsTests : UnitTestsOf 8 | { 9 | public CredentialsTests() 10 | { 11 | UnitUnderTest = new Credentials("theuser", "thepass"); 12 | } 13 | 14 | [Fact] 15 | public void Empty_Should_return_same_instance() 16 | { 17 | Credentials.Empty.Should().BeSameAs(Credentials.Empty); 18 | } 19 | 20 | [Fact] 21 | public void Should_have_equals_operator() 22 | { 23 | (UnitUnderTest == new Credentials(UnitUnderTest.User, UnitUnderTest.Pass)).Should().BeTrue(); 24 | 25 | (UnitUnderTest == new Credentials(UnitUnderTest.User + "a", UnitUnderTest.Pass)).Should().BeFalse(); 26 | 27 | (UnitUnderTest == new Credentials(UnitUnderTest.User, UnitUnderTest.Pass + "a")).Should().BeFalse(); 28 | 29 | (new Credentials("THEUSER", "THEPASS") == new Credentials("theuser", "THEPASS")).Should().BeFalse(); 30 | 31 | (new Credentials("THEUSER", "THEPASS") == new Credentials("THEUSER", "thepass")).Should().BeFalse(); 32 | } 33 | 34 | [Fact] 35 | public void Should_have_not_equals_operator() 36 | { 37 | (UnitUnderTest != new Credentials(UnitUnderTest.User, UnitUnderTest.Pass)).Should().BeFalse(); 38 | 39 | (UnitUnderTest != new Credentials(UnitUnderTest.User + "a", UnitUnderTest.Pass)).Should().BeTrue(); 40 | 41 | (UnitUnderTest != new Credentials(UnitUnderTest.User, UnitUnderTest.Pass + "a")).Should().BeTrue(); 42 | 43 | (new Credentials("THEUSER", "THEPASS") != new Credentials("theuser", "THEPASS")).Should().BeTrue(); 44 | 45 | (new Credentials("THEUSER", "THEPASS") != new Credentials("THEUSER", "thepass")).Should().BeTrue(); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/testing/UnitTests/Encodings/EncodingTestItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProtoBuf; 3 | 4 | namespace UnitTests.Encodings 5 | { 6 | [ProtoContract] 7 | public class EncodingTestItem 8 | { 9 | public static EncodingTestItem Create() => new EncodingTestItem 10 | { 11 | SomeInt = 42, 12 | SomeNullableInt = 42, 13 | SomeNullableIntBeingNull = null, 14 | 15 | SomeDecimal = 3.14M, 16 | SomeNullableDecimal = 3.14M, 17 | SomeNullableDecimalBeingNull = null, 18 | 19 | SomeDateTime = new DateTime(2018, 6, 29, 18, 34, 34), 20 | SomeNullableDateTime = new DateTime(2018, 6, 29, 18, 34, 34), 21 | SomeNullableDateTimeBeingNull = null, 22 | 23 | SomeString = $"This is a string with a random GUID being '{Guid.NewGuid():N}", 24 | SomeStringBeingNull = null 25 | }; 26 | 27 | [ProtoMember(1)] 28 | public int SomeInt { get; set; } 29 | [ProtoMember(2)] 30 | public int? SomeNullableInt { get; set; } 31 | [ProtoMember(3)] 32 | public int? SomeNullableIntBeingNull { get; set; } 33 | 34 | [ProtoMember(4)] 35 | public decimal SomeDecimal { get; set; } 36 | [ProtoMember(5)] 37 | public decimal? SomeNullableDecimal { get; set; } 38 | [ProtoMember(6)] 39 | public decimal? SomeNullableDecimalBeingNull { get; set; } 40 | 41 | [ProtoMember(7)] 42 | public DateTime SomeDateTime { get; set; } 43 | [ProtoMember(8)] 44 | public DateTime? SomeNullableDateTime { get; set; } 45 | [ProtoMember(9)] 46 | public DateTime? SomeNullableDateTimeBeingNull { get; set; } 47 | 48 | [ProtoMember(10)] 49 | public string SomeString { get; set; } 50 | [ProtoMember(11)] 51 | public string SomeStringBeingNull { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /src/testing/UnitTests/Encodings/EncodingTestOf.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient; 3 | using Xunit; 4 | 5 | namespace UnitTests.Encodings 6 | { 7 | public abstract class EncodingTestOf : UnitTestsOf where TEncoding : IEncoding 8 | { 9 | protected EncodingTestOf(TEncoding encoding) 10 | { 11 | UnitUnderTest = encoding; 12 | } 13 | 14 | [Fact] 15 | public void Should_be_able_to_Encode_and_Decode_an_instance() 16 | { 17 | var testItem = EncodingTestItem.Create(); 18 | 19 | var encoded = UnitUnderTest.Encode(testItem); 20 | encoded.Should().NotBeNull(); 21 | encoded.Length.Should().BeGreaterThan(0); 22 | 23 | var decoded = UnitUnderTest.Decode(encoded.ToArray(), testItem.GetType()); 24 | decoded.Should().BeEquivalentTo(testItem); 25 | } 26 | 27 | [Fact] 28 | public void Should_return_null_When_passing_null_payload() 29 | => UnitUnderTest.Decode(null, typeof(EncodingTestItem)).Should().BeNull(); 30 | 31 | [Fact] 32 | public void Should_return_null_When_passing_empty_payload() 33 | => UnitUnderTest.Decode(new byte[0], typeof(EncodingTestItem)).Should().BeNull(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/testing/UnitTests/Encodings/JsonEncodingTests.cs: -------------------------------------------------------------------------------- 1 | using MyNatsClient.Encodings.Json; 2 | 3 | namespace UnitTests.Encodings 4 | { 5 | public class JsonEncodingTests : EncodingTestOf 6 | { 7 | public JsonEncodingTests() : base(JsonEncoding.Default) { } 8 | } 9 | } -------------------------------------------------------------------------------- /src/testing/UnitTests/Encodings/ProtobufEncodingTests.cs: -------------------------------------------------------------------------------- 1 | using MyNatsClient.Encodings.Protobuf; 2 | 3 | namespace UnitTests.Encodings 4 | { 5 | public class ProtobufEncodingTests : EncodingTestOf 6 | { 7 | public ProtobufEncodingTests() : base(ProtobufEncoding.Default) { } 8 | } 9 | } -------------------------------------------------------------------------------- /src/testing/UnitTests/FakeLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Moq; 4 | 5 | namespace UnitTests 6 | { 7 | internal class FakeLoggerFactory : ILoggerFactory 8 | { 9 | internal Mock Logger { get; } = new(); 10 | 11 | public void Dispose() { } 12 | 13 | public ILogger CreateLogger(string categoryName) => Logger.Object; 14 | 15 | public void AddProvider(ILoggerProvider provider) { } 16 | } 17 | 18 | internal static class FakeLoggerExtensions 19 | { 20 | internal static void WasCalledWith(this Mock fakeLogger, LogLevel logLevel, Func messageExpectation, Exception ex = null) 21 | { 22 | fakeLogger.Verify( 23 | logger => logger.Log( 24 | It.Is(l => l == logLevel), 25 | It.IsAny(), 26 | It.Is((message, _) => messageExpectation(message == null ? null : message.ToString())), 27 | It.Is(thrown => ex == null || ex == thrown), 28 | It.Is>((_, __) => true))); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/testing/UnitTests/HostTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient; 3 | using Xunit; 4 | 5 | namespace UnitTests 6 | { 7 | public class HostTests : UnitTestsOf 8 | { 9 | [Fact] 10 | public void Should_have_default_port_4222() 11 | { 12 | UnitUnderTest = new Host("host1"); 13 | UnitUnderTest.Port.Should().Be(4222); 14 | } 15 | 16 | [Theory] 17 | [InlineData("host1", 9999)] 18 | public void Should_have_custom_ToString(string host, int port) 19 | { 20 | UnitUnderTest = new Host(host, port); 21 | 22 | UnitUnderTest.ToString().Should().Be($"{host}:{port}"); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/testing/UnitTests/LoggerManagerTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using MyNatsClient; 4 | using Xunit; 5 | 6 | namespace UnitTests 7 | { 8 | public class LoggerManagerTests : UnitTests 9 | { 10 | [Fact] 11 | public void Should_return_NullLogger_by_default() 12 | { 13 | LoggerManager.ResetToDefaults(); 14 | LoggerManager.CreateLogger(typeof(LoggerManagerTests)).Should().BeOfType(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/testing/UnitTests/MsgHeadersTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using FluentAssertions; 6 | using MyNatsClient; 7 | using Xunit; 8 | 9 | namespace UnitTests 10 | { 11 | public class MsgHeadersTests : UnitTestsOf 12 | { 13 | private static string NewValidKey() => Guid.NewGuid().ToString("N"); 14 | private static string NewInvalidKey() => "\" ?'"; 15 | private static string NewValidValue() => Guid.NewGuid().ToString("N"); 16 | 17 | public MsgHeadersTests() 18 | => UnitUnderTest = MsgHeaders.Create(); 19 | 20 | [Fact] 21 | public void Can_manage_key_values() 22 | { 23 | var kv1 = (Key: NewValidKey(), Values: new[] {NewValidValue(), NewValidValue()}); 24 | var kv2 = (Key: NewValidKey(), Values: new[] {NewValidValue(), NewValidValue()}); 25 | var kv3 = (Key: NewValidKey(), Values: new[] {NewValidValue(), NewValidValue()}); 26 | 27 | UnitUnderTest.Add(kv1.Key, kv1.Values[0]); 28 | UnitUnderTest.TryGetValue(kv1.Key, out var actualValues1).Should().BeTrue(); 29 | UnitUnderTest.Count.Should().Be(1); 30 | actualValues1.Should().BeEquivalentTo(kv1.Values.Take(1)); 31 | 32 | UnitUnderTest.Add(kv2.Key, kv2.Values[0]); 33 | UnitUnderTest.TryGetValue(kv2.Key, out var actualValues2).Should().BeTrue(); 34 | UnitUnderTest.Count.Should().Be(2); 35 | actualValues2.Should().BeEquivalentTo(kv2.Values.Take(1)); 36 | 37 | UnitUnderTest.Add(kv1.Key, kv1.Values[1]); 38 | UnitUnderTest.TryGetValue(kv1.Key, out actualValues1).Should().BeTrue(); 39 | actualValues1.Should().BeEquivalentTo(kv1.Values); 40 | 41 | UnitUnderTest.Add(kv2.Key, kv2.Values[1]); 42 | UnitUnderTest.TryGetValue(kv2.Key, out actualValues2).Should().BeTrue(); 43 | actualValues2.Should().BeEquivalentTo(kv2.Values); 44 | 45 | UnitUnderTest.Add(kv3.Key, NewValidValue()); 46 | UnitUnderTest.Set(kv3.Key, kv3.Values); 47 | UnitUnderTest.TryGetValue(kv3.Key, out var actualValues3).Should().BeTrue(); 48 | UnitUnderTest.Count.Should().Be(3); 49 | actualValues3.Should().BeEquivalentTo(kv3.Values); 50 | 51 | UnitUnderTest.Keys 52 | .OrderBy(k => k) 53 | .Should() 54 | .BeEquivalentTo(new[] {kv1.Key, kv2.Key, kv3.Key}); 55 | 56 | UnitUnderTest.Values 57 | .SelectMany(vs => vs.ToArray()) 58 | .OrderBy(v => v) 59 | .Should().BeEquivalentTo(kv1.Values.Union(kv2.Values).Union(kv3.Values).OrderBy(v => v)); 60 | } 61 | 62 | [Fact] 63 | public void Provides_info_if_empty_or_not() 64 | { 65 | UnitUnderTest.IsEmpty.Should().BeTrue(); 66 | 67 | UnitUnderTest.Add(NewValidKey(), NewValidValue()); 68 | 69 | UnitUnderTest.IsEmpty.Should().BeFalse(); 70 | } 71 | 72 | [Theory] 73 | [InlineData("")] 74 | public void Accepts_missing_value_defined_by(string value) 75 | { 76 | var key = NewValidKey(); 77 | 78 | UnitUnderTest.Add(key, value); 79 | 80 | UnitUnderTest.TryGetValue(key, out var actualValues).Should().BeTrue(); 81 | actualValues.Should().Contain(value); 82 | } 83 | 84 | [Fact] 85 | public void Can_handle_missing_keys() 86 | { 87 | Action missingValidKey = () => UnitUnderTest[NewValidKey()].Should().BeEquivalentTo(ImmutableList.Empty); 88 | Action missingInvalidKey = () => UnitUnderTest[NewInvalidKey()].Should().BeEquivalentTo(ImmutableList.Empty); 89 | 90 | missingValidKey.Should().Throw(); 91 | missingInvalidKey.Should().Throw(); 92 | } 93 | 94 | [Theory] 95 | [InlineData("ABCDEFGHIGKLMNOPQRSTUVWXYZ")] 96 | [InlineData("abcdefghigklmnopqrstuvwxyz")] 97 | [InlineData("0123456789")] 98 | [InlineData("@{}-/+\\.!#$[]()_*^~")] 99 | public void Can_handles_keys_with(string key) 100 | { 101 | UnitUnderTest.Add(key, string.Empty); 102 | UnitUnderTest.Set(key, new[] {string.Empty}); 103 | } 104 | 105 | [Theory] 106 | [InlineData("ABCDEFGHIGKLMNOPQRSTUVWXYZ")] 107 | [InlineData("abcdefghigklmnopqrstuvwxyz")] 108 | [InlineData("0123456789")] 109 | [InlineData("@{}-/+.!#?$[]()_*^~? ")] 110 | public void Can_handles_values_with(string value) 111 | { 112 | UnitUnderTest.Add(NewValidKey(), value); 113 | UnitUnderTest.Set(NewValidKey(), new[] {value}); 114 | } 115 | 116 | [Theory] 117 | [InlineData(" ")] 118 | [InlineData("?")] 119 | [InlineData("'")] 120 | [InlineData("\"")] 121 | public void Key_can_not_contain(string key) 122 | { 123 | UnitUnderTest 124 | .Invoking(a => a.Add(key, string.Empty)) 125 | .Should().Throw(); 126 | 127 | UnitUnderTest 128 | .Invoking(a => a.Set(key, new[] {string.Empty})) 129 | .Should().Throw(); 130 | } 131 | 132 | [Theory] 133 | [InlineData("'")] 134 | [InlineData("\"")] 135 | public void Value_can_not_contain(string key) 136 | { 137 | UnitUnderTest 138 | .Invoking(a => a.Add(key, string.Empty)) 139 | .Should().Throw(); 140 | 141 | UnitUnderTest 142 | .Invoking(a => a.Set(key, new[] {string.Empty})) 143 | .Should().Throw(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/testing/UnitTests/NatsServerInfoParseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using MyNatsClient.Internals; 4 | using Xunit; 5 | 6 | namespace UnitTests 7 | { 8 | public class NatsServerInfoParseTests : UnitTests 9 | { 10 | private static NatsServerInfo Parse(string v) 11 | => NatsServerInfo.Parse(v.AsMemory()); 12 | 13 | [Fact] 14 | public void Should_be_able_to_parse_When_protocol_0_data_is_returned() 15 | { 16 | var parsed = Parse("{\"server_id\":\"Vwp6WDR1NIEuFr0CQ9PtMa\",\"version\":\"0.8.0\",\"go\":\"go1.6.2\",\"host\":\"0.0.0.0\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"tls_verify\":false,\"max_payload\":1048576}"); 17 | 18 | parsed.ServerId.Should().Be("Vwp6WDR1NIEuFr0CQ9PtMa"); 19 | parsed.Version.Should().Be("0.8.0"); 20 | parsed.Go.Should().Be("go1.6.2"); 21 | parsed.Host.Should().Be("0.0.0.0"); 22 | parsed.Port.Should().Be(4222); 23 | parsed.AuthRequired.Should().BeFalse(); 24 | parsed.TlsRequired.Should().BeFalse(); 25 | parsed.TlsVerify.Should().BeFalse(); 26 | parsed.MaxPayload.Should().Be(1048576); 27 | 28 | parsed = Parse("{\"auth_required\":true,\"ssl_required\":true,\"tls_required\":true,\"tls_verify\":true}"); 29 | parsed.AuthRequired.Should().BeTrue(); 30 | parsed.TlsRequired.Should().BeTrue(); 31 | parsed.TlsVerify.Should().BeTrue(); 32 | } 33 | 34 | [Fact] 35 | public void Should_be_able_to_parse_When_protocol_1_data_is_returned() 36 | { 37 | var parsed = Parse("{\"server_id\":\"Vwp6WDR1NIEuFr0CQ9PtMa\",\"version\":\"0.8.0\",\"go\":\"go1.6.2\",\"host\":\"0.0.0.0\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"tls_verify\":false,\"headers\":false,\"max_payload\":1048576,\"connect_urls\":[\"ubuntu01:4302\",\"ubuntu01:4303\"],\"ip\":\"127.0.0.1\"}"); 38 | 39 | parsed.ServerId.Should().Be("Vwp6WDR1NIEuFr0CQ9PtMa"); 40 | parsed.Version.Should().Be("0.8.0"); 41 | parsed.Go.Should().Be("go1.6.2"); 42 | parsed.Host.Should().Be("0.0.0.0"); 43 | parsed.Port.Should().Be(4222); 44 | parsed.AuthRequired.Should().BeFalse(); 45 | parsed.TlsRequired.Should().BeFalse(); 46 | parsed.TlsVerify.Should().BeFalse(); 47 | parsed.Headers.Should().BeFalse(); 48 | parsed.MaxPayload.Should().Be(1048576); 49 | parsed.ConnectUrls.Should().Contain("ubuntu01:4302", "ubuntu01:4303"); 50 | parsed.Ip.Should().Be("127.0.0.1"); 51 | 52 | parsed = Parse("{\"auth_required\":true,\"tls_required\":true,\"tls_verify\":true,\"headers\":true}"); 53 | parsed.AuthRequired.Should().BeTrue(); 54 | parsed.TlsRequired.Should().BeTrue(); 55 | parsed.TlsVerify.Should().BeTrue(); 56 | parsed.Headers.Should().BeTrue(); 57 | } 58 | 59 | [Fact] 60 | public void Should_be_able_to_parse_When_array_is_passed_in_middle() 61 | { 62 | var parsed = Parse("{\"server_id\":\"Vwp6WDR1NIEuFr0CQ9PtMa\",\"connect_urls\":[\"ubuntu01:4302\",\"ubuntu01:4303\"],\"max_payload\":1048576}"); 63 | 64 | parsed.ServerId.Should().Be("Vwp6WDR1NIEuFr0CQ9PtMa"); 65 | parsed.ConnectUrls.Should().Contain("ubuntu01:4302", "ubuntu01:4303"); 66 | parsed.MaxPayload.Should().Be(1048576); 67 | } 68 | 69 | [Fact] 70 | public void Should_be_able_to_parse_When_trailing_comma_arrives() 71 | { 72 | var parsed = Parse("{\"server_id\":\"Vwp6WDR1NIEuFr0CQ9PtMa\",\"connect_urls\":[\"ubuntu01:4302\",\"ubuntu01:4303\"],\"max_payload\":1048576,}"); 73 | parsed.ServerId.Should().Be("Vwp6WDR1NIEuFr0CQ9PtMa"); 74 | parsed.ConnectUrls.Should().Contain("ubuntu01:4302", "ubuntu01:4303"); 75 | parsed.MaxPayload.Should().Be(1048576); 76 | 77 | parsed = Parse("{\"server_id\":\"Vwp6WDR1NIEuFr0CQ9PtMa\",\"max_payload\":1048576,\"connect_urls\":[\"ubuntu01:4302\",\"ubuntu01:4303\"],}"); 78 | parsed.ServerId.Should().Be("Vwp6WDR1NIEuFr0CQ9PtMa"); 79 | parsed.MaxPayload.Should().Be(1048576); 80 | parsed.ConnectUrls.Should().Contain("ubuntu01:4302", "ubuntu01:4303"); 81 | } 82 | 83 | [Fact] 84 | public void Should_be_able_to_parse_with_ipv4_and_ipv6_addresses() 85 | { 86 | var parsed = Parse("{\"connect_urls\":[\"42.47.11.11:4222\",\"[2001:0:47ef:15de:421:2eef:f500:fcf4]:4222\"]}"); 87 | parsed.ConnectUrls.Should().Contain("42.47.11.11:4222"); 88 | parsed.ConnectUrls.Should().Contain("[2001:0:47ef:15de:421:2eef:f500:fcf4]:4222"); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/testing/UnitTests/Ops/ErrOpTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient.Ops; 3 | using Xunit; 4 | 5 | namespace UnitTests.Ops 6 | { 7 | public class ErrOpTests : UnitTestsOf 8 | { 9 | [Fact] 10 | public void Is_initialized_properly() 11 | { 12 | UnitUnderTest = new ErrOp("Foo Bar"); 13 | 14 | UnitUnderTest.Marker.Should().Be("-ERR"); 15 | UnitUnderTest.Message.Should().Be("Foo Bar"); 16 | UnitUnderTest.ToString().Should().Be("-ERR"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/testing/UnitTests/Ops/InfoOpTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using MyNatsClient.Ops; 4 | using Xunit; 5 | 6 | namespace UnitTests.Ops 7 | { 8 | public class InfoOpTests : UnitTestsOf 9 | { 10 | [Fact] 11 | public void Is_initialized_properly() 12 | { 13 | UnitUnderTest = new InfoOp("Foo Bar".AsSpan()); 14 | 15 | UnitUnderTest.Marker.Should().Be("INFO"); 16 | UnitUnderTest.Message.Span.ToString().Should().Be("Foo Bar"); 17 | UnitUnderTest.ToString().Should().Be("INFO"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/testing/UnitTests/Ops/MsgOpTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using FluentAssertions; 5 | using MyNatsClient; 6 | using MyNatsClient.Ops; 7 | using Xunit; 8 | 9 | namespace UnitTests.Ops 10 | { 11 | public class MsgOpTests : UnitTestsOf 12 | { 13 | [Fact] 14 | public void Is_initialized_properly_When_Msg() 15 | { 16 | UnitUnderTest = MsgOp.CreateMsg( 17 | "TestSub", 18 | "TestSubId", 19 | "TestReplyTo", 20 | Encoding.UTF8.GetBytes("TestPayload")); 21 | 22 | UnitUnderTest.Marker.Should().Be("MSG"); 23 | UnitUnderTest.Subject.Should().Be("TestSub"); 24 | UnitUnderTest.SubscriptionId.Should().Be("TestSubId"); 25 | UnitUnderTest.ReplyTo.Should().Be("TestReplyTo"); 26 | UnitUnderTest.Payload.ToArray().Should().BeEquivalentTo(Encoding.UTF8.GetBytes("TestPayload")); 27 | UnitUnderTest.GetPayloadAsString().Should().Be("TestPayload"); 28 | UnitUnderTest.ToString().Should().Be("MSG"); 29 | } 30 | 31 | [Fact] 32 | public void Is_initialized_properly_When_Msg_with_optionals_are_missing() 33 | { 34 | UnitUnderTest = MsgOp.CreateMsg( 35 | "TestSub", 36 | "TestSubId", 37 | ReadOnlySpan.Empty, 38 | ReadOnlySpan.Empty); 39 | 40 | UnitUnderTest.Marker.Should().Be("MSG"); 41 | UnitUnderTest.Subject.Should().Be("TestSub"); 42 | UnitUnderTest.SubscriptionId.Should().Be("TestSubId"); 43 | UnitUnderTest.ReplyTo.Should().BeEmpty(); 44 | UnitUnderTest.Payload.IsEmpty.Should().BeTrue(); 45 | UnitUnderTest.GetPayloadAsString().Should().BeEmpty(); 46 | UnitUnderTest.ToString().Should().Be("MSG"); 47 | } 48 | 49 | [Fact] 50 | public void Is_initialized_properly_When_HMsg() 51 | { 52 | UnitUnderTest = MsgOp.CreateHMsg( 53 | "TestSub", 54 | "TestSubId", 55 | "TestReplyTo", 56 | ReadOnlyMsgHeaders.Create("NATS/1.0", new Dictionary> 57 | { 58 | {"Header1", new List 59 | { 60 | "Value1.1" 61 | }} 62 | }), 63 | Encoding.UTF8.GetBytes("TestPayload")); 64 | 65 | UnitUnderTest.Marker.Should().Be("HMSG"); 66 | UnitUnderTest.Subject.Should().Be("TestSub"); 67 | UnitUnderTest.SubscriptionId.Should().Be("TestSubId"); 68 | UnitUnderTest.ReplyTo.Should().Be("TestReplyTo"); 69 | UnitUnderTest.Headers.Should().HaveCount(1); 70 | UnitUnderTest.Payload.ToArray().Should().BeEquivalentTo(Encoding.UTF8.GetBytes("TestPayload")); 71 | UnitUnderTest.GetPayloadAsString().Should().Be("TestPayload"); 72 | UnitUnderTest.ToString().Should().Be("HMSG"); 73 | } 74 | 75 | [Fact] 76 | public void Is_initialized_properly_When_HMsg_with_optionals_are_missing() 77 | { 78 | UnitUnderTest = MsgOp.CreateHMsg( 79 | "TestSub", 80 | "TestSubId", 81 | ReadOnlySpan.Empty, 82 | ReadOnlyMsgHeaders.Empty, 83 | ReadOnlySpan.Empty); 84 | 85 | UnitUnderTest.Marker.Should().Be("HMSG"); 86 | UnitUnderTest.Subject.Should().Be("TestSub"); 87 | UnitUnderTest.SubscriptionId.Should().Be("TestSubId"); 88 | UnitUnderTest.ReplyTo.Should().BeEmpty(); 89 | UnitUnderTest.Headers.Should().BeEmpty(); 90 | UnitUnderTest.Payload.IsEmpty.Should().BeTrue(); 91 | UnitUnderTest.GetPayloadAsString().Should().BeEmpty(); 92 | UnitUnderTest.ToString().Should().Be("HMSG"); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/testing/UnitTests/Ops/OkOpTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient.Ops; 3 | using Xunit; 4 | 5 | namespace UnitTests.Ops 6 | { 7 | public class OkOpTests : UnitTestsOf 8 | { 9 | [Fact] 10 | public void Is_initialized_properly() 11 | { 12 | UnitUnderTest = OkOp.Instance; 13 | 14 | UnitUnderTest.Marker.Should().Be("+OK"); 15 | UnitUnderTest.ToString().Should().Be("+OK"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/testing/UnitTests/Ops/PingOpTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient.Ops; 3 | using Xunit; 4 | 5 | namespace UnitTests.Ops 6 | { 7 | public class PingOpTests : UnitTestsOf 8 | { 9 | [Fact] 10 | public void Is_initialized_properly() 11 | { 12 | UnitUnderTest = PingOp.Instance; 13 | 14 | UnitUnderTest.Marker.Should().Be("PING"); 15 | UnitUnderTest.ToString().Should().Be("PING"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/testing/UnitTests/Ops/PongOpTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using MyNatsClient.Ops; 3 | using Xunit; 4 | 5 | namespace UnitTests.Ops 6 | { 7 | public class PongOpTests : UnitTestsOf 8 | { 9 | [Fact] 10 | public void Is_initialized_properly() 11 | { 12 | UnitUnderTest = PongOp.Instance; 13 | 14 | UnitUnderTest.Marker.Should().Be("PONG"); 15 | UnitUnderTest.ToString().Should().Be("PONG"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/testing/UnitTests/ReadOnlyMsgHeadersTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using MyNatsClient; 6 | using Xunit; 7 | 8 | namespace UnitTests 9 | { 10 | public class ReadOnlyMsgHeadersTests : UnitTestsOf 11 | { 12 | private const string ValidProtocol = "NATS/1.0"; 13 | 14 | [Theory] 15 | [InlineData(null)] 16 | [InlineData("")] 17 | [InlineData(" ")] 18 | [InlineData("NATS_")] 19 | public void Creating_requires_protocol_to_start_with_NATS(string protocol) 20 | { 21 | Action creating = () => ReadOnlyMsgHeaders.Create(protocol, new Dictionary>()); 22 | 23 | creating 24 | .Should() 25 | .ThrowExactly() 26 | .WithMessage("Protocol must start with 'NATS/'. (Parameter 'protocol')") 27 | .And.ParamName 28 | .Should().Be("protocol"); 29 | } 30 | 31 | [Fact] 32 | public void Has_an_Empty_implementation() 33 | { 34 | UnitUnderTest = ReadOnlyMsgHeaders.Empty; 35 | 36 | UnitUnderTest.Should().BeEmpty(); 37 | UnitUnderTest.Count.Should().Be(0); 38 | UnitUnderTest.Keys.Should().BeEmpty(); 39 | UnitUnderTest.Values.Should().BeEmpty(); 40 | UnitUnderTest.ContainsKey(Guid.NewGuid().ToString("N")).Should().BeFalse(); 41 | UnitUnderTest.TryGetValue(Guid.NewGuid().ToString("N"), out var match).Should().BeFalse(); 42 | match.Should().BeNull(); 43 | } 44 | 45 | [Fact] 46 | public void Exposes_protocol() 47 | { 48 | UnitUnderTest = ReadOnlyMsgHeaders.Create(ValidProtocol, new Dictionary>()); 49 | 50 | UnitUnderTest.Protocol.Should().Be(ValidProtocol); 51 | } 52 | 53 | [Fact] 54 | public void Works_as_a_read_only_dictionary() 55 | { 56 | var initialKvs = new Dictionary> 57 | { 58 | {"Header1", new[] {"Value1.1"}}, 59 | {"Header2", new[] {"Value2.1", "Value2.2"}} 60 | }; 61 | 62 | UnitUnderTest = ReadOnlyMsgHeaders.Create(ValidProtocol, initialKvs); 63 | 64 | UnitUnderTest.Count.Should().Be(2); 65 | UnitUnderTest.Keys.Should().BeEquivalentTo("Header1", "Header2"); 66 | UnitUnderTest.Values.SelectMany(v => v).Should().BeEquivalentTo("Value1.1", "Value2.1", "Value2.2"); 67 | UnitUnderTest["Header1"].Should().BeEquivalentTo("Value1.1"); 68 | UnitUnderTest["Header2"].Should().BeEquivalentTo("Value2.1", "Value2.2"); 69 | UnitUnderTest.ContainsKey("Header1").Should().BeTrue(); 70 | UnitUnderTest.ContainsKey("MissingHeader").Should().BeFalse(); 71 | UnitUnderTest.TryGetValue("Header1", out var matchingValues1).Should().BeTrue(); 72 | matchingValues1.Should().BeEquivalentTo("Value1.1"); 73 | UnitUnderTest.TryGetValue("Header2", out var matchingValues2).Should().BeTrue(); 74 | matchingValues2.Should().BeEquivalentTo("Value2.1", "Value2.2"); 75 | UnitUnderTest.TryGetValue("MissingHeader", out var missingValues).Should().BeFalse(); 76 | missingValues.Should().BeNull(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/testing/UnitTests/SubscriptionInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using MyNatsClient; 8 | using Xunit; 9 | 10 | namespace UnitTests 11 | { 12 | public class SubscriptionInfoTests : UnitTestsOf 13 | { 14 | [Theory] 15 | [InlineData("tests.*")] 16 | [InlineData("tests.*.test1")] 17 | [InlineData("tests.foo.*")] 18 | public void HasWildcardSubject_Should_return_true_When_subject_contains_wildcard(string subject) 19 | { 20 | UnitUnderTest = new SubscriptionInfo(subject); 21 | 22 | UnitUnderTest.HasWildcardSubject.Should().BeTrue(); 23 | } 24 | 25 | [Theory] 26 | [InlineData(">")] 27 | [InlineData("tests.>")] 28 | public void HasWildcardSubject_Should_return_true_When_subject_contains_full_wildcard(string subject) 29 | { 30 | UnitUnderTest = new SubscriptionInfo(subject); 31 | 32 | UnitUnderTest.HasWildcardSubject.Should().BeTrue(); 33 | } 34 | 35 | [Theory] 36 | [InlineData("test.>.*.test")] 37 | [InlineData("test.*.>.test")] 38 | public void Should_throw_exception_When_constructing_using_both_wildcard_and_full_wildcard(string subject) 39 | { 40 | Action test = () => new SubscriptionInfo(subject); 41 | 42 | test.Should().Throw() 43 | .Where(ex => ex.Message.StartsWith("Subject can not contain both the wildcard and full wildcard character.")) 44 | .And.ParamName.Should().Be("subject"); 45 | } 46 | 47 | [Theory] 48 | [InlineData("test", "test", true)] 49 | [InlineData("test", "Test", false)] 50 | public void Matches_Should_match_When_subject_is_equal(string subject, string testSubject, bool expect) 51 | { 52 | new SubscriptionInfo(subject).Matches(testSubject).Should().Be(expect); 53 | } 54 | 55 | [Theory] 56 | [InlineData(">", "tests.level1.level2", true)] 57 | [InlineData("tests.>", "tests.level1.level2", true)] 58 | [InlineData("level1.>", "tests.level1.level2", false)] 59 | [InlineData("tests.*.level2", "tests.level1.level2", true)] 60 | [InlineData("tests.*.level2", "tests.level1.level3", false)] 61 | [InlineData("tests.level1.*", "tests.level1.level2", true)] 62 | public void Matches_Should_match_When_subject_wildcard_makes_it_match(string subject, string testSubject, bool expect) 63 | { 64 | new SubscriptionInfo(subject).Matches(testSubject).Should().Be(expect); 65 | } 66 | 67 | [Theory] 68 | [InlineData(2, 1000)] 69 | [InlineData(4, 250)] 70 | public async Task Should_have_unique_id(int taskCount, int subscriptionCount) 71 | { 72 | var ids = new ConcurrentBag(); 73 | var generationTasks = Enumerable.Range(0, taskCount).Select(_ => Task.Run(() => 74 | { 75 | for (var idNumber = 0; idNumber < subscriptionCount; idNumber++) 76 | { 77 | ids.Add(new SubscriptionInfo("tests.id").Id); 78 | } 79 | })).ToList(); 80 | 81 | await Task.WhenAll(generationTasks); 82 | 83 | ids.Distinct().Should().BeEquivalentTo(ids); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/testing/UnitTests/UnitTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Moq; 4 | using MyNatsClient; 5 | 6 | namespace UnitTests 7 | { 8 | public abstract class UnitTestsOf : UnitTests 9 | { 10 | protected T UnitUnderTest { get; set; } 11 | 12 | protected override void OnDisposing() 13 | => (UnitUnderTest as IDisposable)?.Dispose(); 14 | } 15 | 16 | public abstract class UnitTests : IDisposable 17 | { 18 | private readonly FakeLoggerFactory _fakeLoggerFactory = new(); 19 | 20 | protected Mock FakeLogger => _fakeLoggerFactory.Logger; 21 | 22 | protected UnitTests() 23 | { 24 | LoggerManager.UseFactory(_fakeLoggerFactory); 25 | } 26 | 27 | public void Dispose() 28 | { 29 | LoggerManager.ResetToDefaults(); 30 | _fakeLoggerFactory.Dispose(); 31 | OnAfterEachTest(); 32 | OnDisposing(); 33 | } 34 | 35 | protected virtual void OnAfterEachTest() { } 36 | protected virtual void OnDisposing() { } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/testing/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------