├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── improvement_request.yml ├── SUPPORT.md ├── dependabot.yml └── workflows │ ├── approve-dependencies.yml │ ├── ci.yml │ ├── code-analysis.yml │ ├── nuget-audit.yml │ ├── release.yml │ ├── stale.yml │ ├── tests-cleanup.yml │ └── virus-scan.yml ├── .gitignore ├── .reposync.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── Package-README.md ├── README.md ├── SECURITY.md ├── cleanup ├── Cleanup.csproj ├── Cleanup.sln └── Program.cs ├── docs └── message-drive-pub-sub-compat.md ├── global.json ├── nuget.config └── src ├── .editorconfig ├── .globalconfig ├── CommandLine ├── .editorconfig ├── AmazonServiceExtensions.cs ├── Bucket.cs ├── CommandRunner.cs ├── DefaultConfigurationValues.cs ├── Endpoint.cs ├── NServiceBus.Transports.SQS.CommandLine.csproj ├── PolicyExtensions.cs ├── PolicyStatement.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Queue.cs ├── Topic.cs └── TopicSanitization.cs ├── CommandLineTests ├── .editorconfig ├── CommandLineTests.cs └── NServiceBus.Transports.SQS.CommandLine.Tests.csproj ├── Custom.Build.props ├── Directory.Build.props ├── Directory.Build.targets ├── NServiceBus.AmazonSQS.slnx ├── NServiceBus.Transport.SQS.AcceptanceTests ├── .editorconfig ├── Batching │ ├── When_batching_multiple_outgoing_small_events.cs │ └── When_batching_multiple_outgoing_small_messages.cs ├── ConfigurationHelpers.cs ├── ConfigureEndpointSqsTransport.cs ├── CustomizedServer.cs ├── DelayedDelivery │ ├── SendOnly_Sending_when_receiver_not_properly_configured.cs │ ├── SendOnly_Sending_when_sender_and_receiver_are_properly_configured.cs │ ├── SendOnly_Sending_when_sender_not_properly_configured.cs │ └── SendOnly_when_configured_with_queue_delay_that_requires_multiple_cycles.cs ├── Handling_messages_concurrently.cs ├── Installers │ ├── DefaultServerWithNoInstallers.cs │ ├── ServerWithInstallersDisabled.cs │ ├── When_trying_to_receive_message_from_nonexisting_queue.cs │ └── When_trying_to_subscribe_when_topic_does_not_exist.cs ├── NServiceBus.Transport.SQS.AcceptanceTests.csproj ├── NamePrefixHandler.cs ├── NativeIntegration │ ├── NativeEndpoint.cs │ ├── When_access_to_received_native_message_required.cs │ ├── When_message_moves_through_delayed_delivery_queue.cs │ ├── When_message_moves_through_delayed_delivery_queue_multiple_times.cs │ ├── When_moving_message_to_error_queue.cs │ ├── When_receiving_a_native_message_with_encoding.cs │ └── When_receiving_a_native_message_without_wrapper.cs ├── NativePubSub │ ├── HybridModeRateLimit │ │ ├── TestCase.cs │ │ ├── When_publishing_one_event_type_to_native_and_non_native_subscribers_in_a_loop.cs │ │ ├── When_publishing_one_event_type_to_native_and_non_native_subscribers_in_a_loop_in_the_context_of_incoming_message.cs │ │ ├── When_publishing_two_event_types_to_native_and_non_native_subscribers_in_a_loop.cs │ │ └── When_publishing_two_event_types_to_native_and_non_native_subscribers_in_a_loop_in_the_context_of_incoming_message.cs │ ├── When_concurrent_subscribe.cs │ ├── When_concurrent_subscribe_with_scaled_subscriber.cs │ ├── When_customizing_topic_name_generation.cs │ ├── When_migrating_publisher_first.cs │ ├── When_migrating_subscriber_first.cs │ ├── When_multi_subscribing_to_a_polymorphic_event_using_topic_mappings.cs │ ├── When_publisher_runs_in_compat_mode.cs │ ├── When_subscriber_runs_in_compat_mode.cs │ └── When_subscribing_to_object.cs ├── Policies │ ├── When_subscribing_to_multiple_events_with_account_policy.cs │ ├── When_subscribing_to_multiple_events_with_default_policy.cs │ ├── When_subscribing_to_multiple_events_with_default_policy_assumed.cs │ ├── When_subscribing_to_multiple_events_with_mixed_policy.cs │ ├── When_subscribing_to_multiple_events_with_multiple_namespaces_policy.cs │ ├── When_subscribing_to_multiple_events_with_namespace_policy.cs │ ├── When_subscribing_to_multiple_events_with_topicprefix_policy.cs │ └── When_subscribing_to_multiple_events_with_wildcard_policy_assumed.cs ├── Publishing │ ├── When_publishing_not_wrapped_event.cs │ └── When_publishing_wrapped_event.cs ├── Receiving │ ├── When_renewing_visibility.cs │ └── When_renewing_visibility_failed.cs ├── Routing │ └── When_publishing_an_event_implementing_two_unrelated_interfaces.cs ├── Sending │ ├── When_sending_control_messages_without_body.cs │ ├── When_sending_messages_with_invalid_sqs_chars.cs │ ├── When_sending_not_wrapped_message.cs │ ├── When_trying_to_send_message_to_nonexisting_queue.cs │ ├── When_using_large_message_with_customer_provided_aes.cs │ ├── When_using_large_message_with_kms_encrypted_bucket.cs │ ├── When_using_large_message_with_serverside_aes.cs │ ├── When_using_large_message_with_unencrypted_bucket.cs │ ├── When_using_oversized_message_without_bucket_configured.cs │ └── When_using_small_message_with_no_bucket_configured.cs ├── SetupFixture.cs ├── TestNameHelper.cs ├── TestSuiteConstraints.cs ├── TestingInMemoryPersistence.cs └── When_transport_message_serialized_with_simplejson.cs ├── NServiceBus.Transport.SQS.Tests ├── .editorconfig ├── APIApprovals.cs ├── ApprovalFiles │ ├── APIApprovals.ApproveSqsTransport.approved.txt │ ├── MessageDispatcherTests.Does_not_send_extra_properties_in_payload_by_default.approved.txt │ ├── MessageDispatcherTests.Should_not_wrap_if_configured_not_to.approved.txt │ ├── PolicyExtensionsTests.ExtractsPolicy_if_not_empty.approved.txt │ ├── PolicyExtensionsTests.ExtractsPolicy_returns_empty_policy_if_empty.approved.txt │ ├── PolicyExtensionsTests.Update_sets_full_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_account_condition_sets_full_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_all_conditions_sets_full_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_all_conditions_sets_full_policy_with_replaced_wildcard.approved.txt │ ├── PolicyExtensionsTests.Update_with_empty_add_statements_doesnt_update_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_legacy_policy_does_migrate_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_legacy_policy_with_conditions_does_migrate_to_wildcard_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_does_migrate_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_migration_leaves_unrelated_permissions.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_with_condition_migration_leaves_unrelated_permissions.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_with_conditions_does_migrate_to_wildcard_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_partial_policy_migration_leaves_unrelated_permissions.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_policy_does_extend_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_policy_matching_but_different_order_doesnt_override_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_policy_matching_doesnt_override_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_policy_switched_to_wildcard.approved.txt │ ├── PolicyExtensionsTests.Update_with_existing_wildcard_policy_switched_to_events_will_replace_wildcards.approved.txt │ ├── PolicyExtensionsTests.Update_with_namespace_conditions_and_topic_name_prefix_sets_full_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_namespace_conditions_sets_full_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_nothing_to_subscribe_doesnt_override_existing_policy.approved.txt │ ├── PolicyExtensionsTests.Update_with_topic_name_prefix_sets_full_policy.approved.txt │ ├── SubscriptionManagerTests.SettlePolicy_sets_full_policy.approved.txt │ ├── SubscriptionManagerTests.SettlePolicy_with_all_conditions_sets_full_policy.approved.txt │ └── SubscriptionManagerTests.Subscribe_after_settle_sets_full_policy.approved.txt ├── Cleanup.cs ├── ClientFactories.cs ├── MockS3Client.cs ├── MockSnsClient.cs ├── MockSqsClient.cs ├── NServiceBus.Transport.SQS.Tests.csproj ├── PolicyExtensionsTests.cs ├── PolicyScrubber.cs ├── QueueCacheTests.cs ├── QueueNameGeneratorTests.cs ├── Receiving │ ├── DelayedMessagesPumpTests.cs │ ├── InputQueuePumpTests.cs │ ├── MessageExtractionTests.cs │ ├── RenewalTests.cs │ ├── SubscriptionManagerTests.cs │ └── TransportMessageExtensionsTests.cs ├── SdkClientsDisposeTests.cs ├── Sending │ ├── MessageDispatcherTests.cs │ ├── SnsPreparedMessageBatcherTests.cs │ ├── SnsPreparedMessageTests.cs │ ├── SqsPreparedMessageBatcherTests.cs │ └── SqsPreparedMessageTests.cs ├── TestNameHelper.cs ├── TopicCacheTests.cs └── TransportMessageTests.cs ├── NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages ├── .editorconfig ├── ConfigureSqsTransportInfrastructure.cs ├── NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages.csproj └── Sending_messages_with_invalid_sqs_chars.cs ├── NServiceBus.Transport.SQS.TransportTests ├── .editorconfig ├── ConfigureSqsTransportInfrastructure.cs ├── EnvironmentHelper.cs ├── NServiceBus.Transport.SQS.TransportTests.csproj ├── Sending_poison_messages.cs ├── Sending_to_non_existing_queues.cs ├── SetupFixture.cs └── TestNameHelper.cs ├── NServiceBus.Transport.SQS ├── Administration │ └── QueueCreator.cs ├── AsyncCacheLazy.cs ├── Configure │ ├── DefaultClientFactories.cs │ ├── DiagnosticDescriptors.cs │ ├── EventToEventsMappings.cs │ ├── EventToTopicsMappings.cs │ ├── NullEncryption.cs │ ├── PolicySettings.cs │ ├── S3EncryptionMethod.cs │ ├── S3EncryptionWithCustomerProvidedKey.cs │ ├── S3EncryptionWithManagedKey.cs │ ├── S3Settings.cs │ ├── SettingsKeys.cs │ ├── SqsSubscriptionMigrationModeSettings.cs │ ├── SqsTransport.cs │ ├── SqsTransportInfrastructure.cs │ └── SqsTransportSettings.cs ├── ExceptionExtensions.cs ├── Extensions │ ├── AmazonServiceExtensions.cs │ ├── MessageExtensions.cs │ ├── PolicyExtensions.cs │ └── SnsClientExtensions.cs ├── NServiceBus.Transport.SQS.csproj ├── PreObsolete.cs ├── QueueCache.cs ├── ReadonlyStream.cs ├── Receiving │ ├── DelayedMessagesPump.cs │ ├── InputQueuePump.cs │ ├── MessagePump.cs │ ├── PolicyStatement.cs │ ├── Renewal.cs │ ├── SqsReceivedDelayedMessage.cs │ ├── SubscriptionManager.cs │ └── TransportMessageExtensions.cs ├── Sending │ ├── HybridPubSubChecker.cs │ ├── MessageDispatcher.cs │ ├── OutgoingMessageExtensions.cs │ ├── ReducedPayloadSerializerConverter.cs │ ├── SnsBatchEntry.cs │ ├── SnsPreparedMessage.cs │ ├── SnsPreparedMessageBatcher.cs │ ├── SnsPreparedMessageExtensions.cs │ ├── SqsBatchEntry.cs │ ├── SqsPreparedMessage.cs │ ├── SqsPreparedMessageBatcher.cs │ └── SqsPreparedMessageExtensions.cs ├── TopicCache.cs ├── TopicNameHelper.cs ├── TransportConstraints.cs ├── TransportHeaders.cs ├── TransportMessage.cs └── TransportMessageSerializerContext.cs ├── NServiceBus.snk ├── NServiceBusTests.snk └── msbuild ├── AutomaticVersionRanges.targets └── ConvertToVersionRange.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: ["Bug" ] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | For additional support options, or if this is related to a security vulnerability or sensitive information must be included in the bug report, visit [particular.net/support](https://particular.net/support). 9 | - type: textarea 10 | id: what-happened 11 | attributes: 12 | label: Describe the bug 13 | description: A clear and concise description of the bug. 14 | value: | 15 | #### Description 16 | 17 | #### Expected behavior 18 | 19 | #### Actual behavior 20 | 21 | #### Versions 22 | 23 | Please list the version of the relevant packages or applications in which the bug exists. 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: steps-to-reproduce 28 | attributes: 29 | label: Steps to reproduce 30 | description: Detailed instructions to reproduce the bug. 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: logs 35 | attributes: 36 | label: Relevant log output 37 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 38 | render: shell 39 | - type: textarea 40 | id: additional-information 41 | attributes: 42 | label: Additional Information 43 | description: If there are any possible solutions, workarounds, or additional information please describe them here 44 | value: | 45 | #### Workarounds 46 | 47 | #### Possible solutions 48 | 49 | #### Additional information 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://discuss.particular.net/ 5 | about: Reach out to the ParticularDiscussion community. 6 | - name: Get support 7 | url: https://particular.net/support 8 | about: Contact us to discuss your support requirements. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | 3 | description: Request a new feature. 4 | labels: ["Feature" ] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please check all open and closed issues to see if the feature has already been requested. 10 | 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Describe the feature. 15 | 16 | description: A clear and concise description of the feature. 17 | 18 | value: | 19 | #### Is your feature related to a problem? Please describe. 20 | 21 | #### Describe the requested feature 22 | 23 | #### Describe alternatives you've considered 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: additional-context 28 | attributes: 29 | label: Additional Context 30 | description: Add any other context about the request. 31 | 32 | placeholder: "Add screenshots or additional information." 33 | validations: 34 | required: false 35 | - type: markdown 36 | attributes: 37 | value: | 38 | For additional support options visit [particular.net/support](https://particular.net/support) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement_request.yml: -------------------------------------------------------------------------------- 1 | name: Improvement request 2 | description: Suggest an improvement to an existing code base. 3 | labels: ["Improvement" ] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please check all open and closed issues to see if improvement has already been suggested. 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Describe the suggested improvement 13 | description: A clear and concise description of what the improvement is. 14 | value: | 15 | #### Is your improvement related to a problem? Please describe. 16 | 17 | #### Describe the suggested solution 18 | 19 | #### Describe alternatives you've considered 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: additional-context 24 | attributes: 25 | label: Additional Context 26 | description: Add any other context about the suggestion here. 27 | placeholder: "Add screenshots or additional information." 28 | validations: 29 | required: false 30 | - type: markdown 31 | attributes: 32 | value: | 33 | For additional support options visit [particular.net/support](https://particular.net/support) 34 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Looking for Support 2 | 3 | Check out our [support options](https://particular.net/support). -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | particular-packages: 4 | type: nuget-feed 5 | url: https://f.feedz.io/particular-software/packages/nuget/index.json 6 | updates: 7 | - package-ecosystem: nuget 8 | directory: "/src" 9 | registries: "*" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 1000 13 | groups: 14 | AWSSDK: 15 | patterns: 16 | - "AWSSDK.*" 17 | NServiceBusCore: 18 | patterns: 19 | - "NServiceBus" 20 | - "NServiceBus.AcceptanceTesting" 21 | - "NServiceBus.AcceptanceTests.Sources" 22 | - "NServiceBus.PersistenceTests.Sources" 23 | - "NServiceBus.TransportTests.Sources" 24 | ignore: 25 | # Particular.Analyzers updates are distributed via RepoStandards 26 | - dependency-name: "Particular.Analyzers" 27 | # Changing these 3 dependencies affects the .NET SDK and Visual Studio versions we support 28 | # These types of updates should be more intentional than an automated update 29 | - dependency-name: "Microsoft.Build.Utilities.Core" 30 | - dependency-name: "Microsoft.CodeAnalysis.CSharp" 31 | - dependency-name: "Microsoft.CodeAnalysis.CSharp.Workspaces" 32 | - package-ecosystem: "github-actions" 33 | directory: "/" 34 | schedule: 35 | interval: daily 36 | open-pull-requests-limit: 1000 37 | -------------------------------------------------------------------------------- /.github/workflows/approve-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Auto-approve AWSSDK dependency updates 2 | on: 3 | pull_request_target: 4 | types: [ opened ] 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | defaults: 9 | run: 10 | shell: bash 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | # Prevent run from failing on non-Dependabot PRs 15 | if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} 16 | steps: 17 | - name: Dependabot metadata 18 | id: metadata 19 | uses: dependabot/fetch-metadata@v2.4.0 20 | - name: Approve PR 21 | if: ${{contains(steps.metadata.outputs.dependency-names, 'AWSSDK.SQS') || contains(steps.metadata.outputs.dependency-names, 'AWSSDK.SimpleNotificationService') || contains(steps.metadata.outputs.dependency-names, 'AWSSDK.S3')}} 22 | run: gh pr review --approve "$PR_URL" 23 | env: 24 | PR_URL: ${{github.event.pull_request.html_url}} 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | - name: Enable auto-merge 27 | if: ${{contains(steps.metadata.outputs.dependency-names, 'AWSSDK.SQS') || contains(steps.metadata.outputs.dependency-names, 'AWSSDK.SimpleNotificationService') || contains(steps.metadata.outputs.dependency-names, 'AWSSDK.S3')}} 28 | run: gh pr merge --auto --squash "$PR_URL" 29 | env: 30 | PR_URL: ${{github.event.pull_request.html_url}} 31 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - release-* 7 | pull_request: 8 | workflow_dispatch: 9 | env: 10 | DOTNET_NOLOGO: true 11 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 12 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 13 | AWS_REGION: ${{ secrets.AWS_REGION }} 14 | AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} 15 | NSERVICEBUS_AMAZONSQS_S3BUCKET: ${{ secrets.NSERVICEBUS_AMAZONSQS_S3BUCKET }} 16 | defaults: 17 | run: 18 | shell: pwsh 19 | jobs: 20 | build: 21 | name: ${{ matrix.name }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | include: 26 | - os: windows-2022 27 | name: Windows 28 | - os: ubuntu-22.04 29 | name: Linux 30 | fail-fast: false 31 | steps: 32 | - name: Check for secrets 33 | env: 34 | SECRETS_AVAILABLE: ${{ secrets.SECRETS_AVAILABLE }} 35 | run: exit $(If ($env:SECRETS_AVAILABLE -eq 'true') { 0 } Else { 1 }) 36 | - name: Checkout 37 | uses: actions/checkout@v4.2.2 38 | with: 39 | fetch-depth: 0 40 | - name: Setup .NET SDK 41 | uses: actions/setup-dotnet@v4.3.1 42 | with: 43 | global-json-file: global.json 44 | - name: Build 45 | run: dotnet build src --configuration Release 46 | - name: Upload packages 47 | if: matrix.name == 'Windows' 48 | uses: actions/upload-artifact@v4.6.2 49 | with: 50 | name: NuGet packages 51 | path: nugets/ 52 | retention-days: 7 53 | - name: Run tests 54 | uses: Particular/run-tests-action@v1.7.0 55 | -------------------------------------------------------------------------------- /.github/workflows/code-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Code Analysis 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | - release-* 8 | pull_request: 9 | workflow_dispatch: 10 | jobs: 11 | code-analysis: 12 | uses: particular/shared-workflows/.github/workflows/code-analysis.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/nuget-audit.yml: -------------------------------------------------------------------------------- 1 | name: NuGet Audit 2 | on: 3 | workflow_dispatch: 4 | env: 5 | DOTNET_NOLOGO: true 6 | jobs: 7 | call-shared-nuget-audit: 8 | uses: particular/shared-workflows/.github/workflows/nuget-audit.yml@main 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '[0-9]+.[0-9]+.[0-9]+' 6 | - '[0-9]+.[0-9]+.[0-9]+-*' 7 | env: 8 | DOTNET_NOLOGO: true 9 | defaults: 10 | run: 11 | shell: pwsh 12 | jobs: 13 | release: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4.2.2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup .NET SDK 21 | uses: actions/setup-dotnet@v4.3.1 22 | with: 23 | global-json-file: global.json 24 | - name: Build 25 | run: dotnet build src --configuration Release 26 | - name: Sign NuGet packages 27 | uses: Particular/sign-nuget-packages-action@v1.0.0 28 | with: 29 | client-id: ${{ secrets.AZURE_KEY_VAULT_CLIENT_ID }} 30 | tenant-id: ${{ secrets.AZURE_KEY_VAULT_TENANT_ID }} 31 | client-secret: ${{ secrets.AZURE_KEY_VAULT_CLIENT_SECRET }} 32 | certificate-name: ${{ secrets.AZURE_KEY_VAULT_CERTIFICATE_NAME }} 33 | - name: Publish artifacts 34 | uses: actions/upload-artifact@v4.6.2 35 | with: 36 | name: nugets 37 | path: nugets/* 38 | retention-days: 1 39 | - name: Deploy 40 | # Does not follow standard practice of targeting explicit versions because configuration is tightly coupled to Octopus Deploy configuration 41 | uses: Particular/push-octopus-package-action@main 42 | with: 43 | octopus-deploy-api-key: ${{ secrets.OCTOPUS_DEPLOY_API_KEY }} 44 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: StaleBot 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | jobs: 7 | stalebot: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Mark stale PRs 11 | uses: Particular/stale-action@main 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: SQS Transport Cleanup 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' # Runs daily at midnight UTC 5 | workflow_dispatch: 6 | defaults: 7 | run: 8 | shell: pwsh 9 | jobs: 10 | cleanup: 11 | runs-on: ubuntu-22.04 12 | env: 13 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 14 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 15 | # CLI 16 | AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} 17 | # App 18 | AWS_REGION: ${{ secrets.AWS_REGION }} 19 | AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4.2.2 23 | - name: Setup .NET SDK 24 | uses: actions/setup-dotnet@v4.3.1 25 | with: 26 | global-json-file: global.json 27 | - name: Output AWS CLI version 28 | shell: pwsh 29 | run: aws --version 30 | - name: Clean up S3 Buckets 31 | shell: pwsh 32 | run: | 33 | $dateFilter = ([System.DateTime]::UtcNow).AddHours(-24) 34 | 35 | Write-Host "Current time $(([System.DateTime]::UtcNow).ToString()) UTC" 36 | Write-Host "Looking for test buckets older than $($dateFilter.ToString()) UTC starting with 'cli-'" 37 | 38 | $bucketList = aws s3api list-buckets | ConvertFrom-Json 39 | $buckets = $bucketList.Buckets | Where-Object { $_.Name.StartsWith('cli-') } | Where-Object { $_.CreationDate -lt $dateFilter } 40 | Write-Output "Found $($buckets.length) buckets" 41 | 42 | $buckets | ForEach-Object -Parallel { 43 | Write-Output "Deleting bucket '$($_.Name)'" 44 | aws s3api delete-bucket --bucket $_.Name 45 | } 46 | - name: Clean up SQS queues 47 | shell: pwsh 48 | run: dotnet run --project cleanup/Cleanup.csproj --configuration Release 49 | - name: Notify Slack on failure 50 | if: ${{ failure() }} 51 | shell: pwsh 52 | run: | 53 | $headers = @{ 54 | 'Authorization' = "Bearer ${{ secrets.SLACK_TOKEN }}" 55 | } 56 | $body = @{ 57 | channel = 'aws' 58 | text = "NServiceBus.AmazonSQS clean up has failed: https://github.com/Particular/NServiceBus.AmazonSQS/actions/workflows/tests-cleanup.yml" 59 | username = 'Amazon SQS Transport Test Cleanup' 60 | icon_emoji = 'github_actions' 61 | unfurl_links = false 62 | unfurl_media = false 63 | } | ConvertTo-Json 64 | $result = Invoke-RestMethod -Method POST -Uri https://slack.com/api/chat.postMessage -ContentType "application/json; charset=utf-8" -Headers $headers -Body $body 65 | Write-Output $result 66 | exit $(If ($result.ok) { 0 } Else { 1 }) 67 | -------------------------------------------------------------------------------- /.github/workflows/virus-scan.yml: -------------------------------------------------------------------------------- 1 | name: Virus scan 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | virus-scan: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Scan release for viruses 10 | uses: Particular/virus-scan-action@main 11 | with: 12 | owner: ${{ github.repository_owner }} 13 | repo: ${{ github.event.repository.name }} 14 | tag: ${{ github.event.release.name }} 15 | github-access-token: ${{ secrets.GITHUB_TOKEN }} 16 | slack-token: ${{ secrets.SLACK_TOKEN }} 17 | slack-channel: ${{ vars.VIRUS_REPORTING_SLACK_CHANNEL }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /assets 2 | /binaries 3 | /deploy 4 | /nugets 5 | build32 6 | *.vshost.* 7 | .nu 8 | _UpgradeReport.* 9 | *.cache 10 | Thumbs.db 11 | *~ 12 | *.swp 13 | results 14 | CommonAssemblyInfo.cs 15 | lib/sqlite/System.Data.SQLite.dll 16 | *.orig 17 | *.zip 18 | Samples/DataBus/storage 19 | packages 20 | PrecompiledWeb 21 | tempstorage 22 | .learningtransport 23 | core-only 24 | Release 25 | Artifacts 26 | LogFiles 27 | csx 28 | *.ncrunchproject 29 | *.ncrunchsolution 30 | _NCrunch_NServiceBus/* 31 | logs 32 | run-git.cmd 33 | src/Chocolatey/Build/* 34 | 35 | installer/[F|f]iles 36 | installer/[C|c]ustom[A|a]ctions 37 | installer/ServiceControl-cache 38 | 39 | # Created by https://www.gitignore.io 40 | 41 | ### VisualStudio ### 42 | ## Ignore Visual Studio temporary files, build results, and 43 | ## files generated by popular Visual Studio add-ons. 44 | 45 | # User-specific files 46 | *.suo 47 | *.user 48 | *.json.lock 49 | *.nuget.targets 50 | *.lock.json 51 | *.userosscache 52 | *.sln.docstates 53 | .vs/ 54 | local.settings.json 55 | 56 | # mac temp file ignore 57 | .DS_Store 58 | 59 | # Build results 60 | [Dd]ebug/ 61 | [Dd]ebugPublic/ 62 | [Rr]elease/ 63 | [Rr]eleases/ 64 | x64/ 65 | x86/ 66 | build/ 67 | bld/ 68 | [Bb]in/ 69 | [Oo]bj/ 70 | 71 | # Roslyn cache directories 72 | *.ide/ 73 | 74 | # MSTest test Results 75 | [Tt]est[Rr]esult*/ 76 | [Bb]uild[Ll]og.* 77 | 78 | #NUNIT 79 | *.VisualState.xml 80 | TestResult.xml 81 | 82 | # NCrunch 83 | _NCrunch_* 84 | .*crunch*.local.xml 85 | 86 | # ReSharper is a .NET coding add-in 87 | _ReSharper*/ 88 | *.[Rr]e[Ss]harper 89 | *.DotSettings 90 | *.DotSettings.user 91 | 92 | src/scaffolding.config 93 | 94 | # Approval tests temp file 95 | *.received.* 96 | 97 | # JetBrains Rider 98 | .idea/ 99 | *.sln.iml 100 | 101 | # Visual Studio Code 102 | .vscode 103 | -------------------------------------------------------------------------------- /.reposync.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.AmazonSQS/f1db08bf19341763802f406f9f03f657bfa23807/.reposync.yml -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | For information on contributing, see https://docs.particular.net/platform/contributing. 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | By accessing code under the [Particular Software GitHub Organization](https://github.com/Particular) (Particular Software) here, you are agreeing to the following licensing terms. 2 | If you do not agree to these terms, do not access Particular Software code. 3 | 4 | Your license to Particular Software source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license as described here: 5 | 6 | https://opensource.org/license/rpl-1-5/ 7 | 8 | If you do not wish to release the source of software you build using Particular Software source code and/or binaries under the terms above, you may use Particular Software source code and/or binaries under the License Agreement described here: 9 | 10 | https://particular.net/LicenseAgreement 11 | -------------------------------------------------------------------------------- /Package-README.md: -------------------------------------------------------------------------------- 1 | ## About this package 2 | 3 | This NuGet package is part of the [Particular Service Platform](https://particular.net/service-platform), which includes [NServiceBus](https://particular.net/nservicebus) and tools to build, monitor, and debug distributed systems. 4 | 5 | Click the **Project website** link in the NuGet sidebar to access specific documentation for this package. 6 | 7 | ## About NServiceBus 8 | 9 | With NServiceBus, you can: 10 | 11 | - Focus on business logic, not on plumbing or infrastructure code 12 | - Orchestrate long-running business processes with sagas 13 | - Run on-premises, in the cloud, in containers, or serverless 14 | - Monitor and respond to failures using included platform tooling 15 | - Observe system performance using Open Telemetry integration 16 | 17 | NServiceBus includes: 18 | 19 | - Support for messages queues using Azure Service Bus, Azure Storage Queues, Amazon SQS/SNS, RabbitMQ, and Microsoft SQL Server 20 | - Support for storing data in Microsoft SQL Server, MySQL, PostgreSQL, Oracle, Azure Cosmos DB, Azure Table Storage, Amazon DynamoDB, MongoDB, and RavenDB 21 | - 24x7 professional support from a team of dedicated engineers located around the world 22 | 23 | ## Getting started 24 | 25 | - Visit the [NServiceBus Quick Start](https://docs.particular.net/tutorials/quickstart/) to learn how NServiceBus helps you build better software systems. 26 | - Visit the [NServiceBus step-by-step tutorial](https://docs.particular.net/tutorials/nservicebus-step-by-step/) to learn how to build NServiceBus systems, including how to send commands, publish events, manage multiple message endpoints, and retry failed messages. 27 | - Install the [ParticularTemplates NuGet package](https://www.nuget.org/packages/ParticularTemplates) to get NServiceBus templates to bootstrap projects using either `dotnet new` or in Visual Studio. 28 | - Check out our other [tutorials](https://docs.particular.net/tutorials/) and [samples](https://docs.particular.net/samples/). 29 | - Get [help with a proof-of-concept](https://particular.net/proof-of-concept). 30 | 31 | ## Packages 32 | 33 | Find links to [all our NuGet packages](https://docs.particular.net/nservicebus/platform-nuget-packages) in our documentation. 34 | 35 | ## Support 36 | 37 | - Browse our [documentation](https://docs.particular.net). 38 | - Reach out to the [ParticularDiscussion](https://discuss.particular.net/) community. 39 | - [Contact us](https://particular.net/support) to discuss your support requirements. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NServiceBus.AmazonSQS 2 | 3 | NServiceBus.AmazonSQS is the [Amazon SQS](https://docs.aws.amazon.com/sqs/) transport for NServiceBus. 4 | 5 | It is part of the [Particular Service Platform](https://particular.net/service-platform), which includes [NServiceBus](https://particular.net/nservicebus) and tools to build, monitor, and debug distributed systems. 6 | 7 | See the [Amazon SQS Transport documentation](https://docs.particular.net/transports/sqs/) to learn more. 8 | 9 | ## Running tests locally 10 | 11 | The solution contains the [NServiceBus Acceptance Test suite](https://www.nuget.org/packages/NServiceBus.AcceptanceTests.Sources/) and the [NServiceBus Transport Test suite](https://www.nuget.org/packages/NServiceBus.TransportTests.Sources/). 12 | 13 | To run the tests, the Access Key ID and Secret Access Key of an AWS IAM account need to be set in environment variables on the machine running the tests. Full details on how to set this up can be found in the [Amazon SQS Transport documentation](https://docs.particular.net/transports/sqs/#prerequisites). 14 | 15 | **NSERVICEBUS_AMAZONSQS_S3BUCKET** enables [offloading large messages to S3](https://docs.particular.net/transports/sqs/configuration-options#offload-large-messages-to-s3). This is disabled by default. When enabled, the bucket should not have encryption enabled. For further details review the documentation on [configuration options](https://docs.particular.net/transports/sqs/configuration-options). 16 | 17 | ## AWS Permissions 18 | 19 | In addition to the [permissions required to run the transport](https://docs.particular.net/transports/sqs/#prerequisites), running the tests also requires: 20 | 21 | * sns:ListSubscriptionsByTopic 22 | * sns:CreateTopic 23 | * sns:Subscribe 24 | * sns:Publish 25 | * sqs:DeleteQueue 26 | * sns:DeleteTopic 27 | * s3:DeleteBucket 28 | * sqs:DeleteQueue 29 | * sns:DeleteTopic 30 | 31 | ## Queue Names in Acceptance Tests 32 | 33 | The names of queues used by the acceptance tests take the following form: 34 | 35 | AT- 36 | 37 | Where 38 | 39 | * `AT` stands for "Acceptance Test" 40 | * `unique-prefix` is an identifier that uniquely identifies a single test run so that parallel test runs do not conflict. 41 | * `pre-truncated-queue-name` is the name of the queue, "pre-truncated" (characters are removed from the beginning) so that the entire queue name is 80 characters or less. 42 | 43 | This scheme accomplishes the following goals: 44 | 45 | * Test runs are idempotent - each test run uses its own set of queues 46 | * Queues for a given test run are easily searchable by prefix in the SQS portal 47 | * The discriminator and qualifier at the end of the queue name are not interfered with 48 | * Queue names fit the 80 character limit imposed by SQS 49 | 50 | ## Cleanup scheduled task 51 | 52 | This repo has a [GitHub action](/.github/workflows/tests-cleanup.yml) that deletes stale AWS objects created when the tests run. It takes care of deleting S3 buckets older than 24 hours with the cli- prefix in the name. The same GitHub action code can be updated to delete any other AWS object created by the tests that fail to be deleted during the tests cleanup phase. 53 | 54 | The cleanup workflow requires the same permissions as running the tests. 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Particular Software takes security issues very seriously. We appreciate your efforts to uncover bugs in our software. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Vulnerabilities can be reported by [submitting an issue](https://github.com/Particular/NServiceBus/security/advisories/new) through the security tab on our NServiceBus repository. 8 | -------------------------------------------------------------------------------- /cleanup/Cleanup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /cleanup/Cleanup.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32228.430 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cleanup", "Cleanup.csproj", "{6CF702CF-263B-4E5A-82FA-EE2AC0858842}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6CF702CF-263B-4E5A-82FA-EE2AC0858842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6CF702CF-263B-4E5A-82FA-EE2AC0858842}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6CF702CF-263B-4E5A-82FA-EE2AC0858842}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6CF702CF-263B-4E5A-82FA-EE2AC0858842}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {F546B263-CBAB-4B56-A74E-E1684FF45A5D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /docs/message-drive-pub-sub-compat.md: -------------------------------------------------------------------------------- 1 | ## Publish-subscribe compatibility mode 2 | 3 | Up until version 4, SQS Transport implemented publish-subscribe messaging using Core's message-driven feature. This changed in version 5, in which publish-subscribe has been based on AWS SNS. 4 | 5 | In order to provide a migration path for a system with a mixture of endpoints using message-driven pub-sub and SNS-based pub-sub a [compatibility mode](https://docs.particular.net/transports/sqs/configuration-options#message-driven-pubsub-compatibility-mode) is provided in version 5 of the transport. Compatibility mode enables newer endpoints to act as publishers both for native subscribers and SNS subscribers. 6 | 7 | When running in compatibility mode, a publisher will publish events using both SNS (events passed by Core as multicast messages) and message-driven pub-sub (events passed by Core as unicast messages). This ensures that events get delivered both to older and newer endpoints. 8 | 9 | However in a scenario, when a subscriber upgrades from message-driven to SNS-based pub-sub this leads to duplicated event delivery. In order to prevent that, a publisher running in compatibility mode performs message deduplication before any events get sent. First, messages passed to the transport are grouped by `messageId`. If there is any `messageId` for which a multicast and some unicast messages have been found the deduplication process is performed. In such a case, SNS is queried to get all subscribers for the multicast message topic and all unicast messages with the exact same destination already subscribed to the topic are dropped. 10 | 11 | ## Constraints 12 | 13 | The de-duplication mechanism requires information on the SNS subscribers which is queried by the transport using AWS API. The API in turn comes with built-in throttling of [30 req/sec](https://docs.aws.amazon.com/sns/latest/api/API_ListSubscriptionsByTopic.html). 14 | 15 | The initial version of the compatibility mode assumed that these limits will never be reached in production systems which turned out to be [wrong](https://github.com/Particular/NServiceBus.AmazonSQS/issues/866). As a result, enhancements have been introduced to prevent over extensive SNS querring in high-throughput scenarios: 16 | 17 | * Information on topics and topic subscriptions are cached for a configurable amount of time. 18 | * When an entry is not found in the cache only a single request to SNS is performed and all other send operations wait for the result. 19 | * Requests to the SNS client are rate limited to a maximum of 30 per second. Any follow up requests are delayed until the first request from the 30 passes the second threshold. 20 | * The drawback is that this can limit throughput, especially initially. 21 | * Due to the implemented caching of topics and subscriptions, this should only occur on occassions where there are many different types of events that all get published within a single second. This could theoretically only occur on endpoint startup and should not be an issue later, although there is still a very small chance this can occur. 22 | * Cache entries are removed after the invalidation timeout is due. 23 | 24 | 25 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.0", 4 | "allowPrerelease": true, 5 | "rollForward": "latestFeature" 6 | } 7 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/.globalconfig: -------------------------------------------------------------------------------- 1 | is_global = true 2 | 3 | dotnet_analyzer_diagnostic.severity = none 4 | 5 | # Workaround for https://github.com/dotnet/roslyn/issues/41640 6 | dotnet_diagnostic.EnableGenerateDocumentationFile.severity = none 7 | -------------------------------------------------------------------------------- /src/CommandLine/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Application synchronization contexts don't require ConfigureAwait(false) 4 | dotnet_diagnostic.CA2007.severity = none 5 | 6 | # Justification: Command-line app probably does not need cancellation support 7 | dotnet_diagnostic.PS0013.severity = suggestion 8 | dotnet_diagnostic.PS0018.severity = suggestion 9 | 10 | -------------------------------------------------------------------------------- /src/CommandLine/AmazonServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.CommandLine; 2 | 3 | using System; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | using Amazon.Runtime; 7 | 8 | static class AmazonServiceExtensions 9 | { 10 | public static async Task RetryConflictsAsync(this IAmazonService client, Func> a, Func onRetry) 11 | { 12 | _ = client; 13 | 14 | var tryCount = 0; 15 | var sleepTimeMs = 2000; 16 | const int maxTryCount = 5; 17 | 18 | while (true) 19 | { 20 | try 21 | { 22 | tryCount++; 23 | return await a().ConfigureAwait(false); 24 | } 25 | catch (AmazonServiceException ex) 26 | when (ex.StatusCode == HttpStatusCode.Conflict && 27 | ex.ErrorCode == "OperationAborted") 28 | { 29 | if (tryCount >= maxTryCount) 30 | { 31 | throw; 32 | } 33 | 34 | var sleepTime = sleepTimeMs * tryCount; 35 | await onRetry(sleepTime); 36 | await Task.Delay(sleepTime).ConfigureAwait(false); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/CommandLine/CommandRunner.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.CommandLine; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Amazon; 6 | using Amazon.S3; 7 | using Amazon.SimpleNotificationService; 8 | using Amazon.SQS; 9 | using McMaster.Extensions.CommandLineUtils; 10 | 11 | static class CommandRunner 12 | { 13 | public static async Task Run(CommandOption accessKey, CommandOption secret, CommandOption region, Func func) 14 | { 15 | var useCredentialsFromOptions = accessKey.HasValue() && secret.HasValue(); 16 | var useRegionFromOptions = region.HasValue(); 17 | 18 | var useFullConstructor = useCredentialsFromOptions && useRegionFromOptions; 19 | 20 | using var sqs = useFullConstructor ? new AmazonSQSClient(accessKey.Value(), secret.Value(), new AmazonSQSConfig { RegionEndpoint = RegionEndpoint.GetBySystemName(region.Value()) }) : 21 | useCredentialsFromOptions ? new AmazonSQSClient(accessKey.Value(), secret.Value(), new AmazonSQSConfig()) : 22 | useRegionFromOptions ? new AmazonSQSClient(new AmazonSQSConfig { RegionEndpoint = RegionEndpoint.GetBySystemName(region.Value()) }) : 23 | new AmazonSQSClient(new AmazonSQSConfig()); 24 | using var sns = useFullConstructor ? new AmazonSimpleNotificationServiceClient(accessKey.Value(), secret.Value(), new AmazonSimpleNotificationServiceConfig { RegionEndpoint = RegionEndpoint.GetBySystemName(region.Value()) }) : 25 | useCredentialsFromOptions ? new AmazonSimpleNotificationServiceClient(accessKey.Value(), secret.Value(), new AmazonSimpleNotificationServiceConfig()) : 26 | useRegionFromOptions ? new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig { RegionEndpoint = RegionEndpoint.GetBySystemName(region.Value()) }) : 27 | new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig()); 28 | using var s3 = useFullConstructor ? new AmazonS3Client(accessKey.Value(), secret.Value(), new AmazonS3Config { RegionEndpoint = RegionEndpoint.GetBySystemName(region.Value()) }) : 29 | useCredentialsFromOptions ? new AmazonS3Client(accessKey.Value(), secret.Value(), new AmazonS3Config()) : 30 | useRegionFromOptions ? new AmazonS3Client(new AmazonS3Config { RegionEndpoint = RegionEndpoint.GetBySystemName(region.Value()) }) : 31 | new AmazonS3Client(new AmazonS3Config()); 32 | await func(sqs, sns, s3); 33 | } 34 | 35 | public const string AccessKeyId = "AWS_ACCESS_KEY_ID"; 36 | public const string Region = "AWS_REGION"; 37 | public const string SecretAccessKey = "AWS_SECRET_ACCESS_KEY"; 38 | } -------------------------------------------------------------------------------- /src/CommandLine/DefaultConfigurationValues.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.CommandLine; 2 | 3 | using System; 4 | 5 | public static class DefaultConfigurationValues 6 | { 7 | public static readonly TimeSpan RetentionPeriod = TimeSpan.FromDays(4); 8 | public static readonly TimeSpan MaximumQueueDelayTime = TimeSpan.FromMinutes(15); 9 | public static readonly string S3KeyPrefix = string.Empty; 10 | public static readonly string DelayedDeliveryQueueSuffix = "-delay.fifo"; 11 | 12 | public static readonly string QueueNamePrefix = string.Empty; 13 | public static readonly string TopicNamePrefix = string.Empty; 14 | } -------------------------------------------------------------------------------- /src/CommandLine/NServiceBus.Transports.SQS.CommandLine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | Exe 6 | false 7 | Major 8 | 9 | 10 | 11 | NServiceBus.AmazonSQS.CommandLine 12 | .NET Core global tool to manage SQS, S3 and SNS entities for NServiceBus endpoints 13 | sqs-transport 14 | True 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/CommandLine/PolicyStatement.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.CommandLine; 2 | 3 | using Amazon.Auth.AccessControlPolicy; 4 | 5 | class PolicyStatement(string topicName, string topicArn, string queueArn) 6 | { 7 | public string QueueArn { get; } = queueArn; 8 | public string TopicName { get; } = topicName; 9 | public string TopicArn { get; } = topicArn; 10 | public Statement Statement { get; } = CreatePermissionStatement(queueArn, topicArn); 11 | 12 | #pragma warning disable 618 13 | static Statement CreatePermissionStatement(string queueArn, string topicArn) 14 | { 15 | var statement = new Statement(Statement.StatementEffect.Allow); 16 | statement.Actions.Add(new ActionIdentifier("sqs:SendMessage")); 17 | statement.Resources.Add(new Resource(queueArn)); 18 | statement.Conditions.Add(ConditionFactory.NewSourceArnCondition(topicArn)); 19 | statement.Principals.Add(new Principal("*")); 20 | return statement; 21 | } 22 | #pragma warning restore 618 23 | } -------------------------------------------------------------------------------- /src/CommandLine/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "NServiceBus.Transports.SQS.CommandLine": { 4 | "commandName": "Project", 5 | "commandLineArgs": "endpoint set-policy test2 events --event-type MyMessages.Message35 --prefix yves-" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/CommandLine/TopicSanitization.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.CommandLine; 2 | 3 | using System.Text; 4 | 5 | public class TopicSanitization 6 | { 7 | public static string GetSanitizedTopicName(string topicName) 8 | { 9 | var topicNameBuilder = new StringBuilder(topicName); 10 | // SNS topic names can only have alphanumeric characters, hyphens and underscores. 11 | // Any other characters will be replaced with a hyphen. 12 | for (var i = 0; i < topicNameBuilder.Length; ++i) 13 | { 14 | var c = topicNameBuilder[i]; 15 | if (!char.IsLetterOrDigit(c) 16 | && c != '-' 17 | && c != '_') 18 | { 19 | topicNameBuilder[i] = '-'; 20 | } 21 | } 22 | 23 | return topicNameBuilder.ToString(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/CommandLineTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | dotnet_diagnostic.PS0003.severity = suggestion 6 | dotnet_diagnostic.PS0013.severity = suggestion 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | 9 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 10 | dotnet_diagnostic.NSB0002.severity = suggestion 11 | -------------------------------------------------------------------------------- /src/CommandLineTests/NServiceBus.Transports.SQS.CommandLine.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Custom.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9.0 5 | minor 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | true 8 | 5.0 9 | true 10 | low 11 | all 12 | 13 | 2.1.3 14 | 0024000004800000940000000602000000240000525341310004000001000100dde965e6172e019ac82c2639ffe494dd2e7dd16347c34762a05732b492e110f2e4e2e1b5ef2d85c848ccfb671ee20a47c8d1376276708dc30a90ff1121b647ba3b7259a6bc383b2034938ef0e275b58b920375ac605076178123693c6c4f1331661a62eba28c249386855637780e3ff5f23a6d854700eaa6803ef48907513b92 15 | 00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/NServiceBus.AmazonSQS.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | dotnet_diagnostic.PS0003.severity = suggestion 6 | dotnet_diagnostic.PS0013.severity = suggestion 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | 9 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 10 | dotnet_diagnostic.NSB0002.severity = suggestion 11 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/ConfigurationHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests; 2 | 3 | using EndpointTemplates; 4 | 5 | static class ConfigurationHelpers 6 | { 7 | public static SqsTransport ConfigureSqsTransport(this EndpointConfiguration configuration) => (SqsTransport)configuration.ConfigureTransport(); 8 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/CustomizedServer.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests; 2 | 3 | using NServiceBus.AcceptanceTests.EndpointTemplates; 4 | 5 | public class CustomizedServer : DefaultServer 6 | { 7 | public CustomizedServer(bool supportsPublishSubscribe) 8 | { 9 | TransportConfiguration = new ConfigureEndpointSqsTransport(supportsPublishSubscribe); 10 | } 11 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Handling_messages_concurrently.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests; 2 | 3 | using System; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using AcceptanceTesting; 8 | using AcceptanceTesting.Customization; 9 | using EndpointTemplates; 10 | using NServiceBus; 11 | using NUnit.Framework; 12 | 13 | public class Handling_messages_concurrently : NServiceBusAcceptanceTest 14 | { 15 | [Test] 16 | public async Task Should_not_exceed_max_concurrency_level() 17 | { 18 | var context = await Scenario.Define() 19 | .WithEndpoint(b => b.When(session => 20 | { 21 | var tasks = Enumerable.Range(0, 50).Select(x => session.Send(new MyMessage())); 22 | return Task.WhenAll(tasks); 23 | })) 24 | .WithEndpoint() 25 | .Done(c => c.ReceiveCount >= 50) 26 | .Run(); 27 | 28 | Assert.That(context.MaxConcurrency, Is.LessThanOrEqualTo(4), "The max concurrency was not used"); 29 | } 30 | 31 | public class Context : ScenarioContext 32 | { 33 | public int CurrentConcurrency; 34 | 35 | public int MaxConcurrency { get; set; } 36 | 37 | public int ReceiveCount; 38 | } 39 | 40 | public class Sender : EndpointConfigurationBuilder 41 | { 42 | public Sender() 43 | { 44 | EndpointSetup(builder => 45 | { 46 | builder.ConfigureRouting().RouteToEndpoint(typeof(MyMessage), typeof(Receiver)); 47 | }); 48 | } 49 | } 50 | 51 | public class Receiver : EndpointConfigurationBuilder 52 | { 53 | public Receiver() 54 | { 55 | EndpointSetup(config => config.LimitMessageProcessingConcurrencyTo(4)); 56 | } 57 | 58 | public class MyMessageHandler : IHandleMessages 59 | { 60 | Context testContext; 61 | 62 | public MyMessageHandler(Context testContext) 63 | { 64 | this.testContext = testContext; 65 | } 66 | 67 | public async Task Handle(MyMessage message, IMessageHandlerContext context) 68 | { 69 | Interlocked.Increment(ref testContext.CurrentConcurrency); 70 | 71 | // simulate some work 72 | await Task.Delay(10); 73 | Interlocked.Increment(ref testContext.ReceiveCount); 74 | 75 | testContext.MaxConcurrency = Math.Max(testContext.MaxConcurrency, testContext.CurrentConcurrency); 76 | Interlocked.Decrement(ref testContext.CurrentConcurrency); 77 | } 78 | } 79 | } 80 | 81 | public class MyMessage : ICommand 82 | { 83 | } 84 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Installers/DefaultServerWithNoInstallers.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Installers; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using NServiceBus.AcceptanceTesting.Support; 6 | using NServiceBus.AcceptanceTests.EndpointTemplates; 7 | 8 | public class DefaultServerWithNoInstallers : ServerWithInstallersDisabled 9 | { 10 | public IConfigureEndpointTestExecution PersistenceConfiguration { get; set; } = TestSuiteConstraints.Current.CreatePersistenceConfiguration(); 11 | 12 | public override Task GetConfiguration(RunDescriptor runDescriptor, EndpointCustomizationConfiguration endpointConfiguration, Func configurationBuilderCustomization) => 13 | base.GetConfiguration(runDescriptor, endpointConfiguration, async configuration => 14 | { 15 | await configuration.DefinePersistence(PersistenceConfiguration, runDescriptor, endpointConfiguration); 16 | 17 | await configurationBuilderCustomization(configuration); 18 | }); 19 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Installers/ServerWithInstallersDisabled.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Installers; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using NServiceBus.AcceptanceTesting.Customization; 6 | using NServiceBus.AcceptanceTesting.Support; 7 | using NServiceBus.AcceptanceTests.EndpointTemplates; 8 | 9 | public class ServerWithInstallersDisabled : IEndpointSetupTemplate 10 | { 11 | public IConfigureEndpointTestExecution TransportConfiguration { get; set; } = TestSuiteConstraints.Current.CreateTransportConfiguration(); 12 | 13 | public virtual async Task GetConfiguration(RunDescriptor runDescriptor, EndpointCustomizationConfiguration endpointConfiguration, Func configurationBuilderCustomization) 14 | { 15 | var builder = new EndpointConfiguration(endpointConfiguration.EndpointName); 16 | 17 | builder.Recoverability() 18 | .Delayed(delayed => delayed.NumberOfRetries(0)) 19 | .Immediate(immediate => immediate.NumberOfRetries(0)); 20 | builder.SendFailedMessagesTo("error"); 21 | 22 | await builder.DefineTransport(TransportConfiguration, runDescriptor, endpointConfiguration).ConfigureAwait(false); 23 | 24 | builder.UseSerialization(); 25 | 26 | await configurationBuilderCustomization(builder).ConfigureAwait(false); 27 | 28 | // scan types at the end so that all types used by the configuration have been loaded into the AppDomain 29 | builder.ScanTypesForTest(endpointConfiguration); 30 | 31 | return builder; 32 | } 33 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Installers/When_trying_to_receive_message_from_nonexisting_queue.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Installers; 2 | 3 | using AcceptanceTesting; 4 | using Amazon.SQS.Model; 5 | using NServiceBus; 6 | using NServiceBus.AcceptanceTests; 7 | using NUnit.Framework; 8 | 9 | public class When_trying_to_receive_message_from_nonexisting_queue : NServiceBusAcceptanceTest 10 | { 11 | [Test] 12 | public void Should_throw_exception_with_queue_name() 13 | { 14 | var exception = Assert.ThrowsAsync(async () => 15 | { 16 | await Scenario.Define() 17 | .WithEndpoint() 18 | .Done(context => context.EndpointsStarted) 19 | .Run(); 20 | }); 21 | 22 | Assert.That(exception.Message, Contains.Substring("TryingToReceiveMessageFromNonexistingQueue")); 23 | } 24 | 25 | public class Endpoint : EndpointConfigurationBuilder 26 | { 27 | public Endpoint() => 28 | EndpointSetup(); 29 | } 30 | 31 | public class MyMessage : ICommand 32 | { 33 | } 34 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Installers/When_trying_to_subscribe_when_topic_does_not_exist.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Installers; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using NServiceBus.Features; 6 | using NUnit.Framework; 7 | 8 | public class When_trying_to_subscribe_when_topic_does_not_exist : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public async Task Should_log_exception_with_topic_name() 12 | { 13 | await Scenario.Define(c => 14 | { 15 | c.EnableInstallers = true; 16 | c.DisableAutoSubscribe = true; 17 | }) 18 | .WithEndpoint() 19 | .Done(context => context.EndpointsStarted) 20 | .Run(); 21 | 22 | var context = await Scenario.Define(c => 23 | { 24 | c.EnableInstallers = false; 25 | c.DisableAutoSubscribe = false; 26 | }) 27 | .WithEndpoint() 28 | .Done(context => context.EndpointsStarted) 29 | .Run(); 30 | 31 | Assert.That(context.Logs, Has.One.Matches(x => x.Level == Logging.LogLevel.Error && x.Message.Contains("not found") && x.Message.Contains("When_trying_to_subscribe_when_topic_does_not_exist-MyEvent"))); 32 | } 33 | 34 | public class Context : ScenarioContext 35 | { 36 | public bool EnableInstallers { get; set; } 37 | public bool DisableAutoSubscribe { get; set; } 38 | } 39 | 40 | public class Endpoint : EndpointConfigurationBuilder 41 | { 42 | public Endpoint() => 43 | EndpointSetup((e, c) => 44 | { 45 | var testContext = c.ScenarioContext as Context; 46 | if (testContext.EnableInstallers) 47 | { 48 | e.EnableInstallers(); 49 | } 50 | if (testContext.DisableAutoSubscribe) 51 | { 52 | e.DisableFeature(); 53 | } 54 | }); 55 | 56 | class Handler : IHandleMessages 57 | { 58 | public Task Handle(MyEvent message, IMessageHandlerContext context) 59 | { 60 | return Task.CompletedTask; 61 | } 62 | } 63 | 64 | } 65 | 66 | public class MyMessage : ICommand 67 | { 68 | } 69 | 70 | public class MyEvent : IEvent 71 | { 72 | } 73 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NServiceBus.Transport.SQS.AcceptanceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | true 6 | ..\NServiceBusTests.snk 7 | NServiceBus.AcceptanceTests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NamePrefixHandler.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests; 2 | 3 | using System; 4 | using NUnit.Framework; 5 | 6 | class NamePrefixHandler : IDisposable 7 | { 8 | string namePrefixBackup; 9 | 10 | NamePrefixHandler(string namePrefixBackup) 11 | { 12 | this.namePrefixBackup = namePrefixBackup; 13 | } 14 | 15 | public static IDisposable RunTestWithNamePrefixCustomization(string customization) 16 | { 17 | var namePrefixBackup = SetupFixture.NamePrefix; 18 | SetupFixture.AppendToNamePrefix(customization); 19 | 20 | TestContext.Out.WriteLine($"Customized name prefix: '{SetupFixture.NamePrefix}'"); 21 | 22 | return new NamePrefixHandler(namePrefixBackup); 23 | } 24 | 25 | public void Dispose() => SetupFixture.RestoreNamePrefix(namePrefixBackup); 26 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativeIntegration/NativeEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativeIntegration; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using AcceptanceTesting.Customization; 10 | using Amazon.SQS.Model; 11 | using Transport.SQS.Tests; 12 | 13 | static class NativeEndpoint 14 | { 15 | public static async Task ConsumePoisonQueue(Guid testRunId, string errorQueueAddress, Action nativeMessageAccessor = null, CancellationToken cancellationToken = default) 16 | { 17 | using var sqsClient = ClientFactories.CreateSqsClient(); 18 | var getQueueUrlResponse = await sqsClient.GetQueueUrlAsync(new GetQueueUrlRequest 19 | { 20 | QueueName = TestNameHelper.GetSqsQueueName(errorQueueAddress, SetupFixture.NamePrefix) 21 | }, cancellationToken).ConfigureAwait(false); 22 | 23 | while (true) 24 | { 25 | cancellationToken.ThrowIfCancellationRequested(); 26 | 27 | var receiveMessageResponse = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest 28 | { 29 | QueueUrl = getQueueUrlResponse.QueueUrl, 30 | WaitTimeSeconds = 5, 31 | MessageAttributeNames = ["All"] 32 | }, cancellationToken).ConfigureAwait(false); 33 | 34 | foreach (var msg in receiveMessageResponse.Messages) 35 | { 36 | if (msg.MessageAttributes?.TryGetValue(Headers.MessageId, out var messageIdAttribute) is true && messageIdAttribute?.StringValue == testRunId.ToString()) 37 | { 38 | nativeMessageAccessor?.Invoke(msg); 39 | } 40 | 41 | await sqsClient.DeleteMessageAsync(getQueueUrlResponse.QueueUrl, msg.ReceiptHandle, cancellationToken); 42 | } 43 | } 44 | } 45 | 46 | public static async Task SendTo(Dictionary messageAttributeValues, TMessage message) where TMessage : IMessage 47 | { 48 | var json = JsonSerializer.Serialize(message); 49 | await SendTo(messageAttributeValues, json); 50 | } 51 | 52 | public static async Task SendTo(Dictionary messageAttributeValues, string message, bool base64Encode = true) 53 | { 54 | using var sqsClient = ClientFactories.CreateSqsClient(); 55 | var getQueueUrlResponse = await sqsClient.GetQueueUrlAsync(new GetQueueUrlRequest 56 | { 57 | QueueName = TestNameHelper.GetSqsQueueName(Conventions.EndpointNamingConvention(typeof(TEndpoint)), SetupFixture.NamePrefix) 58 | }).ConfigureAwait(false); 59 | 60 | var body = base64Encode ? Convert.ToBase64String(Encoding.UTF8.GetBytes(message)) : message; 61 | 62 | var sendMessageRequest = new SendMessageRequest 63 | { 64 | QueueUrl = getQueueUrlResponse.QueueUrl, 65 | MessageAttributes = messageAttributeValues, 66 | MessageBody = body 67 | }; 68 | 69 | await sqsClient.SendMessageAsync(sendMessageRequest).ConfigureAwait(false); 70 | } 71 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativeIntegration/When_access_to_received_native_message_required.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativeIntegration; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using EndpointTemplates; 7 | using NServiceBus.Pipeline; 8 | using NUnit.Framework; 9 | 10 | public class When_access_to_received_native_message_required : NServiceBusAcceptanceTest 11 | { 12 | [Test] 13 | public async Task Should_be_available_from_the_pipeline() 14 | { 15 | var scenario = await Scenario.Define() 16 | .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new Message()))) 17 | .Done(c => c.MessageReceived) 18 | .Run(); 19 | 20 | Assert.Multiple(() => 21 | { 22 | Assert.That(scenario.HandlerHasAccessToNativeSqsMessage, Is.True, "The handler should have access to the native message"); 23 | Assert.That(scenario.BehaviorHasAccessToNativeSqsMessage, Is.True, "The behavior should have access to the native message"); 24 | }); 25 | } 26 | 27 | public class Receiver : EndpointConfigurationBuilder 28 | { 29 | public Receiver() => 30 | EndpointSetup(c => 31 | c.Pipeline.Register(typeof(MyCustomBehavior), "Behavior that needs access to native message")); 32 | 33 | class MyCustomBehavior : Behavior 34 | { 35 | public MyCustomBehavior(Context testContext) => this.testContext = testContext; 36 | 37 | public override Task Invoke(IIncomingPhysicalMessageContext context, Func next) 38 | { 39 | testContext.BehaviorHasAccessToNativeSqsMessage = context.Extensions.TryGet(out _); 40 | return next(); 41 | } 42 | 43 | readonly Context testContext; 44 | } 45 | 46 | class MyHandler : IHandleMessages 47 | { 48 | public MyHandler(Context testContext) => this.testContext = testContext; 49 | 50 | public Task Handle(Message message, IMessageHandlerContext context) 51 | { 52 | testContext.HandlerHasAccessToNativeSqsMessage = context.Extensions.TryGet(out _); 53 | testContext.MessageReceived = true; 54 | 55 | return Task.CompletedTask; 56 | } 57 | 58 | readonly Context testContext; 59 | } 60 | } 61 | 62 | public class Message : IMessage 63 | { 64 | } 65 | 66 | class Context : ScenarioContext 67 | { 68 | public bool MessageReceived { get; set; } 69 | public bool BehaviorHasAccessToNativeSqsMessage { get; set; } 70 | public bool HandlerHasAccessToNativeSqsMessage { get; set; } 71 | } 72 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativePubSub/HybridModeRateLimit/TestCase.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativePubSub.HybridModeRateLimit; 2 | 3 | using System; 4 | 5 | public class TestCase 6 | { 7 | public TestCase(int sequence) => Sequence = sequence; 8 | 9 | public int NumberOfEvents { get; internal set; } 10 | public TimeSpan? TestExecutionTimeout { get; internal set; } 11 | public TimeSpan MessageVisibilityTimeout { get; internal set; } = DefaultMessageVisibilityTimeout; 12 | public TimeSpan SubscriptionsCacheTTL { get; internal set; } = DefaultSubscriptionCacheTTL; 13 | public TimeSpan NotFoundTopicsCacheTTL { get; internal set; } = DefaultTopicCacheTTL; 14 | public bool PreDeployInfrastructure { get; internal set; } = DefaultPreDeployInfrastructure; 15 | public int DeployInfrastructureDelay { get; internal set; } = DefaultDeployInfrastructureDelay; 16 | public int Sequence { get; } 17 | 18 | public override string ToString() => $"#{Sequence}, " + 19 | $"{nameof(NumberOfEvents)}: {NumberOfEvents}, " + 20 | $"{nameof(MessageVisibilityTimeout)}: {(MessageVisibilityTimeout == DefaultMessageVisibilityTimeout ? "default" : MessageVisibilityTimeout.ToString())}, " + 21 | $"{nameof(TestExecutionTimeout)}: {TestExecutionTimeout?.ToString() ?? "default"}, " + 22 | $"{nameof(SubscriptionsCacheTTL)}: {(SubscriptionsCacheTTL == DefaultSubscriptionCacheTTL ? "default" : SubscriptionsCacheTTL.ToString())}, " + 23 | $"{nameof(NotFoundTopicsCacheTTL)}: {(NotFoundTopicsCacheTTL == DefaultTopicCacheTTL ? "default" : NotFoundTopicsCacheTTL.ToString())}"; 24 | 25 | static TimeSpan DefaultSubscriptionCacheTTL = TimeSpan.FromSeconds(5); 26 | static TimeSpan DefaultTopicCacheTTL = TimeSpan.FromSeconds(5); 27 | static TimeSpan DefaultMessageVisibilityTimeout = TimeSpan.FromSeconds(30); 28 | static int DefaultDeployInfrastructureDelay = 65000; 29 | static bool DefaultPreDeployInfrastructure = true; 30 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativePubSub/When_customizing_topic_name_generation.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativePubSub; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | 8 | public class When_customizing_topic_name_generation : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public async Task It_can_subscribe_for_event_published_on_custom_topic() 12 | { 13 | var context = await Scenario.Define() 14 | .WithEndpoint(b => b.When(c => c.Subscribed, (session, ctx) => session.Publish(new MyEvent()))) 15 | .WithEndpoint(b => b.When(async (session, ctx) => 16 | { 17 | await session.Subscribe(); 18 | ctx.Subscribed = true; 19 | })) 20 | .Done(c => c.GotTheEvent) 21 | .Run(); 22 | 23 | Assert.That(context.GotTheEvent, Is.True); 24 | } 25 | 26 | public class Context : ScenarioContext 27 | { 28 | public bool GotTheEvent { get; set; } 29 | public bool Subscribed { get; set; } 30 | } 31 | 32 | public class CustomizedPublisher : EndpointConfigurationBuilder 33 | { 34 | public CustomizedPublisher() 35 | { 36 | EndpointSetup(c => 37 | { 38 | c.ConfigureSqsTransport().TopicNameGenerator = (eventType, prefix) => prefix + "-shared-topic"; 39 | }); 40 | } 41 | } 42 | 43 | public class CustomizedSubscriber : EndpointConfigurationBuilder 44 | { 45 | public CustomizedSubscriber() 46 | { 47 | EndpointSetup(c => 48 | { 49 | c.ConfigureSqsTransport().TopicNameGenerator = (eventType, prefix) => prefix + "-shared-topic"; 50 | }); 51 | } 52 | 53 | public class MyHandler : IHandleMessages 54 | { 55 | Context testContext; 56 | 57 | public MyHandler(Context testContext) 58 | { 59 | this.testContext = testContext; 60 | } 61 | 62 | public Task Handle(MyEvent @event, IMessageHandlerContext context) 63 | { 64 | testContext.GotTheEvent = true; 65 | return Task.CompletedTask; 66 | } 67 | } 68 | } 69 | 70 | public class MyEvent : IEvent 71 | { 72 | } 73 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativePubSub/When_publisher_runs_in_compat_mode.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativePubSub; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using AcceptanceTesting.Customization; 7 | using Configuration.AdvancedExtensibility; 8 | using EndpointTemplates; 9 | using Features; 10 | using NServiceBus.Routing.MessageDrivenSubscriptions; 11 | using NUnit.Framework; 12 | 13 | public class When_publisher_runs_in_compat_mode : NServiceBusAcceptanceTest 14 | { 15 | static string PublisherEndpoint => Conventions.EndpointNamingConvention(typeof(MigratedPublisher)); 16 | 17 | [Test] 18 | public async Task Legacy_subscriber_can_subscribe() 19 | { 20 | var publisherMigrated = await Scenario.Define() 21 | .WithEndpoint(b => b.When(c => c.SubscribedMessageDriven, (session, ctx) => session.Publish(new MyEvent()))) 22 | .WithEndpoint(b => b.When((session, ctx) => session.Subscribe())) 23 | .Done(c => c.GotTheEvent) 24 | .Run(TimeSpan.FromSeconds(30)); 25 | 26 | Assert.That(publisherMigrated.GotTheEvent, Is.True); 27 | } 28 | 29 | public class Context : ScenarioContext 30 | { 31 | public bool GotTheEvent { get; set; } 32 | public bool SubscribedMessageDriven { get; set; } 33 | } 34 | 35 | public class MigratedPublisher : EndpointConfigurationBuilder 36 | { 37 | public MigratedPublisher() 38 | { 39 | EndpointSetup(c => 40 | { 41 | c.ConfigureRouting().EnableMessageDrivenPubSubCompatibilityMode(); 42 | 43 | c.OnEndpointSubscribed((s, context) => 44 | { 45 | if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(Subscriber)))) 46 | { 47 | context.SubscribedMessageDriven = true; 48 | } 49 | }); 50 | }).IncludeType(); 51 | } 52 | } 53 | 54 | public class Subscriber : EndpointConfigurationBuilder 55 | { 56 | public Subscriber() 57 | { 58 | EndpointSetup(new CustomizedServer(false), (c, rd) => 59 | { 60 | c.GetSettings().GetOrCreate().AddOrReplacePublishers("LegacyConfig", 61 | [ 62 | new PublisherTableEntry(typeof(MyEvent), PublisherAddress.CreateFromEndpointName(PublisherEndpoint)) 63 | ]); 64 | c.DisableFeature(); 65 | }); 66 | } 67 | 68 | public class MyHandler : IHandleMessages 69 | { 70 | readonly Context testContext; 71 | 72 | public MyHandler(Context testContext) => this.testContext = testContext; 73 | 74 | public Task Handle(MyEvent @event, IMessageHandlerContext context) 75 | { 76 | testContext.GotTheEvent = true; 77 | return Task.CompletedTask; 78 | } 79 | } 80 | } 81 | 82 | public class MyEvent : IEvent 83 | { 84 | } 85 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativePubSub/When_subscriber_runs_in_compat_mode.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativePubSub; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using AcceptanceTesting.Customization; 7 | using Configuration.AdvancedExtensibility; 8 | using EndpointTemplates; 9 | using Features; 10 | using NUnit.Framework; 11 | 12 | public class When_subscriber_runs_in_compat_mode : NServiceBusAcceptanceTest 13 | { 14 | static string PublisherEndpoint => Conventions.EndpointNamingConvention(typeof(LegacyPublisher)); 15 | 16 | [Test] 17 | public async Task It_can_subscribe_for_event_published_by_legacy_publisher() 18 | { 19 | var publisherMigrated = await Scenario.Define() 20 | .WithEndpoint(b => b.When(c => c.SubscribedMessageDriven, (session, ctx) => session.Publish(new MyEvent()))) 21 | .WithEndpoint(b => b.When((session, ctx) => session.Subscribe())) 22 | .Done(c => c.GotTheEvent) 23 | .Run(TimeSpan.FromSeconds(30)); 24 | 25 | Assert.That(publisherMigrated.GotTheEvent, Is.True); 26 | } 27 | 28 | public class Context : ScenarioContext 29 | { 30 | public bool GotTheEvent { get; set; } 31 | public bool SubscribedMessageDriven { get; set; } 32 | } 33 | 34 | public class LegacyPublisher : EndpointConfigurationBuilder 35 | { 36 | public LegacyPublisher() 37 | { 38 | EndpointSetup(new CustomizedServer(false), (c, rd) => 39 | { 40 | c.GetSettings().Set("NServiceBus.AmazonSQS.DisableNativePubSub", true); 41 | c.OnEndpointSubscribed((s, context) => 42 | { 43 | if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(MigratedSubscriber)))) 44 | { 45 | context.SubscribedMessageDriven = true; 46 | } 47 | }); 48 | }).IncludeType(); 49 | } 50 | } 51 | 52 | public class MigratedSubscriber : EndpointConfigurationBuilder 53 | { 54 | public MigratedSubscriber() 55 | { 56 | EndpointSetup(c => 57 | { 58 | var compatMode = c.ConfigureRouting().EnableMessageDrivenPubSubCompatibilityMode(); 59 | compatMode.RegisterPublisher(typeof(MyEvent), PublisherEndpoint); 60 | c.DisableFeature(); 61 | }); 62 | } 63 | 64 | public class MyHandler : IHandleMessages 65 | { 66 | readonly Context testContext; 67 | 68 | public MyHandler(Context context) => testContext = context; 69 | 70 | public Task Handle(MyEvent @event, IMessageHandlerContext context) 71 | { 72 | testContext.GotTheEvent = true; 73 | return Task.CompletedTask; 74 | } 75 | } 76 | } 77 | 78 | public class MyEvent : IEvent 79 | { 80 | } 81 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/NativePubSub/When_subscribing_to_object.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativePubSub; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | 8 | public class When_subscribing_to_object : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public async Task It_should_still_deliver() 12 | { 13 | var context = await Scenario.Define() 14 | .WithEndpoint(b => b.When(c => c.EndpointsStarted, session => session.Publish(new MyEvent()))) 15 | .WithEndpoint(b => { }) 16 | .Done(c => c.GotTheEvent) 17 | .Run(); 18 | 19 | Assert.That(context.GotTheEvent, Is.True); 20 | } 21 | 22 | public class Context : ScenarioContext 23 | { 24 | public bool GotTheEvent { get; set; } 25 | } 26 | 27 | public class Publisher : EndpointConfigurationBuilder 28 | { 29 | public Publisher() 30 | { 31 | EndpointSetup(c => { }); 32 | } 33 | } 34 | 35 | public class Subscriber : EndpointConfigurationBuilder 36 | { 37 | public Subscriber() 38 | { 39 | EndpointSetup(c => 40 | { 41 | c.Conventions().DefiningEventsAs(t => typeof(object).IsAssignableFrom(t) || typeof(IEvent).IsAssignableFrom(t)); 42 | 43 | var transport = c.ConfigureSqsTransport(); 44 | transport.MapEvent(); 45 | }); 46 | } 47 | 48 | public class MyHandler : IHandleMessages 49 | { 50 | Context testContext; 51 | 52 | public MyHandler(Context testContext) 53 | { 54 | this.testContext = testContext; 55 | } 56 | 57 | public Task Handle(object @event, IMessageHandlerContext context) 58 | { 59 | testContext.GotTheEvent = true; 60 | return Task.CompletedTask; 61 | } 62 | } 63 | } 64 | 65 | public class MyEvent : IEvent 66 | { 67 | } 68 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Policies/When_subscribing_to_multiple_events_with_account_policy.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Policies; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | 8 | public class When_subscribing_to_multiple_events_with_account_policy : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public async Task Should_deliver_events() 12 | { 13 | var context = await Scenario.Define() 14 | .WithEndpoint(b => b.When(async session => 15 | { 16 | await session.Publish(new MyEvent()); 17 | await session.Publish(new MyOtherEvent()); 18 | })) 19 | .WithEndpoint(b => 20 | { 21 | b.CustomConfig(c => 22 | { 23 | c.ConfigureSqsTransport().Policies.AccountCondition = true; 24 | }); 25 | }) 26 | .Done(c => c.GotEvents) 27 | .Run(); 28 | 29 | Assert.Multiple(() => 30 | { 31 | Assert.That(context.GotMyEvent, Is.True); 32 | Assert.That(context.GotMyOtherEvent, Is.True); 33 | }); 34 | } 35 | 36 | public class Context : ScenarioContext 37 | { 38 | public bool GotMyEvent { get; set; } 39 | public bool GotMyOtherEvent { get; set; } 40 | 41 | public bool GotEvents => GotMyEvent && GotMyOtherEvent; 42 | } 43 | 44 | public class Publisher : EndpointConfigurationBuilder 45 | { 46 | public Publisher() 47 | { 48 | EndpointSetup(); 49 | } 50 | } 51 | 52 | public class Subscriber : EndpointConfigurationBuilder 53 | { 54 | public Subscriber() 55 | { 56 | EndpointSetup(); 57 | } 58 | 59 | public class MyHandler : IHandleMessages 60 | { 61 | Context testContext; 62 | 63 | public MyHandler(Context testContext) 64 | { 65 | this.testContext = testContext; 66 | } 67 | 68 | public Task Handle(MyEvent message, IMessageHandlerContext context) 69 | { 70 | testContext.GotMyEvent = true; 71 | return Task.CompletedTask; 72 | } 73 | } 74 | 75 | public class MyEventOtherHandler : IHandleMessages 76 | { 77 | Context testContext; 78 | 79 | public MyEventOtherHandler(Context testContext) 80 | { 81 | this.testContext = testContext; 82 | } 83 | 84 | public Task Handle(MyEvent message, IMessageHandlerContext context) 85 | { 86 | testContext.GotMyOtherEvent = true; 87 | return Task.CompletedTask; 88 | } 89 | } 90 | } 91 | 92 | public class MyEvent : IEvent 93 | { 94 | } 95 | 96 | public class MyOtherEvent : IEvent 97 | { 98 | } 99 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Policies/When_subscribing_to_multiple_events_with_default_policy.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Policies; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | 8 | public class When_subscribing_to_multiple_events_with_default_policy : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public async Task Should_deliver_events() 12 | { 13 | var context = await Scenario.Define() 14 | .WithEndpoint(b => b.When(async session => 15 | { 16 | await session.Publish(new MyEvent()); 17 | await session.Publish(new MyOtherEvent()); 18 | })) 19 | .WithEndpoint() 20 | .Done(c => c.GotEvents) 21 | .Run(); 22 | 23 | Assert.Multiple(() => 24 | { 25 | Assert.That(context.GotMyEvent, Is.True); 26 | Assert.That(context.GotMyOtherEvent, Is.True); 27 | }); 28 | } 29 | 30 | public class Context : ScenarioContext 31 | { 32 | public bool GotMyEvent { get; set; } 33 | public bool GotMyOtherEvent { get; set; } 34 | 35 | public bool GotEvents => GotMyEvent && GotMyOtherEvent; 36 | } 37 | 38 | public class Publisher : EndpointConfigurationBuilder 39 | { 40 | public Publisher() 41 | { 42 | EndpointSetup(); 43 | } 44 | } 45 | 46 | public class Subscriber : EndpointConfigurationBuilder 47 | { 48 | public Subscriber() 49 | { 50 | EndpointSetup(); 51 | } 52 | 53 | public class MyHandler : IHandleMessages 54 | { 55 | Context testContext; 56 | 57 | public MyHandler(Context testContext) 58 | { 59 | this.testContext = testContext; 60 | } 61 | 62 | public Task Handle(MyEvent message, IMessageHandlerContext context) 63 | { 64 | testContext.GotMyEvent = true; 65 | return Task.CompletedTask; 66 | } 67 | } 68 | 69 | public class MyEventOtherHandler : IHandleMessages 70 | { 71 | Context testContext; 72 | 73 | public MyEventOtherHandler(Context testContext) 74 | { 75 | this.testContext = testContext; 76 | } 77 | 78 | public Task Handle(MyEvent message, IMessageHandlerContext context) 79 | { 80 | testContext.GotMyOtherEvent = true; 81 | return Task.CompletedTask; 82 | } 83 | } 84 | } 85 | 86 | public class MyEvent : IEvent 87 | { 88 | } 89 | 90 | public class MyOtherEvent : IEvent 91 | { 92 | } 93 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Publishing/When_publishing_not_wrapped_event.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Publishing; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using EndpointTemplates; 7 | using NUnit.Framework; 8 | 9 | public class When_publishing_not_wrapped_event : NServiceBusAcceptanceTest 10 | { 11 | public static object[] Payload = 12 | { 13 | new object[] { new byte[4] }, 14 | new object[] { new byte[500 * 1024] } 15 | }; 16 | 17 | [Test] 18 | [TestCaseSource(nameof(Payload))] 19 | public async Task Should_receive_event(byte[] payload) 20 | { 21 | var context = await Scenario.Define() 22 | .WithEndpoint(b => b.When(c => c.EndpointsStarted, session => session.Publish(new MyEventWithPayload() { Payload = payload }))) 23 | .WithEndpoint(b => { }) 24 | .Done(c => c.GotTheEvent) 25 | .Run(TimeSpan.FromSeconds(30)); 26 | 27 | Assert.Multiple(() => 28 | { 29 | Assert.That(context.GotTheEvent, Is.True); 30 | Assert.That(context.ReceivedPayload, Is.EqualTo(payload), "The payload should be handled correctly"); 31 | }); 32 | } 33 | 34 | public class Context : ScenarioContext 35 | { 36 | public bool GotTheEvent { get; set; } 37 | public byte[] ReceivedPayload { get; set; } 38 | } 39 | 40 | public class Publisher : EndpointConfigurationBuilder 41 | { 42 | public Publisher() 43 | { 44 | EndpointSetup(c => 45 | { 46 | c.ConfigureSqsTransport().DoNotWrapOutgoingMessages = true; 47 | }).IncludeType(); 48 | } 49 | } 50 | 51 | public class Subscriber : EndpointConfigurationBuilder 52 | { 53 | public Subscriber() 54 | { 55 | EndpointSetup(c => { }); 56 | } 57 | 58 | public class MyHandler : IHandleMessages 59 | { 60 | readonly Context testContext; 61 | 62 | public MyHandler(Context testContext) => this.testContext = testContext; 63 | 64 | public Task Handle(MyEventWithPayload @event, IMessageHandlerContext context) 65 | { 66 | testContext.GotTheEvent = true; 67 | testContext.ReceivedPayload = @event.Payload; 68 | return Task.CompletedTask; 69 | } 70 | } 71 | } 72 | 73 | public class MyEventWithPayload : IEvent 74 | { 75 | public byte[] Payload { get; set; } 76 | } 77 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Publishing/When_publishing_wrapped_event.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Publishing; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using EndpointTemplates; 7 | using NUnit.Framework; 8 | 9 | public class When_publishing_wrapped_event : NServiceBusAcceptanceTest 10 | { 11 | public static object[] Payload = 12 | { 13 | new object[] { new byte[4] }, 14 | new object[] { new byte[500 * 1024] } 15 | }; 16 | 17 | [Test] 18 | [TestCaseSource(nameof(Payload))] 19 | public async Task Should_receive_event(byte[] payload) 20 | { 21 | var context = await Scenario.Define() 22 | .WithEndpoint(b => b.When(c => c.EndpointsStarted, session => session.Publish(new MyEventWithPayload() { Payload = payload }))) 23 | .WithEndpoint(b => { }) 24 | .Done(c => c.GotTheEvent) 25 | .Run(TimeSpan.FromSeconds(30)); 26 | 27 | Assert.Multiple(() => 28 | { 29 | Assert.That(context.GotTheEvent, Is.True); 30 | Assert.That(context.ReceivedPayload, Is.EqualTo(payload), "The payload should be handled correctly"); 31 | }); 32 | } 33 | 34 | public class Context : ScenarioContext 35 | { 36 | public bool GotTheEvent { get; set; } 37 | public byte[] ReceivedPayload { get; set; } 38 | } 39 | 40 | public class Publisher : EndpointConfigurationBuilder 41 | { 42 | public Publisher() 43 | { 44 | EndpointSetup(c => { }).IncludeType(); 45 | } 46 | } 47 | 48 | public class Subscriber : EndpointConfigurationBuilder 49 | { 50 | public Subscriber() 51 | { 52 | EndpointSetup(c => { }); 53 | } 54 | 55 | public class MyHandler : IHandleMessages 56 | { 57 | readonly Context testContext; 58 | 59 | public MyHandler(Context testContext) => this.testContext = testContext; 60 | 61 | public Task Handle(MyEventWithPayload @event, IMessageHandlerContext context) 62 | { 63 | testContext.GotTheEvent = true; 64 | testContext.ReceivedPayload = @event.Payload; 65 | return Task.CompletedTask; 66 | } 67 | } 68 | } 69 | 70 | public class MyEventWithPayload : IEvent 71 | { 72 | public byte[] Payload { get; set; } 73 | } 74 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Receiving/When_renewing_visibility.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Receiving; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using EndpointTemplates; 7 | using NUnit.Framework; 8 | 9 | public class When_renewing_visibility : NServiceBusAcceptanceTest 10 | { 11 | [Test] 12 | public async Task Should_allow_handler_to_process() 13 | { 14 | var context = await Scenario.Define() 15 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessage()))) 16 | .Done(c => c.Handled) 17 | .Run(); 18 | 19 | Assert.That(context.Handled, Is.True, "The message visibility timeout was not properly extended"); 20 | } 21 | 22 | public class Context : ScenarioContext 23 | { 24 | public bool Handled { get; set; } 25 | } 26 | 27 | public class Receiver : EndpointConfigurationBuilder 28 | { 29 | public Receiver() => 30 | EndpointSetup(config => 31 | { 32 | var transport = config.ConfigureSqsTransport(); 33 | transport.MessageVisibilityTimeout = TimeSpan.FromSeconds(1); 34 | }); 35 | 36 | public class MyMessageHandler(Context testContext) : IHandleMessages 37 | { 38 | public async Task Handle(MyMessage message, IMessageHandlerContext context) 39 | { 40 | await Task.Delay(TimeSpan.FromSeconds(2), context.CancellationToken); 41 | testContext.Handled = true; 42 | } 43 | } 44 | } 45 | 46 | public class MyMessage : ICommand; 47 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Receiving/When_renewing_visibility_failed.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Receiving; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using AcceptanceTesting.Customization; 7 | using Amazon.SQS.Model; 8 | using EndpointTemplates; 9 | using NUnit.Framework; 10 | 11 | public class When_renewing_visibility_failed : NServiceBusAcceptanceTest 12 | { 13 | [Test] 14 | public async Task Should_indicate_cancellation_to_handler() 15 | { 16 | var context = await Scenario.Define() 17 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessage()))) 18 | .Done(c => c.Cancelled) 19 | .Run(); 20 | 21 | Assert.That(context.Cancelled, Is.True, "The cancellation was not properly propagated to the handler"); 22 | } 23 | 24 | public class Context : ScenarioContext 25 | { 26 | public bool Cancelled { get; set; } 27 | } 28 | 29 | public class Receiver : EndpointConfigurationBuilder 30 | { 31 | public Receiver() => 32 | EndpointSetup(config => 33 | { 34 | config.LimitMessageProcessingConcurrencyTo(1); 35 | var transport = config.ConfigureSqsTransport(); 36 | // This is a very low value to make sure the renewal loop triggers early 37 | transport.MessageVisibilityTimeout = TimeSpan.FromSeconds(2); 38 | }); 39 | 40 | public class MyMessageHandler(Context testContext) : IHandleMessages 41 | { 42 | public async Task Handle(MyMessage message, IMessageHandlerContext context) 43 | { 44 | using var sqsClient = DefaultClientFactories.SqsFactory(); 45 | var queueUrlResponse = await sqsClient.GetQueueUrlAsync(new GetQueueUrlRequest 46 | { 47 | QueueName = TestNameHelper.GetSqsQueueName(Conventions.EndpointNamingConvention(typeof(Receiver)), SetupFixture.NamePrefix) 48 | }).ConfigureAwait(false); 49 | 50 | Message originalNativeMessage = context.Extensions.Get(); 51 | // Stealing the message from the queue to simulate competing consumers. 52 | await sqsClient.DeleteMessageAsync(queueUrlResponse.QueueUrl, originalNativeMessage.ReceiptHandle).ConfigureAwait(false); 53 | 54 | try 55 | { 56 | await Task.Delay(TimeSpan.FromSeconds(60), context.CancellationToken); 57 | } 58 | catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) 59 | { 60 | testContext.Cancelled = true; 61 | } 62 | } 63 | } 64 | } 65 | 66 | public class MyMessage : ICommand; 67 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_sending_not_wrapped_message.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using AcceptanceTesting.Customization; 6 | using EndpointTemplates; 7 | using NUnit.Framework; 8 | 9 | public class When_sending_not_wrapped_message : NServiceBusAcceptanceTest 10 | { 11 | public static object[] Payload = 12 | { 13 | new object[] { new byte[4] }, 14 | new object[] { new byte[500 * 1024] } 15 | }; 16 | 17 | [Test] 18 | [TestCaseSource(nameof(Payload))] 19 | public async Task Should_receive_message(byte[] payload) 20 | { 21 | var context = await Scenario.Define() 22 | .WithEndpoint(b => b.When(session => session.Send(new MyMessageWithPayload() { Payload = payload }))) 23 | .WithEndpoint() 24 | .Done(c => c.Received) 25 | .Run(); 26 | 27 | Assert.Multiple(() => 28 | { 29 | Assert.That(context.Received, Is.True); 30 | Assert.That(context.ReceivedPayload, Is.EqualTo(payload), "The payload should be handled correctly"); 31 | }); 32 | } 33 | 34 | public class Context : ScenarioContext 35 | { 36 | public byte[] ReceivedPayload { get; set; } 37 | public bool Received { get; set; } 38 | } 39 | 40 | public class Sender : EndpointConfigurationBuilder 41 | { 42 | public Sender() => 43 | EndpointSetup(builder => 44 | { 45 | builder.ConfigureRouting().RouteToEndpoint(typeof(MyMessageWithPayload), typeof(Receiver)); 46 | builder.ConfigureSqsTransport().DoNotWrapOutgoingMessages = true; 47 | }); 48 | 49 | public class Handler : IHandleMessages 50 | { 51 | public Handler(Context testContext) 52 | => this.testContext = testContext; 53 | 54 | public Task Handle(Reply message, IMessageHandlerContext context) 55 | { 56 | testContext.Received = true; 57 | 58 | return Task.CompletedTask; 59 | } 60 | 61 | readonly Context testContext; 62 | } 63 | } 64 | 65 | public class Receiver : EndpointConfigurationBuilder 66 | { 67 | public Receiver() => EndpointSetup(); 68 | 69 | public class MyMessageHandler : IHandleMessages 70 | { 71 | public MyMessageHandler(Context testContext) 72 | => this.testContext = testContext; 73 | 74 | public Task Handle(MyMessageWithPayload message, IMessageHandlerContext context) 75 | { 76 | testContext.ReceivedPayload = message.Payload; 77 | return context.Reply(new Reply()); 78 | } 79 | 80 | readonly Context testContext; 81 | } 82 | 83 | } 84 | 85 | public class MyMessageWithPayload : ICommand 86 | { 87 | public byte[] Payload { get; set; } 88 | } 89 | 90 | public class Reply : IMessage 91 | { 92 | } 93 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_trying_to_send_message_to_nonexisting_queue.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System; 4 | using System.Net; 5 | using AcceptanceTesting; 6 | using Amazon.Runtime; 7 | using Amazon.SQS.Model; 8 | using EndpointTemplates; 9 | using NUnit.Framework; 10 | 11 | public class When_trying_to_send_message_to_nonexisting_queue : NServiceBusAcceptanceTest 12 | { 13 | [Test] 14 | public void Should_throw_exception_with_queue_name() 15 | { 16 | var destination = "myfakequeue"; 17 | var messageId = Guid.NewGuid(); 18 | 19 | var exception = Assert.ThrowsAsync(async () => 20 | { 21 | await Scenario.Define(c => 22 | { 23 | c.MessageId = messageId; 24 | }) 25 | .WithEndpoint(b => b 26 | .When(session => session.Send(destination, new MyMessage()))) 27 | .Done(context => true) 28 | .Run(); 29 | }); 30 | 31 | Assert.Multiple(() => 32 | { 33 | Assert.That(exception.Message, Does.Contain(destination)); 34 | Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); 35 | Assert.That(exception.ErrorType, Is.EqualTo(ErrorType.Sender)); 36 | Assert.That(exception.ErrorCode, Is.EqualTo("AWS.SimpleQueueService.NonExistentQueue")); 37 | Assert.That(exception.RequestId, Is.Not.Null.Or.Empty); 38 | }); 39 | } 40 | 41 | public class Context : ScenarioContext 42 | { 43 | public Guid MessageId { get; set; } 44 | } 45 | 46 | public class Endpoint : EndpointConfigurationBuilder 47 | { 48 | public Endpoint() => 49 | EndpointSetup(); 50 | } 51 | 52 | public class MyMessage : ICommand 53 | { 54 | } 55 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_using_large_message_with_kms_encrypted_bucket.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | using Transport.SQS.Tests; 8 | 9 | public class When_using_large_message_with_kms_encrypted_bucket : NServiceBusAcceptanceTest 10 | { 11 | [Test] 12 | public async Task Should_receive_message() 13 | { 14 | var payloadToSend = new byte[PayloadSize]; 15 | 16 | var context = await Scenario.Define() 17 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessageWithLargePayload 18 | { 19 | Payload = payloadToSend 20 | }))) 21 | .Done(c => c.ReceivedPayload != null) 22 | .Run(); 23 | 24 | Assert.That(context.ReceivedPayload, Is.EqualTo(payloadToSend), "The large payload should be handled correctly using the kms encrypted S3 bucket"); 25 | 26 | using var s3Client = ClientFactories.CreateS3Client(); 27 | 28 | Assert.DoesNotThrowAsync(async () => await s3Client.GetObjectAsync(BucketName, $"{ConfigureEndpointSqsTransport.S3Prefix}/{context.MessageId}")); 29 | } 30 | 31 | const int PayloadSize = 150 * 1024; 32 | 33 | static string BucketName; 34 | 35 | public class Context : ScenarioContext 36 | { 37 | public byte[] ReceivedPayload { get; set; } 38 | public string MessageId { get; set; } 39 | } 40 | 41 | public class Endpoint : EndpointConfigurationBuilder 42 | { 43 | public Endpoint() => 44 | EndpointSetup(c => 45 | { 46 | var transportConfig = c.ConfigureSqsTransport(); 47 | 48 | BucketName = $"{ConfigureEndpointSqsTransport.S3BucketName}.kms"; 49 | 50 | transportConfig.S3 = new S3Settings(BucketName, ConfigureEndpointSqsTransport.S3Prefix, ClientFactories.CreateS3Client()); 51 | }); 52 | 53 | public class MyMessageHandler : IHandleMessages 54 | { 55 | public MyMessageHandler(Context testContext) => this.testContext = testContext; 56 | 57 | public Task Handle(MyMessageWithLargePayload messageWithLargePayload, IMessageHandlerContext context) 58 | { 59 | testContext.MessageId = context.MessageId; 60 | testContext.ReceivedPayload = messageWithLargePayload.Payload; 61 | 62 | return Task.CompletedTask; 63 | } 64 | 65 | readonly Context testContext; 66 | } 67 | } 68 | 69 | public class MyMessageWithLargePayload : ICommand 70 | { 71 | public byte[] Payload { get; set; } 72 | } 73 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_using_large_message_with_serverside_aes.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using Amazon.S3; 6 | using EndpointTemplates; 7 | using NUnit.Framework; 8 | using Transport.SQS.Tests; 9 | 10 | public class When_using_large_message_with_serverside_aes : NServiceBusAcceptanceTest 11 | { 12 | [Test] 13 | public async Task Should_receive_message() 14 | { 15 | var payloadToSend = new byte[PayloadSize]; 16 | 17 | var context = await Scenario.Define() 18 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessageWithLargePayload 19 | { 20 | Payload = payloadToSend 21 | }))) 22 | .Done(c => c.ReceivedPayload != null) 23 | .Run(); 24 | 25 | Assert.That(context.ReceivedPayload, Is.EqualTo(payloadToSend), "The large payload should be handled correctly using the kms encrypted S3 bucket"); 26 | 27 | using var s3Client = ClientFactories.CreateS3Client(); 28 | var getObjectResponse = await s3Client.GetObjectAsync(BucketName, $"{ConfigureEndpointSqsTransport.S3Prefix}/{context.MessageId}"); 29 | 30 | Assert.Multiple(() => 31 | { 32 | Assert.That(getObjectResponse.ServerSideEncryptionMethod, Is.EqualTo(ServerSideEncryptionMethod.AES256)); 33 | Assert.That(getObjectResponse.ServerSideEncryptionCustomerMethod, Is.EqualTo(ServerSideEncryptionCustomerMethod.None)); 34 | }); 35 | } 36 | 37 | const int PayloadSize = 150 * 1024; 38 | 39 | static string BucketName; 40 | 41 | public class Context : ScenarioContext 42 | { 43 | public byte[] ReceivedPayload { get; set; } 44 | public string MessageId { get; set; } 45 | } 46 | 47 | public class Endpoint : EndpointConfigurationBuilder 48 | { 49 | public Endpoint() => 50 | EndpointSetup(c => 51 | { 52 | var transportConfig = c.ConfigureSqsTransport(); 53 | 54 | BucketName = $"{ConfigureEndpointSqsTransport.S3BucketName}"; 55 | 56 | transportConfig.S3 = new S3Settings(BucketName, ConfigureEndpointSqsTransport.S3Prefix, ClientFactories.CreateS3Client()) 57 | { 58 | Encryption = new S3EncryptionWithManagedKey(ServerSideEncryptionMethod.AES256) 59 | }; 60 | }); 61 | 62 | public class MyMessageHandler : IHandleMessages 63 | { 64 | public MyMessageHandler(Context testContext) => this.testContext = testContext; 65 | 66 | public Task Handle(MyMessageWithLargePayload messageWithLargePayload, IMessageHandlerContext context) 67 | { 68 | testContext.MessageId = context.MessageId; 69 | testContext.ReceivedPayload = messageWithLargePayload.Payload; 70 | 71 | return Task.CompletedTask; 72 | } 73 | 74 | readonly Context testContext; 75 | } 76 | } 77 | 78 | public class MyMessageWithLargePayload : ICommand 79 | { 80 | public byte[] Payload { get; set; } 81 | } 82 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_using_large_message_with_unencrypted_bucket.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | using Transport.SQS.Tests; 8 | 9 | public class When_using_large_message_with_unencrypted_bucket : NServiceBusAcceptanceTest 10 | { 11 | [Test] 12 | public async Task Should_receive_message() 13 | { 14 | var payloadToSend = new byte[PayloadSize]; 15 | 16 | var context = await Scenario.Define() 17 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessageWithLargePayload 18 | { 19 | Payload = payloadToSend 20 | }))) 21 | .Done(c => c.ReceivedPayload != null) 22 | .Run(); 23 | 24 | Assert.That(context.ReceivedPayload, Is.EqualTo(payloadToSend), "The large payload should be handled correctly using the unencrypted S3 bucket"); 25 | 26 | using var s3Client = ClientFactories.CreateS3Client(); 27 | 28 | Assert.DoesNotThrowAsync(async () => await s3Client.GetObjectAsync(ConfigureEndpointSqsTransport.S3BucketName, $"{ConfigureEndpointSqsTransport.S3Prefix}/{context.MessageId}")); 29 | } 30 | 31 | const int PayloadSize = 150 * 1024; 32 | 33 | public class Context : ScenarioContext 34 | { 35 | public byte[] ReceivedPayload { get; set; } 36 | public string MessageId { get; set; } 37 | } 38 | 39 | public class Endpoint : EndpointConfigurationBuilder 40 | { 41 | public Endpoint() => 42 | EndpointSetup(c => 43 | { 44 | c.ConfigureSqsTransport().S3 45 | = new S3Settings(ConfigureEndpointSqsTransport.S3BucketName, ConfigureEndpointSqsTransport.S3Prefix, ClientFactories.CreateS3Client()); 46 | }); 47 | 48 | public class MyMessageHandler : IHandleMessages 49 | { 50 | public MyMessageHandler(Context testContext) => this.testContext = testContext; 51 | 52 | public Task Handle(MyMessageWithLargePayload messageWithLargePayload, IMessageHandlerContext context) 53 | { 54 | testContext.MessageId = context.MessageId; 55 | testContext.ReceivedPayload = messageWithLargePayload.Payload; 56 | 57 | return Task.CompletedTask; 58 | } 59 | 60 | readonly Context testContext; 61 | } 62 | } 63 | 64 | public class MyMessageWithLargePayload : ICommand 65 | { 66 | public byte[] Payload { get; set; } 67 | } 68 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_using_oversized_message_without_bucket_configured.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | 8 | public class When_using_oversized_message_without_bucket_configured : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public void Should_fail() 12 | { 13 | var exception = Assert.ThrowsAsync(async () => 14 | { 15 | await Scenario.Define() 16 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessageWithLargePayload 17 | { 18 | Payload = new byte[PayloadSize] 19 | }))) 20 | .Done(c => false) 21 | .Run(); 22 | }); 23 | 24 | Assert.That(exception.Message, Is.EqualTo("Cannot send large message because no S3 bucket was configured. Add an S3 bucket name to your configuration.")); 25 | } 26 | 27 | const int PayloadSize = 150 * 1024; 28 | 29 | public class Context : ScenarioContext 30 | { 31 | } 32 | 33 | public class Endpoint : EndpointConfigurationBuilder 34 | { 35 | public Endpoint() => 36 | EndpointSetup(c => 37 | { 38 | c.ConfigureSqsTransport().S3 = null; //Disable S3 39 | }); 40 | } 41 | 42 | public class MyMessageWithLargePayload : ICommand 43 | { 44 | public byte[] Payload { get; set; } 45 | } 46 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/Sending/When_using_small_message_with_no_bucket_configured.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.Sending; 2 | 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using EndpointTemplates; 6 | using NUnit.Framework; 7 | 8 | public class When_using_small_message_with_no_bucket_configured : NServiceBusAcceptanceTest 9 | { 10 | [Test] 11 | public async Task Should_receive_message() 12 | { 13 | var payloadToSend = new byte[10]; 14 | var context = await Scenario.Define() 15 | .WithEndpoint(b => b.When(session => session.SendLocal(new MyMessage 16 | { 17 | Payload = payloadToSend 18 | }))) 19 | .Done(c => c.ReceivedPayload != null) 20 | .Run(); 21 | 22 | Assert.That(context.ReceivedPayload, Is.EqualTo(payloadToSend), "Payload should be received"); 23 | } 24 | 25 | public class Context : ScenarioContext 26 | { 27 | public byte[] ReceivedPayload { get; set; } 28 | } 29 | 30 | public class Endpoint : EndpointConfigurationBuilder 31 | { 32 | public Endpoint() => 33 | EndpointSetup(); 34 | 35 | public class MyMessageHandler(Context testContext) : IHandleMessages 36 | { 37 | public Task Handle(MyMessage messageWithLargePayload, IMessageHandlerContext context) 38 | { 39 | testContext.ReceivedPayload = messageWithLargePayload.Payload; 40 | 41 | return Task.CompletedTask; 42 | } 43 | } 44 | } 45 | 46 | public class MyMessage : ICommand 47 | { 48 | public byte[] Payload { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/SetupFixture.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests; 2 | 3 | using System; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using NUnit.Framework; 7 | using Transport.SQS.Tests; 8 | 9 | [SetUpFixture] 10 | public class SetupFixture 11 | { 12 | /// 13 | /// The name prefix for the current run of the test suite. 14 | /// 15 | public static string NamePrefix { get; private set; } 16 | 17 | public static void AppendToNamePrefix(string customization) => NamePrefix += customization; 18 | 19 | public static void RestoreNamePrefix(string namePrefixBackup) 20 | { 21 | if (!string.IsNullOrWhiteSpace(namePrefixBackup)) 22 | { 23 | NamePrefix = namePrefixBackup; 24 | } 25 | } 26 | 27 | [OneTimeSetUp] 28 | public void OneTimeSetUp() 29 | { 30 | // Generate a new name prefix for acceptance tests 31 | // every time the tests are run. 32 | // This is to work around an SQS limitation that prevents 33 | // us from deleting then creating a queue with the 34 | // same name in a 60 second period. 35 | NamePrefix = $"AT{Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "").ToUpperInvariant()}"; 36 | TestContext.Out.WriteLine($"Generated name prefix: '{NamePrefix}'"); 37 | } 38 | 39 | [OneTimeTearDown] 40 | public async Task OneTimeTearDown() 41 | { 42 | using var sqsClient = ClientFactories.CreateSqsClient(); 43 | using var snsClient = ClientFactories.CreateSnsClient(); 44 | using var s3Client = ClientFactories.CreateS3Client(); 45 | 46 | await Cleanup.DeleteAllResourcesWithPrefix(sqsClient, snsClient, s3Client, NamePrefix).ConfigureAwait(false); 47 | } 48 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.AcceptanceTests/TestSuiteConstraints.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests; 2 | 3 | using AcceptanceTesting.Support; 4 | 5 | public partial class TestSuiteConstraints 6 | { 7 | public bool SupportsCrossQueueTransactions => false; 8 | 9 | public bool SupportsDtc => false; 10 | 11 | public bool SupportsDelayedDelivery => true; 12 | 13 | public bool SupportsNativePubSub => true; 14 | 15 | public bool SupportsOutbox => false; 16 | 17 | public bool SupportsPurgeOnStartup => false; 18 | 19 | public IConfigureEndpointTestExecution CreateTransportConfiguration() 20 | { 21 | return new ConfigureEndpointSqsTransport(); 22 | } 23 | 24 | public IConfigureEndpointTestExecution CreatePersistenceConfiguration() 25 | { 26 | return new ConfigureEndpointAcceptanceTestingPersistence(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | dotnet_diagnostic.PS0003.severity = suggestion 6 | dotnet_diagnostic.PS0013.severity = suggestion 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | 9 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 10 | dotnet_diagnostic.NSB0002.severity = suggestion 11 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/APIApprovals.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Tests; 2 | 3 | using NServiceBus; 4 | using NUnit.Framework; 5 | using Particular.Approvals; 6 | using PublicApiGenerator; 7 | 8 | [TestFixture] 9 | public class APIApprovals 10 | { 11 | [Test] 12 | public void ApproveSqsTransport() 13 | { 14 | var publicApi = typeof(SqsTransport).Assembly.GeneratePublicApi(new ApiGeneratorOptions 15 | { 16 | ExcludeAttributes = ["System.Runtime.Versioning.TargetFrameworkAttribute", "System.Reflection.AssemblyMetadataAttribute"] 17 | }); 18 | Approver.Verify(publicApi); 19 | } 20 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/MessageDispatcherTests.Does_not_send_extra_properties_in_payload_by_default.approved.txt: -------------------------------------------------------------------------------- 1 | {"Body":"e30=","S3BodyKey":null,"Headers":{"NServiceBus.AmazonSQS.TimeToBeReceived":"10675199.01:48:05.4775807","NServiceBus.ReplyToAddress":"TestReplyToAddress","NServiceBus.MessageId":"093C17C6-D32E-44FE-9134-65C10C1287EB"}} -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/MessageDispatcherTests.Should_not_wrap_if_configured_not_to.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "DelaySeconds": 0, 3 | "MessageAttributes": { 4 | "NServiceBus.MessageId": { 5 | "BinaryListValues": null, 6 | "BinaryValue": null, 7 | "DataType": "String", 8 | "StringListValues": null, 9 | "StringValue": "1234" 10 | }, 11 | "NServiceBus.AmazonSQS.Headers": { 12 | "BinaryListValues": null, 13 | "BinaryValue": null, 14 | "DataType": "String", 15 | "StringListValues": null, 16 | "StringValue": "{\"NServiceBus.AmazonSQS.TimeToBeReceived\":\"10675199.01:48:05.4775807\",\"NServiceBus.ReplyToAddress\":\"TestReplyToAddress\",\"NServiceBus.MessageId\":\"74d4f8e4-0fc7-4f09-8d46-0b76994e76d6\"}" 17 | } 18 | }, 19 | "MessageBody": "my message body", 20 | "MessageDeduplicationId": null, 21 | "MessageGroupId": null, 22 | "MessageSystemAttributes": null, 23 | "QueueUrl": "address" 24 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.ExtractsPolicy_if_not_empty.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "CustomPolicy", 3 | "Version": "2012-10-17", 4 | "Statements": [] 5 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.ExtractsPolicy_returns_empty_policy_if_empty.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Id": null, 3 | "Version": "2012-10-17", 4 | "Statements": [] 5 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_account_condition_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": "arn:aws:sns:us-west-2:123456789012:*" 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_all_conditions_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:*", 15 | "arn:aws:sns:us-west-2:123456789012:DEV-*", 16 | "arn:aws:sns:us-west-2:123456789012:DEV-Sales-HighValueOrders-*", 17 | "arn:aws:sns:us-west-2:123456789012:DEV-Shipping-*" 18 | ] 19 | } 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_all_conditions_sets_full_policy_with_replaced_wildcard.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": "arn:aws:sns:us-west-2:123456789012:*" 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_empty_add_statements_doesnt_update_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [] 4 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_legacy_policy_does_migrate_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_legacy_policy_with_conditions_does_migrate_to_wildcard_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:*", 15 | "arn:aws:sns:us-west-2:123456789012:DEV-*", 16 | "arn:aws:sns:us-west-2:123456789012:DEV-NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-*" 17 | ] 18 | } 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_does_migrate_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_migration_leaves_unrelated_permissions.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": "arn:aws:sns:us-west-2:123456789012:UnrelatedEvent" 14 | } 15 | } 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Principal": { 20 | "AWS": "*" 21 | }, 22 | "Action": "sqs:SendMessage", 23 | "Resource": "arn:fakeQueue", 24 | "Condition": { 25 | "ArnLike": { 26 | "aws:SourceArn": [ 27 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 28 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 29 | ] 30 | } 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_with_condition_migration_leaves_unrelated_permissions.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": "arn:aws:sns:us-west-2:123456789012:DEV-UnrelatedEvent" 14 | } 15 | } 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Principal": { 20 | "AWS": "*" 21 | }, 22 | "Action": "sqs:SendMessage", 23 | "Resource": "arn:fakeQueue", 24 | "Condition": { 25 | "ArnLike": { 26 | "aws:SourceArn": [ 27 | "arn:aws:sns:us-west-2:123456789012:*", 28 | "arn:aws:sns:us-west-2:123456789012:DEV-*", 29 | "arn:aws:sns:us-west-2:123456789012:DEV-NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-*" 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_partial_legacy_policy_with_conditions_does_migrate_to_wildcard_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:*", 15 | "arn:aws:sns:us-west-2:123456789012:DEV-*", 16 | "arn:aws:sns:us-west-2:123456789012:DEV-NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-*" 17 | ] 18 | } 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_partial_policy_migration_leaves_unrelated_permissions.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": "arn:aws:sns:us-west-2:123456789012:UnrelatedEvent" 14 | } 15 | } 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Principal": { 20 | "AWS": "*" 21 | }, 22 | "Action": "sqs:SendMessage", 23 | "Resource": "arn:fakeQueue", 24 | "Condition": { 25 | "ArnLike": { 26 | "aws:SourceArn": [ 27 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 28 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event", 29 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-YetAnotherEvent" 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_policy_does_extend_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_policy_matching_but_different_order_doesnt_override_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event", 16 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-YetAnotherEvent", 17 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-YetYetAnotherEvent" 18 | ] 19 | } 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_policy_matching_doesnt_override_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_policy_switched_to_wildcard.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:*", 15 | "arn:aws:sns:us-west-2:123456789012:DEV-*", 16 | "arn:aws:sns:us-west-2:123456789012:DEV-NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-*" 17 | ] 18 | } 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_existing_wildcard_policy_switched_to_events_will_replace_wildcards.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_namespace_conditions_and_topic_name_prefix_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:DEV-Sales-HighValueOrders-*", 15 | "arn:aws:sns:us-west-2:123456789012:DEV-Shipping-*" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_namespace_conditions_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:Sales-HighValueOrders-*", 15 | "arn:aws:sns:us-west-2:123456789012:Shipping-*" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_nothing_to_subscribe_doesnt_override_existing_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/PolicyExtensionsTests.Update_with_topic_name_prefix_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "*" 8 | }, 9 | "Action": "sqs:SendMessage", 10 | "Resource": "arn:fakeQueue", 11 | "Condition": { 12 | "ArnLike": { 13 | "aws:SourceArn": "arn:aws:sns:us-west-2:123456789012:DEV-*" 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/SubscriptionManagerTests.SettlePolicy_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version" : "2012-10-17", 3 | "Statement" : [ 4 | { 5 | "Effect" : "Allow", 6 | "Principal" : { 7 | "AWS" : "*" 8 | }, 9 | "Action" : "sqs:SendMessage", 10 | "Resource" : "arn:fakeQueue", 11 | "Condition" : { 12 | "ArnLike" : { 13 | "aws:SourceArn" : [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event" 16 | ] 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/SubscriptionManagerTests.SettlePolicy_with_all_conditions_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version" : "2012-10-17", 3 | "Statement" : [ 4 | { 5 | "Effect" : "Allow", 6 | "Principal" : { 7 | "AWS" : "*" 8 | }, 9 | "Action" : "sqs:SendMessage", 10 | "Resource" : "arn:fakeQueue", 11 | "Condition" : { 12 | "ArnLike" : { 13 | "aws:SourceArn" : [ 14 | "arn:aws:sns:us-west-2:123456789012:*", 15 | "arn:aws:sns:us-west-2:123456789012:DEV-*", 16 | "arn:aws:sns:us-west-2:123456789012:DEV-Sales-HighValueOrders-*", 17 | "arn:aws:sns:us-west-2:123456789012:DEV-Shipping-*" 18 | ] 19 | } 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ApprovalFiles/SubscriptionManagerTests.Subscribe_after_settle_sets_full_policy.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Version" : "2012-10-17", 3 | "Statement" : [ 4 | { 5 | "Effect" : "Allow", 6 | "Principal" : { 7 | "AWS" : "*" 8 | }, 9 | "Action" : "sqs:SendMessage", 10 | "Resource" : "arn:fakeQueue", 11 | "Condition" : { 12 | "ArnLike" : { 13 | "aws:SourceArn" : [ 14 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-AnotherEvent", 15 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-Event", 16 | "arn:aws:sns:us-west-2:123456789012:NServiceBus-Transport-SQS-Tests-SubscriptionManagerTests-YetAnotherEvent" 17 | ] 18 | } 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/ClientFactories.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS.Tests; 4 | 5 | using System; 6 | using Amazon.Runtime; 7 | using Amazon.S3; 8 | using Amazon.SimpleNotificationService; 9 | using Amazon.SQS; 10 | 11 | public static class ClientFactories 12 | { 13 | public static IAmazonSQS CreateSqsClient(Action? configure = null) 14 | { 15 | var credentials = new EnvironmentVariablesAWSCredentials(); 16 | var config = new AmazonSQSConfig(); 17 | configure?.Invoke(config); 18 | return new AmazonSQSClient(credentials, config); 19 | } 20 | 21 | public static IAmazonSimpleNotificationService CreateSnsClient( 22 | Action? configure = null) 23 | { 24 | var credentials = new EnvironmentVariablesAWSCredentials(); 25 | var config = new AmazonSimpleNotificationServiceConfig(); 26 | configure?.Invoke(config); 27 | return new AmazonSimpleNotificationServiceClient(credentials, config); 28 | } 29 | 30 | public static IAmazonS3 CreateS3Client(Action? configure = null) 31 | { 32 | var credentials = new EnvironmentVariablesAWSCredentials(); 33 | var config = new AmazonS3Config(); 34 | configure?.Invoke(config); 35 | return new AmazonS3Client(credentials, config); 36 | } 37 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/NServiceBus.Transport.SQS.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | true 6 | ..\NServiceBusTests.snk 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/PolicyScrubber.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Tests; 2 | 3 | using System; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | public static class PolicyScrubber 8 | { 9 | public static string ScrubPolicy(string policyAsString) 10 | { 11 | var scrubbed = Regex.Replace(policyAsString, "\"Sid\": \"(.*)\",", string.Empty); 12 | return RemoveUnnecessaryWhiteSpace(scrubbed); 13 | } 14 | 15 | static string RemoveUnnecessaryWhiteSpace(string policyAsString) => 16 | string.Join(Environment.NewLine, policyAsString.Split([ 17 | Environment.NewLine 18 | ], StringSplitOptions.RemoveEmptyEntries) 19 | .Where(l => !string.IsNullOrWhiteSpace(l)) 20 | .Select(l => l.TrimEnd()) 21 | ); 22 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/QueueNameGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Tests; 2 | 3 | using System; 4 | using NUnit.Framework; 5 | 6 | [TestFixture] 7 | public class QueueNameGeneratorTests 8 | { 9 | [Test] 10 | public void Default_queue_name_generator_is_idempotent() 11 | { 12 | const string prefix = "Prefix"; 13 | const string destination = "Destination"; 14 | 15 | var once = QueueCache.GetSqsQueueName(destination, prefix); 16 | var twice = QueueCache.GetSqsQueueName(once, prefix); 17 | 18 | Assert.That(twice, Is.EqualTo(once), "Applying the default queue name generator twice should impact the outcome"); 19 | } 20 | 21 | [Test] 22 | public void Idempotent_custom_queue_name_generator_is_accepted() 23 | { 24 | var transport = new SqsTransport 25 | { 26 | QueueNameGenerator = IdempotentQueueNameGenerator 27 | }; 28 | var hostSettings = new HostSettings("name", "displayName", new StartupDiagnosticEntries(), (_, __, ___) => { }, false); 29 | 30 | 31 | Assert.DoesNotThrowAsync(async () => 32 | { 33 | await transport.Initialize(hostSettings, Array.Empty(), Array.Empty()); 34 | }, "A custom queue name generator that is idempotent should be accepted."); 35 | } 36 | 37 | [Test] 38 | public void Non_idempotent_custom_queue_name_generator_throws() 39 | { 40 | var transport = new SqsTransport 41 | { 42 | QueueNameGenerator = NonIdempotentQueueNameGenerator 43 | }; 44 | 45 | Assert.ThrowsAsync(async () => 46 | { 47 | await transport.Initialize(null, null, null); 48 | }, "A custom queue name generator that is not idempotent should throw an exception."); 49 | } 50 | 51 | static string IdempotentQueueNameGenerator(string destination, string prefix) 52 | { 53 | if (destination.StartsWith(prefix)) 54 | { 55 | return destination; 56 | } 57 | return $"{prefix}{destination}"; 58 | } 59 | 60 | static string NonIdempotentQueueNameGenerator(string destination, string prefix) 61 | { 62 | return $"{prefix}{destination}"; 63 | } 64 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/Receiving/TransportMessageExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Tests; 2 | 3 | using System; 4 | using System.Buffers; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Extensions; 8 | using NUnit.Framework; 9 | 10 | [TestFixture] 11 | public class TransportMessageExtensionsTests 12 | { 13 | readonly ArrayPool arrayPool = ArrayPool.Shared; 14 | byte[] bodyBuffer; 15 | 16 | [TearDown] 17 | public void TearDown() 18 | { 19 | if (bodyBuffer != null) 20 | { 21 | arrayPool.Return(bodyBuffer); 22 | } 23 | } 24 | 25 | [Test] 26 | public async Task Empty_body_is_received_ok() 27 | { 28 | var messageId = Guid.NewGuid().ToString(); 29 | var body = Array.Empty(); 30 | var outgoingMessage = new OutgoingMessage(messageId, [], body); 31 | 32 | var transportMessage = new TransportMessage(outgoingMessage, []); 33 | 34 | (var receivedBodyArray, bodyBuffer) = await transportMessage.RetrieveBody(messageId, null, arrayPool); 35 | var receivedBody = Encoding.UTF8.GetString(receivedBodyArray.ToArray()); 36 | 37 | Assert.That(body, Is.EqualTo(receivedBodyArray.ToArray()).AsCollection); 38 | Assert.That(receivedBody, Is.Null.Or.Empty); 39 | } 40 | 41 | [Test] 42 | public async Task Null_body_is_received_ok() 43 | { 44 | var messageId = Guid.NewGuid().ToString(); 45 | var outgoingMessage = new OutgoingMessage(messageId, [], null); 46 | 47 | var transportMessage = new TransportMessage(outgoingMessage, []); 48 | 49 | (var receivedBodyArray, bodyBuffer) = await transportMessage.RetrieveBody(messageId, null, arrayPool); 50 | var receivedBody = Encoding.UTF8.GetString(receivedBodyArray.ToArray()); 51 | 52 | Assert.That(receivedBody, Is.Null.Or.Empty); 53 | } 54 | 55 | [Test] 56 | public async Task Empty_message_string_body_is_received_as_empty() 57 | { 58 | var transportMessage = new TransportMessage 59 | { 60 | Body = "empty message", 61 | }; 62 | 63 | (var receivedBodyArray, bodyBuffer) = await transportMessage.RetrieveBody(Guid.NewGuid().ToString(), null, arrayPool); 64 | var receivedBody = Encoding.UTF8.GetString(receivedBodyArray.ToArray()); 65 | 66 | Assert.That(receivedBody, Is.Null.Or.Empty); 67 | } 68 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/Sending/SnsPreparedMessageTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Tests; 2 | 3 | using System; 4 | using System.IO; 5 | using Amazon.SimpleNotificationService.Model; 6 | using NServiceBus; 7 | using NUnit.Framework; 8 | using SQS; 9 | 10 | [TestFixture] 11 | public class SnsPreparedMessageTests 12 | { 13 | [Test] 14 | public void CalculateSize_BodyTakenIntoAccount() 15 | { 16 | var expectedSize = 10; 17 | 18 | var message = new SnsPreparedMessage 19 | { 20 | Body = new string('a', expectedSize) 21 | }; 22 | 23 | message.CalculateSize(); 24 | 25 | Assert.That(message.Size, Is.EqualTo(expectedSize)); 26 | } 27 | 28 | [Test] 29 | public void CalculateSize_BodyAndReservedBytesTakenIntoAccount() 30 | { 31 | var expectedSize = 15; 32 | 33 | var message = new SnsPreparedMessage 34 | { 35 | Body = new string('a', 10), 36 | ReserveBytesInMessageSizeCalculation = 5 37 | }; 38 | 39 | message.CalculateSize(); 40 | 41 | Assert.That(message.Size, Is.EqualTo(expectedSize)); 42 | } 43 | 44 | [Test] 45 | public void CalculateSize_TakesAttributesIntoAccount() 46 | { 47 | var message = new SnsPreparedMessage(); 48 | message.MessageAttributes.Add("Key1", new MessageAttributeValue { DataType = "string", StringValue = "SomeString" }); 49 | message.MessageAttributes.Add("Key3", new MessageAttributeValue { BinaryValue = new MemoryStream(new byte[1]) }); 50 | 51 | message.CalculateSize(); 52 | 53 | Assert.That(message.Size, Is.EqualTo(25)); 54 | } 55 | 56 | [Test] 57 | public void MessageId_SetAttribute() 58 | { 59 | var expectedMessageId = Guid.NewGuid().ToString(); 60 | 61 | var message = new SnsPreparedMessage 62 | { 63 | MessageId = expectedMessageId 64 | }; 65 | 66 | Assert.That(message.MessageAttributes[Headers.MessageId].StringValue, Is.EqualTo(expectedMessageId)); 67 | } 68 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.Tests/Sending/SqsPreparedMessageTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Tests; 2 | 3 | using System; 4 | using System.IO; 5 | using Amazon.SQS.Model; 6 | using NServiceBus; 7 | using NUnit.Framework; 8 | using SQS; 9 | 10 | [TestFixture] 11 | public class SqsPreparedMessageTests 12 | { 13 | [Test] 14 | public void CalculateSize_BodyTakenIntoAccount() 15 | { 16 | var expectedSize = 10; 17 | 18 | var message = new SqsPreparedMessage 19 | { 20 | Body = new string('a', expectedSize) 21 | }; 22 | 23 | message.CalculateSize(); 24 | 25 | Assert.That(message.Size, Is.EqualTo(expectedSize)); 26 | } 27 | 28 | [Test] 29 | public void CalculateSize_BodyAndReservedBytesTakenIntoAccount() 30 | { 31 | var expectedSize = 15; 32 | 33 | var message = new SqsPreparedMessage 34 | { 35 | Body = new string('a', 10), 36 | ReserveBytesInMessageSizeCalculation = 5 37 | }; 38 | 39 | message.CalculateSize(); 40 | 41 | Assert.That(message.Size, Is.EqualTo(expectedSize)); 42 | } 43 | 44 | [Test] 45 | public void CalculateSize_TakesAttributesIntoAccount() 46 | { 47 | var message = new SqsPreparedMessage(); 48 | message.MessageAttributes.Add("Key1", new MessageAttributeValue { DataType = "string", StringValue = "SomeString" }); 49 | message.MessageAttributes.Add("Key2", new MessageAttributeValue { StringListValues = ["SomeString"] }); 50 | message.MessageAttributes.Add("Key3", new MessageAttributeValue { BinaryValue = new MemoryStream(new byte[1]) }); 51 | message.MessageAttributes.Add("Key4", new MessageAttributeValue { BinaryListValues = [new MemoryStream(new byte[2])] }); 52 | 53 | message.CalculateSize(); 54 | 55 | Assert.That(message.Size, Is.EqualTo(45)); 56 | } 57 | 58 | [Test] 59 | public void MessageId_SetAttribute() 60 | { 61 | var expectedMessageId = Guid.NewGuid().ToString(); 62 | 63 | var message = new SqsPreparedMessage 64 | { 65 | MessageId = expectedMessageId 66 | }; 67 | 68 | Assert.That(message.MessageAttributes[Headers.MessageId].StringValue, Is.EqualTo(expectedMessageId)); 69 | } 70 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | dotnet_diagnostic.PS0003.severity = suggestion 6 | dotnet_diagnostic.PS0013.severity = suggestion 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | 9 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 10 | dotnet_diagnostic.NSB0002.severity = suggestion 11 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages/ConfigureSqsTransportInfrastructure.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NServiceBus; 4 | using NServiceBus.Transport; 5 | using NServiceBus.Transport.SQS.Tests; 6 | using NServiceBus.TransportTests; 7 | using TransportTests; 8 | 9 | public class ConfigureSqsTransportInfrastructure : IConfigureTransportInfrastructure 10 | { 11 | const string S3BucketEnvironmentVariableName = "NSERVICEBUS_AMAZONSQS_S3BUCKET"; 12 | public const string S3Prefix = "test"; 13 | public static string S3BucketName; 14 | 15 | public TransportDefinition CreateTransportDefinition() 16 | { 17 | var transport = new SqsTransport(ClientFactories.CreateSqsClient(), ClientFactories.CreateSnsClient()) 18 | { 19 | QueueNamePrefix = SetupFixture.GetNamePrefix(), 20 | TopicNamePrefix = SetupFixture.GetNamePrefix(), 21 | QueueNameGenerator = TestNameHelper.GetSqsQueueName, 22 | TopicNameGenerator = TestNameHelper.GetSnsTopicName, 23 | DoNotWrapOutgoingMessages = true 24 | }; 25 | 26 | S3BucketName = EnvironmentHelper.GetEnvironmentVariable(S3BucketEnvironmentVariableName); 27 | 28 | if (!string.IsNullOrEmpty(S3BucketName)) 29 | { 30 | transport.S3 = new S3Settings(S3BucketName, S3Prefix, ClientFactories.CreateS3Client()); 31 | } 32 | 33 | return transport; 34 | } 35 | 36 | public Task Configure(TransportDefinition transportDefinition, HostSettings hostSettings, QueueAddress inputQueueName, 37 | string errorQueueName, CancellationToken cancellationToken) => 38 | transportDefinition.Initialize(hostSettings, new[] 39 | { 40 | new ReceiveSettings(inputQueueName.ToString(), inputQueueName, false, false, errorQueueName), 41 | }, 42 | [ 43 | errorQueueName 44 | ], cancellationToken); 45 | 46 | public Task Cleanup(CancellationToken cancellationToken) => Task.CompletedTask; 47 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages/NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net10.0 6 | true 7 | ..\NServiceBusTests.snk 8 | TransportTests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests.DoNotWrapOutgoingMessages/Sending_messages_with_invalid_sqs_chars.cs: -------------------------------------------------------------------------------- 1 | namespace TransportTests 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using NServiceBus; 6 | using NServiceBus.Transport; 7 | using NServiceBus.TransportTests; 8 | using NUnit.Framework; 9 | 10 | public class Sending_messages_with_invalid_sqs_chars : NServiceBusTransportTest 11 | { 12 | [TestCase(TransportTransactionMode.None)] 13 | [TestCase(TransportTransactionMode.ReceiveOnly)] 14 | public async Task Should_receive_message( 15 | TransportTransactionMode transactionMode) 16 | { 17 | var messageProcessed = CreateTaskCompletionSource(); 18 | byte[] copyOfTheBody = null; 19 | 20 | await StartPump( 21 | (context, _) => 22 | { 23 | // This is crucial due to internal buffer pooling in SQS transport 24 | copyOfTheBody = context.Body.ToArray(); 25 | return messageProcessed.SetCompleted(context); 26 | }, 27 | (_, __) => Task.FromResult(ErrorHandleResult.Handled), 28 | TransportTransactionMode.None); 29 | 30 | var headers = new Dictionary 31 | { 32 | { "SomeHeader", "header value with invalid chars: \0" }, 33 | }; 34 | 35 | var body = "body with invalid chars: \0"u8.ToArray(); 36 | 37 | await SendMessage(InputQueueName, headers, body: body); 38 | 39 | var messageContext = await messageProcessed.Task; 40 | 41 | Assert.That(messageContext.Headers, Is.Not.Empty); 42 | Assert.Multiple(() => 43 | { 44 | Assert.That(messageContext.Headers, Is.SupersetOf(headers)); 45 | Assert.That(copyOfTheBody, Is.EquivalentTo(body)); 46 | }); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | dotnet_diagnostic.PS0003.severity = suggestion 6 | dotnet_diagnostic.PS0013.severity = suggestion 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | 9 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 10 | dotnet_diagnostic.NSB0002.severity = suggestion 11 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests/ConfigureSqsTransportInfrastructure.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NServiceBus; 4 | using NServiceBus.Transport; 5 | using NServiceBus.Transport.SQS.Tests; 6 | using NServiceBus.TransportTests; 7 | using TransportTests; 8 | 9 | public class ConfigureSqsTransportInfrastructure : IConfigureTransportInfrastructure 10 | { 11 | const string S3BucketEnvironmentVariableName = "NSERVICEBUS_AMAZONSQS_S3BUCKET"; 12 | public const string S3Prefix = "test"; 13 | public static string S3BucketName; 14 | 15 | public TransportDefinition CreateTransportDefinition() 16 | { 17 | var transport = new SqsTransport(ClientFactories.CreateSqsClient(), ClientFactories.CreateSnsClient()) 18 | { 19 | QueueNamePrefix = SetupFixture.GetNamePrefix(), 20 | TopicNamePrefix = SetupFixture.GetNamePrefix(), 21 | QueueNameGenerator = TestNameHelper.GetSqsQueueName, 22 | TopicNameGenerator = TestNameHelper.GetSnsTopicName 23 | }; 24 | 25 | S3BucketName = EnvironmentHelper.GetEnvironmentVariable(S3BucketEnvironmentVariableName); 26 | 27 | if (!string.IsNullOrEmpty(S3BucketName)) 28 | { 29 | transport.S3 = new S3Settings(S3BucketName, S3Prefix, ClientFactories.CreateS3Client()); 30 | } 31 | 32 | return transport; 33 | } 34 | 35 | public Task Configure(TransportDefinition transportDefinition, HostSettings hostSettings, QueueAddress inputQueueName, 36 | string errorQueueName, CancellationToken cancellationToken) => 37 | transportDefinition.Initialize(hostSettings, new[] 38 | { 39 | new ReceiveSettings(inputQueueName.ToString(), inputQueueName, false, false, errorQueueName), 40 | }, 41 | [ 42 | errorQueueName 43 | ], cancellationToken); 44 | 45 | public Task Cleanup(CancellationToken cancellationToken) => Task.CompletedTask; 46 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests/EnvironmentHelper.cs: -------------------------------------------------------------------------------- 1 | namespace TransportTests; 2 | 3 | using System; 4 | 5 | class EnvironmentHelper 6 | { 7 | public static string GetEnvironmentVariable(string variable) 8 | { 9 | string candidate = Environment.GetEnvironmentVariable(variable, EnvironmentVariableTarget.User); 10 | 11 | if (string.IsNullOrWhiteSpace(candidate)) 12 | { 13 | return Environment.GetEnvironmentVariable(variable); 14 | } 15 | 16 | return candidate; 17 | } 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests/NServiceBus.Transport.SQS.TransportTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | true 6 | ..\NServiceBusTests.snk 7 | TransportTests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests/Sending_to_non_existing_queues.cs: -------------------------------------------------------------------------------- 1 | namespace TransportTests; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using NServiceBus; 6 | using NServiceBus.Transport; 7 | using NServiceBus.TransportTests; 8 | using NUnit.Framework; 9 | 10 | public class Sending_to_non_existing_queues : NServiceBusTransportTest 11 | { 12 | [TestCase(TransportTransactionMode.None)] 13 | [TestCase(TransportTransactionMode.ReceiveOnly)] 14 | public async Task Should_include_queue_name_in_exception_details(TransportTransactionMode transactionMode) 15 | { 16 | await StartPump((_, __) => Task.CompletedTask, 17 | (_, __) => Task.FromResult(ErrorHandleResult.Handled), 18 | transactionMode); 19 | 20 | string nonExistingQueueName = "some-non-existing-queue"; 21 | 22 | Exception exception = Assert.CatchAsync(async () => await SendMessage(nonExistingQueueName)); 23 | 24 | Assert.That(exception!.ToString(), Does.Contain(nonExistingQueueName)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS.TransportTests/SetupFixture.cs: -------------------------------------------------------------------------------- 1 | namespace TransportTests; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Amazon.S3; 6 | using Amazon.SimpleNotificationService; 7 | using Amazon.SQS; 8 | using NServiceBus.Transport.SQS.Tests; 9 | using NUnit.Framework; 10 | 11 | [SetUpFixture] 12 | public class SetupFixture 13 | { 14 | /// 15 | /// The name prefix for the current run of the test suite. 16 | /// 17 | static string NamePrefix { get; set; } 18 | 19 | public static string GetNamePrefix() 20 | { 21 | TestContext ctx = TestContext.CurrentContext; 22 | int methodHashPositive = Math.Abs(ctx.Test.ID.GetHashCode()); 23 | 24 | return NamePrefix + methodHashPositive; 25 | } 26 | 27 | [OneTimeSetUp] 28 | public void OneTimeSetUp() => 29 | // Generate a new queue name prefix for acceptance tests 30 | // every time the tests are run. 31 | // This is to work around an SQS limitation that prevents 32 | // us from deleting then creating a queue with the 33 | // same name in a 60 second period. 34 | NamePrefix = $"TT{DateTime.UtcNow:yyyyMMddHHmmss}"; 35 | 36 | [OneTimeTearDown] 37 | public async Task OneTimeTearDown() 38 | { 39 | using IAmazonSQS sqsClient = ClientFactories.CreateSqsClient(); 40 | using IAmazonSimpleNotificationService snsClient = ClientFactories.CreateSnsClient(); 41 | using IAmazonS3 s3Client = ClientFactories.CreateS3Client(); 42 | 43 | await Cleanup.DeleteAllResourcesWithPrefix(sqsClient, snsClient, s3Client, NamePrefix).ConfigureAwait(false); 44 | } 45 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/AsyncCacheLazy.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | namespace NServiceBus.Transport.SQS; 3 | 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | // Dedicated Lazy wrapper that is not general purpose but written for the caching use cases in 10 | // the topic and subscription cache 11 | sealed class AsyncCacheLazy : Lazy> 12 | { 13 | // Uses Task.Run here to dispatch the value factory on the worker thread pool to make sure the value factory 14 | // is never run under the context of the thread accessing .Value since the caches are used in the dispatcher 15 | // which could be used in an environment with a UI thread. Given topics and subscriptions have a cache ttl 16 | // and we have a finite number of items to cache double dispatching for safety reasons should be OK and is 17 | // hopefully better than trying to pretend the value factory might not capture context. 18 | #pragma warning disable PS0013 19 | public AsyncCacheLazy(Func> valueFactory) : base(() => Task.Run(() => valueFactory()), LazyThreadSafetyMode.ExecutionAndPublication) 20 | #pragma warning restore PS0013 21 | { 22 | } 23 | 24 | public new bool IsValueCreated => base.IsValueCreated && Value is { IsCompleted: true }; 25 | 26 | public TaskAwaiter GetAwaiter() => Value.GetAwaiter(); 27 | 28 | public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) 29 | => Value.ConfigureAwait(continueOnCapturedContext); 30 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/DefaultClientFactories.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus; 4 | 5 | using System; 6 | using Amazon.S3; 7 | using Amazon.SimpleNotificationService; 8 | using Amazon.SQS; 9 | 10 | static class DefaultClientFactories 11 | { 12 | public static Func SqsFactory = () => new AmazonSQSClient(new AmazonSQSConfig()); 13 | 14 | public static Func SnsFactory = () => 15 | new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig()); 16 | 17 | public static Func S3Factory = () => new AmazonS3Client(new AmazonS3Config()); 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/DiagnosticDescriptors.cs: -------------------------------------------------------------------------------- 1 | static class DiagnosticDescriptors 2 | { 3 | // https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/choosing-diagnostic-ids 4 | // This diagnostic ID was introduced in 7.1 and was used to mark a constructor overload as experimental. 5 | // Subsequent releases made the constructor overload officially supported and removed the experimental attribute. 6 | // Given this ID was already used it shouldn't be reused for a different diagnostic since that might constitute 7 | // a source breaking change. 8 | public const string ExperimentalDisableDelayedDelivery = "NSBSQSEXP0001"; 9 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/EventToEventsMappings.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Configure; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | class EventToEventsMappings 8 | { 9 | public void Add(Type subscribedEventType, Type publishedEventType) 10 | { 11 | if (!eventsToEventsMappings.TryGetValue(subscribedEventType, out var mapping)) 12 | { 13 | mapping = []; 14 | eventsToEventsMappings.Add(subscribedEventType, mapping); 15 | } 16 | 17 | mapping.Add(publishedEventType); 18 | } 19 | 20 | public IEnumerable GetMappedTypes(Type eventType) 21 | { 22 | return eventsToEventsMappings.ContainsKey(eventType) ? eventsToEventsMappings[eventType] : Enumerable.Empty(); 23 | } 24 | 25 | Dictionary> eventsToEventsMappings = []; 26 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/EventToTopicsMappings.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Configure; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | class EventToTopicsMappings 8 | { 9 | public void Add(Type subscribedEventType, IEnumerable topicsNames) 10 | { 11 | if (!eventsToTopicsMappings.TryGetValue(subscribedEventType, out var mapping)) 12 | { 13 | mapping = []; 14 | eventsToTopicsMappings.Add(subscribedEventType, mapping); 15 | } 16 | 17 | foreach (var topicName in topicsNames) 18 | { 19 | mapping.Add(topicName); 20 | } 21 | } 22 | 23 | public IEnumerable GetMappedTopicsNames(Type subscribedEventType) 24 | { 25 | return eventsToTopicsMappings.ContainsKey(subscribedEventType) ? eventsToTopicsMappings[subscribedEventType] : Enumerable.Empty(); 26 | } 27 | 28 | Dictionary> eventsToTopicsMappings = []; 29 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/NullEncryption.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using Amazon.S3.Model; 4 | 5 | class NullEncryption : S3EncryptionMethod 6 | { 7 | public static readonly NullEncryption Instance = new NullEncryption(); 8 | 9 | protected internal override void ModifyGetRequest(GetObjectRequest get) 10 | { 11 | } 12 | 13 | protected internal override void ModifyPutRequest(PutObjectRequest put) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/S3EncryptionMethod.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using Amazon.S3.Model; 4 | 5 | /// 6 | /// S3 encryption settings. 7 | /// 8 | public abstract class S3EncryptionMethod 9 | { 10 | /// 11 | /// Modifies the get request to retrieve the message body from S3. 12 | /// 13 | protected internal abstract void ModifyGetRequest(GetObjectRequest get); 14 | 15 | /// 16 | /// Modifies the put request to upload the message body to S3. 17 | /// 18 | protected internal abstract void ModifyPutRequest(PutObjectRequest put); 19 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/S3EncryptionWithCustomerProvidedKey.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using Amazon.S3; 4 | using Amazon.S3.Model; 5 | 6 | /// 7 | /// S3 customer-provided key encryption. 8 | /// 9 | public sealed class S3EncryptionWithCustomerProvidedKey : S3EncryptionMethod 10 | { 11 | /// 12 | /// Creates new S3 encryption settings. 13 | /// 14 | /// Encryption method. 15 | /// Encryption key. 16 | /// Encryption key MD5 checksum. 17 | public S3EncryptionWithCustomerProvidedKey(ServerSideEncryptionCustomerMethod method, string key, string keyMd5 = null) 18 | { 19 | Method = method; 20 | Key = key; 21 | KeyMD5 = keyMd5; 22 | } 23 | 24 | /// 25 | /// Encryption method. 26 | /// 27 | public ServerSideEncryptionCustomerMethod Method { get; } 28 | 29 | /// 30 | /// Encryption key. 31 | /// 32 | public string Key { get; } 33 | 34 | /// 35 | /// Encryption key MD5 checksum. 36 | /// 37 | public string KeyMD5 { get; } 38 | 39 | /// 40 | /// Modifies the get request to retrieve the message body from S3. 41 | /// 42 | protected internal override void ModifyGetRequest(GetObjectRequest get) 43 | { 44 | get.ServerSideEncryptionCustomerMethod = Method; 45 | get.ServerSideEncryptionCustomerProvidedKey = Key; 46 | 47 | if (!string.IsNullOrEmpty(KeyMD5)) 48 | { 49 | get.ServerSideEncryptionCustomerProvidedKeyMD5 = KeyMD5; 50 | } 51 | } 52 | 53 | /// 54 | /// Modifies the put request to upload the message body to S3. 55 | /// 56 | protected internal override void ModifyPutRequest(PutObjectRequest put) 57 | { 58 | put.ServerSideEncryptionCustomerMethod = Method; 59 | put.ServerSideEncryptionCustomerProvidedKey = Key; 60 | 61 | if (!string.IsNullOrEmpty(KeyMD5)) 62 | { 63 | put.ServerSideEncryptionCustomerProvidedKeyMD5 = KeyMD5; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/S3EncryptionWithManagedKey.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using Amazon.S3; 4 | using Amazon.S3.Model; 5 | 6 | /// 7 | /// S3 managed key encryption. 8 | /// 9 | public sealed class S3EncryptionWithManagedKey : S3EncryptionMethod 10 | { 11 | /// 12 | /// Creates new S3 encryption settings. 13 | /// 14 | /// Encryption method. 15 | /// Encryption key id. 16 | public S3EncryptionWithManagedKey(ServerSideEncryptionMethod method, string keyId = null) 17 | { 18 | Method = method; 19 | KeyId = keyId; 20 | } 21 | 22 | /// 23 | /// Encryption method. 24 | /// 25 | public ServerSideEncryptionMethod Method { get; } 26 | 27 | /// 28 | /// Encryption key ID. 29 | /// 30 | public string KeyId { get; } 31 | 32 | /// 33 | /// Modifies the get request to retrieve the message body from S3. 34 | /// 35 | protected internal override void ModifyGetRequest(GetObjectRequest get) 36 | { 37 | } 38 | 39 | /// 40 | /// Modifies the put request to upload the message body to S3. 41 | /// 42 | protected internal override void ModifyPutRequest(PutObjectRequest put) 43 | { 44 | put.ServerSideEncryptionMethod = Method; 45 | 46 | if (!string.IsNullOrEmpty(KeyId)) 47 | { 48 | put.ServerSideEncryptionKeyManagementServiceKeyId = KeyId; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/SettingsKeys.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Configure; 2 | 3 | static class SettingsKeys 4 | { 5 | const string Prefix = "NServiceBus.AmazonSQS."; 6 | 7 | public const string SubscriptionsCacheTTL = Prefix + nameof(SubscriptionsCacheTTL); 8 | public const string NotFoundTopicsCacheTTL = Prefix + nameof(NotFoundTopicsCacheTTL); 9 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Configure/SqsSubscriptionMigrationModeSettings.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Configure; 2 | 3 | using System; 4 | using Settings; 5 | 6 | /// 7 | /// Publish-subscribe migration mode configuration. 8 | /// 9 | [PreObsolete("https://github.com/Particular/NServiceBus/issues/6471", 10 | Note = "Hybrid pub/sub support cannot be obsolete until there is a viable migration path to native pub/sub", 11 | Message = "Hybrid pub/sub is no longer supported, use native pub/sub instead")] 12 | public partial class SqsSubscriptionMigrationModeSettings : SubscriptionMigrationModeSettings 13 | { 14 | SettingsHolder settings; 15 | 16 | internal SqsSubscriptionMigrationModeSettings(SettingsHolder settings) : base(settings) => this.settings = settings; 17 | 18 | /// 19 | /// Overrides the default value of 5 seconds for SNS topic cache. 20 | /// 21 | /// Topic cache TTL. 22 | public SqsSubscriptionMigrationModeSettings TopicCacheTTL(TimeSpan ttl) 23 | { 24 | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(ttl, TimeSpan.Zero); 25 | 26 | settings.Set(SettingsKeys.NotFoundTopicsCacheTTL, ttl); 27 | 28 | return this; 29 | } 30 | 31 | /// 32 | /// Overrides the default value of 5 seconds for SNS topic subscribers cache. 33 | /// 34 | /// Subscription cache TTL. 35 | public SubscriptionMigrationModeSettings SubscriptionsCacheTTL(TimeSpan ttl) 36 | { 37 | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(ttl, TimeSpan.Zero); 38 | 39 | settings.Set(SettingsKeys.SubscriptionsCacheTTL, ttl); 40 | 41 | return this; 42 | } 43 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.Net; 5 | using System.Threading; 6 | using Amazon.Runtime; 7 | using Amazon.SQS; 8 | 9 | static class ExceptionExtensions 10 | { 11 | #pragma warning disable PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional 12 | public static bool IsCausedBy(this Exception ex, CancellationToken cancellationToken) => 13 | ex is OperationCanceledException && cancellationToken.IsCancellationRequested; 14 | #pragma warning restore PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional 15 | 16 | public static bool IsCausedByMessageVisibilityExpiry(this AmazonSQSException exception) => 17 | exception.ErrorCode == "InvalidParameterValue" && exception.ErrorType == ErrorType.Sender && 18 | exception.StatusCode == HttpStatusCode.BadRequest; 19 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Extensions/AmazonServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Extensions; 2 | 3 | using System; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Amazon.Runtime; 8 | 9 | static class AmazonServiceExtensions 10 | { 11 | public static async Task RetryConflictsAsync(this IAmazonService client, Func> a, Action onRetry, CancellationToken cancellationToken = default) 12 | { 13 | _ = client; 14 | 15 | var tryCount = 0; 16 | var sleepTimeMs = 2000; 17 | const int maxTryCount = 5; 18 | 19 | while (true) 20 | { 21 | cancellationToken.ThrowIfCancellationRequested(); 22 | 23 | try 24 | { 25 | tryCount++; 26 | return await a(cancellationToken).ConfigureAwait(false); 27 | } 28 | catch (AmazonServiceException ex) 29 | when (ex.StatusCode == HttpStatusCode.Conflict && 30 | ex.ErrorCode == "OperationAborted") 31 | { 32 | if (tryCount >= maxTryCount) 33 | { 34 | throw; 35 | } 36 | 37 | var sleepTime = sleepTimeMs * tryCount; 38 | onRetry(sleepTime); 39 | await Task.Delay(sleepTime, cancellationToken).ConfigureAwait(false); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Extensions/MessageExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS.Extensions; 4 | 5 | using System; 6 | using System.Globalization; 7 | using Amazon.SQS.Model; 8 | 9 | static class MessageExtensions 10 | { 11 | public static DateTimeOffset GetAdjustedDateTimeFromServerSetAttributes(this Message message, string attributeName, TimeSpan clockOffset) 12 | { 13 | var result = UnixEpoch.AddMilliseconds(long.Parse(message.Attributes[attributeName], NumberFormatInfo.InvariantInfo)); 14 | // Adjust for clock skew between this endpoint and aws. 15 | // https://aws.amazon.com/blogs/developer/clock-skew-correction/ 16 | return result + clockOffset; 17 | } 18 | 19 | static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); 20 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Extensions/SnsClientExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS.Extensions; 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Amazon.SimpleNotificationService; 7 | using Amazon.SimpleNotificationService.Model; 8 | 9 | static class SnsClientExtensions 10 | { 11 | public static Task FindMatchingSubscription(this IAmazonSimpleNotificationService snsClient, QueueCache queueCache, TopicCache topicCache, Type eventType, string queueName, CancellationToken cancellationToken = default) 12 | { 13 | var topicName = topicCache.GetTopicName(eventType); 14 | return snsClient.FindMatchingSubscription(queueCache, topicName, queueName, cancellationToken: cancellationToken); 15 | } 16 | 17 | public static async Task FindMatchingSubscription(this IAmazonSimpleNotificationService snsClient, QueueCache queueCache, string topicName, string queueName, CancellationToken cancellationToken = default) 18 | { 19 | var existingTopic = await snsClient.FindTopicAsync(topicName).ConfigureAwait(false); 20 | if (existingTopic == null) 21 | { 22 | return null; 23 | } 24 | 25 | return await snsClient.FindMatchingSubscription(queueCache, existingTopic, queueName, cancellationToken: cancellationToken) 26 | .ConfigureAwait(false); 27 | } 28 | 29 | public static async Task FindMatchingSubscription(this IAmazonSimpleNotificationService snsClient, QueueCache queueCache, Topic topic, string queueName, CancellationToken cancellationToken = default) 30 | { 31 | var physicalQueueName = queueCache.GetPhysicalQueueName(queueName); 32 | 33 | ListSubscriptionsByTopicResponse upToAHundredSubscriptions = null; 34 | 35 | do 36 | { 37 | upToAHundredSubscriptions = await snsClient.ListSubscriptionsByTopicAsync(topic.TopicArn, upToAHundredSubscriptions?.NextToken, cancellationToken) 38 | .ConfigureAwait(false); 39 | 40 | foreach (var upToAHundredSubscription in upToAHundredSubscriptions.Subscriptions) 41 | { 42 | if (upToAHundredSubscription.Endpoint.EndsWith($":{physicalQueueName}", StringComparison.Ordinal)) 43 | { 44 | return upToAHundredSubscription.SubscriptionArn; 45 | } 46 | } 47 | } 48 | while (upToAHundredSubscriptions.NextToken != null && upToAHundredSubscriptions.Subscriptions.Count > 0); 49 | 50 | return null; 51 | } 52 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/NServiceBus.Transport.SQS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net10.0 5 | true 6 | ..\NServiceBus.snk 7 | NServiceBus.AmazonSQS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/PreObsolete.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using System; 4 | 5 | /// 6 | /// Meant for staging future obsoletes. 7 | /// 8 | [AttributeUsage(AttributeTargets.All)] 9 | sealed class PreObsoleteAttribute(string contextUrl) : Attribute 10 | { 11 | public string ContextUrl { get; } = contextUrl; 12 | 13 | public string ReplacementTypeOrMember { get; set; } 14 | 15 | public string Message { get; set; } 16 | 17 | public string Note { get; set; } 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/ReadonlyStream.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.IO; 5 | 6 | class ReadonlyStream(ReadOnlyMemory memory) : Stream 7 | { 8 | long position = 0; 9 | 10 | public override void Flush() => throw new NotSupportedException(); 11 | 12 | public override long Seek(long offset, SeekOrigin origin) 13 | { 14 | switch (origin) 15 | { 16 | case SeekOrigin.Begin: 17 | position = offset; 18 | break; 19 | case SeekOrigin.Current: 20 | position += offset; 21 | break; 22 | case SeekOrigin.End: 23 | position = memory.Length + offset; 24 | break; 25 | default: 26 | break; 27 | } 28 | 29 | return position; 30 | } 31 | 32 | public override void SetLength(long value) => throw new NotSupportedException(); 33 | 34 | public override int Read(byte[] buffer, int offset, int count) 35 | { 36 | var bytesToCopy = (int)Math.Min(count, memory.Length - position); 37 | 38 | var destination = buffer.AsSpan().Slice(offset, bytesToCopy); 39 | var source = memory.Span.Slice((int)position, bytesToCopy); 40 | 41 | source.CopyTo(destination); 42 | 43 | position += bytesToCopy; 44 | 45 | return bytesToCopy; 46 | } 47 | 48 | public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); 49 | 50 | public override bool CanRead => true; 51 | public override bool CanSeek => true; 52 | public override bool CanWrite => false; 53 | public override long Length => memory.Length; 54 | public override long Position { get => position; set => position = value; } 55 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Receiving/MessagePump.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Amazon.SQS; 7 | 8 | class MessagePump : IMessageReceiver 9 | { 10 | readonly bool disableDelayedDelivery; 11 | readonly InputQueuePump inputQueuePump; 12 | readonly DelayedMessagesPump delayedMessagesPump; 13 | 14 | public MessagePump(string receiverId, 15 | string receiveAddress, 16 | string errorQueueAddress, 17 | bool purgeOnStartup, 18 | IAmazonSQS sqsClient, 19 | QueueCache queueCache, 20 | S3Settings s3Settings, 21 | SubscriptionManager subscriptionManager, 22 | int queueDelayTimeSeconds, 23 | int? visibilityTimeoutInSeconds, 24 | TimeSpan maxAutoMessageVisibilityRenewalDuration, 25 | Action criticalErrorAction, 26 | bool setupInfrastructure, 27 | bool disableDelayedDelivery) 28 | { 29 | this.disableDelayedDelivery = disableDelayedDelivery; 30 | inputQueuePump = new InputQueuePump(receiverId, receiveAddress, errorQueueAddress, purgeOnStartup, sqsClient, queueCache, s3Settings, subscriptionManager, criticalErrorAction, visibilityTimeoutInSeconds, maxAutoMessageVisibilityRenewalDuration, setupInfrastructure); 31 | if (!disableDelayedDelivery) 32 | { 33 | delayedMessagesPump = 34 | new DelayedMessagesPump(receiveAddress, sqsClient, queueCache, queueDelayTimeSeconds); 35 | } 36 | } 37 | 38 | public async Task Initialize(PushRuntimeSettings limitations, OnMessage onMessage, OnError onError, CancellationToken cancellationToken = default) 39 | { 40 | await inputQueuePump.Initialize(limitations, onMessage, onError, cancellationToken).ConfigureAwait(false); 41 | if (!disableDelayedDelivery) 42 | { 43 | await delayedMessagesPump.Initialize(cancellationToken).ConfigureAwait(false); 44 | } 45 | } 46 | 47 | public async Task StartReceive(CancellationToken cancellationToken = default) 48 | { 49 | await inputQueuePump.StartReceive(cancellationToken).ConfigureAwait(false); 50 | if (!disableDelayedDelivery) 51 | { 52 | delayedMessagesPump.Start(cancellationToken); 53 | } 54 | } 55 | 56 | public Task StopReceive(CancellationToken cancellationToken = default) 57 | { 58 | var stopDelayed = !disableDelayedDelivery 59 | ? delayedMessagesPump.Stop(cancellationToken) 60 | : Task.CompletedTask; 61 | var stopPump = inputQueuePump.StopReceive(cancellationToken); 62 | 63 | return Task.WhenAll(stopDelayed, stopPump); 64 | } 65 | 66 | public Task ChangeConcurrency(PushRuntimeSettings limitations, CancellationToken cancellationToken = default) 67 | { 68 | return inputQueuePump.ChangeConcurrency(limitations, cancellationToken); 69 | } 70 | 71 | public ISubscriptionManager Subscriptions => inputQueuePump.Subscriptions; 72 | public string Id => inputQueuePump.Id; 73 | public string ReceiveAddress => inputQueuePump.ReceiveAddress; 74 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Receiving/PolicyStatement.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.Linq; 5 | using Amazon.Auth.AccessControlPolicy; 6 | 7 | #pragma warning disable 618 8 | class PolicyStatement 9 | { 10 | public PolicyStatement(string topicName, string topicArn, string queueArn) 11 | { 12 | TopicName = topicName; 13 | TopicArn = topicArn; 14 | Statement = CreatePermissionStatement(queueArn, topicArn); 15 | QueueArn = queueArn; 16 | 17 | var splittedTopicArn = TopicArn.Split(ArnSeperator, StringSplitOptions.RemoveEmptyEntries); 18 | AccountArn = string.Join(":", splittedTopicArn.Take(5)); 19 | } 20 | 21 | public string QueueArn { get; } 22 | public string TopicName { get; } 23 | public string TopicArn { get; } 24 | public string AccountArn { get; } 25 | public Statement Statement { get; } 26 | 27 | internal static Statement CreatePermissionStatement(string queueArn, string topicArn) 28 | { 29 | var statement = new Statement(Statement.StatementEffect.Allow); 30 | statement.Actions.Add(new ActionIdentifier("sqs:SendMessage")); 31 | statement.Resources.Add(new Resource(queueArn)); 32 | statement.Conditions.Add(ConditionFactory.NewSourceArnCondition(topicArn)); 33 | statement.Principals.Add(new Principal("*")); 34 | return statement; 35 | } 36 | 37 | static readonly string[] ArnSeperator = { ":" }; 38 | } 39 | #pragma warning restore 618 -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Receiving/SqsReceivedDelayedMessage.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS; 4 | 5 | class SqsReceivedDelayedMessage(string receivedMessageId, string receiptHandle) : SqsPreparedMessage 6 | { 7 | public string ReceivedMessageId { get; } = receivedMessageId; 8 | public string ReceiptHandle { get; } = receiptHandle; 9 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/OutgoingMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using Transport; 5 | 6 | static class OutgoingMessageExtensions 7 | { 8 | public static MessageIntent GetMessageIntent(this OutgoingMessage message) 9 | { 10 | var messageIntent = default(MessageIntent); 11 | if (message.Headers.TryGetValue(Headers.MessageIntent, out var messageIntentString)) 12 | { 13 | Enum.TryParse(messageIntentString, true, out messageIntent); 14 | } 15 | 16 | return messageIntent; 17 | } 18 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/ReducedPayloadSerializerConverter.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | 8 | /// 9 | /// This converter is only used to make sure we are not double storing TimeToBeReceived and ReplyAddress both in 10 | /// the headers and on the transport message wrapper to save space on the SQS message body. 11 | /// The read method is implemented but never used because only the dispatcher uses it to serialize the transport 12 | /// message. 13 | /// 14 | sealed class ReducedPayloadSerializerConverter : JsonConverter 15 | { 16 | static readonly JsonConverter> DefaultDictionaryConverter = 17 | (JsonConverter>)JsonSerializerOptions.Default.GetConverter(typeof(Dictionary)); 18 | public override TransportMessage Read(ref Utf8JsonReader reader, Type typeToConvert, 19 | JsonSerializerOptions options) => 20 | throw new NotImplementedException("The converter should only be used to serialize outgoing transport messages but never to read incoming transport messages."); 21 | 22 | public override void Write(Utf8JsonWriter writer, TransportMessage value, JsonSerializerOptions options) 23 | { 24 | writer.WriteStartObject(); 25 | 26 | writer.WriteString(nameof(TransportMessage.Body), value.Body); 27 | writer.WriteString(nameof(TransportMessage.S3BodyKey), value.S3BodyKey); 28 | 29 | writer.WritePropertyName(nameof(TransportMessage.Headers)); 30 | 31 | DefaultDictionaryConverter.Write(writer, value.Headers, options); 32 | 33 | writer.WriteEndObject(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SnsBatchEntry.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System.Collections.Generic; 4 | using Amazon.SimpleNotificationService.Model; 5 | 6 | readonly struct SnsBatchEntry( 7 | PublishBatchRequest batchRequest, 8 | Dictionary preparedMessagesBydId) 9 | { 10 | public readonly PublishBatchRequest BatchRequest = batchRequest; 11 | public readonly Dictionary PreparedMessagesBydId = preparedMessagesBydId; 12 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SnsPreparedMessage.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Amazon.SimpleNotificationService.Model; 6 | 7 | class SnsPreparedMessage 8 | { 9 | public string MessageId 10 | { 11 | get => MessageAttributes.ContainsKey(Headers.MessageId) ? MessageAttributes[Headers.MessageId].StringValue : null; 12 | init => 13 | // because message attributes are part of the content size restriction we want to prevent message size from changing thus we add it 14 | // for native delayed deliver as well even though the information is slightly redundant (MessageId is assigned to MessageDeduplicationId for example) 15 | MessageAttributes[Headers.MessageId] = new MessageAttributeValue 16 | { 17 | StringValue = value, 18 | DataType = "String" 19 | }; 20 | } 21 | public string Body { get; set; } 22 | public string Destination { get; set; } 23 | public long Size { get; private set; } 24 | public long ReserveBytesInMessageSizeCalculation { get; init; } 25 | 26 | public Dictionary MessageAttributes { get; } = []; 27 | 28 | public void CalculateSize() 29 | { 30 | Size = Body?.Length ?? 0; 31 | Size += CalculateAttributesSize(); 32 | Size += ReserveBytesInMessageSizeCalculation; 33 | } 34 | 35 | long CalculateAttributesSize() 36 | { 37 | var size = 0L; 38 | foreach ((string key, MessageAttributeValue attributeValue) in MessageAttributes) 39 | { 40 | size += key.Length; 41 | size += attributeValue.DataType?.Length ?? 0; 42 | size += attributeValue.StringValue?.Length ?? 0; 43 | 44 | try 45 | { 46 | size += attributeValue.BinaryValue?.Length ?? 0; 47 | } 48 | catch (Exception) 49 | { 50 | // if we can't determine the length we ignore it for now 51 | } 52 | } 53 | 54 | return size; 55 | } 56 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SnsPreparedMessageBatcher.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS; 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | static class SnsPreparedMessageBatcher 10 | { 11 | public static IReadOnlyList Batch(IEnumerable preparedMessages) 12 | { 13 | var allBatches = new List(); 14 | var currentDestinationBatches = new Dictionary(TransportConstraints.MaximumItemsInBatch); 15 | 16 | var groupByDestination = preparedMessages.GroupBy(m => m.Destination, StringComparer.Ordinal); 17 | foreach (var group in groupByDestination) 18 | { 19 | SnsPreparedMessage? firstMessage = null; 20 | var payloadSize = 0L; 21 | foreach (var message in group) 22 | { 23 | if (string.IsNullOrEmpty(message.Destination)) 24 | { 25 | continue; 26 | } 27 | 28 | firstMessage ??= message; 29 | 30 | // Assumes the size was already calculated by the dispatcher 31 | var size = message.Size; 32 | payloadSize += size; 33 | 34 | if (payloadSize > TransportConstraints.MaximumMessageSize) 35 | { 36 | allBatches.Add(message.ToBatchRequest(currentDestinationBatches)); 37 | currentDestinationBatches.Clear(); 38 | payloadSize = size; 39 | } 40 | 41 | // we don't have to recheck payload size here because the support layer checks that a request can always fit 256 KB size limit 42 | // we can't take MessageId because batch request ID can only contain alphanumeric characters, hyphen and underscores, message id could be overloaded 43 | currentDestinationBatches.Add(Guid.NewGuid().ToString(), message); 44 | 45 | var currentCount = currentDestinationBatches.Count; 46 | if (currentCount != TransportConstraints.MaximumItemsInBatch) 47 | { 48 | continue; 49 | } 50 | 51 | allBatches.Add(message.ToBatchRequest(currentDestinationBatches)); 52 | currentDestinationBatches.Clear(); 53 | payloadSize = 0; 54 | } 55 | 56 | if (currentDestinationBatches.Count > 0) 57 | { 58 | allBatches.Add(firstMessage!.ToBatchRequest(currentDestinationBatches)); 59 | currentDestinationBatches.Clear(); 60 | } 61 | } 62 | 63 | return allBatches; 64 | } 65 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SnsPreparedMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS; 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Amazon.SimpleNotificationService.Model; 8 | 9 | static class SnsPreparedMessageExtensions 10 | { 11 | public static PublishRequest ToPublishRequest(this SnsPreparedMessage message) => 12 | new(message.Destination, message.Body) 13 | { 14 | MessageAttributes = message.MessageAttributes.ToDictionary(x => x.Key, x => new MessageAttributeValue 15 | { 16 | DataType = x.Value.DataType, 17 | StringValue = x.Value.StringValue, 18 | BinaryValue = x.Value.BinaryValue, 19 | }) 20 | }; 21 | 22 | static PublishBatchRequestEntry ToBatchEntry(this SnsPreparedMessage message, string batchEntryId) => 23 | new() 24 | { 25 | MessageAttributes = message.MessageAttributes, 26 | Id = batchEntryId, 27 | Message = message.Body 28 | }; 29 | 30 | public static SnsBatchEntry ToBatchRequest(this SnsPreparedMessage message, Dictionary batchEntries) 31 | { 32 | var preparedMessagesBydId = batchEntries.ToDictionary(x => x.Key, x => x.Value); 33 | 34 | var batchRequestEntries = new List(batchEntries.Count); 35 | foreach (var kvp in preparedMessagesBydId) 36 | { 37 | batchRequestEntries.Add(kvp.Value.ToBatchEntry(kvp.Key)); 38 | } 39 | 40 | return new SnsBatchEntry( 41 | new PublishBatchRequest 42 | { 43 | TopicArn = message.Destination, 44 | PublishBatchRequestEntries = batchRequestEntries 45 | }, 46 | preparedMessagesBydId); 47 | } 48 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SqsBatchEntry.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System.Collections.Generic; 4 | using Amazon.SQS.Model; 5 | 6 | readonly struct SqsBatchEntry( 7 | SendMessageBatchRequest batchRequest, 8 | Dictionary preparedMessagesBydId) 9 | { 10 | public readonly SendMessageBatchRequest BatchRequest = batchRequest; 11 | public readonly Dictionary PreparedMessagesBydId = preparedMessagesBydId; 12 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SqsPreparedMessageBatcher.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS; 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | static class SqsPreparedMessageBatcher 10 | { 11 | public static IReadOnlyList Batch(IEnumerable preparedMessages) 12 | { 13 | var allBatches = new List(); 14 | var currentBatch = new Dictionary(TransportConstraints.MaximumItemsInBatch); 15 | 16 | // Group messages by destination to ensure batches only contain messages for the same queue 17 | var groupByDestination = preparedMessages.GroupBy(m => m.QueueUrl, StringComparer.Ordinal); 18 | 19 | foreach (var destinationGroup in groupByDestination) 20 | { 21 | SqsPreparedMessage? referenceMessage = null; 22 | var currentBatchSize = 0L; 23 | 24 | foreach (var message in destinationGroup) 25 | { 26 | referenceMessage ??= message; 27 | var messageSize = message.Size; // Size calculation is assumed to be done previously 28 | 29 | // Check if this message would push the batch over the size limit 30 | if (currentBatchSize + messageSize > TransportConstraints.MaximumMessageSize) 31 | { 32 | // Finalize current batch if it has any messages 33 | if (currentBatch.Count > 0) 34 | { 35 | allBatches.Add(referenceMessage.ToBatchRequest(currentBatch)); 36 | currentBatch.Clear(); 37 | } 38 | currentBatchSize = messageSize; 39 | } 40 | else 41 | { 42 | // Message will fit within the current batch 43 | currentBatchSize += messageSize; 44 | } 45 | 46 | // Add message to the current batch with a unique batch ID 47 | currentBatch.Add(Guid.NewGuid().ToString(), message); 48 | 49 | // Check if we've reached the maximum items per batch 50 | if (currentBatch.Count == TransportConstraints.MaximumItemsInBatch) 51 | { 52 | allBatches.Add(message.ToBatchRequest(currentBatch)); 53 | currentBatch.Clear(); 54 | currentBatchSize = 0; 55 | } 56 | } 57 | 58 | // Finalize any remaining messages in the batch 59 | if (currentBatch.Count > 0) 60 | { 61 | allBatches.Add(referenceMessage!.ToBatchRequest(currentBatch)); 62 | currentBatch.Clear(); 63 | } 64 | } 65 | 66 | return allBatches; 67 | } 68 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/Sending/SqsPreparedMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.SQS; 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Amazon.SQS.Model; 8 | using MessageAttributeValue = Amazon.SQS.Model.MessageAttributeValue; 9 | 10 | static class SqsPreparedMessageExtensions 11 | { 12 | public static SendMessageRequest ToRequest(this SqsPreparedMessage message) => 13 | new(message.QueueUrl, message.Body) 14 | { 15 | MessageGroupId = message.MessageGroupId, 16 | MessageDeduplicationId = message.MessageDeduplicationId, 17 | MessageAttributes = message.MessageAttributes, 18 | DelaySeconds = message.DelaySeconds 19 | }; 20 | 21 | public static void CopyMessageAttributes(this SqsPreparedMessage message, Dictionary? nativeMessageAttributes) 22 | { 23 | foreach (var messageAttribute in nativeMessageAttributes ?? 24 | Enumerable.Empty>()) 25 | { 26 | if (message.MessageAttributes.ContainsKey(messageAttribute.Key)) 27 | { 28 | continue; 29 | } 30 | 31 | message.MessageAttributes.Add(messageAttribute.Key, messageAttribute.Value); 32 | } 33 | } 34 | 35 | /// 36 | /// When receiving native messages, we expect some message attributes to be available in order to consume the message 37 | /// Once we've been able to process a native message *once*, we move those message attributes into the header collection we use across the framework 38 | /// 39 | public static void RemoveNativeHeaders(this SqsPreparedMessage message) 40 | { 41 | // We're removing these message attributes as we've copied them over into the header dictionary 42 | message.MessageAttributes.Remove(TransportHeaders.MessageTypeFullName); 43 | message.MessageAttributes.Remove(TransportHeaders.S3BodyKey); 44 | } 45 | 46 | static SendMessageBatchRequestEntry ToBatchEntry(this SqsPreparedMessage message, string batchEntryId) => 47 | new(batchEntryId, message.Body) 48 | { 49 | MessageAttributes = message.MessageAttributes, 50 | MessageGroupId = message.MessageGroupId, 51 | MessageDeduplicationId = message.MessageDeduplicationId, 52 | DelaySeconds = message.DelaySeconds 53 | }; 54 | 55 | public static SqsBatchEntry ToBatchRequest(this SqsPreparedMessage message, Dictionary batchEntries) 56 | { 57 | var preparedMessagesBydId = batchEntries.ToDictionary(x => x.Key, x => x.Value); 58 | 59 | var batchRequestEntries = new List(batchEntries.Count); 60 | foreach (var kvp in preparedMessagesBydId) 61 | { 62 | batchRequestEntries.Add(kvp.Value.ToBatchEntry(kvp.Key)); 63 | } 64 | 65 | return new SqsBatchEntry( 66 | new SendMessageBatchRequest(message.QueueUrl, batchRequestEntries), 67 | preparedMessagesBydId); 68 | } 69 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/TopicNameHelper.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace NServiceBus.Transport.SQS; 3 | 4 | using System; 5 | using System.Text; 6 | 7 | static class TopicNameHelper 8 | { 9 | public static string GetSnsTopicName(Type eventType, string topicNamePrefix) 10 | { 11 | var destination = eventType.FullName; 12 | 13 | var s = topicNamePrefix + destination; 14 | if (s.Length > 256) 15 | { 16 | throw new Exception($"Address {destination} with configured prefix {topicNamePrefix} is longer than 256 characters and therefore cannot be used to create an SNS topic. Use a shorter topic name."); 17 | } 18 | 19 | var topicNameBuilder = new StringBuilder(s); 20 | 21 | return GetSanitizedTopicName(topicNameBuilder); 22 | } 23 | 24 | public static string GetSanitizedTopicName(StringBuilder topicNameBuilder) 25 | { 26 | // SNS topic names can only have alphanumeric characters, hyphens and underscores. 27 | // Any other characters will be replaced with a hyphen. 28 | for (var i = 0; i < topicNameBuilder.Length; ++i) 29 | { 30 | var c = topicNameBuilder[i]; 31 | if (!char.IsLetterOrDigit(c) 32 | && c != '-' 33 | && c != '_') 34 | { 35 | topicNameBuilder[i] = '-'; 36 | } 37 | } 38 | 39 | return topicNameBuilder.ToString(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/TransportConstraints.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | 5 | class TransportConstraints 6 | { 7 | public const int MaximumMessageSize = 256 * 1024; 8 | public const int MaximumItemsInBatch = 10; 9 | public const string DelayedDeliveryQueueSuffix = "-delay.fifo"; 10 | public static readonly TimeSpan DelayedDeliveryQueueMessageRetentionPeriod = TimeSpan.FromDays(4); 11 | public static readonly int AwsMaximumQueueDelayTime = (int)TimeSpan.FromMinutes(15).TotalSeconds; 12 | 13 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/TransportHeaders.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | static class TransportHeaders 4 | { 5 | const string Prefix = "NServiceBus.AmazonSQS."; 6 | public const string TimeToBeReceived = Prefix + nameof(TimeToBeReceived); 7 | public const string DelaySeconds = Prefix + nameof(DelaySeconds); 8 | public const string Headers = Prefix + nameof(Headers); 9 | public const string S3BodyKey = "S3BodyKey"; 10 | public const string MessageTypeFullName = "MessageTypeFullName"; 11 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/TransportMessage.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Transport; 6 | 7 | class TransportMessage 8 | { 9 | //MessageBody of Amazon.SQS SendRequest must have a minimum length of 1: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html 10 | public const string EmptyMessage = "empty message"; 11 | 12 | // Empty constructor required for deserialization. 13 | public TransportMessage() 14 | { 15 | } 16 | 17 | public TransportMessage(OutgoingMessage outgoingMessage, DispatchProperties properties) 18 | { 19 | Headers = outgoingMessage.Headers; 20 | 21 | Headers.TryGetValue(NServiceBus.Headers.MessageId, out var messageId); 22 | if (string.IsNullOrEmpty(messageId)) 23 | { 24 | messageId = Guid.NewGuid().ToString(); 25 | Headers[NServiceBus.Headers.MessageId] = messageId; 26 | } 27 | 28 | if (properties.DiscardIfNotReceivedBefore != null) 29 | { 30 | TimeToBeReceived = properties.DiscardIfNotReceivedBefore.MaxTime.ToString(); 31 | } 32 | 33 | if (outgoingMessage.Body.Length != 0) 34 | { 35 | Body = Convert.ToBase64String(outgoingMessage.Body.Span); 36 | } 37 | else 38 | { 39 | Body = EmptyMessage; 40 | } 41 | } 42 | 43 | public Dictionary Headers { get; set; } 44 | 45 | public string Body { get; set; } 46 | 47 | public string S3BodyKey { get; set; } 48 | 49 | public string TimeToBeReceived 50 | { 51 | get => Headers.ContainsKey(TransportHeaders.TimeToBeReceived) ? Headers[TransportHeaders.TimeToBeReceived] : TimeSpan.MaxValue.ToString(); 52 | set 53 | { 54 | if (value != null) 55 | { 56 | Headers[TransportHeaders.TimeToBeReceived] = value; 57 | } 58 | } 59 | } 60 | 61 | public Address? ReplyToAddress 62 | { 63 | get => Headers.ContainsKey(NServiceBus.Headers.ReplyToAddress) ? new Address { Queue = Headers[NServiceBus.Headers.ReplyToAddress] } : null; 64 | set 65 | { 66 | if (!string.IsNullOrWhiteSpace(value?.Queue)) 67 | { 68 | Headers[NServiceBus.Headers.ReplyToAddress] = value.Value.Queue; 69 | } 70 | } 71 | } 72 | 73 | public struct Address 74 | { 75 | public string Queue { get; set; } 76 | public string Machine { get; set; } 77 | } 78 | } -------------------------------------------------------------------------------- /src/NServiceBus.Transport.SQS/TransportMessageSerializerContext.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.SQS; 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | [JsonSourceGenerationOptions] 6 | [JsonSerializable(typeof(TransportMessage))] 7 | partial class TransportMessageSerializerContext : JsonSerializerContext 8 | { 9 | } -------------------------------------------------------------------------------- /src/NServiceBus.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.AmazonSQS/f1db08bf19341763802f406f9f03f657bfa23807/src/NServiceBus.snk -------------------------------------------------------------------------------- /src/NServiceBusTests.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.AmazonSQS/f1db08bf19341763802f406f9f03f657bfa23807/src/NServiceBusTests.snk -------------------------------------------------------------------------------- /src/msbuild/AutomaticVersionRanges.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @(_ProjectReferencesWithVersions->Count()) 19 | 20 | 21 | 22 | 23 | 24 | <_ProjectReferencesWithVersions Remove="@(_ProjectReferencesWithVersions)" /> 25 | <_ProjectReferencesWithVersions Include="@(_ProjectReferencesWithVersionRanges)" /> 26 | 27 | 28 | 29 | 30 | 31 | @(PackageReference->Count()) 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/msbuild/ConvertToVersionRange.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.Build.Framework; 4 | using Microsoft.Build.Utilities; 5 | 6 | public class ConvertToVersionRange : Task 7 | { 8 | [Required] 9 | public ITaskItem[] References { get; set; } = []; 10 | 11 | [Required] 12 | public string VersionProperty { get; set; } = string.Empty; 13 | 14 | [Output] 15 | public ITaskItem[] ReferencesWithVersionRanges { get; private set; } = []; 16 | 17 | public override bool Execute() 18 | { 19 | var success = true; 20 | 21 | foreach (var reference in References) 22 | { 23 | var automaticVersionRange = reference.GetMetadata("AutomaticVersionRange"); 24 | 25 | if (automaticVersionRange.Equals("false", StringComparison.OrdinalIgnoreCase)) 26 | { 27 | continue; 28 | } 29 | 30 | var privateAssets = reference.GetMetadata("PrivateAssets"); 31 | 32 | if (privateAssets.Equals("All", StringComparison.OrdinalIgnoreCase)) 33 | { 34 | continue; 35 | } 36 | 37 | var version = reference.GetMetadata(VersionProperty); 38 | var match = Regex.Match(version, @"^\d+"); 39 | 40 | if (match.Value.Equals(string.Empty, StringComparison.Ordinal)) 41 | { 42 | Log.LogError("Reference '{0}' with version '{1}' is not valid for automatic version range conversion. Fix the version or exclude the reference from conversion by setting 'AutomaticVersionRange=\"false\"' on the reference.", reference.ItemSpec, version); 43 | success = false; 44 | continue; 45 | } 46 | 47 | var nextMajor = Convert.ToInt32(match.Value) + 1; 48 | 49 | var versionRange = $"[{version}, {nextMajor}.0.0)"; 50 | reference.SetMetadata(VersionProperty, versionRange); 51 | } 52 | 53 | ReferencesWithVersionRanges = References; 54 | 55 | return success; 56 | } 57 | } 58 | --------------------------------------------------------------------------------