├── .appveyor.yml ├── .codecov.yml ├── .gitignore ├── .travis.yml ├── Greentube.Messaging.sln ├── README.md ├── coverage.ps1 ├── global.json ├── samples ├── Greentube.Messaging.Sample.Kafka │ ├── Greentube.Messaging.Sample.Kafka.csproj │ ├── Program.cs │ ├── SomeOtherMessage.cs │ ├── SomeOtherMessageHandler.cs │ └── docker-compose.yml ├── Greentube.Messaging.Sample.Redis │ ├── Greentube.Messaging.Sample.Redis.csproj │ ├── Program.cs │ └── docker-compose.yml └── Greentube.Messaging.Sample │ ├── Greentube.Messaging.Sample.csproj │ ├── SomeApp.cs │ ├── SomeMessage.cs │ └── SomeMessageHandler.cs ├── src ├── Greentube.Messaging.All │ ├── Greentube.Messaging.All.nuspec │ └── README.md ├── Greentube.Messaging.DependencyInjection.Kafka │ ├── Greentube.Messaging.DependencyInjection.Kafka.csproj │ └── KafkaMessagingBuilderExtensions.cs ├── Greentube.Messaging.DependencyInjection.Redis │ ├── Greentube.Messaging.DependencyInjection.Redis.csproj │ └── RedisMessagingBuilderExtensions.cs ├── Greentube.Messaging.DependencyInjection │ ├── DefaultMessagingOptionsSetup.cs │ ├── DiscoveryOptions.cs │ ├── Greentube.Messaging.DependencyInjection.csproj │ ├── MessagingBuilder.cs │ ├── MessagingServiceCollectionExtensions.cs │ └── Properties │ │ └── AssemblyInfo.cs ├── Greentube.Messaging.Kafka │ ├── Greentube.Messaging.Kafka.csproj │ ├── IKafkaConsumer.cs │ ├── IKafkaProducer.cs │ ├── KafkaBlockingRawMessageReader.cs │ ├── KafkaBlockingRawMessageReaderFactory.cs │ ├── KafkaConsumerAdapter.cs │ ├── KafkaOptions.cs │ ├── KafkaProducerAdapter.cs │ ├── KafkaProperties.cs │ ├── KafkaRawMessagePublisher.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── PublisherOptions.cs │ └── SubscriberOptions.cs ├── Greentube.Messaging.Redis │ ├── Greentube.Messaging.Redis.csproj │ ├── RedisRawMessageHandlerSubscriber.cs │ └── RedisRawMessagePublisher.cs └── Greentube.Messaging │ ├── BlockingReaderRawMessageHandlerSubscriber.cs │ ├── DispatchingRawMessageHandler.cs │ ├── Greentube.Messaging.csproj │ ├── IBlockingRawMessageReader.cs │ ├── IBlockingRawMessageReaderFactory.cs │ ├── IMessageHandler.cs │ ├── IMessageHandlerInfoProvider.cs │ ├── IMessageHandlerInvoker.cs │ ├── IMessagePublisher.cs │ ├── IMessageTypeTopicMap.cs │ ├── IPollingOptions.cs │ ├── IRawMessageHandler.cs │ ├── IRawMessageHandlerSubscriber.cs │ ├── IRawMessagePublisher.cs │ ├── MessageHandlerInfoProvider.cs │ ├── MessageHandlerInvoker.cs │ ├── MessageTypeTopicMap.cs │ ├── MessagingOptions.cs │ ├── PollingReaderOptions.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── SerializedMessagePublisher.cs └── test ├── Greentube.Messaging.DependencyInjection.Kafka.Tests ├── Greentube.Messaging.DependencyInjection.Kafka.Tests.csproj └── KafkaMessagingBuilderExtensionsTests.cs ├── Greentube.Messaging.DependencyInjection.Redis.Tests ├── Greentube.Messaging.DependencyInjection.Redis.Tests.csproj └── RedisMessagingBuilderExtensionsTests.cs ├── Greentube.Messaging.DependencyInjection.Tests ├── Greentube.Messaging.DependencyInjection.Tests.csproj ├── MessagingBuilderTests.cs └── MessagingServiceCollectionExtensionsTests.cs ├── Greentube.Messaging.Kafka.Tests ├── Greentube.Messaging.Kafka.Tests.csproj ├── KafkaBlockingRawMessageReaderFactoryTests.cs ├── KafkaBlockingRawMessageReaderTests.cs ├── KafkaPropertiesTests.cs └── KafkaRawMessagePublisherTests.cs ├── Greentube.Messaging.Redis.Tests ├── Greentube.Messaging.Redis.Tests.csproj ├── RedisRawMessageHandlerSubscriberTests.cs └── RedisRawMessagePublisherTests.cs ├── Greentube.Messaging.Testing ├── AutoSubstituteDataAttribute.cs └── Greentube.Messaging.Testing.csproj └── Greentube.Messaging.Tests ├── BlockingReaderRawMessageHandlerSubscriberTests.cs ├── DispatchingRawMessageHandlerTests.cs ├── Greentube.Messaging.Tests.csproj ├── IDisposableBlockingRawMessageReader.cs ├── MessageHandlerInfoProviderTests.cs ├── MessageHandlerInvokerTests.cs ├── MessageTypeTopicMapTests.cs └── SerializedMessagePublisherTests.cs /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '1.0.0-alpha-{build}' 2 | init: 3 | - git config --global core.autocrlf true 4 | # If there's a tag, use that as the version. 5 | - ps: >- 6 | if($env:APPVEYOR_REPO_TAG -eq "true"){Update-AppveyorBuild -Version "$env:APPVEYOR_REPO_TAG_NAME"} 7 | # Will build dependencies in release (optimize and portable pdbs) mode: 8 | build_script: 9 | - cmd: for /f %%a in ('dir /b test') do dotnet test -c Release test/%%a/%%a.csproj 10 | after_build: 11 | # packing the current Release build of all projects under src/ 12 | - cmd: for /f %%a in ('dir /b src ^| find /v "Greentube.Messaging.All"') do dotnet pack --no-build -c Release src/%%a/%%a.csproj 13 | # pack the metapackage which is based on nuspec 14 | - cmd: nuget pack -version %APPVEYOR_BUILD_VERSION% src\Greentube.Messaging.All\Greentube.Messaging.All.nuspec 15 | # Will build in Coverage mode (full pdbs) and upload coverage to Codecov 16 | on_success: 17 | - ps: .\coverage.ps1 -UploadCodecov 18 | environment: 19 | global: 20 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 21 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 22 | test: off 23 | os: Visual Studio 2017 24 | dotnet_csproj: 25 | patch: true 26 | file: 'src\**\*.csproj' 27 | version: '{version}' 28 | package_version: '{version}' 29 | artifacts: 30 | - path: '**\*.nupkg' 31 | name: messaging-nuget-packages 32 | # builds on tags will publish all nupkgs to GitHub as a Draft release 33 | deploy: 34 | release: $(appveyor_build_version) 35 | provider: GitHub 36 | auth_token: 37 | secure: DsKyNX5x7EJOCaRUpZu17qwOfpd/NWaZzmQd0aE62nBAGkqI3nASvvHfvoQTjL8y 38 | artifact: /.*\.nupkg/ 39 | draft: true 40 | on: 41 | appveyor_repo_tag: true -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | # keep track of overall coverage to show diffs 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | enabled: yes 7 | target: 75% -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs/ 3 | .vscode/ 4 | *.user 5 | bin/ 6 | obj/ 7 | coverage/ 8 | packages/ 9 | coverage-results.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: false 3 | mono: none 4 | os: 5 | - linux 6 | - osx 7 | osx_image: xcode8.1 8 | dotnet: 2.0.0 9 | dist: trusty 10 | env: 11 | global: 12 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true 13 | - DOTNET_CLI_TELEMETRY_OPTOUT=1 14 | script: 15 | - ulimit -n 512; dotnet restore && ls test/**/*.csproj | grep -v 'Greentube.Messaging.Testing' | xargs -L1 dotnet test -c Release 16 | -------------------------------------------------------------------------------- /Greentube.Messaging.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2005 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FD29A32D-EE38-4C99-B172-8F830AEE3E07}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{4E92BAA4-3800-4290-BB6E-752437B37031}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4F495D2F-83E8-4BC6-88B5-404C3EC8D914}" 11 | ProjectSection(SolutionItems) = preProject 12 | .appveyor.yml = .appveyor.yml 13 | .codecov.yml = .codecov.yml 14 | .gitignore = .gitignore 15 | .travis.yml = .travis.yml 16 | coverage.ps1 = coverage.ps1 17 | global.json = global.json 18 | README.md = README.md 19 | EndProjectSection 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging", "src\Greentube.Messaging\Greentube.Messaging.csproj", "{B6BCB723-BCCD-4AB0-8947-28203FA4A76B}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.DependencyInjection", "src\Greentube.Messaging.DependencyInjection\Greentube.Messaging.DependencyInjection.csproj", "{C06FAF2A-0741-4D66-AC44-B6DA022436B3}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.DependencyInjection.Kafka", "src\Greentube.Messaging.DependencyInjection.Kafka\Greentube.Messaging.DependencyInjection.Kafka.csproj", "{293B140B-060A-437A-A0E5-8470958DB094}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.DependencyInjection.Redis", "src\Greentube.Messaging.DependencyInjection.Redis\Greentube.Messaging.DependencyInjection.Redis.csproj", "{9256485D-ACE3-4B8B-B542-A320D18D1B54}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Kafka", "src\Greentube.Messaging.Kafka\Greentube.Messaging.Kafka.csproj", "{B0996A73-A7C4-439A-8ABB-10C5508E38FE}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Redis", "src\Greentube.Messaging.Redis\Greentube.Messaging.Redis.csproj", "{A69AFF09-CBD4-4D6F-9783-389D3A9A512C}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Sample", "samples\Greentube.Messaging.Sample\Greentube.Messaging.Sample.csproj", "{C3CD6754-24CB-4922-93ED-1BD5AD244585}" 34 | EndProject 35 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Sample.Kafka", "samples\Greentube.Messaging.Sample.Kafka\Greentube.Messaging.Sample.Kafka.csproj", "{654AEC31-045A-4A7B-9B41-6410FFE11B3D}" 36 | EndProject 37 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Sample.Redis", "samples\Greentube.Messaging.Sample.Redis\Greentube.Messaging.Sample.Redis.csproj", "{0420F7B3-36A8-4892-83FC-D6A502A294DE}" 38 | EndProject 39 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{454C1E74-61F0-4E54-9ABA-9BD1711B5A83}" 40 | EndProject 41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Tests", "test\Greentube.Messaging.Tests\Greentube.Messaging.Tests.csproj", "{651DDE93-D051-4080-850C-D74D7978DB92}" 42 | EndProject 43 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Redis.Tests", "test\Greentube.Messaging.Redis.Tests\Greentube.Messaging.Redis.Tests.csproj", "{8F6C83BC-6206-41DB-B717-88ADDC9797B2}" 44 | EndProject 45 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Kafka.Tests", "test\Greentube.Messaging.Kafka.Tests\Greentube.Messaging.Kafka.Tests.csproj", "{1F1F187F-4DEB-495F-B011-C965427A0528}" 46 | EndProject 47 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.DependencyInjection.Kafka.Tests", "test\Greentube.Messaging.DependencyInjection.Kafka.Tests\Greentube.Messaging.DependencyInjection.Kafka.Tests.csproj", "{153A57C7-28EB-42A6-A08B-AA5CA641EBB4}" 48 | EndProject 49 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.DependencyInjection.Redis.Tests", "test\Greentube.Messaging.DependencyInjection.Redis.Tests\Greentube.Messaging.DependencyInjection.Redis.Tests.csproj", "{A7A3FC56-3F9F-4499-810E-2F34F8151FAE}" 50 | EndProject 51 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.DependencyInjection.Tests", "test\Greentube.Messaging.DependencyInjection.Tests\Greentube.Messaging.DependencyInjection.Tests.csproj", "{DA424882-0DA9-42AA-9AC7-D7347FD53A1A}" 52 | EndProject 53 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greentube.Messaging.Testing", "test\Greentube.Messaging.Testing\Greentube.Messaging.Testing.csproj", "{CB19D65C-571C-41D3-828C-0BCC86DAC252}" 54 | EndProject 55 | Global 56 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 57 | Debug|Any CPU = Debug|Any CPU 58 | Release|Any CPU = Release|Any CPU 59 | EndGlobalSection 60 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 61 | {B6BCB723-BCCD-4AB0-8947-28203FA4A76B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {B6BCB723-BCCD-4AB0-8947-28203FA4A76B}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {B6BCB723-BCCD-4AB0-8947-28203FA4A76B}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {B6BCB723-BCCD-4AB0-8947-28203FA4A76B}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {C06FAF2A-0741-4D66-AC44-B6DA022436B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {C06FAF2A-0741-4D66-AC44-B6DA022436B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {C06FAF2A-0741-4D66-AC44-B6DA022436B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {C06FAF2A-0741-4D66-AC44-B6DA022436B3}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {293B140B-060A-437A-A0E5-8470958DB094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {293B140B-060A-437A-A0E5-8470958DB094}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {293B140B-060A-437A-A0E5-8470958DB094}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {293B140B-060A-437A-A0E5-8470958DB094}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {9256485D-ACE3-4B8B-B542-A320D18D1B54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {9256485D-ACE3-4B8B-B542-A320D18D1B54}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {9256485D-ACE3-4B8B-B542-A320D18D1B54}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {9256485D-ACE3-4B8B-B542-A320D18D1B54}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {B0996A73-A7C4-439A-8ABB-10C5508E38FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {B0996A73-A7C4-439A-8ABB-10C5508E38FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {B0996A73-A7C4-439A-8ABB-10C5508E38FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {B0996A73-A7C4-439A-8ABB-10C5508E38FE}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {A69AFF09-CBD4-4D6F-9783-389D3A9A512C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 82 | {A69AFF09-CBD4-4D6F-9783-389D3A9A512C}.Debug|Any CPU.Build.0 = Debug|Any CPU 83 | {A69AFF09-CBD4-4D6F-9783-389D3A9A512C}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {A69AFF09-CBD4-4D6F-9783-389D3A9A512C}.Release|Any CPU.Build.0 = Release|Any CPU 85 | {C3CD6754-24CB-4922-93ED-1BD5AD244585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 86 | {C3CD6754-24CB-4922-93ED-1BD5AD244585}.Debug|Any CPU.Build.0 = Debug|Any CPU 87 | {C3CD6754-24CB-4922-93ED-1BD5AD244585}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {C3CD6754-24CB-4922-93ED-1BD5AD244585}.Release|Any CPU.Build.0 = Release|Any CPU 89 | {654AEC31-045A-4A7B-9B41-6410FFE11B3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 90 | {654AEC31-045A-4A7B-9B41-6410FFE11B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU 91 | {654AEC31-045A-4A7B-9B41-6410FFE11B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU 92 | {654AEC31-045A-4A7B-9B41-6410FFE11B3D}.Release|Any CPU.Build.0 = Release|Any CPU 93 | {0420F7B3-36A8-4892-83FC-D6A502A294DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 94 | {0420F7B3-36A8-4892-83FC-D6A502A294DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 95 | {0420F7B3-36A8-4892-83FC-D6A502A294DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 96 | {0420F7B3-36A8-4892-83FC-D6A502A294DE}.Release|Any CPU.Build.0 = Release|Any CPU 97 | {651DDE93-D051-4080-850C-D74D7978DB92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 98 | {651DDE93-D051-4080-850C-D74D7978DB92}.Debug|Any CPU.Build.0 = Debug|Any CPU 99 | {651DDE93-D051-4080-850C-D74D7978DB92}.Release|Any CPU.ActiveCfg = Release|Any CPU 100 | {651DDE93-D051-4080-850C-D74D7978DB92}.Release|Any CPU.Build.0 = Release|Any CPU 101 | {8F6C83BC-6206-41DB-B717-88ADDC9797B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 102 | {8F6C83BC-6206-41DB-B717-88ADDC9797B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 103 | {8F6C83BC-6206-41DB-B717-88ADDC9797B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 104 | {8F6C83BC-6206-41DB-B717-88ADDC9797B2}.Release|Any CPU.Build.0 = Release|Any CPU 105 | {1F1F187F-4DEB-495F-B011-C965427A0528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 106 | {1F1F187F-4DEB-495F-B011-C965427A0528}.Debug|Any CPU.Build.0 = Debug|Any CPU 107 | {1F1F187F-4DEB-495F-B011-C965427A0528}.Release|Any CPU.ActiveCfg = Release|Any CPU 108 | {1F1F187F-4DEB-495F-B011-C965427A0528}.Release|Any CPU.Build.0 = Release|Any CPU 109 | {153A57C7-28EB-42A6-A08B-AA5CA641EBB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 110 | {153A57C7-28EB-42A6-A08B-AA5CA641EBB4}.Debug|Any CPU.Build.0 = Debug|Any CPU 111 | {153A57C7-28EB-42A6-A08B-AA5CA641EBB4}.Release|Any CPU.ActiveCfg = Release|Any CPU 112 | {153A57C7-28EB-42A6-A08B-AA5CA641EBB4}.Release|Any CPU.Build.0 = Release|Any CPU 113 | {A7A3FC56-3F9F-4499-810E-2F34F8151FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 114 | {A7A3FC56-3F9F-4499-810E-2F34F8151FAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 115 | {A7A3FC56-3F9F-4499-810E-2F34F8151FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 116 | {A7A3FC56-3F9F-4499-810E-2F34F8151FAE}.Release|Any CPU.Build.0 = Release|Any CPU 117 | {DA424882-0DA9-42AA-9AC7-D7347FD53A1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 118 | {DA424882-0DA9-42AA-9AC7-D7347FD53A1A}.Debug|Any CPU.Build.0 = Debug|Any CPU 119 | {DA424882-0DA9-42AA-9AC7-D7347FD53A1A}.Release|Any CPU.ActiveCfg = Release|Any CPU 120 | {DA424882-0DA9-42AA-9AC7-D7347FD53A1A}.Release|Any CPU.Build.0 = Release|Any CPU 121 | {CB19D65C-571C-41D3-828C-0BCC86DAC252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 122 | {CB19D65C-571C-41D3-828C-0BCC86DAC252}.Debug|Any CPU.Build.0 = Debug|Any CPU 123 | {CB19D65C-571C-41D3-828C-0BCC86DAC252}.Release|Any CPU.ActiveCfg = Release|Any CPU 124 | {CB19D65C-571C-41D3-828C-0BCC86DAC252}.Release|Any CPU.Build.0 = Release|Any CPU 125 | EndGlobalSection 126 | GlobalSection(SolutionProperties) = preSolution 127 | HideSolutionNode = FALSE 128 | EndGlobalSection 129 | GlobalSection(NestedProjects) = preSolution 130 | {B6BCB723-BCCD-4AB0-8947-28203FA4A76B} = {FD29A32D-EE38-4C99-B172-8F830AEE3E07} 131 | {C06FAF2A-0741-4D66-AC44-B6DA022436B3} = {FD29A32D-EE38-4C99-B172-8F830AEE3E07} 132 | {293B140B-060A-437A-A0E5-8470958DB094} = {FD29A32D-EE38-4C99-B172-8F830AEE3E07} 133 | {9256485D-ACE3-4B8B-B542-A320D18D1B54} = {FD29A32D-EE38-4C99-B172-8F830AEE3E07} 134 | {B0996A73-A7C4-439A-8ABB-10C5508E38FE} = {FD29A32D-EE38-4C99-B172-8F830AEE3E07} 135 | {A69AFF09-CBD4-4D6F-9783-389D3A9A512C} = {FD29A32D-EE38-4C99-B172-8F830AEE3E07} 136 | {C3CD6754-24CB-4922-93ED-1BD5AD244585} = {4E92BAA4-3800-4290-BB6E-752437B37031} 137 | {654AEC31-045A-4A7B-9B41-6410FFE11B3D} = {4E92BAA4-3800-4290-BB6E-752437B37031} 138 | {0420F7B3-36A8-4892-83FC-D6A502A294DE} = {4E92BAA4-3800-4290-BB6E-752437B37031} 139 | {651DDE93-D051-4080-850C-D74D7978DB92} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 140 | {8F6C83BC-6206-41DB-B717-88ADDC9797B2} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 141 | {1F1F187F-4DEB-495F-B011-C965427A0528} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 142 | {153A57C7-28EB-42A6-A08B-AA5CA641EBB4} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 143 | {A7A3FC56-3F9F-4499-810E-2F34F8151FAE} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 144 | {DA424882-0DA9-42AA-9AC7-D7347FD53A1A} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 145 | {CB19D65C-571C-41D3-828C-0BCC86DAC252} = {454C1E74-61F0-4E54-9ABA-9BD1711B5A83} 146 | EndGlobalSection 147 | GlobalSection(ExtensibilityGlobals) = postSolution 148 | SolutionGuid = {ECC8E5D6-3F47-4B40-A007-E12C770C9F45} 149 | EndGlobalSection 150 | EndGlobal 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # messaging [![Build Status](https://travis-ci.org/Greentube/messaging.svg?branch=master)](https://travis-ci.org/Greentube/messaging) [![Build status](https://ci.appveyor.com/api/projects/status/a3pstjg357fn8it9/branch/master?svg=true)](https://ci.appveyor.com/project/Greentube/messaging) [![codecov](https://codecov.io/gh/Greentube/messaging/branch/master/graph/badge.svg)](https://codecov.io/gh/Greentube/messaging) 2 | 3 | An opinionated messaging library for simple pub/sub with different serialization and message broker/middleware. 4 | 5 | To use this library, you need to decide on which [serialization](#serialization) and [messaging middleware](#messaging-middleware) to use. 6 | 7 | ## Samples 8 | 9 | This repository includes two samples: 10 | 11 | * [Redis with Json serialization](https://github.com/Greentube/messaging/tree/master/samples/Greentube.Messaging.Sample.Redis) 12 | * [Kafka with ProtoBuf serialization](https://github.com/Greentube/messaging/tree/master/samples/Greentube.Messaging.Sample.Kafka) 13 | 14 | ## Example publishing and handling a message on an ASP.NET Core application 15 | 16 | ```csharp 17 | // Class without annotations 18 | public class SomeMessage 19 | { 20 | public string Body { get; set; } 21 | } 22 | 23 | // Handling messages: Class implementing IMessageHandler to be invoked when T arrives 24 | public class SomeMessageHandler : IMessageHandler 25 | { 26 | public Task Handle(SomeMessage message, CancellationToken _) 27 | { 28 | Console.WriteLine($"Handled: {message}."); 29 | return Task.CompletedTask; 30 | } 31 | } 32 | 33 | // Startup.cs 34 | public void ConfigureServices(IServiceCollection services) 35 | { 36 | services.AddMessaging(builder => builder 37 | .AddRedis() 38 | .AddSerialization(b => b.AddProtoBuf()) 39 | .AddTopic("some.topic")) 40 | } 41 | public void Configure(IApplicationBuilder app) 42 | { 43 | app.UseMessagingSubscriptions(); // Subscribes to the topics defined via Services 44 | } 45 | 46 | // Publishing via ASP.NET Core MVC: 47 | [Route("some-message")] 48 | public class SomeMessageController 49 | { 50 | private readonly IMessagePublisher _publisher; 51 | public SomeMessageController(IMessagePublisher publisher) => _publisher; 52 | 53 | [HttpPut] 54 | public async Task PublishSomeMessage([FromBody] SomeMessage message, CancellationToken token) 55 | { 56 | await _publisher.Publish(message, token); 57 | return Accepted(); 58 | } 59 | } 60 | ``` 61 | 62 | ## Highlights 63 | 64 | * Supports multiple serialization formats and messaging middlewares. 65 | * Message handlers automatically discovered, registered 66 | * Handlers are resolved through DI with configurable lifetimes 67 | * Handler invocation done with [Expression trees](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/) 68 | * Doesn't require implementing any interfaces or declaring attributes 69 | 70 | ## Messaging middleware 71 | 72 | The current supported messaging systems are: 73 | 74 | * [Redis](https://redis.io/topics/pubsub) 75 | * [Apache Kafka](https://kafka.apache.org/) 76 | 77 | ## Serialization 78 | 79 | The supported serialization formats are: 80 | 81 | * MessagePack - with [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp) 82 | * ProtoBuf - with [protobuf-net](https://github.com/mgravell/protobuf-net) 83 | * JSON - with [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) 84 | * XML - with [System.Xml.XmlSerializer](https://github.com/dotnet/corefx/tree/master/src/System.Xml.XmlSerializer) 85 | 86 | All of the above can be 'plugged-in' with a single line when using the Messaging.DependencyInjection.* packages. 87 | Example serialization setup: 88 | 89 | ```csharp 90 | services.AddMessaging(builder => 91 | builder.AddSerialization(s => 92 | { 93 | s.AddMessagePack(); 94 | // or 95 | s.AddProtoBuf(); 96 | // or 97 | s.AddJson(); 98 | // or 99 | s.AddXml(); 100 | }) 101 | ``` 102 | 103 | Each implementation has some additional settings. 104 | 105 | **For more information on serialization, please refer the [Greentube.Serialization](https://github.com/Greentube/serialization) repository.** 106 | 107 | ## Discovering and Invoking handlers 108 | 109 | By default. Implementations of `IMessageHandler` are discovered and registered dynamically. 110 | When classes are in assemblies other than the entry assembly or the assemblies where `TMessage` is defined, 111 | the library might need some help to find them. Similar to the [MVC Application parts](https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/app-parts) concept. 112 | 113 | Only `public` implementations of `IMessageHandler` will be automatically registered by default. 114 | 115 | Your handlers will be created via DI. That means you can take dependencies via the constructor but mind the lifetime: 116 | The lifetime of the automatically registered Handlers is `Transient`. Each time a message arrives, the library will ask the container 117 | for a message handler. Something like: `provider.GetService>();` for each call. 118 | It's advisable you switch to Singleton lifetime in case your handlers are thread-safe. 119 | 120 | The characteristics mentioned above can be customized with: 121 | 122 | ```csharp 123 | builder.AddHandlerDiscovery(d => 124 | { 125 | d.IncludeNonPublic = true; 126 | d.DiscoveredHandlersLifetime = ServiceLifetime.Singleton; 127 | d.MessageHandlerAssemblies.Add(typeof(SomeMessage).Assembly); 128 | }); 129 | ``` 130 | 131 | ## Features planned 132 | 133 | * Add Message to Topic name map via Attributes 134 | 135 | ## Limitations 136 | 137 | Currently it only supports a single Handler per Message type. You can easily work around it by implementing some 138 | composite pattern on your Handler by dispatching a call to multiple, pontentially in multiple threads, etc. 139 | We can consider adding such support if there's demand. 140 | -------------------------------------------------------------------------------- /coverage.ps1: -------------------------------------------------------------------------------- 1 | # Run locally to see html report: powershell ./coverage.ps1 -generateReport 2 | # To publish results to Codecov, set var: CODECOV_TOKEN, powershell ./coverage.ps1 -uploadCodecov 3 | # appveyor has variable CODECOV_TOKEN set 4 | 5 | Param ( 6 | [switch] $generateReport, 7 | [switch] $uploadCodecov 8 | ) 9 | 10 | $currentPath = Split-Path $MyInvocation.MyCommand.Path 11 | $coverageOutputDirectory = Join-Path $currentPath "coverage" 12 | $coverageFile = "coverage-results.xml" 13 | 14 | Remove-Item $coverageOutputDirectory -Force -Recurse -ErrorAction SilentlyContinue 15 | Remove-Item $coverageFile -ErrorAction SilentlyContinue 16 | 17 | nuget install -Verbosity quiet -OutputDirectory packages -Version 4.6.519 OpenCover 18 | 19 | $openCoverConsole = "packages\OpenCover.4.6.519\tools\OpenCover.Console.exe" 20 | # OpenCover currently not supporting portable pdbs (https://github.com/OpenCover/opencover/issues/601) 21 | $configuration = "Coverage" 22 | 23 | Get-ChildItem -Filter .\test\ | 24 | ForEach-Object { 25 | $csprojPath = $_.FullName 26 | $testProjectName = $_.Name 27 | $projectName = $testProjectName -replace ".{6}$" 28 | cmd.exe /c $openCoverConsole ` 29 | -target:"c:\Program Files\dotnet\dotnet.exe" ` 30 | -targetargs:"test -c $configuration $csprojPath\$testProjectName.csproj" ` 31 | -mergeoutput ` 32 | -hideskipped:File ` 33 | -output:$coverageFile ` 34 | -oldStyle ` 35 | -filter:"+[$projectName]* -[$testProjectName]* -[Greentube.Messaging]*Attribute -[xunit*]*" ` 36 | -searchdirs:"$csprojPath\bin\$configuration\netcoreapp2.0\" ` 37 | -register:user 38 | } 39 | 40 | If ($generateReport) { 41 | nuget install -Verbosity quiet -OutputDirectory packages -Version 3.0.2 ReportGenerator 42 | $reportGenerator = "packages\ReportGenerator.3.0.2\tools\ReportGenerator.exe" 43 | cmd.exe /c $reportGenerator ` 44 | -reports:$coverageFile ` 45 | -targetdir:$coverageOutputDirectory ` 46 | -verbosity:Error 47 | } 48 | 49 | # requires variable set: CODECOV_TOKEN 50 | If ($uploadCodeCov) { 51 | nuget install -Verbosity quiet -OutputDirectory packages -Version 1.0.3 Codecov 52 | $Codecov = "packages\Codecov.1.0.3\tools\Codecov.exe" 53 | cmd.exe /c $Codecov -f $coverageFile 54 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.0.3" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Kafka/Greentube.Messaging.Sample.Kafka.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Kafka/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using ProtoBuf.Meta; 5 | 6 | namespace Greentube.Messaging.Sample.Kafka 7 | { 8 | public class Program 9 | { 10 | static void Main() 11 | { 12 | using (var app = new SomeApp(s => 13 | s.AddMessaging(builder => builder 14 | .AddTopic("some.topic") 15 | .AddTopic("some.topic.other") 16 | .AddSerialization(b => b.AddProtoBuf()) 17 | .AddKafka(o => 18 | { 19 | o.Properties.BrokerList = "localhost:9092"; 20 | o.Properties.GroupId = "Sample-GroupId"; 21 | o.Properties.Add("socket.timeout.ms", 1000); // sample unmapped setting 22 | o.Subscriber.ConsumerCreatedCallback = 23 | consumer => consumer.OnError += (sender, error) 24 | => Console.WriteLine($"Consumer error: ${error}"); 25 | }) 26 | .ConfigureOptions(o => { }) 27 | .AddHandlerDiscovery(d => 28 | { 29 | d.IncludeNonPublic = true; 30 | d.DiscoveredHandlersLifetime = ServiceLifetime.Singleton; 31 | d.MessageHandlerAssemblies.Add(typeof(SomeMessage).Assembly); 32 | }) 33 | ))) 34 | { 35 | // Adds proto definition to the type (another option is to add [ProtoContract] to the class directly) 36 | RuntimeTypeModel.Default.Add(typeof(SomeMessage), false).Add(1, nameof(SomeMessage.Body)); 37 | RuntimeTypeModel.Default.Add(typeof(SomeOtherMessage), false).Add(1, nameof(SomeOtherMessage.Number)); 38 | 39 | var publisher = app.Run(); 40 | for (int i = 0; i < 3; i++) 41 | { 42 | publisher.Publish(new SomeOtherMessage { Number = i }, CancellationToken.None); 43 | } 44 | } // Graceful shutdown 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Kafka/SomeOtherMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Greentube.Messaging.Sample.Kafka 2 | { 3 | public class SomeOtherMessage 4 | { 5 | public int Number { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Kafka/SomeOtherMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Greentube.Messaging.Sample.Kafka 6 | { 7 | // Will be discovered (auto-registered) 8 | internal class SomeOtherMessageHandler : IMessageHandler 9 | { 10 | public Task Handle(SomeOtherMessage message, CancellationToken _) 11 | { 12 | Console.WriteLine($"Some other handler Handled SomeOtherMessage: #{message.Number}"); 13 | return Task.CompletedTask; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Kafka/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | kafka-zookeeper: 4 | image: spotify/kafka 5 | ports: 6 | - "2181:2181" 7 | - "9092:9092" 8 | environment: 9 | ADVERTISED_HOST: "localhost" 10 | ADVERTISED_PORT: 9092 11 | AUTO_CREATE_TOPICS: "true" -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Redis/Greentube.Messaging.Sample.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Redis/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using StackExchange.Redis; 3 | 4 | namespace Greentube.Messaging.Sample.Redis 5 | { 6 | public class Program 7 | { 8 | static void Main() 9 | { 10 | using (var app = new SomeApp(s => s 11 | .AddMessaging(builder => builder 12 | .AddRedis() 13 | .AddSerialization(b => b.AddJson()) 14 | .AddTopic("topic")) 15 | .AddSingleton( 16 | ConnectionMultiplexer.Connect("localhost:6379")))) 17 | { 18 | app.Run(); 19 | } // Graceful shutdown 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample.Redis/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | redis: 4 | image: "redis:alpine" 5 | ports: 6 | - "6379:6379" -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample/Greentube.Messaging.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample/SomeApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Greentube.Messaging.Sample 6 | { 7 | public class SomeApp : IDisposable 8 | { 9 | private readonly ServiceProvider _serviceProvider; 10 | 11 | public SomeApp(Action servicesAction) 12 | { 13 | // ConfigureServices() 14 | var services = new ServiceCollection(); 15 | 16 | // Sample specific setup: .AddMessaging() 17 | servicesAction(services); 18 | 19 | services.AddOptions(); 20 | 21 | _serviceProvider = services.BuildServiceProvider(); 22 | 23 | // Configure() 24 | // app.AddMessagingSubscriptions() 25 | var map = _serviceProvider.GetRequiredService(); 26 | var rawSubscriber = _serviceProvider.GetRequiredService(); 27 | var rawHandler = _serviceProvider.GetRequiredService(); 28 | foreach (var topic in map.GetTopics()) 29 | { 30 | rawSubscriber.Subscribe(topic, rawHandler, CancellationToken.None) 31 | .GetAwaiter() 32 | .GetResult(); 33 | } 34 | } 35 | 36 | public IMessagePublisher Run() 37 | { 38 | var publisher = _serviceProvider.GetRequiredService(); 39 | 40 | Console.WriteLine("Publishing 'SomeMessage'..."); 41 | publisher.Publish(new SomeMessage { Body = $"{DateTime.Now} Some message body: {Guid.NewGuid()}" }, CancellationToken.None) 42 | .GetAwaiter() 43 | .GetResult(); 44 | 45 | return publisher; 46 | } 47 | 48 | public void Dispose() 49 | { 50 | Console.WriteLine("Running... Press any key to quit."); 51 | Console.ReadKey(); 52 | 53 | _serviceProvider.Dispose(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample/SomeMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Greentube.Messaging.Sample 2 | { 3 | // Message, POCO class 4 | public class SomeMessage 5 | { 6 | public string Body { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Greentube.Messaging.Sample/SomeMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Greentube.Messaging.Sample 6 | { 7 | // Class implementing IMessageHandler to be invoked when T arrives 8 | public class SomeMessageHandler : IMessageHandler 9 | { 10 | public Task Handle(SomeMessage message, CancellationToken _) 11 | { 12 | Console.WriteLine($"Handled: {message.Body}."); 13 | return Task.CompletedTask; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.All/Greentube.Messaging.All.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | Greentube.Messaging.All 4 | $version$ 5 | Bruno Garcia 6 | Bruno Garcia 7 | Metapackage which includes all Greentube Messaging packages. 8 | https://github.com/Greentube/messaging 9 | Metapackage which includes all Greentube Messaging packages. 10 | messaging;pubsub;publish-subscribe;redis;kafka 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.All/README.md: -------------------------------------------------------------------------------- 1 | # Greentube.Messaging.All metapackage [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.All.svg)](https://www.nuget.org/packages/Greentube.Messaging.All/) 2 | 3 | A metapackage to include all abstractions, implementations and DI extensions. 4 | 5 | It does so by depending on all of the following packages: 6 | 7 | * [Greentube.Messaging](https://github.com/Greentube/Messaging/tree/master/src/Greentube.Messaging) [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.svg)](https://www.nuget.org/packages/Greentube.Messaging/) 8 | * [Greentube.Messaging.Redis](https://github.com/Greentube/Messaging/tree/master/src/Greentube.Messaging.Redis) [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.Redis.svg)](https://www.nuget.org/packages/Greentube.Messaging.Redis/) 9 | * [Greentube.Messaging.Kafka](https://github.com/Greentube/Messaging/tree/master/src/Greentube.Messaging.Kafka) [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.Kafka.svg)](https://www.nuget.org/packages/Greentube.Messaging.Kafka/) 10 | * [Greentube.Messaging.DependencyInjection](https://github.com/Greentube/Messaging/tree/master/src/Greentube.Messaging.DependencyInjection) [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.DependencyInjection.svg)](https://www.nuget.org/packages/Greentube.Messaging.DependencyInjection/) 11 | * [Greentube.Messaging.DependencyInjection.Redis](https://github.com/Greentube/Messaging/tree/master/src/Greentube.Messaging.DependencyInjection.Redis) [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.DependencyInjection.Redis.svg)](https://www.nuget.org/packages/Greentube.Messaging.DependencyInjection.Redis/) 12 | * [Greentube.Messaging.DependencyInjection.Kafka](https://github.com/Greentube/Messaging/tree/master/src/Greentube.Messaging.DependencyInjection.Kafka) [![NuGet](https://img.shields.io/nuget/v/Greentube.Messaging.DependencyInjection.Kafka.svg)](https://www.nuget.org/packages/Greentube.Messaging.DependencyInjection.Kafka/) -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection.Kafka/Greentube.Messaging.DependencyInjection.Kafka.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 0.0.0 5 | Bruno Garcia 6 | Bruno Garcia; Greentube 7 | Extensions to configure Greentube.Messaging.Kafka via Microsoft.Extensions.DependencyInjection. 8 | https://github.com/Greentube/messaging 9 | kafka;dependency-injection;messaging;pubsub;publish-subscribe 10 | true 11 | 12 | 13 | full 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection.Kafka/KafkaMessagingBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Greentube.Messaging; 3 | using Greentube.Messaging.DependencyInjection; 4 | using Greentube.Messaging.Kafka; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Microsoft.Extensions.Options; 7 | 8 | // ReSharper disable once CheckNamespace - Discoverability 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | public static class KafkaMessagingBuilderExtensions 12 | { 13 | /// 14 | /// Adds both Kafka publisher and subscriber 15 | /// 16 | /// Messaging builder 17 | /// Setup Kafka options 18 | /// 19 | public static MessagingBuilder AddKafka(this MessagingBuilder builder, Action actionSetup) 20 | { 21 | builder.Services.Configure(actionSetup); 22 | return builder.AddKafka(); 23 | } 24 | 25 | /// 26 | /// Adds both Kafka publisher and subscriber 27 | /// 28 | /// Messaging builder 29 | /// 30 | public static MessagingBuilder AddKafka(this MessagingBuilder builder) 31 | { 32 | builder.AddKafkaPublisher(); 33 | builder.AddKafkaSubscriber(); 34 | return builder; 35 | } 36 | 37 | /// 38 | /// Register Kafka raw publisher service 39 | /// 40 | /// Messaging builder 41 | /// Setup Kafka options 42 | /// 43 | public static MessagingBuilder AddKafkaPublisher(this MessagingBuilder builder, Action actionSetup) 44 | { 45 | builder.Services.Configure(actionSetup); 46 | builder.AddKafkaPublisher(); 47 | return builder; 48 | } 49 | 50 | /// 51 | /// Register Kafka raw subscriber service 52 | /// 53 | /// Messaging builder 54 | /// Setup Kafka options 55 | /// 56 | public static MessagingBuilder AddKafkaSubscriber(this MessagingBuilder builder, Action actionSetup) 57 | { 58 | builder.Services.Configure(actionSetup); 59 | builder.AddKafkaSubscriber(); 60 | return builder; 61 | } 62 | 63 | /// 64 | /// Register Kafka raw publisher service 65 | /// 66 | /// Messaging builder 67 | /// 68 | public static MessagingBuilder AddKafkaPublisher(this MessagingBuilder builder) 69 | { 70 | builder.Services.TryAddSingleton(c => c.GetRequiredService>().Value); 71 | builder.AddRawMessagePublisher(); 72 | return builder; 73 | } 74 | 75 | /// 76 | /// Register Kafka raw subscriber service 77 | /// 78 | /// Messaging builder 79 | /// 80 | public static MessagingBuilder AddKafkaSubscriber(this MessagingBuilder builder) 81 | { 82 | builder.Services.TryAddSingleton(c => c.GetRequiredService>().Value); 83 | builder.Services.TryAddSingleton, KafkaBlockingRawMessageReaderFactory>(); 84 | builder.AddRawMessageHandlerSubscriber>(); 85 | return builder; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection.Redis/Greentube.Messaging.DependencyInjection.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 0.0.0 5 | Bruno Garcia 6 | Bruno Garcia; Greentube 7 | Extensions to configure Greentube.Messaging.Redis via Microsoft.Extensions.DependencyInjection. 8 | https://github.com/Greentube/messaging 9 | redis;dependency-injection;messaging;pubsub;publish-subscribe 10 | true 11 | 12 | 13 | full 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection.Redis/RedisMessagingBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Greentube.Messaging.DependencyInjection; 2 | using Greentube.Messaging.Redis; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using StackExchange.Redis; 5 | 6 | // ReSharper disable once CheckNamespace - Discoverability 7 | namespace Microsoft.Extensions.DependencyInjection 8 | { 9 | public static class RedisMessagingBuilderExtensions 10 | { 11 | /// 12 | /// Adds both Redis publisher and subscriber 13 | /// 14 | /// 15 | /// These services expect to be registered 16 | /// 17 | /// Messaging builder 18 | /// 19 | public static MessagingBuilder AddRedis(this MessagingBuilder builder) 20 | { 21 | builder.AddRedisPublisher(); 22 | builder.AddRedisSubscriber(); 23 | return builder; 24 | } 25 | 26 | /// 27 | /// Adds both Redis publisher and subscriber with 28 | /// 29 | /// Messaging builder 30 | /// StackExchange.Redis ConnectionMultiplexer 31 | /// 32 | public static MessagingBuilder AddRedis(this MessagingBuilder builder, IConnectionMultiplexer multiplexer) 33 | { 34 | builder.Services.TryAddSingleton(multiplexer); 35 | return builder.AddRedis(); 36 | } 37 | 38 | /// 39 | /// Register Redis raw publisher service 40 | /// 41 | /// Messaging builder 42 | /// 43 | public static MessagingBuilder AddRedisPublisher(this MessagingBuilder builder) 44 | { 45 | builder.AddRawMessagePublisher(); 46 | return builder; 47 | } 48 | 49 | /// 50 | /// Register Redis raw subscriber service 51 | /// 52 | /// Messaging builder 53 | /// 54 | public static MessagingBuilder AddRedisSubscriber(this MessagingBuilder builder) 55 | { 56 | builder.AddRawMessageHandlerSubscriber(); 57 | return builder; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection/DefaultMessagingOptionsSetup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace Greentube.Messaging.DependencyInjection 4 | { 5 | internal class DefaultMessagingOptionsSetup : ConfigureOptions 6 | { 7 | public DefaultMessagingOptionsSetup() 8 | : base(ConfigureOptions) 9 | { 10 | } 11 | 12 | private static void ConfigureOptions(MessagingOptions options) 13 | { 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection/DiscoveryOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Scrutor; 5 | 6 | namespace Greentube.Messaging.DependencyInjection 7 | { 8 | /// 9 | /// Type discovery settings 10 | /// 11 | /// 12 | /// This is no a 'IOptions' class. It drives the behavior of the registration of services 13 | /// 14 | public class DiscoverySettings 15 | { 16 | /// 17 | /// Assemblies to scan for implementations of 18 | /// 19 | public ISet MessageHandlerAssemblies { get; } = new HashSet 20 | { 21 | Assembly.GetEntryAssembly() 22 | }; 23 | /// 24 | /// The lifetime to define all services found via assembly scan 25 | /// 26 | public ServiceLifetime DiscoveredHandlersLifetime { get; set; } = ServiceLifetime.Transient; 27 | /// 28 | /// Include implementations of with non 'public' access modifier 29 | /// 30 | public bool IncludeNonPublic { get; set; } 31 | /// 32 | /// How to handle cases where the service is already registered 33 | /// 34 | /// 35 | /// When discovering handlers in assemblies, it's possible the handler has already been registered. 36 | /// That's the case when the application code has explicitly registered it with custom Lifetime or some decorator pattern. 37 | /// This setting defines what to do in these cases. By default, the discovered handler will NOT be overwriten by the discovered one. 38 | /// 39 | internal RegistrationStrategy RegistrationStrategy { get; set; } = RegistrationStrategy.Skip; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection/Greentube.Messaging.DependencyInjection.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 0.0.0 5 | Bruno Garcia 6 | Bruno Garcia; Greentube 7 | Extensions to configure Greentube.Messaging via Microsoft.Extensions.DependencyInjection. 8 | https://github.com/Greentube/messaging 9 | dependency-injection;messaging;pubsub;publish-subscribe 10 | true 11 | 12 | 13 | full 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection/MessagingBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using Microsoft.Extensions.Options; 6 | using Greentube.Serialization; 7 | using Greentube.Serialization.DependencyInjection; 8 | 9 | namespace Greentube.Messaging.DependencyInjection 10 | { 11 | public class MessagingBuilder 12 | { 13 | public IServiceCollection Services { get; } 14 | private readonly MessageTypeTopicMap _messageTypeTopic = new MessageTypeTopicMap(); 15 | private readonly DiscoverySettings _discoverySettings = new DiscoverySettings(); 16 | 17 | internal MessagingBuilder(IServiceCollection services) 18 | { 19 | Services = services ?? throw new ArgumentNullException(nameof(services)); 20 | } 21 | 22 | public MessagingBuilder ConfigureOptions(Action options) 23 | { 24 | Services.Configure(options); 25 | return this; 26 | } 27 | 28 | public MessagingBuilder AddSerialization(Action builderAction) 29 | { 30 | Services.AddSerialization(builderAction); 31 | return this; 32 | } 33 | 34 | public MessagingBuilder AddRawMessagePublisher(ServiceLifetime lifetime = ServiceLifetime.Singleton) 35 | where TRawMessagePublisher : IRawMessagePublisher 36 | { 37 | Services.Add(ServiceDescriptor.Describe(typeof(IRawMessagePublisher), typeof(TRawMessagePublisher), lifetime)); 38 | return this; 39 | } 40 | 41 | public MessagingBuilder AddRawMessageHandlerSubscriber(ServiceLifetime lifetime = ServiceLifetime.Singleton) 42 | where TRawMessageHandlerSubscriber : IRawMessageHandlerSubscriber 43 | { 44 | Services.Add(ServiceDescriptor.Describe(typeof(IRawMessageHandlerSubscriber), typeof(TRawMessageHandlerSubscriber), lifetime)); 45 | return this; 46 | } 47 | 48 | public MessagingBuilder AddTopic(string topic) 49 | { 50 | _messageTypeTopic.Add(typeof(TMessage), topic); 51 | _discoverySettings.MessageHandlerAssemblies.Add(typeof(TMessage).Assembly); 52 | return this; 53 | } 54 | 55 | public MessagingBuilder AddHandlerDiscovery(Action discoverySettings) 56 | { 57 | discoverySettings(_discoverySettings); 58 | return this; 59 | } 60 | 61 | private void AddHandlerDiscovery() 62 | { 63 | Services.Scan(s => 64 | s.FromAssemblies(_discoverySettings.MessageHandlerAssemblies) 65 | .AddClasses(f => f.AssignableTo(typeof(IMessageHandler<>)), !_discoverySettings.IncludeNonPublic) 66 | .UsingRegistrationStrategy(_discoverySettings.RegistrationStrategy) 67 | .AsImplementedInterfaces() 68 | .WithLifetime(_discoverySettings.DiscoveredHandlersLifetime)); 69 | 70 | Services.TryAddSingleton(_discoverySettings.MessageHandlerAssemblies); 71 | } 72 | 73 | public void Build() 74 | { 75 | var support = VerifyMessagingSupport(); 76 | 77 | // Configuration 78 | Services.TryAddEnumerable(ServiceDescriptor.Transient, DefaultMessagingOptionsSetup>()); 79 | Services.AddSingleton(c => c.GetRequiredService>().Value); 80 | 81 | // Map: used by both Publishing and subscribing 82 | Services.TryAddSingleton(_messageTypeTopic); 83 | 84 | if (support.supportPublishing) 85 | { 86 | Services.TryAddSingleton(); 87 | } 88 | 89 | if (support.supportHandling) 90 | { 91 | Services.TryAddSingleton(); 92 | Services.TryAddSingleton(); 93 | Services.TryAddSingleton(c => 94 | new MessageHandlerInvoker( 95 | c.GetService(), 96 | c.GetService)); 97 | 98 | AddHandlerDiscovery(); 99 | } 100 | } 101 | 102 | private (bool supportPublishing, bool supportHandling) VerifyMessagingSupport() 103 | { 104 | var serializers = Services.Count(s => s.ServiceType == typeof(ISerializer)); 105 | if (serializers == 0) throw new InvalidOperationException($"No serializer has been configured. Call builder.{nameof(AddSerialization)}"); 106 | if (serializers > 1) throw new InvalidOperationException("More than one serializer has been configured. Please define a single one."); 107 | 108 | var rawPublishers = Services.Count(s => s.ServiceType == typeof(IRawMessagePublisher)); 109 | if (rawPublishers > 1) throw new InvalidOperationException("More than one raw publisher has been configured. Please define a single one."); 110 | 111 | var rawHandlerSubscribers = Services.Count(s => s.ServiceType == typeof(IRawMessageHandlerSubscriber)); 112 | if (rawHandlerSubscribers > 1) throw new InvalidOperationException("More than one raw handler subscriber has been configured. Please define a single one."); 113 | 114 | (bool supportPublishing, bool supportHandling) support = (rawPublishers == 1, rawHandlerSubscribers == 1); 115 | 116 | if (!support.supportHandling && !support.supportPublishing) 117 | throw new InvalidOperationException("No raw publisher or raw handler subscriber have been configured. " + 118 | "It won't be possible to either Publish nor Handle messages. " + 119 | $"To Publish messages, call builder.{nameof(AddRawMessagePublisher)} " + 120 | $"To Handle messages, call builder.{nameof(AddRawMessageHandlerSubscriber)}."); 121 | 122 | // No calls to AddTopic and no IMessageTypeTopicMap registered yet, we fail 123 | if (!_messageTypeTopic.Any() && 124 | Services.All(s => s.ServiceType != typeof(IMessageTypeTopicMap))) 125 | { 126 | throw new InvalidOperationException($"Can't build messaging without any topics. Consider calling '{nameof(AddTopic)}' on the builder " + 127 | $"or register your own: {nameof(IMessageTypeTopicMap)} before adding messaging."); 128 | } 129 | 130 | return support; 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection/MessagingServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Greentube.Messaging.DependencyInjection; 3 | 4 | // ReSharper disable once CheckNamespace - Discoverability 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class MessagingServiceCollectionExtensions 8 | { 9 | /// 10 | /// Add Messaging services 11 | /// 12 | /// ServicesCollection 13 | /// Action to handle the messaging builder 14 | /// ServicesCollection 15 | /// 16 | public static ServiceCollection AddMessaging( 17 | this ServiceCollection services, 18 | Action builderAction) 19 | { 20 | if (services == null) throw new ArgumentNullException(nameof(services)); 21 | if (builderAction == null) throw new ArgumentNullException(nameof(builderAction)); 22 | 23 | var builder = new MessagingBuilder(services); 24 | builderAction.Invoke(builder); 25 | builder.Build(); 26 | 27 | return services; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.DependencyInjection/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Tests")] 4 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Redis.Tests")] 5 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Kafka.Tests")] 6 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Serialization.Json.Tests")] 7 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Serialization.ProtoBuf.Tests")] 8 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Serialization.Xml.Tests")] 9 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Serialization.MessagePack.Tests")] 10 | [assembly: InternalsVisibleTo("Greentube.Messaging.DependencyInjection.Serialization.Tests")] -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/Greentube.Messaging.Kafka.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard1.5 4 | 0.0.0 5 | Bruno Garcia 6 | Bruno Garcia; Greentube 7 | Kafka implementation of Greentube.Messaging. 8 | https://github.com/Greentube/messaging 9 | kafka;messaging;pubsub;publish-subscribe 10 | true 11 | 12 | 13 | full 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/IKafkaConsumer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | 4 | namespace Greentube.Messaging.Kafka 5 | { 6 | internal interface IKafkaConsumer : IDisposable 7 | { 8 | Consumer KafkaConsumer { get; } 9 | bool Consume(out Message message, TimeSpan timeout); 10 | void Subscribe(string topic); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/IKafkaProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Confluent.Kafka; 4 | 5 | namespace Greentube.Messaging.Kafka 6 | { 7 | internal interface IKafkaProducer: IDisposable 8 | { 9 | Task> ProduceAsync(string topic, Null key, byte[] val); 10 | int Flush(TimeSpan timeout); 11 | Producer KafkaProducer { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaBlockingRawMessageReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Greentube.Messaging.Kafka 4 | { 5 | /// 6 | /// An Apache Kafka implementation of 7 | /// 8 | /// 9 | /// 10 | public class KafkaBlockingRawMessageReader : IBlockingRawMessageReader, IDisposable 11 | { 12 | private readonly IKafkaConsumer _consumer; 13 | 14 | /// 15 | /// Creates an new instance of 16 | /// 17 | /// 18 | internal KafkaBlockingRawMessageReader(IKafkaConsumer consumer) => 19 | _consumer = consumer ?? throw new ArgumentNullException(nameof(consumer)); 20 | 21 | /// 22 | /// Tries to read a message from the inner Confluent.Consumer implementation 23 | /// 24 | /// The message read if true was returned 25 | /// Kafka options 26 | /// 27 | /// 28 | public bool TryGetMessage(out byte[] message, KafkaOptions options) 29 | { 30 | if (options == null) throw new ArgumentNullException(nameof(options)); 31 | 32 | var read = _consumer.Consume(out var kafkaMessage, options.Subscriber.ConsumeTimeout); 33 | message = read ? kafkaMessage.Value : null; 34 | return read; 35 | } 36 | 37 | /// 38 | public void Dispose() 39 | { 40 | _consumer.Dispose(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaBlockingRawMessageReaderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | using Confluent.Kafka.Serialization; 4 | 5 | namespace Greentube.Messaging.Kafka 6 | { 7 | /// 8 | /// Creates instances of which are already subscribed to the specified topic 9 | /// 10 | /// 11 | public class KafkaBlockingRawMessageReaderFactory : IBlockingRawMessageReaderFactory 12 | { 13 | private static readonly ByteArrayDeserializer Deserializer = new ByteArrayDeserializer(); 14 | 15 | /// 16 | /// Creates a new subscribed already to the specified 17 | /// 18 | /// The topic to subscribe the reader 19 | /// Kafka Options 20 | /// subscribed to 21 | /// 22 | public IBlockingRawMessageReader Create(string topic, KafkaOptions options) 23 | { 24 | IKafkaConsumer ConsumerFunc() => 25 | new KafkaConsumerAdapter(new Consumer(options.Properties, null, Deserializer)); 26 | 27 | return Create(ConsumerFunc, topic, options); 28 | } 29 | 30 | internal IBlockingRawMessageReader Create( 31 | Func consumerFunc, 32 | string topic, 33 | KafkaOptions options) 34 | { 35 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 36 | if (options == null) throw new ArgumentNullException(nameof(options)); 37 | 38 | var consumer = consumerFunc(); 39 | try 40 | { 41 | options.Subscriber.ConsumerCreatedCallback?.Invoke(consumer.KafkaConsumer); 42 | consumer.Subscribe(topic); 43 | } 44 | catch 45 | { 46 | consumer.Dispose(); 47 | throw; 48 | } 49 | 50 | return new KafkaBlockingRawMessageReader(consumer); 51 | } 52 | 53 | private class ByteArrayDeserializer : IDeserializer 54 | { 55 | public byte[] Deserialize(byte[] data) => data; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaConsumerAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | 4 | namespace Greentube.Messaging.Kafka 5 | { 6 | internal class KafkaConsumerAdapter : IKafkaConsumer 7 | { 8 | public Consumer KafkaConsumer { get; } 9 | 10 | public KafkaConsumerAdapter(Consumer consumer) => 11 | KafkaConsumer = consumer ?? throw new ArgumentNullException(nameof(consumer)); 12 | 13 | public bool Consume(out Message message, TimeSpan timeout) 14 | => KafkaConsumer.Consume(out message, timeout); 15 | 16 | public void Subscribe(string topic) => KafkaConsumer.Subscribe(topic); 17 | 18 | public void Dispose() => KafkaConsumer.Dispose(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Greentube.Messaging.Kafka 4 | { 5 | /// 6 | /// Kafka Options 7 | /// 8 | /// 9 | public class KafkaOptions : PollingReaderOptions 10 | { 11 | /// 12 | /// Kafka Properties 13 | /// 14 | /// 15 | /// 16 | /// 17 | public KafkaProperties Properties { get; set; } = new KafkaProperties(); 18 | /// 19 | /// Subscriber options 20 | /// 21 | public SubscriberOptions Subscriber { get; set; } = new SubscriberOptions(); 22 | /// 23 | /// Publisher options 24 | /// 25 | public PublisherOptions Publisher { get; set; } = new PublisherOptions(); 26 | /// 27 | /// There's no need to block between reads as it's done by the implementation of Consumer.Consume 28 | /// 29 | /// 30 | public override TimeSpan SleepBetweenPolling => default(TimeSpan); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaProducerAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Confluent.Kafka; 4 | 5 | namespace Greentube.Messaging.Kafka 6 | { 7 | internal class KafkaProducerAdapter : IKafkaProducer 8 | { 9 | public Producer KafkaProducer { get; } 10 | 11 | public KafkaProducerAdapter(Producer producer) => 12 | KafkaProducer = producer ?? throw new ArgumentNullException(nameof(producer)); 13 | 14 | public int Flush(TimeSpan timeout) => KafkaProducer.Flush(timeout); 15 | 16 | public Task> ProduceAsync(string topic, Null key, byte[] val) 17 | => KafkaProducer.ProduceAsync(topic, key, val); 18 | 19 | public void Dispose() => KafkaProducer.Dispose(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaProperties.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Greentube.Messaging.Kafka 4 | { 5 | /// 6 | /// The key-value pair of configurations for Apache Kafka 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | public class KafkaProperties : Dictionary 13 | { 14 | public string ClientId 15 | { 16 | get => (string) this["client.id"]; 17 | set => this["client.id"] = value; 18 | } 19 | 20 | public string AutoOffset 21 | { 22 | get => (string) this["auto.offset.reset"]; 23 | set => this["auto.offset.reset"] = value; 24 | } 25 | 26 | public int BatchSize 27 | { 28 | get => (int) this["batch.num.messages"]; 29 | set => this["batch.num.messages"] = value; 30 | } 31 | 32 | public string BrokerList 33 | { 34 | get => (string) this["bootstrap.servers"]; 35 | set => this["bootstrap.servers"] = value; 36 | } 37 | 38 | public string GroupId 39 | { 40 | get => (string) this["group.id"]; 41 | set => this["group.id"] = value; 42 | } 43 | 44 | public int QueueBufferSize 45 | { 46 | get => (int) this["queue.buffering.max.messages"]; 47 | set => this["queue.buffering.max.messages"] = value; 48 | } 49 | 50 | public int QueueBufferTime 51 | { 52 | get => (int) this["queue.buffering.max.ms"]; 53 | set => this["queue.buffering.max.ms"] = value; 54 | } 55 | 56 | public int FetchWaitTime 57 | { 58 | get => (int) this["fetch.wait.max.ms"]; 59 | set => this["fetch.wait.max.ms"] = value; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/KafkaRawMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Confluent.Kafka; 5 | using Confluent.Kafka.Serialization; 6 | 7 | namespace Greentube.Messaging.Kafka 8 | { 9 | /// 10 | /// A Kafka Raw Message Publisher 11 | /// 12 | /// 13 | /// 14 | public class KafkaRawMessagePublisher : IRawMessagePublisher, IDisposable 15 | { 16 | private readonly KafkaOptions _options; 17 | private readonly IKafkaProducer _producer; 18 | private static readonly ByteArraySerializer Serializer = new ByteArraySerializer(); 19 | 20 | /// 21 | /// Creates a new instance of 22 | /// 23 | /// 24 | public KafkaRawMessagePublisher(KafkaOptions options) 25 | : this( 26 | () => new KafkaProducerAdapter( 27 | new Producer(options.Properties, null, Serializer)), 28 | options) { } 29 | 30 | internal KafkaRawMessagePublisher( 31 | Func producerFunc, 32 | KafkaOptions options) 33 | { 34 | _options = options ?? throw new ArgumentNullException(nameof(options)); 35 | var producer = producerFunc(); 36 | try 37 | { 38 | _options.Publisher.ProducerCreatedCallback?.Invoke(producer.KafkaProducer); 39 | } 40 | catch 41 | { 42 | producer.Dispose(); 43 | throw; 44 | } 45 | _producer = producer; 46 | } 47 | 48 | /// 49 | /// Publishes the raw message to the topic using Kafka Producer 50 | /// 51 | /// The topic to send the message to. 52 | /// The message to send. 53 | /// The ignored cancellation token due to the implementation not supporting. 54 | /// 55 | public Task Publish(string topic, byte[] message, CancellationToken _) 56 | { 57 | return _producer.ProduceAsync(topic, null, message); 58 | } 59 | 60 | private class ByteArraySerializer : ISerializer 61 | { 62 | public byte[] Serialize(byte[] data) => data; 63 | } 64 | 65 | /// 66 | /// Flushes the Producer data with the timeout and disposes it. 67 | /// 68 | /// 69 | public void Dispose() 70 | { 71 | _producer.Flush(_options.Publisher.FlushTimeout); 72 | _producer.Dispose(); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 4 | [assembly: InternalsVisibleTo("Greentube.Messaging.Kafka.Tests")] 5 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/PublisherOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | 4 | namespace Greentube.Messaging.Kafka 5 | { 6 | /// 7 | /// Publisher options 8 | /// 9 | public class PublisherOptions 10 | { 11 | /// 12 | /// The maximum length of time to block. 13 | /// You should typically use a relatively short timout period because this operation cannot be cancelled. 14 | /// 15 | public TimeSpan FlushTimeout { get; set; } = TimeSpan.FromSeconds(3); 16 | /// 17 | /// Invoked when a Kafka Producer is instantiated 18 | /// 19 | public Action> ProducerCreatedCallback { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Kafka/SubscriberOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | 4 | namespace Greentube.Messaging.Kafka 5 | { 6 | /// 7 | /// Subscriber options 8 | /// 9 | public class SubscriberOptions 10 | { 11 | /// 12 | /// The maximum time to block. 13 | /// You should typically use a relatively short timout period because this operation cannot be cancelled 14 | /// 15 | public TimeSpan ConsumeTimeout { get; set; } = TimeSpan.FromSeconds(3); 16 | /// 17 | /// Invoked when a Kafka Consumer is instantiated 18 | /// 19 | public Action> ConsumerCreatedCallback { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Redis/Greentube.Messaging.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard1.5 4 | 0.0.0 5 | Bruno Garcia 6 | Bruno Garcia; Greentube 7 | Redis implementation of Greentube.Messaging. 8 | https://github.com/Greentube/messaging 9 | redis;messaging;pubsub;publish-subscribe 10 | true 11 | 12 | 13 | full 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Greentube.Messaging.Redis/RedisRawMessageHandlerSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using StackExchange.Redis; 5 | using static System.Threading.CancellationToken; 6 | 7 | namespace Greentube.Messaging.Redis 8 | { 9 | /// 10 | /// A Redis Raw Message Handler Subscriber 11 | /// 12 | /// 13 | public class RedisRawMessageHandlerSubscriber : IRawMessageHandlerSubscriber 14 | { 15 | private readonly IConnectionMultiplexer _connectionMultiplexer; 16 | 17 | public RedisRawMessageHandlerSubscriber(IConnectionMultiplexer connectionMultiplexer) => 18 | _connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentNullException(nameof(connectionMultiplexer)); 19 | 20 | /// 21 | /// Subscribes to the specified topic with Redis Pub/Sub 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | public Task Subscribe(string topic, IRawMessageHandler rawHandler, CancellationToken _) 29 | { 30 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 31 | if (rawHandler == null) throw new ArgumentNullException(nameof(rawHandler)); 32 | 33 | void HandleRedisMessage(RedisChannel channel, RedisValue value) => 34 | rawHandler.Handle(channel, value, None) 35 | .GetAwaiter() 36 | .GetResult(); 37 | 38 | var subscriber = _connectionMultiplexer.GetSubscriber() 39 | ?? throw new InvalidOperationException("Redis Multiplexer returned no subscription."); 40 | 41 | return subscriber.SubscribeAsync(topic, HandleRedisMessage); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging.Redis/RedisRawMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using StackExchange.Redis; 5 | 6 | namespace Greentube.Messaging.Redis 7 | { 8 | /// 9 | /// A Redis Raw Message Publisher 10 | /// 11 | /// 12 | public class RedisRawMessagePublisher : IRawMessagePublisher 13 | { 14 | private readonly ISubscriber _subscriber; 15 | 16 | /// 17 | /// Creates an instance of 18 | /// 19 | /// Redis ConnectionMultiplexer 20 | public RedisRawMessagePublisher(IConnectionMultiplexer connectionMultiplexer) 21 | { 22 | if (connectionMultiplexer == null) throw new ArgumentNullException(nameof(connectionMultiplexer)); 23 | _subscriber = connectionMultiplexer.GetSubscriber() 24 | ?? throw new ArgumentException("Redis Multiplexer returned no subscription.", nameof(connectionMultiplexer)); 25 | } 26 | 27 | /// 28 | /// Publishes the raw message to the topic using Redis Pub/Sub 29 | /// 30 | /// The topic to send the message to. 31 | /// The message to send. 32 | /// Ignored token as SE.Redis doesn't support it. 33 | /// 34 | public Task Publish(string topic, byte[] message, CancellationToken _) 35 | { 36 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 37 | if (message == null) throw new ArgumentNullException(nameof(message)); 38 | 39 | return _subscriber.PublishAsync(topic, message); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/BlockingReaderRawMessageHandlerSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Greentube.Messaging 7 | { 8 | /// 9 | /// An adapter to a blocking/polling Raw Message Handler Subscriber 10 | /// 11 | /// 12 | /// Creates a task per subscription to call a blocking for messages 13 | /// A call to subscribe will return a task which completes when the subcription is done. 14 | /// A subscription is considered to be done when a is created 15 | /// using the provided factory 16 | /// 17 | /// 18 | /// 19 | public class BlockingReaderRawMessageHandlerSubscriber : IRawMessageHandlerSubscriber, IDisposable 20 | where TOptions : IPollingOptions 21 | { 22 | private readonly TOptions _options; 23 | private readonly IBlockingRawMessageReaderFactory _factory; 24 | 25 | private readonly ConcurrentDictionary<(string topic, IRawMessageHandler rawHandler), 26 | (Task task, CancellationTokenSource tokenSource)> _readers 27 | = new ConcurrentDictionary<(string topic, IRawMessageHandler rawHandler), 28 | (Task task, CancellationTokenSource tokenSource)>(); 29 | 30 | private readonly ConcurrentDictionary _readerCreationLocks 31 | = new ConcurrentDictionary(); 32 | 33 | /// 34 | /// Create a new instance of 35 | /// 36 | /// The factory to be called on each new subscription 37 | /// The polling options 38 | public BlockingReaderRawMessageHandlerSubscriber( 39 | IBlockingRawMessageReaderFactory factory, 40 | TOptions options) 41 | { 42 | _factory = factory ?? throw new ArgumentNullException(nameof(factory)); 43 | _options = options != null ? options : throw new ArgumentNullException(nameof(options)); 44 | } 45 | 46 | /// 47 | /// Subscribes to the specified topic with the provided blocking/polling-based raw message handler 48 | /// 49 | /// 50 | /// A Task is created to call the blocking "/>. 51 | /// 52 | /// The topic to subscribe to 53 | /// The raw handler to invoke with the bytes received 54 | /// A token to cancel the topic subscription 55 | /// Task that completes when the subscription process is finished 56 | /// 57 | public Task Subscribe(string topic, IRawMessageHandler rawHandler, CancellationToken subscriptionCancellation) 58 | { 59 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 60 | if (rawHandler == null) throw new ArgumentNullException(nameof(rawHandler)); 61 | 62 | var subscriptionTask = new TaskCompletionSource(); 63 | 64 | _readers.AddOrUpdate( 65 | (topic, rawHandler), 66 | CreateReader(topic, rawHandler, subscriptionTask, subscriptionCancellation), 67 | (_, tuple) => 68 | { 69 | if (tuple.task.IsFaulted || tuple.task.IsCompleted) 70 | { 71 | return CreateReader(topic, rawHandler, subscriptionTask, subscriptionCancellation); 72 | } 73 | 74 | subscriptionTask.SetResult(true); 75 | return tuple; 76 | }); 77 | 78 | return subscriptionTask.Task; 79 | } 80 | 81 | private (Task task, CancellationTokenSource token) CreateReader( 82 | string topic, 83 | IRawMessageHandler rawHandler, 84 | TaskCompletionSource subscriptionTask, 85 | CancellationToken subscriptionCancellation) 86 | { 87 | var readerCreationLock = _readerCreationLocks.GetOrAdd(topic, new object()); 88 | Monitor.Enter(readerCreationLock); 89 | try 90 | { 91 | if (_readers.TryGetValue((topic, rawHandler), out var tuple)) 92 | { 93 | if (tuple.task.IsCanceled) subscriptionTask.SetCanceled(); 94 | else if (tuple.task.IsCompleted) subscriptionTask.SetResult(true); 95 | else if (tuple.task.IsFaulted) subscriptionTask.SetException(tuple.task.Exception); 96 | 97 | return tuple; 98 | } 99 | 100 | // Reader cancellation will let us stop this task on Dispose/Unsubscribe 101 | var readerCancellation = new CancellationTokenSource(); 102 | 103 | var consumerTask = Task.Run( 104 | async () => await ReaderTaskCreation( 105 | topic, 106 | rawHandler, 107 | subscriptionTask, 108 | subscriptionCancellation, 109 | readerCancellation.Token), 110 | // The Reader will handle the cancellation by gracefully shutting down. 111 | CancellationToken.None); 112 | 113 | return (consumerTask, readerCancellation); 114 | } 115 | finally 116 | { 117 | Monitor.Exit(readerCreationLock); 118 | _readerCreationLocks.TryRemove(topic, out var _); 119 | } 120 | } 121 | 122 | private async Task ReaderTaskCreation( 123 | string topic, 124 | IRawMessageHandler rawHandler, 125 | TaskCompletionSource subscriptionTask, 126 | CancellationToken subscriptionCancellation, 127 | CancellationToken readerCancellation) 128 | { 129 | if (subscriptionCancellation.IsCancellationRequested) 130 | { 131 | subscriptionTask.SetCanceled(); 132 | return; 133 | } 134 | 135 | IBlockingRawMessageReader reader; 136 | try 137 | { 138 | reader = _factory.Create(topic, _options); 139 | } 140 | catch (Exception e) 141 | { 142 | subscriptionTask.SetException(e); 143 | return; 144 | } 145 | 146 | subscriptionTask.SetResult(true); 147 | 148 | await ReadMessageLoop(topic, rawHandler, reader, _options, readerCancellation); 149 | } 150 | 151 | private static async Task ReadMessageLoop( 152 | string topic, 153 | IRawMessageHandler rawHandler, 154 | IBlockingRawMessageReader reader, 155 | TOptions options, 156 | CancellationToken consumerCancellation) 157 | { 158 | try 159 | { 160 | do 161 | { 162 | // Blocking call to the reader to retrieve message 163 | if (reader.TryGetMessage(out var msg, options)) 164 | { 165 | await rawHandler.Handle(topic, msg, consumerCancellation); 166 | } 167 | else if (options.SleepBetweenPolling != default(TimeSpan)) 168 | { 169 | // Implementations where TryGetMessage will block wait for a message 170 | // there's no need to sleep here.. 171 | await Task.Delay(options.SleepBetweenPolling, consumerCancellation); 172 | } 173 | 174 | consumerCancellation.ThrowIfCancellationRequested(); 175 | } while (true); 176 | } 177 | catch (OperationCanceledException oce) 178 | { 179 | options.ReaderStoppingCallback?.Invoke(topic, rawHandler, oce); 180 | } 181 | catch (Exception ex) 182 | { 183 | options.ErrorCallback?.Invoke(topic, rawHandler, ex); 184 | } 185 | finally 186 | { 187 | (reader as IDisposable)?.Dispose(); 188 | } 189 | } 190 | 191 | /// 192 | /// Unsubscribes the specified raw handler from the topic 193 | /// 194 | /// The topic to unsubscribe 195 | /// The raw handle to unsubscribe 196 | /// The unsubsctiption cancellation token 197 | /// 198 | /// Await to ensure continuation only happens once reader is disposed 199 | /// 200 | /// 201 | /// A task that completes once the reader task is no longer executing 202 | /// 203 | public Task Unsubscribe(string topic, IRawMessageHandler rawHandler, CancellationToken token) 204 | { 205 | if (_readers.TryRemove((topic, rawHandler), out var tuple)) 206 | { 207 | tuple.tokenSource.Cancel(); 208 | tuple.tokenSource.Dispose(); 209 | } 210 | 211 | var tcs = new TaskCompletionSource(); 212 | // Unsubscribing is done once the reader has stopped 213 | tuple.task.ContinueWith(readerTask => tcs.SetResult(true), token); 214 | 215 | return tcs.Task; 216 | } 217 | 218 | public void Dispose() 219 | { 220 | foreach (var (topic, rawHandler) in _readers.Keys) 221 | { 222 | try 223 | { 224 | Unsubscribe(topic, rawHandler, CancellationToken.None) 225 | .GetAwaiter() 226 | .GetResult(); 227 | } 228 | catch // CA1065 229 | { 230 | // ignored 231 | } 232 | } 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/DispatchingRawMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Greentube.Serialization; 5 | 6 | namespace Greentube.Messaging 7 | { 8 | /// 9 | /// Handles raw messages by deserializing them and dispatching with using 10 | /// 11 | /// 12 | public class DispatchingRawMessageHandler : IRawMessageHandler 13 | { 14 | private readonly IMessageHandlerInvoker _messageHandlerInvoker; 15 | private readonly IMessageTypeTopicMap _typeTopicMap; 16 | private readonly ISerializer _serializer; 17 | 18 | public DispatchingRawMessageHandler( 19 | IMessageTypeTopicMap typeTopicMap, 20 | ISerializer serializer, 21 | IMessageHandlerInvoker messageHandlerInvoker) 22 | { 23 | _messageHandlerInvoker = messageHandlerInvoker ?? throw new ArgumentNullException(nameof(messageHandlerInvoker)); 24 | _typeTopicMap = typeTopicMap ?? throw new ArgumentNullException(nameof(typeTopicMap)); 25 | _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); 26 | } 27 | 28 | /// 29 | public Task Handle(string topic, byte[] message, CancellationToken token) 30 | { 31 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 32 | if (message == null) throw new ArgumentNullException(nameof(message)); 33 | 34 | var messageType = _typeTopicMap.Get(topic); 35 | if (messageType == null) 36 | { 37 | throw new InvalidOperationException( 38 | $"Topic '{topic}' has no message type registered with: {_typeTopicMap.GetType()}."); 39 | } 40 | 41 | var deserialized = _serializer.Deserialize(messageType, message); 42 | if (deserialized == null) 43 | { 44 | throw new InvalidOperationException( 45 | $"Serializer {_serializer.GetType()} returned null for the {message.Length}-byte message of type {messageType}."); 46 | } 47 | 48 | return _messageHandlerInvoker.Invoke(deserialized, token); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/Greentube.Messaging.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard1.5 4 | 0.0.0 5 | Bruno Garcia 6 | Bruno Garcia; Greentube 7 | Provides a simple API for messaging. 8 | https://github.com/Greentube/messaging 9 | messaging;pubsub;publish-subscribe 10 | true 11 | 12 | 13 | full 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Greentube.Messaging/IBlockingRawMessageReader.cs: -------------------------------------------------------------------------------- 1 | namespace Greentube.Messaging 2 | { 3 | /// 4 | /// Raw message reader which doesn't support TPL and hence blocks on I/O 5 | /// 6 | /// 7 | public interface IBlockingRawMessageReader 8 | where TOptions : IPollingOptions 9 | { 10 | /// 11 | /// Tries to read a message from the bus 12 | /// 13 | /// The message read if true was returned 14 | /// options 15 | /// true when a message is successfully read. Otherwise false 16 | bool TryGetMessage(out byte[] message, TOptions options); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IBlockingRawMessageReaderFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Greentube.Messaging 2 | { 3 | /// 4 | /// Blocking raw message reader factory 5 | /// 6 | /// 7 | /// Create instances of 8 | /// 9 | /// The polling options 10 | public interface IBlockingRawMessageReaderFactory 11 | where TOptions : IPollingOptions 12 | { 13 | /// 14 | /// Creates a new reader for the specified topic 15 | /// 16 | /// The topic to read from 17 | /// The polling options 18 | /// 19 | IBlockingRawMessageReader Create(string topic, TOptions options); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Handles a message of type 8 | /// 9 | /// 10 | /// 11 | public interface IMessageHandler : IMessageHandler 12 | { 13 | /// 14 | /// Handles 15 | /// 16 | /// The message to be handled 17 | /// A cancellation token 18 | /// 19 | Task Handle(TMessage message, CancellationToken token); 20 | } 21 | 22 | /// 23 | /// Message handler 24 | /// 25 | /// Marker interface 26 | public interface IMessageHandler 27 | { 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IMessageHandlerInfoProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Greentube.Messaging 7 | { 8 | /// 9 | /// Provider of reflection data. 10 | /// 11 | public interface IMessageHandlerInfoProvider 12 | { 13 | IEnumerable<(Type messageType, Type handlerType, Func handleMethod)> GetHandlerInfo(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IMessageHandlerInvoker.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Message handler invoker 8 | /// 9 | public interface IMessageHandlerInvoker 10 | { 11 | /// 12 | /// Invokes a for the specified message based on its type 13 | /// 14 | /// The message to invoke the handler with 15 | /// Cancellation token 16 | /// 17 | Task Invoke(object message, CancellationToken token); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Message Publisher 8 | /// 9 | public interface IMessagePublisher 10 | { 11 | /// 12 | /// Publishes a message of type 13 | /// 14 | /// The message to be published 15 | /// A cancellation token 16 | /// 17 | /// 18 | Task Publish(TMessage message, CancellationToken token); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IMessageTypeTopicMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Map of message type and correspondent topic 8 | /// 9 | /// 10 | public interface IMessageTypeTopicMap : IReadOnlyCollection> 11 | { 12 | /// 13 | /// Add a map of message type and topic 14 | /// 15 | /// Type of message 16 | /// Topic 17 | void Add(Type type, string topic); 18 | /// 19 | /// Remove a map by the message type 20 | /// 21 | /// 22 | void Remove(Type type); 23 | /// 24 | /// Get the topic of a corresponding message type 25 | /// 26 | /// 27 | /// 28 | string Get(Type type); 29 | /// 30 | /// Get the message type of a corresponding topic 31 | /// 32 | /// 33 | /// 34 | Type Get(string topic); 35 | /// 36 | /// Get all topics 37 | /// 38 | /// 39 | IEnumerable GetTopics(); 40 | /// 41 | /// Get all message types 42 | /// 43 | /// 44 | IEnumerable GetMessageTypes(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IPollingOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Greentube.Messaging 4 | { 5 | /// 6 | /// Options for a polling based message readers 7 | /// 8 | public interface IPollingOptions 9 | { 10 | /// 11 | /// Time to wait between calls to Read 12 | /// 13 | TimeSpan SleepBetweenPolling { get; set; } 14 | /// 15 | /// Invoked when the reader task is requested to stop. 16 | /// 17 | Action ReaderStoppingCallback { get; set; } 18 | /// 19 | /// Invoked when an unhandled exception happened while reading messages from a topic 20 | /// 21 | Action ErrorCallback { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IRawMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Handles raw messages 8 | /// 9 | public interface IRawMessageHandler 10 | { 11 | /// 12 | /// Handles a serialized message from a specific topic 13 | /// 14 | /// The topic which this message arrived 15 | /// The serialized message 16 | /// A cancellation token 17 | /// 18 | Task Handle(string topic, byte[] message, CancellationToken token); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IRawMessageHandlerSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Subscribes raw message handlers to topics 8 | /// 9 | public interface IRawMessageHandlerSubscriber 10 | { 11 | /// 12 | /// Subscribes to the specified topic with the provided 13 | /// 14 | /// To topic to subscribe to 15 | /// The handler to invoke 16 | /// A cancellation token 17 | /// 18 | Task Subscribe(string topic, IRawMessageHandler rawHandler, CancellationToken token); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/IRawMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Raw message publisher 8 | /// 9 | public interface IRawMessagePublisher 10 | { 11 | /// 12 | /// Publishes the byte array into the specified topic 13 | /// 14 | /// The destination to send the message 15 | /// The deserialized message 16 | /// A cancellation token 17 | Task Publish(string topic, byte[] message, CancellationToken token); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/MessageHandlerInfoProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Greentube.Messaging 10 | { 11 | /// 12 | /// Provider of type information for implementations 13 | /// 14 | /// 15 | public class MessageHandlerInfoProvider : IMessageHandlerInfoProvider 16 | { 17 | private readonly IMessageTypeTopicMap _typeTopicMap; 18 | 19 | /// 20 | /// Creates an instance of with the specified 21 | /// 22 | /// A map between Topic names and Message types 23 | public MessageHandlerInfoProvider(IMessageTypeTopicMap typeTopicMap) 24 | { 25 | _typeTopicMap = typeTopicMap ?? throw new ArgumentNullException(nameof(typeTopicMap)); 26 | 27 | if (!_typeTopicMap.Any()) 28 | throw new ArgumentException($"{nameof(IMessageTypeTopicMap)} is empty."); 29 | } 30 | 31 | public IEnumerable<( 32 | Type messageType, 33 | Type handlerType, 34 | Func handleMethod)> 35 | GetHandlerInfo() 36 | { 37 | // Creates a a tuple with the type of handler and message with a delegate to invoke the Handle. 38 | return from messageType in _typeTopicMap.GetMessageTypes() 39 | let handlerType = typeof(IMessageHandler<>).MakeGenericType(messageType) 40 | let handleMethod = handlerType.GetTypeInfo().GetMethod(nameof(IMessageHandler.Handle)) 41 | let handlerInstance = Expression.Parameter(typeof(object)) 42 | let messageInstance = Expression.Parameter(typeof(object)) 43 | let tokenInstance = Expression.Parameter(typeof(CancellationToken)) 44 | let handleFunc = Expression.Lambda>( 45 | Expression.Call( 46 | Expression.Convert( 47 | handlerInstance, 48 | handlerType), 49 | handleMethod, 50 | Expression.Convert( 51 | messageInstance, 52 | messageType), 53 | tokenInstance), 54 | handlerInstance, 55 | messageInstance, 56 | tokenInstance) 57 | .Compile() 58 | select (messageType, handlerType, handleFunc); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/MessageHandlerInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Greentube.Messaging 8 | { 9 | /// 10 | public class MessageHandlerInvoker : IMessageHandlerInvoker 11 | { 12 | private readonly Dictionary< 13 | Type, 14 | (Type handlerType, Func handleMethod)> 15 | _messageToHandlerDictionary; 16 | 17 | private readonly Func _handlerFactory; 18 | 19 | public MessageHandlerInvoker( 20 | IMessageHandlerInfoProvider messageHandlerInfoProvider, 21 | Func handlerFactory) 22 | { 23 | if (messageHandlerInfoProvider == null) throw new ArgumentNullException(nameof(messageHandlerInfoProvider)); 24 | _handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory)); 25 | 26 | _messageToHandlerDictionary = messageHandlerInfoProvider.GetHandlerInfo() 27 | .ToDictionary( 28 | k => k.messageType, 29 | v => (v.handlerType, v.handleMethod)); 30 | 31 | if (!_messageToHandlerDictionary.Any()) 32 | throw new InvalidOperationException( 33 | $"{nameof(IMessageHandlerInfoProvider)}.{nameof(IMessageHandlerInfoProvider.GetHandlerInfo)} " + 34 | "hasn't resolved any handler information."); 35 | } 36 | 37 | /// 38 | public Task Invoke(object message, CancellationToken token) 39 | { 40 | if (!_messageToHandlerDictionary.TryGetValue(message.GetType(), out var handlerInfo)) 41 | { 42 | throw new InvalidOperationException($"No message handler found for message type {message.GetType()}."); 43 | } 44 | 45 | var handler = _handlerFactory(handlerInfo.handlerType); 46 | if (handler == null) throw new InvalidOperationException($"Message of type {message.GetType()} yielded no handler from factory."); 47 | 48 | return handlerInfo.handleMethod(handler, message, token); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/MessageTypeTopicMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | 6 | namespace Greentube.Messaging 7 | { 8 | /// 9 | public class MessageTypeTopicMap : IMessageTypeTopicMap 10 | { 11 | private readonly ConcurrentDictionary _messageTopicMap = 12 | new ConcurrentDictionary(); 13 | private readonly ConcurrentDictionary _topicMessageMap = 14 | new ConcurrentDictionary(); 15 | 16 | public int Count => _messageTopicMap.Count; 17 | 18 | public IEnumerator> GetEnumerator() => _messageTopicMap.GetEnumerator(); 19 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 20 | 21 | /// 22 | public void Add(Type type, string topic) 23 | { 24 | if (type == null) throw new ArgumentNullException(nameof(type)); 25 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 26 | 27 | if (_messageTopicMap.TryAdd(type, topic)) 28 | { 29 | _topicMessageMap.TryAdd(topic, type); 30 | } 31 | } 32 | 33 | /// 34 | public string Get(Type type) 35 | { 36 | if (type == null) throw new ArgumentNullException(nameof(type)); 37 | 38 | _messageTopicMap.TryGetValue(type, out var topic); 39 | return topic; 40 | } 41 | 42 | /// 43 | public Type Get(string topic) 44 | { 45 | if (topic == null) throw new ArgumentNullException(nameof(topic)); 46 | 47 | _topicMessageMap.TryGetValue(topic, out var type); 48 | return type; 49 | } 50 | 51 | /// 52 | public IEnumerable GetTopics() 53 | { 54 | return _messageTopicMap.Values; 55 | } 56 | 57 | /// 58 | public IEnumerable GetMessageTypes() 59 | { 60 | return _topicMessageMap.Values; 61 | } 62 | 63 | /// 64 | public void Remove(Type type) 65 | { 66 | if (type == null) throw new ArgumentNullException(nameof(type)); 67 | 68 | if (_messageTopicMap.TryRemove(type, out var topic)) 69 | { 70 | _topicMessageMap.TryRemove(topic, out var _); 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/MessagingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Greentube.Messaging 2 | { 3 | public class MessagingOptions 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Greentube.Messaging/PollingReaderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | 4 | namespace Greentube.Messaging 5 | { 6 | /// 7 | /// Options for a polling based reader 8 | /// 9 | /// 10 | public class PollingReaderOptions : IPollingOptions 11 | { 12 | /// 13 | public virtual TimeSpan SleepBetweenPolling { get; set; } 14 | /// 15 | /// Allows access to the once the blocking read loop is exiting. 16 | /// It's invoked before disposing the reader 17 | /// 18 | /// 19 | public Action ReaderStoppingCallback { get; set; } 20 | /// 21 | /// By default it will rethrow the exception. It'll end the task and dispose the reader 22 | /// 23 | /// 24 | public Action ErrorCallback { get; set; } 25 | = (topic, handler, ex) => ExceptionDispatchInfo.Capture(ex).Throw(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Greentube.Messaging/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Greentube.Messaging.Tests")] 4 | -------------------------------------------------------------------------------- /src/Greentube.Messaging/SerializedMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Greentube.Serialization; 5 | 6 | namespace Greentube.Messaging 7 | { 8 | /// 9 | /// Publishes a serialized message via raw publisher 10 | /// 11 | /// 12 | public class SerializedMessagePublisher : IMessagePublisher 13 | { 14 | private readonly IMessageTypeTopicMap _typeTopicMap; 15 | private readonly IRawMessagePublisher _rawMessagePublisher; 16 | private readonly ISerializer _serializer; 17 | 18 | public SerializedMessagePublisher( 19 | IMessageTypeTopicMap typeTopicMap, 20 | ISerializer serializer, 21 | IRawMessagePublisher rawMessagePublisher) 22 | { 23 | _typeTopicMap = typeTopicMap ?? throw new ArgumentNullException(nameof(typeTopicMap)); 24 | _rawMessagePublisher = rawMessagePublisher ?? throw new ArgumentNullException(nameof(rawMessagePublisher)); 25 | _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); 26 | } 27 | 28 | /// 29 | public Task Publish(TMessage message, CancellationToken token) 30 | { 31 | if (message == null) throw new ArgumentNullException(nameof(message)); 32 | 33 | var topic = _typeTopicMap.Get(message.GetType()); 34 | if (topic == null) 35 | { 36 | throw new InvalidOperationException( 37 | $"Message type {message.GetType()} is not registered with: {_typeTopicMap.GetType()}."); 38 | } 39 | 40 | var serialized = _serializer.Serialize(message); 41 | if (serialized == null) 42 | { 43 | throw new InvalidOperationException( 44 | $"Serializer {_serializer.GetType()} returned null for message of type {message.GetType()}."); 45 | } 46 | 47 | return _rawMessagePublisher.Publish(topic, serialized, token); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Kafka.Tests/Greentube.Messaging.DependencyInjection.Kafka.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Kafka.Tests/KafkaMessagingBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Greentube.Messaging.Kafka; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Options; 6 | using Xunit; 7 | 8 | namespace Greentube.Messaging.DependencyInjection.Kafka.Tests 9 | { 10 | public class KafkaMessagingBuilderExtensionsTests 11 | { 12 | class Fixture 13 | { 14 | public ServiceCollection ServiceCollection { get; } = new ServiceCollection(); 15 | public MessagingBuilder GetBuilder() => new MessagingBuilder(ServiceCollection); 16 | } 17 | 18 | private readonly Fixture _fixture = new Fixture(); 19 | 20 | [Fact] 21 | public void AddKafka_RegistersPublisherSubscriberAndOptions() 22 | { 23 | // Arrange 24 | var builder = _fixture.GetBuilder(); 25 | 26 | // Act 27 | builder.AddKafka(); 28 | 29 | // Assert 30 | AssertPublisher(); 31 | AssertSubscriber(); 32 | AssertOptions(); 33 | } 34 | 35 | [Fact] 36 | public void AddKafka_Action_RegistersPublisherSubscriberOptionsAndSetupAction() 37 | { 38 | // Arrange 39 | var builder = _fixture.GetBuilder(); 40 | // ReSharper disable once ConvertToLocalFunction - Reference is needed for comparison 41 | Action configAction = _ => { }; 42 | 43 | // Act 44 | builder.AddKafka(configAction); 45 | 46 | // Assert 47 | AssertOptionSetup(configAction); 48 | AssertPublisher(); 49 | AssertSubscriber(); 50 | AssertOptions(); 51 | } 52 | 53 | [Fact] 54 | public void AddKafkaPublisher_RegisterOnlyPublisher() 55 | { 56 | // Arrange 57 | var builder = _fixture.GetBuilder(); 58 | 59 | // Act 60 | builder.AddKafkaPublisher(); 61 | 62 | // Assert 63 | AssertPublisher(); 64 | AssertOptions(); 65 | Assert.DoesNotContain(_fixture.ServiceCollection, d => d.ServiceType == typeof(IRawMessageHandlerSubscriber)); 66 | } 67 | 68 | [Fact] 69 | public void AddKafkaPublisher_Action_RegisterOnlyPublisherAndSetupAction() 70 | { 71 | // Arrange 72 | var builder = _fixture.GetBuilder(); 73 | // ReSharper disable once ConvertToLocalFunction - Reference is needed for comparison 74 | Action configAction = _ => { }; 75 | 76 | // Act 77 | builder.AddKafkaPublisher(configAction); 78 | 79 | // Assert 80 | AssertOptionSetup(configAction); 81 | AssertPublisher(); 82 | AssertOptions(); 83 | Assert.DoesNotContain(_fixture.ServiceCollection, d => d.ServiceType == typeof(IRawMessageHandlerSubscriber)); 84 | } 85 | 86 | [Fact] 87 | public void AddKafkaSubscriber_RegisterOnlySubscriber() 88 | { 89 | // Arrange 90 | var builder = _fixture.GetBuilder(); 91 | 92 | // Act 93 | builder.AddKafkaSubscriber(); 94 | 95 | // Assert 96 | AssertOptions(); 97 | AssertSubscriber(); 98 | Assert.DoesNotContain(_fixture.ServiceCollection, d => d.ServiceType == typeof(IRawMessagePublisher)); 99 | } 100 | 101 | [Fact] 102 | public void AddKafkaSubscriber_Action_RegisterOnlySubscriberAndSetupAction() 103 | { 104 | // Arrange 105 | var builder = _fixture.GetBuilder(); 106 | // ReSharper disable once ConvertToLocalFunction - Reference is needed for comparison 107 | Action configAction = _ => { }; 108 | 109 | // Act 110 | builder.AddKafkaSubscriber(configAction); 111 | 112 | // Assert 113 | AssertOptionSetup(configAction); 114 | AssertSubscriber(); 115 | AssertOptions(); 116 | Assert.DoesNotContain(_fixture.ServiceCollection, d => d.ServiceType == typeof(IRawMessagePublisher)); 117 | } 118 | 119 | private void AssertSubscriber() 120 | { 121 | var subscriber = 122 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IRawMessageHandlerSubscriber)); 123 | Assert.NotNull(subscriber); 124 | 125 | Assert.Equal(ServiceLifetime.Singleton, subscriber.Lifetime); 126 | Assert.Equal(typeof(BlockingReaderRawMessageHandlerSubscriber), subscriber.ImplementationType); 127 | 128 | var blockingReaderFactory = 129 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IBlockingRawMessageReaderFactory)); 130 | Assert.NotNull(blockingReaderFactory); 131 | 132 | Assert.Equal(ServiceLifetime.Singleton, blockingReaderFactory.Lifetime); 133 | Assert.Equal(typeof(KafkaBlockingRawMessageReaderFactory), blockingReaderFactory.ImplementationType); 134 | } 135 | 136 | private void AssertPublisher() 137 | { 138 | var publisher = 139 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IRawMessagePublisher)); 140 | Assert.NotNull(publisher); 141 | 142 | Assert.Equal(ServiceLifetime.Singleton, publisher.Lifetime); 143 | Assert.Equal(typeof(KafkaRawMessagePublisher), publisher.ImplementationType); 144 | } 145 | 146 | private void AssertOptions() 147 | { 148 | var options = 149 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(KafkaOptions)); 150 | Assert.NotNull(options); 151 | } 152 | 153 | // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local 154 | private void AssertOptionSetup(Action configAction) 155 | { 156 | var optionsConfiguration = 157 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IConfigureOptions)); 158 | 159 | Assert.NotNull(optionsConfiguration); 160 | Assert.Same(configAction, 161 | ((ConfigureNamedOptions) optionsConfiguration.ImplementationInstance).Action); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Redis.Tests/Greentube.Messaging.DependencyInjection.Redis.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Redis.Tests/RedisMessagingBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Greentube.Messaging.Redis; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using NSubstitute; 5 | using StackExchange.Redis; 6 | using Xunit; 7 | 8 | namespace Greentube.Messaging.DependencyInjection.Redis.Tests 9 | { 10 | public class RedisMessagingBuilderExtensionsTests 11 | { 12 | class Fixture 13 | { 14 | public ServiceCollection ServiceCollection { get; } = new ServiceCollection(); 15 | public MessagingBuilder GetBuilder() => new MessagingBuilder(ServiceCollection); 16 | } 17 | 18 | private readonly Fixture _fixture = new Fixture(); 19 | 20 | [Fact] 21 | public void AddRedis_RegistersBothPublisherAndSubscriber() 22 | { 23 | // Arrange 24 | var builder = _fixture.GetBuilder(); 25 | 26 | // Act 27 | builder.AddRedis(); 28 | 29 | // Assert 30 | AssertPublisher(); 31 | AssertSubscriber(); 32 | } 33 | 34 | [Fact] 35 | public void AddRedis_Multiplexer_RegistersPublisherSubscriberAndMultiplexer() 36 | { 37 | // Arrange 38 | var multiplexer = Substitute.For(); 39 | var builder = _fixture.GetBuilder(); 40 | 41 | // Act 42 | builder.AddRedis(multiplexer); 43 | 44 | // Assert 45 | var multiplexerDescriptor = 46 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IConnectionMultiplexer)); 47 | Assert.NotNull(multiplexerDescriptor); 48 | 49 | Assert.Equal(ServiceLifetime.Singleton, multiplexerDescriptor.Lifetime); 50 | Assert.Same(multiplexer, multiplexerDescriptor.ImplementationInstance); 51 | 52 | AssertPublisher(); 53 | AssertSubscriber(); 54 | } 55 | 56 | [Fact] 57 | public void AddRedisPublisher_RegisterOnlyPublisher() 58 | { 59 | // Arrange 60 | var builder = _fixture.GetBuilder(); 61 | 62 | // Act 63 | builder.AddRedisPublisher(); 64 | 65 | // Assert 66 | AssertPublisher(); 67 | Assert.DoesNotContain(_fixture.ServiceCollection, d => d.ServiceType == typeof(IRawMessageHandlerSubscriber)); 68 | } 69 | 70 | [Fact] 71 | public void AddRedisSubscriber_RegisterOnlySubscriber() 72 | { 73 | // Arrange 74 | var builder = _fixture.GetBuilder(); 75 | 76 | // Act 77 | builder.AddRedisSubscriber(); 78 | 79 | // Assert 80 | AssertSubscriber(); 81 | Assert.DoesNotContain(_fixture.ServiceCollection, d => d.ServiceType == typeof(IRawMessagePublisher)); 82 | } 83 | 84 | 85 | private void AssertSubscriber() 86 | { 87 | var subscriber = 88 | _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IRawMessageHandlerSubscriber)); 89 | Assert.NotNull(subscriber); 90 | 91 | Assert.Equal(ServiceLifetime.Singleton, subscriber.Lifetime); 92 | Assert.Equal(typeof(RedisRawMessageHandlerSubscriber), subscriber.ImplementationType); 93 | } 94 | 95 | private void AssertPublisher() 96 | { 97 | var publisher = _fixture.ServiceCollection.FirstOrDefault(d => d.ServiceType == typeof(IRawMessagePublisher)); 98 | Assert.NotNull(publisher); 99 | 100 | Assert.Equal(ServiceLifetime.Singleton, publisher.Lifetime); 101 | Assert.Equal(typeof(RedisRawMessagePublisher), publisher.ImplementationType); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Tests/Greentube.Messaging.DependencyInjection.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Tests/MessagingBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Scrutor; 8 | using Greentube.Serialization; 9 | using Xunit; 10 | 11 | namespace Greentube.Messaging.DependencyInjection.Tests 12 | { 13 | public class MessagingBuilderTests 14 | { 15 | class Fixture 16 | { 17 | public ServiceCollection ServiceCollection { get; } = new ServiceCollection(); 18 | public MessagingBuilder GetBuilder() => new MessagingBuilder(ServiceCollection); 19 | } 20 | 21 | private readonly Fixture _fixture = new Fixture(); 22 | 23 | [Fact] 24 | public void Build_AddHandlerDiscovery_MessageHandlerAssembliesContainsEntryAssembly() 25 | { 26 | // Arrange 27 | var builder = _fixture.GetBuilder(); 28 | ValidBuilderSetup(builder); 29 | 30 | var assemblyScanHasEntryAssembly = false; 31 | builder.AddHandlerDiscovery(h => 32 | assemblyScanHasEntryAssembly = h.MessageHandlerAssemblies.Any(a => a == Assembly.GetEntryAssembly())); 33 | 34 | // Act 35 | builder.Build(); 36 | 37 | // Assert 38 | Assert.True(assemblyScanHasEntryAssembly); 39 | } 40 | 41 | private class TestMessageHandler : IMessageHandler 42 | { 43 | public Task Handle(MessagingBuilderTests message, CancellationToken token) => Task.CompletedTask; 44 | } 45 | 46 | [Fact] 47 | public void Build_FindsPrivateHandler() 48 | { 49 | // Arrange 50 | var builder = _fixture.GetBuilder(); 51 | ValidBuilderSetup(builder); 52 | 53 | // Act 54 | builder.AddHandlerDiscovery(h => h.IncludeNonPublic = true); 55 | 56 | // Assert 57 | builder.Build(); 58 | 59 | // Assert 60 | var handler = builder.Services.FirstOrDefault( 61 | s => s.ServiceType == typeof(IMessageHandler)); 62 | 63 | Assert.NotNull(handler); 64 | Assert.Equal(typeof(TestMessageHandler), handler.ImplementationType); 65 | } 66 | 67 | [Fact] 68 | public void Build_ReplacesHandler() 69 | { 70 | // Arrange 71 | var builder = _fixture.GetBuilder(); 72 | ValidBuilderSetup(builder); 73 | 74 | var expectedService = typeof(IMessageHandler); 75 | var originalLifetime = ServiceLifetime.Singleton; 76 | 77 | // Registration expected to be overwriten 78 | builder.Services.Add(ServiceDescriptor.Describe( 79 | expectedService, 80 | s => null, 81 | originalLifetime)); 82 | 83 | var expectedLifetime = ServiceLifetime.Scoped; 84 | 85 | // Act 86 | builder.AddHandlerDiscovery(h => 87 | { 88 | h.IncludeNonPublic = true; 89 | h.DiscoveredHandlersLifetime = expectedLifetime; 90 | // Configuration to replace any matching service registrations 91 | h.RegistrationStrategy = RegistrationStrategy.Replace(ReplacementBehavior.ServiceType); 92 | }); 93 | 94 | // Assert 95 | builder.Build(); 96 | 97 | // Assert 98 | var actualDescriptor = _fixture.ServiceCollection.SingleOrDefault(s => s.ServiceType == expectedService); 99 | 100 | Assert.NotNull(actualDescriptor); 101 | Assert.NotEqual(originalLifetime, actualDescriptor.Lifetime); 102 | Assert.Equal(typeof(TestMessageHandler), actualDescriptor.ImplementationType); 103 | } 104 | 105 | [Fact] 106 | public void Build_ExternalRequiredServicesRegistered_RegistersAllRequiredServices() 107 | { 108 | // Arrange 109 | var builder = _fixture.GetBuilder(); 110 | 111 | var s = _fixture.ServiceCollection; 112 | s.AddSingleton(); 113 | s.AddSingleton(); 114 | s.AddSingleton(); 115 | s.AddSingleton(new MessageTypeTopicMap 116 | { 117 | { 118 | typeof(MessagingServiceCollectionExtensionsTests), 119 | nameof(MessagingServiceCollectionExtensionsTests) 120 | } 121 | }); 122 | 123 | // Act 124 | builder.Build(); 125 | 126 | // Assert 127 | AssertRequiredServices(s, true, true); 128 | } 129 | 130 | [Fact] 131 | public void Build_NoRawSubscriber_NoSubscriptionRelatedServices() 132 | { 133 | // Arrange 134 | var builder = _fixture.GetBuilder(); 135 | 136 | var s = _fixture.ServiceCollection; 137 | s.AddSingleton(); 138 | s.AddSingleton(); 139 | s.AddSingleton(new MessageTypeTopicMap 140 | { 141 | { 142 | typeof(MessagingServiceCollectionExtensionsTests), 143 | nameof(MessagingServiceCollectionExtensionsTests) 144 | } 145 | }); 146 | 147 | // Act 148 | builder.Build(); 149 | 150 | // Assert 151 | AssertRequiredServices(s, true, false); 152 | } 153 | 154 | [Fact] 155 | public void Build_NoRawPublisher_NoPublisherRelatedServices() 156 | { 157 | // Arrange 158 | var builder = _fixture.GetBuilder(); 159 | 160 | var s = _fixture.ServiceCollection; 161 | s.AddSingleton(); 162 | s.AddSingleton(); 163 | s.AddSingleton(new MessageTypeTopicMap 164 | { 165 | { 166 | typeof(MessagingServiceCollectionExtensionsTests), 167 | nameof(MessagingServiceCollectionExtensionsTests) 168 | } 169 | }); 170 | 171 | // Act 172 | builder.Build(); 173 | 174 | // Assert 175 | AssertRequiredServices(s, false, true); 176 | } 177 | 178 | [Fact] 179 | public void Build_NoCallsOnBuilder_ThrowsInvalidOperation() 180 | { 181 | // Arrange 182 | var builder = _fixture.GetBuilder(); 183 | 184 | // Act/Assert 185 | Assert.Throws(() => builder.Build()); 186 | } 187 | 188 | [Fact] 189 | public void Build_NeitherPublisherNorSubscriber_ThrowsInvalidOperation() 190 | { 191 | var builder = _fixture.GetBuilder(); 192 | builder 193 | .AddTopic(nameof(MessagingBuilderTests)) 194 | .AddSerialization(b => b.AddSerializer(ServiceLifetime.Singleton)); 195 | 196 | Assert.Throws(() => builder.Build()); 197 | } 198 | 199 | [Fact] 200 | public void Build_NoTopicDefined_ThrowsInvalidOperation() 201 | { 202 | var builder = _fixture.GetBuilder(); 203 | 204 | builder 205 | .AddRawMessageHandlerSubscriber() 206 | .AddRawMessagePublisher() 207 | .AddSerialization(b => b.AddSerializer(ServiceLifetime.Singleton)); 208 | 209 | Assert.Throws(() => builder.Build()); 210 | } 211 | 212 | [Fact] 213 | public void Build_ValidBuilderSetupHelper_DoesntThrow() 214 | { 215 | var builder = _fixture.GetBuilder(); 216 | ValidBuilderSetup(builder); 217 | builder.Build(); 218 | } 219 | 220 | [Fact] 221 | public void Build_TwoSerializers_ThrowsInvalidOperation() 222 | { 223 | // Arrange 224 | var builder = _fixture.GetBuilder(); 225 | ValidBuilderSetup(builder); 226 | 227 | // Act: Second Serializer 228 | builder.AddSerialization(b => b.AddSerializer(ServiceLifetime.Singleton)); 229 | 230 | // Assert 231 | Assert.Throws(() => builder.Build()); 232 | } 233 | 234 | [Fact] 235 | public void Build_TwoRawMessagePublisher_ThrowsInvalidOperation() 236 | { 237 | // Arrange 238 | var builder = _fixture.GetBuilder(); 239 | ValidBuilderSetup(builder); 240 | 241 | // Act 242 | builder.AddRawMessagePublisher(); 243 | 244 | // Assert 245 | Assert.Throws(() => builder.Build()); 246 | } 247 | 248 | [Fact] 249 | public void Build_TwoRawMessageHandlerSubscriber_ThrowsInvalidOperation() 250 | { 251 | // Arrange 252 | var builder = _fixture.GetBuilder(); 253 | ValidBuilderSetup(builder); 254 | 255 | // Act 256 | builder.AddRawMessageHandlerSubscriber(); 257 | 258 | // Assert 259 | Assert.Throws(() => builder.Build()); 260 | } 261 | 262 | [Fact] 263 | public void Constructor_RequiresServicesCollection() 264 | { 265 | Assert.Throws(() => new MessagingBuilder(null)); 266 | } 267 | 268 | private static void ValidBuilderSetup(MessagingBuilder builder) 269 | { 270 | builder 271 | .AddRawMessageHandlerSubscriber() 272 | .AddRawMessagePublisher() 273 | .AddTopic(nameof(MessagingBuilderTests)) 274 | .AddSerialization(b => b.AddSerializer(ServiceLifetime.Singleton)); 275 | } 276 | 277 | // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local 278 | private void AssertRequiredServices(IServiceCollection s, bool publishing, bool subscribing) 279 | { 280 | // Services required by the Messaging package: 281 | // Provided by the application code 282 | Assert.Single(s, d => d.ServiceType == typeof(ISerializer)); 283 | Assert.Single(s, d => d.ServiceType == typeof(IMessageTypeTopicMap)); 284 | // Provided by the builder 285 | Assert.Single(s, d => d.ServiceType == typeof(MessagingOptions)); 286 | 287 | if (publishing) 288 | { 289 | // Provided by the application code 290 | Assert.Single(s, d => d.ServiceType == typeof(IRawMessagePublisher)); 291 | // Provided by the builder 292 | Assert.Single(s, d => d.ServiceType == typeof(IMessagePublisher)); 293 | } 294 | if (subscribing) 295 | { 296 | // Provided by the application code 297 | Assert.Single(s, d => d.ServiceType == typeof(IRawMessageHandlerSubscriber)); 298 | // Provided by the builder 299 | Assert.Single(s, d => d.ServiceType == typeof(IRawMessageHandler)); 300 | Assert.Single(s, d => d.ServiceType == typeof(IMessageHandlerInfoProvider)); 301 | } 302 | } 303 | } 304 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.DependencyInjection.Tests/MessagingServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Greentube.Serialization; 5 | using Xunit; 6 | 7 | namespace Greentube.Messaging.DependencyInjection.Tests 8 | { 9 | public class MessagingServiceCollectionExtensionsTests 10 | { 11 | [Fact] 12 | public void AddMessaging_NoCallsOnBuilder_ThrowsInvalidOperation() 13 | { 14 | // Arrange 15 | var services = new ServiceCollection(); 16 | 17 | // Act/Assert 18 | Assert.Throws(() => services.AddMessaging(builder => { })); 19 | } 20 | 21 | [Fact] 22 | public void AddMessaging_RequiresBuilderAction() 23 | { 24 | // Arrange 25 | var services = new ServiceCollection(); 26 | 27 | // Act/Assert 28 | Assert.Throws(() => services.AddMessaging(null)); 29 | } 30 | 31 | [Fact] 32 | public void AddMessaging_TopicSerializerRawPublisherAndRawSubscriber_BuildsMessaging() 33 | { 34 | // Arrange 35 | var s = new ServiceCollection(); 36 | 37 | // Act 38 | s.AddMessaging(builder => 39 | { 40 | builder.AddSerialization(b => b.AddSerializer(ServiceLifetime.Singleton)); 41 | builder.AddRawMessageHandlerSubscriber(); 42 | builder.AddRawMessagePublisher(); 43 | builder.AddTopic( 44 | nameof(MessagingServiceCollectionExtensionsTests)); 45 | }); 46 | 47 | // Assert 48 | AssertMessagingBuilt(s); 49 | } 50 | 51 | [Fact] 52 | public void AddMessaging_TopicSerializerRawPublisherAndRawSubscriber_AddedViaServices_BuildsMessaging() 53 | { 54 | // Arrange 55 | var s = new ServiceCollection(); 56 | s.AddSingleton(); 57 | s.AddSingleton(); 58 | s.AddSingleton(); 59 | s.AddSingleton(new MessageTypeTopicMap 60 | { 61 | {typeof(MessagingServiceCollectionExtensionsTests), 62 | nameof(MessagingServiceCollectionExtensionsTests)} 63 | }); 64 | 65 | // Act 66 | s.AddMessaging(builder => { }); 67 | 68 | // Assert 69 | AssertMessagingBuilt(s); 70 | } 71 | 72 | // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local 73 | private static void AssertMessagingBuilt(ServiceCollection s) 74 | { 75 | // Services required to be provided by the caller of AddMessaging: 76 | Assert.Equal(1, s.Count(d => d.ServiceType == typeof(ISerializer))); 77 | Assert.Equal(1, s.Count(d => d.ServiceType == typeof(IRawMessageHandlerSubscriber))); 78 | Assert.Equal(1, s.Count(d => d.ServiceType == typeof(IRawMessagePublisher))); 79 | Assert.Equal(1, s.Count(d => d.ServiceType == typeof(IMessageTypeTopicMap))); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Kafka.Tests/Greentube.Messaging.Kafka.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Kafka.Tests/KafkaBlockingRawMessageReaderFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | using NSubstitute; 4 | using Xunit; 5 | 6 | namespace Greentube.Messaging.Kafka.Tests 7 | { 8 | public class KafkaBlockingRawMessageReaderFactoryTests 9 | { 10 | private readonly KafkaBlockingRawMessageReaderFactory _sut = new KafkaBlockingRawMessageReaderFactory(); 11 | 12 | [Fact] 13 | public void Create_DisposesOnCallbackError() 14 | { 15 | var consumer = Substitute.For(); 16 | Assert.Throws(() => _sut.Create(() => consumer, "topic", new KafkaOptions 17 | { 18 | Properties = { GroupId = "groupId" }, 19 | Subscriber = 20 | { 21 | ConsumerCreatedCallback = _ => 22 | throw new DivideByZeroException() 23 | } 24 | })); 25 | 26 | consumer.Received(1).Dispose(); 27 | } 28 | 29 | [Theory, AutoSubstituteData] 30 | public void Create_NullCallback_Subscribes(string topic) 31 | { 32 | var consumer = Substitute.For(); 33 | _sut.Create(() => consumer, topic, new KafkaOptions 34 | { 35 | Properties = { GroupId = "groupId" } 36 | }); 37 | 38 | consumer.Received(1).Subscribe(topic); 39 | } 40 | 41 | [Fact] 42 | public void Create_ReturnsKafkaBlockingRawMessageReader() 43 | { 44 | var reader = _sut.Create("topic", new KafkaOptions 45 | { 46 | Properties = { GroupId = "groupId" }, 47 | Subscriber = { ConsumerCreatedCallback = consumer => 48 | Assert.Equal(typeof(Consumer), consumer.GetType()) } 49 | }); 50 | 51 | try 52 | { 53 | Assert.Equal(typeof(KafkaBlockingRawMessageReader), reader.GetType()); 54 | } 55 | finally 56 | { 57 | (reader as IDisposable)?.Dispose(); 58 | } 59 | } 60 | 61 | [Fact] 62 | public void Create_NullTopic_ThrowsArgumentNull() 63 | { 64 | Assert.Throws(() => _sut.Create(null, new KafkaOptions())); 65 | } 66 | 67 | [Fact] 68 | public void Create_NullTopic_NoReaderCreated() 69 | { 70 | bool called = false; 71 | 72 | Assert.Throws(() => 73 | _sut.Create(() => 74 | { 75 | called = false; 76 | return null; 77 | }, null, new KafkaOptions())); 78 | 79 | Assert.False(called); 80 | } 81 | 82 | [Fact] 83 | public void Create_NullOptions_ThrowsArgumentNull() 84 | { 85 | Assert.Throws(() => _sut.Create("topic", null)); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Kafka.Tests/KafkaBlockingRawMessageReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Confluent.Kafka; 3 | using NSubstitute; 4 | using Xunit; 5 | 6 | namespace Greentube.Messaging.Kafka.Tests 7 | { 8 | public class KafkaBlockingRawMessageReaderTests 9 | { 10 | [Fact] 11 | public void TryGetMessage_ConsumerReturnsFalse_ReaderReturnsFalse() 12 | { 13 | // Arrange 14 | var options = new KafkaOptions(); 15 | 16 | var consumer = Substitute.For(); 17 | consumer.Consume(out _, Arg.Any()) 18 | .Returns(r => { 19 | r[0] = null; 20 | return false; 21 | }); 22 | 23 | // Act 24 | var sut = new KafkaBlockingRawMessageReader(consumer); 25 | var read = sut.TryGetMessage(out var readMessage, options); 26 | 27 | // Assert 28 | Assert.False(read); 29 | Assert.Null(readMessage); 30 | } 31 | 32 | [Theory, AutoSubstituteData] 33 | public void TryGetMessage_ReturnsConsumerMessage(byte[] expectedMessage, string topic) 34 | { 35 | // Arrange 36 | var options = new KafkaOptions(); 37 | 38 | var consumer = Substitute.For(); 39 | consumer.Consume(out _, Arg.Any()) 40 | .Returns(r => { 41 | r[0] = new Message(topic, 0, 0, null, expectedMessage, new Timestamp(), null); 42 | return true; 43 | }); 44 | 45 | // Act 46 | var sut = new KafkaBlockingRawMessageReader(consumer); 47 | var read = sut.TryGetMessage(out var actualMessage, options); 48 | 49 | // Assert 50 | Assert.True(read); 51 | Assert.Equal(expectedMessage, actualMessage); 52 | } 53 | 54 | [Fact] 55 | public void TryGetMessage_NullOptions_ThrowsArgumentNull() 56 | { 57 | var consumer = Substitute.For(); 58 | 59 | var sut = new KafkaBlockingRawMessageReader(consumer); 60 | 61 | Assert.Throws(() => sut.TryGetMessage(out var _, null)); 62 | } 63 | 64 | [Fact] 65 | public void Dispose_DisposesKafkaConsumer() 66 | { 67 | var consumer = Substitute.For(); 68 | var sut = new KafkaBlockingRawMessageReader(consumer); 69 | 70 | sut.Dispose(); 71 | 72 | consumer.Received(1).Dispose(); 73 | } 74 | 75 | [Fact] 76 | public void Constructor_NullConsumer_ThrowsArgumentNull() 77 | { 78 | Assert.Throws(() => new KafkaBlockingRawMessageReader(null)); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Kafka.Tests/KafkaPropertiesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Xunit; 6 | 7 | namespace Greentube.Messaging.Kafka.Tests 8 | { 9 | public class KafkaPropertiesTests 10 | { 11 | // KafkaProperties extends dictionary of string,object to have a reference on some commonly used fields 12 | // This test ensures the map is correctly on get and set accessors 13 | [Theory] 14 | [MemberData(nameof(KafkaPropertiesProperties))] 15 | public void PropertyGetSetMapCorrectly(PropertyInfo propertyInfo) 16 | { 17 | // Arrange 18 | var sut = new KafkaProperties(); 19 | var expected = Convert.ChangeType(1, propertyInfo.PropertyType); 20 | // Act 21 | propertyInfo.SetValue(sut, expected); 22 | var actual = propertyInfo.GetValue(sut); 23 | // Assert 24 | Assert.Equal(expected, actual); 25 | } 26 | 27 | // [xUnit1016] MemberData must reference a public member 28 | // ReSharper disable once MemberCanBePrivate.Global 29 | public static IEnumerable KafkaPropertiesProperties() 30 | => typeof(KafkaProperties).GetProperties( 31 | BindingFlags.DeclaredOnly 32 | | BindingFlags.Instance 33 | | BindingFlags.Public) 34 | .Select(p => new object[] { p }); 35 | } 36 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Kafka.Tests/KafkaRawMessagePublisherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NSubstitute; 4 | using Xunit; 5 | using static System.Threading.CancellationToken; 6 | 7 | namespace Greentube.Messaging.Kafka.Tests 8 | { 9 | // Since KafkaRawMessagePublisher is coupled somewhat to the concrete Producer class of Confluent.Kafka 10 | // the scope of these tests are reduced 11 | public class KafkaRawMessagePublisherTests 12 | { 13 | [Theory, AutoSubstituteData] 14 | public async Task Publish_CallsInnerPublisher(string topic, byte[] message) 15 | { 16 | var options = new KafkaOptions(); 17 | 18 | var producer = Substitute.For(); 19 | var sut = new KafkaRawMessagePublisher(() => producer, options); 20 | await sut.Publish(topic, message, None); 21 | 22 | await producer.Received(1).ProduceAsync(topic, null, message); 23 | } 24 | 25 | [Fact] 26 | public void Dispose_CallsFlushWithTimeout() 27 | { 28 | var options = new KafkaOptions 29 | { 30 | Publisher = { FlushTimeout = TimeSpan.MaxValue } 31 | }; 32 | 33 | var producer = Substitute.For(); 34 | var sut = new KafkaRawMessagePublisher(() => producer, options); 35 | 36 | sut.Dispose(); 37 | 38 | producer.Received(1).Dispose(); 39 | producer.Received(1).Flush(TimeSpan.MaxValue); 40 | } 41 | 42 | [Fact] 43 | public void Constructor_CallbackThrows_DisposesProducer() 44 | { 45 | var options = new KafkaOptions 46 | { 47 | Publisher = {ProducerCreatedCallback = _ => throw new DivideByZeroException()} 48 | }; 49 | 50 | var producer = Substitute.For(); 51 | Assert.Throws(() => new KafkaRawMessagePublisher(() => producer, options)); 52 | 53 | producer.Received(1).Dispose(); 54 | } 55 | 56 | [Fact] 57 | public void Constructor_NullOptions_ThrowsArgumentNull() 58 | { 59 | Assert.Throws(() => new KafkaRawMessagePublisher(null)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Redis.Tests/Greentube.Messaging.Redis.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Redis.Tests/RedisRawMessageHandlerSubscriberTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoFixture.Xunit2; 4 | using NSubstitute; 5 | using NSubstitute.ReturnsExtensions; 6 | using StackExchange.Redis; 7 | using Xunit; 8 | using static System.Threading.CancellationToken; 9 | 10 | namespace Greentube.Messaging.Redis.Tests 11 | { 12 | public class RedisRawMessageHandlerSubscriberTests 13 | { 14 | [Theory, AutoSubstituteData] 15 | public async Task Subscribe_SubscribersRawMessageHandler( 16 | [Frozen] ISubscriber subscriber, 17 | string topic, 18 | byte[] message, 19 | IRawMessageHandler rawMessageHandler, 20 | RedisRawMessageHandlerSubscriber sut) 21 | { 22 | Action redisCallback = null; 23 | await subscriber.SubscribeAsync(topic, Arg.Do>(action => redisCallback = action)); 24 | 25 | await sut.Subscribe(topic, rawMessageHandler, None); 26 | 27 | Assert.NotNull(redisCallback); 28 | redisCallback(topic, message); 29 | await rawMessageHandler.Received(1).Handle(topic, message, None); 30 | } 31 | 32 | [Theory, AutoSubstituteData] 33 | public async Task Subscribe_MultiplexerReturnsNoSubscription_ThrowsInvalidOperation( 34 | [Frozen] IConnectionMultiplexer connectionMultiplexer, 35 | string topic, 36 | IRawMessageHandler rawMessageHandler, 37 | RedisRawMessageHandlerSubscriber sut) 38 | { 39 | connectionMultiplexer.GetSubscriber().ReturnsNull(); 40 | 41 | var ex = await Assert.ThrowsAsync(() => sut.Subscribe(topic, rawMessageHandler, None)); 42 | Assert.Equal("Redis Multiplexer returned no subscription.", ex.Message); 43 | } 44 | 45 | [Theory, AutoSubstituteData] 46 | public async Task Subscribe_NullTopic_ThrowsArgumentNull( 47 | IRawMessageHandler rawMessageHandler, 48 | RedisRawMessageHandlerSubscriber sut) 49 | { 50 | var ex = await Assert.ThrowsAsync(() => sut.Subscribe(null, rawMessageHandler, None)); 51 | Assert.Equal("topic", ex.ParamName); 52 | } 53 | 54 | [Theory, AutoSubstituteData] 55 | public async Task Subscribe_NullRawMessageHandler_ThrowsArgumentNull( 56 | string topic, 57 | RedisRawMessageHandlerSubscriber sut) 58 | { 59 | var ex = await Assert.ThrowsAsync(() => sut.Subscribe(topic, null, None)); 60 | Assert.Equal("rawHandler", ex.ParamName); 61 | } 62 | 63 | [Fact] 64 | public void Constructor_NullMultiplexer_ThrowsArgumentNull() 65 | { 66 | var ex = Assert.Throws(() => new RedisRawMessageHandlerSubscriber(null)); 67 | Assert.Equal("connectionMultiplexer", ex.ParamName); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Redis.Tests/RedisRawMessagePublisherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoFixture.Xunit2; 4 | using NSubstitute; 5 | using NSubstitute.ReturnsExtensions; 6 | using StackExchange.Redis; 7 | using Xunit; 8 | using static System.Threading.CancellationToken; 9 | 10 | namespace Greentube.Messaging.Redis.Tests 11 | { 12 | public class RedisRawMessagePublisherTests 13 | { 14 | [Theory, AutoSubstituteData] 15 | public async Task Publish_DataPassedToRedisPublisher( 16 | [Frozen] ISubscriber subscriber, 17 | string topic, 18 | byte[] message, 19 | RedisRawMessagePublisher sut) 20 | { 21 | await sut.Publish(topic, message, None); 22 | 23 | await subscriber.Received(1).PublishAsync(topic, message); 24 | } 25 | 26 | [Theory, AutoSubstituteData] 27 | public async Task Publish_NullMessage_ThrowsArgumentNull(RedisRawMessagePublisher sut, string topic) 28 | { 29 | var ex = await Assert.ThrowsAsync(() => sut.Publish(topic, null, None)); 30 | Assert.Equal("message", ex.ParamName); 31 | } 32 | 33 | [Theory, AutoSubstituteData] 34 | public async Task Publish_NullTopic_ThrowsArgumentNull(RedisRawMessagePublisher sut, byte[] message) 35 | { 36 | var ex = await Assert.ThrowsAsync(() => sut.Publish(null, message, None)); 37 | Assert.Equal("topic", ex.ParamName); 38 | } 39 | 40 | [Theory, AutoSubstituteData] 41 | public void Constructor_MultiplexerReturnsNoSubscription_ThrowsArgumentException( 42 | IConnectionMultiplexer connectionMultiplexer) 43 | { 44 | connectionMultiplexer.GetSubscriber().ReturnsNull(); 45 | 46 | var ex = Assert.Throws(() => new RedisRawMessagePublisher(connectionMultiplexer)); 47 | 48 | Assert.Equal("connectionMultiplexer", ex.ParamName); 49 | Assert.Equal($@"Redis Multiplexer returned no subscription. 50 | Parameter name: {ex.ParamName}", ex.Message); 51 | } 52 | 53 | [Fact] 54 | public void Constructor_NullMultiplexer_ThrowsArgumentNull() 55 | { 56 | var ex = Assert.Throws(() => new RedisRawMessagePublisher(null)); 57 | Assert.Equal("connectionMultiplexer", ex.ParamName); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Testing/AutoSubstituteDataAttribute.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoFixture.AutoNSubstitute; 3 | using AutoFixture.Xunit2; 4 | 5 | // ReSharper disable once CheckNamespace - discoverability 6 | namespace Greentube.Messaging 7 | { 8 | public class AutoSubstituteDataAttribute : AutoDataAttribute 9 | { 10 | public AutoSubstituteDataAttribute() 11 | : base(() => new Fixture().Customize(new AutoConfiguredNSubstituteCustomization())) 12 | { 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Testing/Greentube.Messaging.Testing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/BlockingReaderRawMessageHandlerSubscriberTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using AutoFixture.Xunit2; 7 | using NSubstitute; 8 | using NSubstitute.ExceptionExtensions; 9 | using Xunit; 10 | using static System.Threading.CancellationToken; 11 | 12 | namespace Greentube.Messaging.Tests 13 | { 14 | public class BlockingReaderRawMessageHandlerSubscriberTests 15 | { 16 | [Theory, AutoSubstituteData] 17 | public async Task Subscribe_CallbackNull_DoesntThrow( 18 | string topic, 19 | IDisposableBlockingRawMessageReader reader, 20 | [Frozen] IPollingOptions options, 21 | [Frozen] IRawMessageHandler rawMessageHandler, 22 | BlockingReaderRawMessageHandlerSubscriber sut) 23 | { 24 | // Arrange 25 | options.ReaderStoppingCallback = null; 26 | options.ErrorCallback = null; 27 | reader.TryGetMessage(out var _, options).Throws(); 28 | 29 | // Act 30 | await sut.Subscribe(topic, rawMessageHandler, None); 31 | // await doesn't throw on OperationCancelledException 32 | await sut.Unsubscribe(topic, rawMessageHandler, None); 33 | } 34 | 35 | 36 | [Theory, AutoSubstituteData] 37 | public async Task Subscribe_CallbackRethrows_UnsubscribeDoesNotFailAwait( 38 | string topic, 39 | [Frozen] IPollingOptions options, 40 | [Frozen] IRawMessageHandler rawMessageHandler, 41 | BlockingReaderRawMessageHandlerSubscriber sut) 42 | { 43 | // Arrange 44 | options.ReaderStoppingCallback = (s, handler, ex) => throw ex; 45 | 46 | // Act 47 | await sut.Subscribe(topic, rawMessageHandler, None); 48 | // await doesn't throw on OperationCancelledException 49 | await sut.Unsubscribe(topic, rawMessageHandler, None); 50 | } 51 | 52 | [Theory, AutoSubstituteData] 53 | public async Task Subscribe_AfterUnsubscribing_CreatesNewReader( 54 | string topic, 55 | IDisposableBlockingRawMessageReader reader, 56 | [Frozen] IPollingOptions options, 57 | [Frozen] IRawMessageHandler rawMessageHandler, 58 | [Frozen] IBlockingRawMessageReaderFactory factory, 59 | BlockingReaderRawMessageHandlerSubscriber sut) 60 | { 61 | // Arrange 62 | factory.Create(topic, options).Returns(reader); 63 | 64 | var disposeEvent = new ManualResetEventSlim(); 65 | reader.When(r => r.Dispose()).Do(r => disposeEvent.Set()); 66 | 67 | // Act 68 | await sut.Subscribe(topic, rawMessageHandler, None); 69 | await sut.Unsubscribe(topic, rawMessageHandler, None); 70 | Assert.True(disposeEvent.Wait(1000)); // wait task complete 71 | await sut.Subscribe(topic, rawMessageHandler, None); 72 | 73 | // Assert 74 | factory.Received(2).Create(topic, options); 75 | } 76 | 77 | [Theory, AutoSubstituteData] 78 | public async Task Unsubscribe_CallbackInvoked( 79 | string topic, 80 | IDisposableBlockingRawMessageReader reader, 81 | [Frozen] IPollingOptions options, 82 | [Frozen] IRawMessageHandler rawMessageHandler, 83 | [Frozen] IBlockingRawMessageReaderFactory factory, 84 | BlockingReaderRawMessageHandlerSubscriber sut) 85 | { 86 | // Arrange 87 | var callbackInvokedEvent = new ManualResetEventSlim(); 88 | options.ReaderStoppingCallback = (t, handler, arg3) => callbackInvokedEvent.Set(); 89 | factory.Create(topic, options).Returns(reader); 90 | 91 | // Act 92 | await sut.Subscribe(topic, rawMessageHandler, None); 93 | await sut.Unsubscribe(topic, rawMessageHandler, None); 94 | 95 | // Assert 96 | Assert.True(callbackInvokedEvent.Wait(1000)); 97 | } 98 | 99 | [Theory, AutoSubstituteData] 100 | public async Task Unsubscribe_DisposesReader( 101 | string topic, 102 | IDisposableBlockingRawMessageReader reader, 103 | [Frozen] IPollingOptions options, 104 | [Frozen] IRawMessageHandler rawMessageHandler, 105 | [Frozen] IBlockingRawMessageReaderFactory factory, 106 | BlockingReaderRawMessageHandlerSubscriber sut) 107 | { 108 | // Arrange 109 | factory.Create(topic, options).Returns(reader); 110 | 111 | var disposeEvent = new ManualResetEventSlim(); 112 | reader.When(r => r.Dispose()).Do(r => disposeEvent.Set()); 113 | 114 | // Act 115 | await sut.Subscribe(topic, rawMessageHandler, None); 116 | await sut.Unsubscribe(topic, rawMessageHandler, None); 117 | 118 | // Assert 119 | Assert.True(disposeEvent.Wait(1000)); 120 | } 121 | 122 | [Theory, AutoSubstituteData] 123 | public async Task Unsubscribe_DifferentHandlers_DisposesSingleReader( 124 | string topic, 125 | IDisposableBlockingRawMessageReader firstReader, 126 | IDisposableBlockingRawMessageReader secondReader, 127 | [Frozen] IPollingOptions options, 128 | IRawMessageHandler firstRawMessageHandler, 129 | IRawMessageHandler secondRawMessageHandler, 130 | [Frozen] IBlockingRawMessageReaderFactory factory, 131 | BlockingReaderRawMessageHandlerSubscriber sut) 132 | { 133 | // Arrange 134 | factory.Create(topic, options).Returns(firstReader, secondReader); 135 | 136 | var disposeEvent = new ManualResetEventSlim(); 137 | firstReader.When(r => r.Dispose()).Do(r => disposeEvent.Set()); 138 | 139 | // Act 140 | await sut.Subscribe(topic, firstRawMessageHandler, None); 141 | await sut.Subscribe(topic, secondRawMessageHandler, None); 142 | await sut.Unsubscribe(topic, firstRawMessageHandler, None); 143 | 144 | // Assert 145 | Assert.True(disposeEvent.Wait(1000)); 146 | secondReader.DidNotReceive().Dispose(); 147 | } 148 | 149 | [Theory, AutoSubstituteData] 150 | public async Task Unsubscribe_DifferentTopics_DisposesSingleReader( 151 | string firstTopic, 152 | string secondTopic, 153 | IDisposableBlockingRawMessageReader firstReader, 154 | IDisposableBlockingRawMessageReader secondReader, 155 | [Frozen] IPollingOptions options, 156 | IRawMessageHandler firstRawMessageHandler, 157 | IRawMessageHandler secondRawMessageHandler, 158 | [Frozen] IBlockingRawMessageReaderFactory factory, 159 | BlockingReaderRawMessageHandlerSubscriber sut) 160 | { 161 | // Arrange 162 | factory.Create(firstTopic, options).Returns(firstReader); 163 | factory.Create(secondTopic, options).Returns(secondReader); 164 | 165 | var disposeEvent = new ManualResetEventSlim(); 166 | firstReader.When(r => r.Dispose()).Do(r => disposeEvent.Set()); 167 | 168 | // Act 169 | await sut.Subscribe(firstTopic, firstRawMessageHandler, None); 170 | await sut.Subscribe(secondTopic, secondRawMessageHandler, None); 171 | await sut.Unsubscribe(firstTopic, firstRawMessageHandler, None); 172 | 173 | // Assert 174 | Assert.True(disposeEvent.Wait(1000)); 175 | secondReader.DidNotReceive().Dispose(); 176 | } 177 | 178 | [Theory, AutoSubstituteData] 179 | public async Task Subscribe_HandlerThrows_DisposesReader( 180 | string topic, 181 | byte[] message, 182 | IDisposableBlockingRawMessageReader reader, 183 | [Frozen] IPollingOptions options, 184 | [Frozen] IRawMessageHandler rawMessageHandler, 185 | [Frozen] IBlockingRawMessageReaderFactory factory, 186 | BlockingReaderRawMessageHandlerSubscriber sut) 187 | { 188 | // Arrange 189 | factory.Create(topic, options).Returns(reader); 190 | 191 | var disposeEvent = new ManualResetEventSlim(); 192 | reader.When(r => r.Dispose()).Do(r => disposeEvent.Set()); 193 | 194 | reader.TryGetMessage(out var _, options) 195 | .Returns(r => 196 | { 197 | r[0] = message; 198 | return true; 199 | }); 200 | 201 | rawMessageHandler.Handle(topic, message, Arg.Any()) 202 | .Throws(); 203 | 204 | // Act 205 | await sut.Subscribe(topic, rawMessageHandler, None); 206 | 207 | // Assert 208 | Assert.True(disposeEvent.Wait(1000)); 209 | } 210 | 211 | [Theory, AutoSubstituteData] 212 | public async Task Subscribe_ReaderThrows_DisposesReader( 213 | string topic, 214 | IRawMessageHandler rawMessageHandler, 215 | IDisposableBlockingRawMessageReader reader, 216 | [Frozen] IPollingOptions options, 217 | [Frozen] IBlockingRawMessageReaderFactory factory, 218 | BlockingReaderRawMessageHandlerSubscriber sut) 219 | { 220 | factory.Create(topic, options).Returns(reader); 221 | 222 | var disposeEvent = new ManualResetEventSlim(); 223 | reader.When(r => r.Dispose()).Do(r => disposeEvent.Set()); 224 | 225 | reader.TryGetMessage(out var _, options) 226 | .Throws(); 227 | 228 | await sut.Subscribe(topic, rawMessageHandler, None); 229 | 230 | Assert.True(disposeEvent.Wait(1000)); 231 | } 232 | 233 | [Theory, AutoSubstituteData] 234 | public async Task Subscribe_ReaderFactoryThrows_SubscriptionFails( 235 | string topic, 236 | IRawMessageHandler rawMessageHandler, 237 | [Frozen] IPollingOptions options, 238 | [Frozen] IBlockingRawMessageReaderFactory factory, 239 | BlockingReaderRawMessageHandlerSubscriber sut) 240 | { 241 | factory.Create(topic, options).Throws(new DivideByZeroException()); 242 | await Assert.ThrowsAsync(() => sut.Subscribe(topic, rawMessageHandler, None)); 243 | } 244 | 245 | [Theory, AutoSubstituteData] 246 | public async Task Subscribe_MultipleCallsDifferentRawHandlers_CreatesSingleReader( 247 | string topic, 248 | IRawMessageHandler firstRawMessageHandler, 249 | IRawMessageHandler secondRawMessageHandler, 250 | [Frozen] IPollingOptions options, 251 | [Frozen] IBlockingRawMessageReaderFactory factory, 252 | BlockingReaderRawMessageHandlerSubscriber sut) 253 | { 254 | await Task.WhenAll(Subscrioptions()); 255 | 256 | factory.Received(2).Create(topic, options); 257 | 258 | IEnumerable Subscrioptions() 259 | { 260 | for (int i = 0; i < 5; i++) 261 | { 262 | yield return sut.Subscribe(topic, firstRawMessageHandler, None); 263 | yield return sut.Subscribe(topic, secondRawMessageHandler, None); 264 | } 265 | } 266 | } 267 | 268 | [Theory, AutoSubstituteData] 269 | public async Task Subscribe_MultipleCallsSameRawHandler_CreatesSingleReader( 270 | string topic, 271 | IRawMessageHandler rawMessageHandler, 272 | [Frozen] IPollingOptions options, 273 | [Frozen] IBlockingRawMessageReaderFactory factory, 274 | BlockingReaderRawMessageHandlerSubscriber sut) 275 | { 276 | var tasks = Enumerable.Range(1, 5).Select(_ => sut.Subscribe(topic, rawMessageHandler, None)); 277 | await Task.WhenAll(tasks); 278 | 279 | factory.Received(1).Create(topic, options); 280 | } 281 | 282 | [Theory, AutoSubstituteData] 283 | public async Task Subscribe_CancelledToken_CancelsSubscriptionTask( 284 | string topic, 285 | IRawMessageHandler rawMessageHandler, 286 | [Frozen] IPollingOptions options, 287 | [Frozen] IBlockingRawMessageReaderFactory factory, 288 | BlockingReaderRawMessageHandlerSubscriber sut) 289 | { 290 | // Arrange 291 | var cts = new CancellationTokenSource(); 292 | cts.Cancel(); 293 | 294 | // Act 295 | var task = sut.Subscribe(topic, rawMessageHandler, cts.Token); 296 | 297 | // Assert 298 | await Assert.ThrowsAsync(() => task); 299 | 300 | factory.DidNotReceiveWithAnyArgs().Create(topic, options); 301 | } 302 | 303 | [Theory, AutoSubstituteData] 304 | public async Task Dispose_DisposesAllReaders( 305 | string topic, 306 | IDisposableBlockingRawMessageReader firstReader, 307 | IDisposableBlockingRawMessageReader secondReader, 308 | [Frozen] IPollingOptions options, 309 | IRawMessageHandler firstRawMessageHandler, 310 | IRawMessageHandler secondRawMessageHandler, 311 | [Frozen] IBlockingRawMessageReaderFactory factory, 312 | BlockingReaderRawMessageHandlerSubscriber sut) 313 | { 314 | // Arrange 315 | factory.Create(topic, options).Returns(firstReader, secondReader); 316 | 317 | var firstDisposeEvent = new ManualResetEventSlim(); 318 | firstReader.When(r => r.Dispose()).Do(r => firstDisposeEvent.Set()); 319 | var secondDisposeEvent = new ManualResetEventSlim(); 320 | firstReader.When(r => r.Dispose()).Do(r => secondDisposeEvent.Set()); 321 | 322 | await sut.Subscribe(topic, firstRawMessageHandler, None); 323 | await sut.Subscribe(topic, secondRawMessageHandler, None); 324 | 325 | // Act 326 | sut.Dispose(); 327 | 328 | // Assert 329 | Assert.True(firstDisposeEvent.Wait(1000)); 330 | Assert.True(secondDisposeEvent.Wait(1000)); 331 | } 332 | 333 | [Theory, AutoSubstituteData] 334 | public async Task Subscribe_NullRawHandler_ThrowsArgumentNull( 335 | string topic, 336 | BlockingReaderRawMessageHandlerSubscriber sut) 337 | { 338 | var ex = await Assert.ThrowsAsync(() => sut.Subscribe(topic, null, None)); 339 | Assert.Equal("rawHandler", ex.ParamName); 340 | } 341 | 342 | [Theory, AutoSubstituteData] 343 | public async Task Subscribe_NullTopic_ThrowsArgumentNull( 344 | IRawMessageHandler rawMessageHandler, 345 | BlockingReaderRawMessageHandlerSubscriber sut) 346 | { 347 | var ex = await Assert.ThrowsAsync(() => sut.Subscribe(null, rawMessageHandler, None)); 348 | Assert.Equal("topic", ex.ParamName); 349 | } 350 | 351 | [Theory, AutoSubstituteData] 352 | public void Constructor_NullOptions_ThrowsArgumentNull(IBlockingRawMessageReaderFactory factory) 353 | { 354 | var ex = Assert.Throws(() => 355 | new BlockingReaderRawMessageHandlerSubscriber(factory, null)); 356 | Assert.Equal("options", ex.ParamName); 357 | } 358 | 359 | [Theory, AutoSubstituteData] 360 | public void Constructor_NullFactory_ThrowsArgumentNull(IPollingOptions options) 361 | { 362 | var ex = Assert.Throws(() => 363 | new BlockingReaderRawMessageHandlerSubscriber(null, options)); 364 | Assert.Equal("factory", ex.ParamName); 365 | } 366 | } 367 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/DispatchingRawMessageHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoFixture.Xunit2; 4 | using NSubstitute; 5 | using NSubstitute.ReturnsExtensions; 6 | using Greentube.Serialization; 7 | using Xunit; 8 | using static System.Threading.CancellationToken; 9 | 10 | namespace Greentube.Messaging.Tests 11 | { 12 | public class DispatchingRawMessageHandlerTests 13 | { 14 | [Theory, AutoSubstituteData] 15 | public async Task Handle_DeserializedMessage_CallsInvoker( 16 | string topic, 17 | byte[] message, 18 | object deserializedObject, 19 | [Frozen] IMessageHandlerInvoker messageHandlerInvoker, 20 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 21 | [Frozen] ISerializer serializer, 22 | DispatchingRawMessageHandler sut) 23 | { 24 | // Arrange 25 | var messageType = GetType(); 26 | messageTypeTopicMap.Get(topic).Returns(messageType); 27 | serializer.Deserialize(messageType, message).Returns(deserializedObject); 28 | 29 | // Act 30 | await sut.Handle(topic, message, None); 31 | 32 | // Assert 33 | await messageHandlerInvoker.Received().Invoke(deserializedObject, None); 34 | } 35 | 36 | [Theory, AutoSubstituteData] 37 | public async Task Handle_DeserializerReturnsNull_ThrowsInvalidOperation( 38 | string topic, 39 | byte[] message, 40 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 41 | [Frozen] ISerializer serializer, 42 | DispatchingRawMessageHandler sut) 43 | { 44 | var messageType = GetType(); 45 | messageTypeTopicMap.Get(topic).Returns(messageType); 46 | serializer.Deserialize(messageType, message).ReturnsNull(); 47 | 48 | var ex = await Assert.ThrowsAsync(() => sut.Handle(topic, message, None)); 49 | Assert.Equal($"Serializer {serializer.GetType()} returned null for the {message.Length}-byte message of type {messageType}.", ex.Message); 50 | } 51 | 52 | [Theory, AutoSubstituteData] 53 | public async Task Handle_TopicMapNotDefined_ThrowsInvalidOperation( 54 | string topic, 55 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 56 | DispatchingRawMessageHandler sut) 57 | { 58 | messageTypeTopicMap.Get(topic).ReturnsNull(); 59 | var ex = await Assert.ThrowsAsync(() => sut.Handle(topic, new byte[0], None)); 60 | Assert.Equal($"Topic '{topic}' has no message type registered with: {messageTypeTopicMap.GetType()}.", ex.Message); 61 | } 62 | 63 | [Theory, AutoSubstituteData] 64 | public async Task Handle_NullTopic_ThrowsArgumentNull(DispatchingRawMessageHandler sut) 65 | { 66 | var ex = await Assert.ThrowsAsync(() => sut.Handle(null, new byte[0], None)); 67 | Assert.Equal("topic", ex.ParamName); 68 | } 69 | 70 | [Theory, AutoSubstituteData] 71 | public async Task Handle_NullMessage_ThrowsArgumentNull(DispatchingRawMessageHandler sut) 72 | { 73 | var ex = await Assert.ThrowsAsync(() => sut.Handle("topic.name", null, None)); 74 | Assert.Equal("message", ex.ParamName); 75 | } 76 | 77 | [Theory, AutoSubstituteData] 78 | public void Constructor_NullMessageHandlerInvoker_ThrowsNullArgument( 79 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 80 | [Frozen] ISerializer serializer) 81 | { 82 | var ex = Assert.Throws(() => new DispatchingRawMessageHandler(messageTypeTopicMap, serializer, null)); 83 | Assert.Equal("messageHandlerInvoker", ex.ParamName); 84 | } 85 | 86 | [Theory, AutoSubstituteData] 87 | public void Constructor_NullMessageTypeTopicMap_ThrowsNullArgument( 88 | [Frozen] IMessageHandlerInvoker messageHandlerInvoker, 89 | [Frozen] ISerializer serializer) 90 | { 91 | var ex = Assert.Throws(() => new DispatchingRawMessageHandler(null, serializer, messageHandlerInvoker)); 92 | Assert.Equal("typeTopicMap", ex.ParamName); 93 | } 94 | 95 | [Theory, AutoSubstituteData] 96 | public void Constructor_NullISerialize_ThrowsNullArgument( 97 | [Frozen] IMessageHandlerInvoker messageHandlerInvoker, 98 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap) 99 | { 100 | var ex = Assert.Throws(() => new DispatchingRawMessageHandler(messageTypeTopicMap, null, messageHandlerInvoker)); 101 | Assert.Equal("serializer", ex.ParamName); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/Greentube.Messaging.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | false 5 | $(NoWarn);CS1591 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/IDisposableBlockingRawMessageReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Greentube.Messaging.Tests 4 | { 5 | public interface IDisposableBlockingRawMessageReader 6 | : IDisposable, IBlockingRawMessageReader where TOptions : IPollingOptions 7 | { } 8 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/MessageHandlerInfoProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using AutoFixture.Xunit2; 5 | using NSubstitute; 6 | using Xunit; 7 | using static System.Threading.CancellationToken; 8 | 9 | namespace Greentube.Messaging.Tests 10 | { 11 | public class MessageHandlerInfoProviderTests 12 | { 13 | [Theory, AutoSubstituteData] 14 | public async Task GetHandlerInfo_FindInnerClassHandlerInfo( 15 | string message, 16 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 17 | MessageHandlerInfoProvider sut) 18 | { 19 | var messageType = typeof(string); 20 | messageTypeTopicMap.GetMessageTypes().Returns(new[] { messageType }); 21 | 22 | var actual = sut.GetHandlerInfo(); 23 | 24 | var handlerInfo = Assert.Single(actual); 25 | 26 | Assert.Equal(messageType, handlerInfo.messageType); 27 | Assert.Equal(typeof(IMessageHandler), handlerInfo.handlerType); 28 | 29 | bool callbackFired = false; 30 | var handler = new TestStringHandler(m => 31 | { 32 | callbackFired = true; 33 | Assert.Equal(message, m); 34 | }); 35 | await handlerInfo.handleMethod(handler, message, None); 36 | Assert.True(callbackFired); 37 | } 38 | 39 | [Fact] 40 | public void Constructor_EmptyTypeTopicMap_ThrowsArgumentException() 41 | { 42 | var emptyMap = new MessageTypeTopicMap(); 43 | 44 | var ex = Assert.Throws(() => new MessageHandlerInfoProvider(emptyMap)); 45 | Assert.Equal($"{nameof(IMessageTypeTopicMap)} is empty.", ex.Message); 46 | } 47 | 48 | [Fact] 49 | public void Constructor_NullMessageTypeTopicMap_ThrowsNullArgument() 50 | { 51 | var ex = Assert.Throws(() => new MessageHandlerInfoProvider(null)); 52 | Assert.Equal("typeTopicMap", ex.ParamName); 53 | } 54 | 55 | private class TestStringHandler : IMessageHandler 56 | { 57 | private readonly Action _callback; 58 | public TestStringHandler(Action callback) => _callback = callback; 59 | 60 | public Task Handle(string message, CancellationToken token) 61 | { 62 | _callback(message); 63 | return Task.CompletedTask; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/MessageHandlerInvokerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using AutoFixture; 5 | using AutoFixture.Xunit2; 6 | using NSubstitute; 7 | using Xunit; 8 | using static System.Threading.CancellationToken; 9 | 10 | namespace Greentube.Messaging.Tests 11 | { 12 | public class MessageHandlerInvokerTests 13 | { 14 | [Theory, AutoSubstituteData] 15 | public async Task Invoke_CallsHandlerInstance( 16 | [Frozen] IMessageHandlerInfoProvider messageHandlerInfoProvider, 17 | IMessageHandler messageHandler, 18 | Guid message, 19 | Fixture fixture) 20 | { 21 | // Arrange 22 | var callbackFired = false; 23 | IMessageHandler handlerInvoked = null; 24 | Guid messageInvoked; 25 | 26 | Task Callback(object hi, object mi, CancellationToken _) 27 | { 28 | callbackFired = true; 29 | messageInvoked = (Guid) mi; 30 | handlerInvoked = hi as IMessageHandler; 31 | return Task.CompletedTask; 32 | } 33 | 34 | messageHandlerInfoProvider.GetHandlerInfo() 35 | .Returns(new[] 36 | { 37 | (typeof(Guid), typeof(IMessageHandler), 38 | (Func) Callback) 39 | }); 40 | 41 | var handlerFactory = new Func(t => messageHandler); 42 | 43 | fixture.Register(handlerFactory); 44 | var sut = fixture.Create(); 45 | 46 | // Act 47 | await sut.Invoke(message, None); 48 | 49 | // Assert 50 | messageHandlerInfoProvider.Received(1).GetHandlerInfo(); 51 | Assert.True(callbackFired); 52 | Assert.Equal(message, messageInvoked); 53 | Assert.Equal(messageHandler, handlerInvoked); 54 | } 55 | 56 | [Theory, AutoSubstituteData] 57 | public async Task Invoke_FactoryReturnsNull_ThrowsInvalidOperation( 58 | [Frozen] IMessageHandlerInfoProvider messageHandlerInfoProvider, 59 | IMessageHandler messageHandler, 60 | string message, 61 | Fixture fixture) 62 | { 63 | Task Callback(object hi, object mi, CancellationToken _) => Task.CompletedTask; 64 | messageHandlerInfoProvider.GetHandlerInfo() 65 | .Returns(new[] 66 | { 67 | (message.GetType(), typeof(IMessageHandler), 68 | (Func) Callback) 69 | }); 70 | 71 | var handlerFactory = new Func(t => null); 72 | 73 | fixture.Register(handlerFactory); 74 | var sut = fixture.Create(); 75 | 76 | var ex = await Assert.ThrowsAsync(() => sut.Invoke(message, None)); 77 | Assert.Equal($"Message of type {message.GetType()} yielded no handler from factory.", ex.Message); 78 | } 79 | 80 | [Theory, AutoSubstituteData] 81 | public async Task Invoke_NoHandlerForMessage_ThrowsInvalidOperation( 82 | [Frozen] IMessageHandlerInfoProvider messageHandlerInfoProvider, 83 | IMessageHandler messageHandler, 84 | string message, 85 | Fixture fixture) 86 | { 87 | Task Callback(object hi, object mi, CancellationToken _) => Task.CompletedTask; 88 | messageHandlerInfoProvider.GetHandlerInfo() 89 | .Returns(new[] 90 | { 91 | (typeof(bool), typeof(IMessageHandler), 92 | (Func) Callback) 93 | }); 94 | 95 | var handlerFactory = new Func(t => messageHandler); 96 | 97 | fixture.Register(handlerFactory); 98 | var sut = fixture.Create(); 99 | 100 | var ex = await Assert.ThrowsAsync(() => sut.Invoke(message, None)); 101 | Assert.Equal($"No message handler found for message type {message.GetType()}.", ex.Message); 102 | } 103 | 104 | [Theory, AutoSubstituteData] 105 | public void Constructor_NoHandlerInfo_ThrowsInvalidOperation( 106 | IMessageHandlerInfoProvider messageHandlerInfoProvider) 107 | { 108 | messageHandlerInfoProvider.GetHandlerInfo() 109 | .Returns(new ( 110 | Type messageType, 111 | Type handlerType, 112 | Func handleMethod)[0]); 113 | 114 | var ex = Assert.Throws( 115 | () => new MessageHandlerInvoker(messageHandlerInfoProvider, type => new object())); 116 | 117 | Assert.Equal( 118 | $"{nameof(IMessageHandlerInfoProvider)}.{nameof(IMessageHandlerInfoProvider.GetHandlerInfo)} " + 119 | "hasn't resolved any handler information.", ex.Message); 120 | } 121 | 122 | [Fact] 123 | public void Constructor_NoHandlerInfoProvider_ThrowsArgumentNull() 124 | { 125 | var ex = Assert.Throws(() => new MessageHandlerInvoker(null, type => new object())); 126 | Assert.Equal("messageHandlerInfoProvider", ex.ParamName); 127 | } 128 | 129 | [Theory, AutoSubstituteData] 130 | public void Constructor_NoHandlerFactory_ThrowsArgumentNull( 131 | IMessageHandlerInfoProvider messageHandlerInfoProvider) 132 | { 133 | var ex = Assert.Throws(() => 134 | new MessageHandlerInvoker(messageHandlerInfoProvider, null)); 135 | Assert.Equal("handlerFactory", ex.ParamName); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/MessageTypeTopicMapTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace Greentube.Messaging.Tests 8 | { 9 | public class MessageTypeTopicMapTests 10 | { 11 | private readonly MessageTypeTopicMap _sut = new MessageTypeTopicMap(); 12 | 13 | [Fact] 14 | public void Add_RetrievableByTopic() 15 | { 16 | _sut.Add(GetType(), GetType().FullName); 17 | Assert.Equal(GetType(), _sut.Get(GetType().FullName)); 18 | } 19 | 20 | [Fact] 21 | public void Add_RetrievableByType() 22 | { 23 | _sut.Add(GetType(), GetType().FullName); 24 | Assert.Equal(GetType().FullName, _sut.Get(GetType())); 25 | } 26 | 27 | [Fact] 28 | public void Add_RetrievableByGetTopics() 29 | { 30 | _sut.Add(GetType(), GetType().FullName); 31 | Assert.Equal(GetType().FullName, _sut.GetTopics().Single()); 32 | } 33 | 34 | [Fact] 35 | public void Add_RetrievableByGetMessageTypes() 36 | { 37 | _sut.Add(GetType(), GetType().FullName); 38 | Assert.Equal(GetType(), _sut.GetMessageTypes().Single()); 39 | } 40 | 41 | [Fact] 42 | public void Add_NullTopic_ThrowsArgumentNull() 43 | { 44 | Assert.Throws(() => _sut.Add(GetType(), null)); 45 | } 46 | 47 | [Fact] 48 | public void Add_NullType_ThrowsArgumentNull() 49 | { 50 | Assert.Throws(() => _sut.Add(null, GetType().FullName)); 51 | } 52 | 53 | [Fact] 54 | public void Get_NullType_ThrowsArgumentNull() 55 | { 56 | Assert.Throws(() => _sut.Get((Type)null)); 57 | } 58 | 59 | [Fact] 60 | public void Get_NullTopic_ThrowsArgumentNull() 61 | { 62 | Assert.Throws(() => _sut.Get((string)null)); 63 | } 64 | 65 | [Fact] 66 | public void Add_NoDuplicates() 67 | { 68 | _sut.Add(GetType(), GetType().FullName); 69 | _sut.Add(GetType(), GetType().FullName); 70 | Assert.Single(_sut); 71 | } 72 | 73 | [Fact] 74 | public void Remove_UnknownType_NoOp() 75 | { 76 | _sut.Remove(GetType()); 77 | Assert.Empty(_sut); 78 | } 79 | 80 | [Fact] 81 | public void Remove_RemovesOnlySpecifiedType() 82 | { 83 | _sut.Add(GetType(), GetType().FullName); 84 | _sut.Add(typeof(Assert), typeof(Assert).FullName); 85 | 86 | _sut.Remove(GetType()); 87 | 88 | Assert.Single(_sut); 89 | Assert.Equal(typeof(Assert), _sut.GetMessageTypes().Single()); 90 | Assert.Equal(typeof(Assert).FullName, _sut.GetTopics().Single()); 91 | } 92 | 93 | [Fact] 94 | public void Remove_NullType_ThrowsArgumentNull() 95 | { 96 | Assert.Throws(() => _sut.Remove(null)); 97 | } 98 | 99 | [Fact] 100 | public void GetEnumerator_AsEnumerator() 101 | { 102 | _sut.Add(GetType(), GetType().FullName); 103 | IEnumerable enuSut = _sut; 104 | var enumerator = enuSut.GetEnumerator(); 105 | Assert.True(enumerator.MoveNext()); 106 | Assert.Equal(new KeyValuePair(GetType(), GetType().FullName), enumerator.Current); 107 | Assert.False(enumerator.MoveNext()); 108 | } 109 | 110 | #pragma warning disable xUnit2013 // Do not use equality check to check for collection size. - Let me test a call to Count? 111 | [Fact] 112 | public void Count_StartsAtZero() => Assert.Equal(0, _sut.Count); 113 | 114 | [Fact] 115 | public void Count_ReflectsAddCount() 116 | { 117 | _sut.Add(GetType(), GetType().FullName); 118 | Assert.Equal(1, _sut.Count); 119 | } 120 | #pragma warning restore xUnit2013 // Do not use equality check to check for collection size. 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/Greentube.Messaging.Tests/SerializedMessagePublisherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoFixture.Xunit2; 4 | using NSubstitute; 5 | using NSubstitute.ReturnsExtensions; 6 | using Greentube.Serialization; 7 | using Xunit; 8 | using static System.Threading.CancellationToken; 9 | 10 | namespace Greentube.Messaging.Tests 11 | { 12 | public class SerializedMessagePublisherTests 13 | { 14 | [Theory, AutoSubstituteData] 15 | public async Task Publish_SerializedMessage_CallsRawPublisher( 16 | string topic, 17 | bool message, 18 | byte[] serializedMessage, 19 | [Frozen] IRawMessagePublisher rawMessagePublisher, 20 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 21 | [Frozen] ISerializer serializer, 22 | SerializedMessagePublisher sut) 23 | { 24 | // Arrange 25 | var messageType = message.GetType(); 26 | messageTypeTopicMap.Get(messageType).Returns(topic); 27 | serializer.Serialize(message).Returns(serializedMessage); 28 | 29 | // Act 30 | await sut.Publish(message, None); 31 | 32 | // Assert 33 | await rawMessagePublisher.Received().Publish(topic, serializedMessage, None); 34 | } 35 | 36 | [Theory, AutoSubstituteData] 37 | public async Task Publish_SerializerReturnsNull_ThrowsInvalidOperation( 38 | string topic, 39 | string message, 40 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 41 | [Frozen] ISerializer serializer, 42 | SerializedMessagePublisher sut) 43 | { 44 | var messageType = GetType(); 45 | messageTypeTopicMap.Get(topic).Returns(messageType); 46 | serializer.Serialize( message).ReturnsNull(); 47 | 48 | var ex = await Assert.ThrowsAsync(() => sut.Publish(message, None)); 49 | Assert.Equal( 50 | $"Serializer {serializer.GetType()} returned null for message of type {message.GetType()}.", 51 | ex.Message); 52 | } 53 | 54 | [Theory, AutoSubstituteData] 55 | public async Task Publish_TopicMapNotDefined_ThrowsInvalidOperation( 56 | string message, 57 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 58 | SerializedMessagePublisher sut) 59 | { 60 | messageTypeTopicMap.Get(message.GetType()).ReturnsNull(); 61 | var ex = await Assert.ThrowsAsync(() => sut.Publish(message, None)); 62 | Assert.Equal($"Message type {message.GetType()} is not registered with: {messageTypeTopicMap.GetType()}.", 63 | ex.Message); 64 | } 65 | 66 | [Theory, AutoSubstituteData] 67 | public async Task Publish_NullMessage_ThrowsArgumentNull(SerializedMessagePublisher sut) 68 | { 69 | var ex = await Assert.ThrowsAsync(() => sut.Publish(null as object, None)); 70 | Assert.Equal("message", ex.ParamName); 71 | } 72 | 73 | [Theory, AutoSubstituteData] 74 | public void Constructor_NullRawMessagePublisher_ThrowsNullArgument( 75 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap, 76 | [Frozen] ISerializer serializer) 77 | { 78 | var ex = Assert.Throws(() => 79 | new SerializedMessagePublisher(messageTypeTopicMap, serializer, null)); 80 | Assert.Equal("rawMessagePublisher", ex.ParamName); 81 | } 82 | 83 | [Theory, AutoSubstituteData] 84 | public void Constructor_NullMessageTypeTopicMap_ThrowsNullArgument( 85 | [Frozen] IRawMessagePublisher rawMessagePublisher, 86 | [Frozen] ISerializer serializer) 87 | { 88 | var ex = Assert.Throws(() => 89 | new SerializedMessagePublisher(null, serializer, rawMessagePublisher)); 90 | Assert.Equal("typeTopicMap", ex.ParamName); 91 | } 92 | 93 | [Theory, AutoSubstituteData] 94 | public void Constructor_NullISerialize_ThrowsNullArgument( 95 | [Frozen] IRawMessagePublisher rawMessagePublisher, 96 | [Frozen] IMessageTypeTopicMap messageTypeTopicMap) 97 | { 98 | var ex = Assert.Throws(() => 99 | new SerializedMessagePublisher(messageTypeTopicMap, null, rawMessagePublisher)); 100 | Assert.Equal("serializer", ex.ParamName); 101 | } 102 | } 103 | } --------------------------------------------------------------------------------