├── .csharpierignore ├── .csharpierrc.json ├── icon.png ├── NOTICE ├── src ├── AWS.WCF.Extensions │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── SQS │ │ ├── SqsDefaults.cs │ │ ├── SqsConstants.cs │ │ ├── AwsSqsBinding.cs │ │ ├── AwsSqsTransportBindingElement.cs │ │ ├── SqsChannelFactory.cs │ │ ├── SqsOutputChannel.cs │ │ └── Runtime │ │ │ └── TaskHelper.cs │ ├── Common │ │ ├── SQSClientExtensions.cs │ │ └── AmazonServiceExtensions.cs │ └── AWS.WCF.Extensions.csproj ├── AWS.CoreWCF.Extensions │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── SQS │ │ ├── Channels │ │ │ ├── AwsSqsMessageContext.cs │ │ │ ├── AwsSqsBinding.cs │ │ │ ├── AwsSqsReceiveContext.cs │ │ │ ├── AwsSqsTransportBindingElement.cs │ │ │ └── AwsSqsTransport.cs │ │ ├── DispatchCallbacks │ │ │ ├── IDispatchCallbacksCollection.cs │ │ │ ├── DispatchCallbacksCollection.cs │ │ │ └── DispatchCallbacks.cs │ │ └── Infrastructure │ │ │ ├── NamedSQSClient.cs │ │ │ ├── SQSServiceCollectionExtensions.cs │ │ │ ├── ApplicationBuilderExtensions.cs │ │ │ └── SQSMessageProvider.cs │ ├── Common │ │ ├── StringExtensions.cs │ │ ├── AmazonServiceExtensions.cs │ │ ├── BasicPolicyTemplates.cs │ │ └── CreateQueueRequestExtensions.cs │ └── AWS.CoreWCF.Extensions.csproj └── CodeSigningHelper │ ├── CodeSigningHelper.csproj │ └── Program.cs ├── test ├── AWS.Extensions.IntegrationTests │ ├── SQS │ │ ├── appsettings.test.json │ │ ├── IntegrationSetupTests.cs │ │ ├── TestService │ │ │ ├── ServiceContract │ │ │ │ ├── LoggingService.cs │ │ │ │ └── SimulateWorkBehavior.cs │ │ │ └── ServiceHelper.cs │ │ ├── SnsCallbackIntegrationTests.cs │ │ ├── NegativeIntegrationTests.cs │ │ └── TestHelpers │ │ │ └── ClientAndServerFixture.cs │ ├── Common │ │ ├── Settings.cs │ │ ├── XUnitLoggingProvider.cs │ │ └── AssertExtensions.cs │ └── AWS.Extensions.IntegrationTests.csproj ├── AWS.CoreWCF.Extensions.Tests │ ├── AqsSqsBindingTests.cs │ ├── AWS.CoreWCF.Extensions.Tests.csproj │ ├── ApplicationBuilderExtensionsTests.cs │ ├── AwsSqsTransportTests.cs │ ├── AmazonServiceExtensionTests.cs │ ├── DispatchCallbackFactoryTests.cs │ └── SQSClientExtensionTests.cs ├── AWS.WCF.Extensions.Tests │ ├── SQSClientExtensionTests.cs │ ├── AWS.WCF.Extensions.Tests.csproj │ ├── AwsSqsTransportBindingElementTests.cs │ ├── SqsChannelFactoryTests.cs │ ├── AmazonServiceExtensionTests.cs │ └── SqsOutputChannelTests.cs └── AWS.Extensions.PerformanceTests │ ├── AWS.Extensions.PerformanceTests.csproj │ ├── Common │ ├── ServerFactory.cs │ └── ClientMessageGenerator.cs │ ├── ServerSingleClientPerformanceTests.cs │ ├── ClientPerformanceTests.cs │ ├── AwsSdkPerformanceTest.cs │ ├── ServerMultipleClientsPerformanceTests.cs │ └── ServerPerformanceTests.cs ├── sample ├── Server │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Server.csproj │ ├── Properties │ │ └── launchSettings.json │ └── Program.cs ├── Client │ ├── Client.csproj │ └── Program.cs ├── Shared │ ├── Shared.csproj │ └── LoggingService.cs ├── README.md └── AWSCoreWCFSample.sln ├── cdk ├── GlobalSuppressions.cs ├── buildspecs │ ├── build.yml │ ├── nuget-deploy.yml │ └── sign.yml ├── AwsTicketingSystemAlarmAction.cs ├── AWS.CoreWCF.ServerExtensions.Cdk.csproj └── Program.cs ├── NuGet.Config ├── .husky ├── task-runner.json └── pre-commit ├── .github ├── ISSUE_TEMPLATE │ ├── other_issue.md │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── secret-detection.yml │ ├── canary.yml │ └── build-and-deploy.yml └── dependabot.yml ├── .config └── dotnet-tools.json ├── version.json ├── SECURITY.md ├── benchmark-aws-config.json ├── Directory.Build.props ├── nuspec.props ├── AWS.CoreWCF.Extensions.sln.DotSettings ├── cdk.json ├── CONTRIBUTING.md └── .gitlab-ci.yml /.csharpierignore: -------------------------------------------------------------------------------- 1 | sample-data/ -------------------------------------------------------------------------------- /.csharpierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-corewcf-extensions/HEAD/icon.png -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS CoreWCF Extensions 2 | Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("AWS.WCF.Extensions.Tests")] 4 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("AWS.CoreWCF.Extensions.Tests")] 4 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/appsettings.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWS": { 3 | "AWS_ACCESS_KEY_ID": "", 4 | "AWS_SECRET_ACCESS_KEY": "", 5 | "AWS_REGION": "" 6 | } 7 | } -------------------------------------------------------------------------------- /sample/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /sample/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/SqsDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace AWS.WCF.Extensions.SQS; 2 | 3 | static class SqsDefaults 4 | { 5 | internal const long MaxBufferPoolSize = 64 * 1024; 6 | internal const int MaxSendMessageSize = 262144; // Max size for SQS message is 262144 (2^18) 7 | } 8 | -------------------------------------------------------------------------------- /cdk/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( 2 | "Potential Code Quality Issues", 3 | "RECS0026:Possible unassigned object created by 'new'", 4 | Justification = "Constructs add themselves to the scope in which they are created" 5 | )] 6 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Channels/AwsSqsMessageContext.cs: -------------------------------------------------------------------------------- 1 | using CoreWCF.Queue.Common; 2 | 3 | namespace AWS.CoreWCF.Extensions.SQS.Channels; 4 | 5 | public class AwsSqsMessageContext : QueueMessageContext 6 | { 7 | public string MessageReceiptHandle { get; set; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /.husky/task-runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment-enablePreCommitWith": "dotnet husky set pre-commit -c 'dotnet husky run'", 3 | "tasks": [ 4 | { 5 | "name": "Run csharpier", 6 | "command": "dotnet", 7 | "args": [ "csharpier", "${staged}" ], 8 | "include": [ "**/*.cs" ] 9 | }, 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issues 3 | about: Not a bug or feature request? Let us know how else we can improve. 4 | title: "[Other Issue]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description of issue 11 | Describe the issue 12 | 13 | ## Additional context 14 | Provide any additional information 15 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/DispatchCallbacks/IDispatchCallbacksCollection.cs: -------------------------------------------------------------------------------- 1 | namespace AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 2 | 3 | public interface IDispatchCallbacksCollection 4 | { 5 | public NotificationDelegate? NotificationDelegateForSuccessfulDispatch { get; set; } 6 | public NotificationDelegate? NotificationDelegateForFailedDispatch { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/CodeSigningHelper/CodeSigningHelper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "CSharpier": { 6 | "version": "0.28.2", 7 | "commands": [ 8 | "dotnet-csharpier" 9 | ] 10 | }, 11 | "husky": { 12 | "version": "0.5.4", 13 | "commands": [ 14 | "husky" 15 | ] 16 | }, 17 | "nbgv": { 18 | "version": "3.6.133", 19 | "commands": [ 20 | "nbgv" 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/SqsConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel.Channels; 3 | 4 | namespace AWS.WCF.Extensions.SQS; 5 | 6 | [ExcludeFromCodeCoverage] 7 | internal static class SqsConstants 8 | { 9 | internal const string Scheme = "https"; 10 | 11 | internal static MessageEncoderFactory DefaultMessageEncoderFactory { get; } = 12 | new TextMessageEncodingBindingElement().CreateMessageEncoderFactory(); 13 | } 14 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "2.0", 4 | "versionHeightOffset": -1, 5 | "publicReleaseRefSpec": [ 6 | "^refs/heads/main$", 7 | "^refs/heads/release/v\\d+(?:\\.\\d+)?$" 8 | ], 9 | "cloudBuild": { 10 | "buildNumber": { 11 | "enabled": true 12 | } 13 | }, 14 | "release": { 15 | "branchName": "release/v{version}" 16 | } 17 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Solution idea(s) 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Additional context 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /sample/Client/Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Description of change 2 | [//]: # (What are you trying to fix? What did you change) 3 | 4 | #### Issue 5 | [//]: # (Having an issue # for the PR is required for tracking purposes. If an existing issue does not exist please create one.) 6 | 7 | #### PR reviewer notes 8 | [//]: # (Let us know if there is anything we should focus on.) 9 | 10 | 11 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 12 | -------------------------------------------------------------------------------- /sample/Shared/Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/secret-detection.yml: -------------------------------------------------------------------------------- 1 | name: Secret Detection 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | scan-for-secrets: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: TruffleHog OSS 16 | uses: trufflesecurity/trufflehog@main 17 | with: 18 | path: ./ 19 | base: 9a92a70 20 | head: HEAD 21 | extra_args: --debug --only-verified 22 | 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting Security Issues 2 | 3 | We take all security reports seriously. 4 | When we receive such reports, 5 | we will investigate and subsequently address 6 | any potential vulnerabilities as quickly as possible. 7 | If you discover a potential security issue in this project, 8 | please notify AWS/Amazon Security via our 9 | [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) 10 | or directly via email to [AWS Security](mailto:aws-security@amazon.com). 11 | Please do *not* create a public GitHub issue in this project. 12 | -------------------------------------------------------------------------------- /benchmark-aws-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Region": "us-west-2", 3 | "Targets": [ 4 | { 5 | "Ami": "ami-05044d26cbbf3c8cf", 6 | "IAMRole": "BENCHMARK_EC2_INSTANCE_PROFILE_ARN", 7 | "Instances": [ 8 | { "Value": "c4.large" }, 9 | { "Value": "c4.xlarge" }, 10 | { "Value": "c4.2xlarge" } 11 | ], 12 | "InstallDotNetSdk": true 13 | } 14 | ], 15 | "BenchmarkDotnetCliToolNugetSource": "BENCHMARK_TOOL_PRIVATE_FEED", 16 | "S3BucketName": "BENCHMARK_BUCKET_NAME" 17 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ## husky task runner examples ------------------- 5 | ## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' 6 | 7 | ## run all tasks 8 | #husky run 9 | 10 | ### run all tasks with group: 'group-name' 11 | #husky run --group group-name 12 | 13 | ## run task with name: 'task-name' 14 | #husky run --name task-name 15 | 16 | ## pass hook arguments to task 17 | #husky run --args "$1" "$2" 18 | 19 | ## or put your custom commands ------------------- 20 | #echo 'Husky.Net is awesome!' 21 | 22 | dotnet husky run 23 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Infrastructure/NamedSQSClient.cs: -------------------------------------------------------------------------------- 1 | using Amazon.SQS; 2 | 3 | namespace AWS.CoreWCF.Extensions.SQS.Infrastructure; 4 | 5 | internal class NamedSQSClient 6 | { 7 | public string? QueueName { get; set; } 8 | public IAmazonSQS? SQSClient { get; set; } 9 | } 10 | 11 | internal class NamedSQSClientCollection : List 12 | { 13 | public NamedSQSClientCollection(IEnumerable items) 14 | : base(items) { } 15 | 16 | public NamedSQSClientCollection(params NamedSQSClient[] items) 17 | : this(items.AsEnumerable()) { } 18 | } 19 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/Common/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace AWS.Extensions.IntegrationTests.Common; 4 | 5 | [SuppressMessage("ReSharper", "InconsistentNaming")] 6 | public class Settings 7 | { 8 | public AWSSettings AWS { get; set; } = new(); 9 | 10 | public class AWSSettings 11 | { 12 | //public string? PROFILE { get; set; } 13 | public string? AWS_ACCESS_KEY_ID { get; set; } 14 | public string? AWS_SECRET_ACCESS_KEY { get; set; } 15 | public string? AWS_REGION { get; set; } = "us-west-2"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/Server/Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/Common/SQSClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.Runtime; 2 | 3 | namespace AWS.WCF.Extensions.Common; 4 | 5 | public static class SQSClientExtensions 6 | { 7 | /// 8 | /// Uses the response object to determine if the http request was successful. 9 | /// 10 | /// Response to validate with 11 | /// Thrown if http request was unsuccessful 12 | public static void Validate(this AmazonWebServiceResponse response) 13 | { 14 | var statusCode = (int)response.HttpStatusCode; 15 | if (statusCode < 200 || statusCode >= 300) 16 | throw new HttpRequestException($"HttpStatusCode: {statusCode}"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/Common/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace AWS.CoreWCF.Extensions.Common; 4 | 5 | public static class StringExtensions 6 | { 7 | // private static readonly Encoding _defaultEncoding = Encoding.UTF8; 8 | 9 | /// 10 | /// Converts a string to a Stream object using a specified encoding 11 | /// 12 | /// String to convert to a stream 13 | /// Encoding to apply to the string data 14 | /// A stream containing the string data encoded with specified encoding 15 | public static Stream ToStream(this string str, Encoding encoding) 16 | { 17 | return new MemoryStream(encoding.GetBytes(str)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cdk/buildspecs/build.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | # .NET 8 is already installed in CodeBuild Standard 7 image. 7 | #- curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 8 | build: 9 | commands: 10 | - ls 11 | - if test -d AWSCoreWCFServerExtensions; then cd AWSCoreWCFServerExtensions; fi 12 | # force codebuild to use latest SDK version, otherwise it will default to 6: 13 | # https://github.com/aws/aws-codebuild-docker-images/blob/master/ubuntu/standard/7.0/Dockerfile#L197C26-L197C126 14 | - dotnet new globaljson --force --sdk-version "8.0.0" --roll-forward latestMajor 15 | - dotnet restore 16 | - dotnet tool restore 17 | - dotnet build -c Release 18 | artifacts: 19 | files: 20 | - '**/*' -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | ## Sample 2 | 3 | This contains a working sample demonstrating how to use AWS.CoreWCF.Extensions 4 | 5 | ### Pre-Requisites 6 | - Setup your local environment with your [AWS Credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). One way to do this is by installing the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) and running `aws configure`. 7 | 8 | ### Running 9 | 10 | In a PowerShell/Terminal run: 11 | 12 | ``` 13 | dotnet run --project .\sample\Server\Server.csproj 14 | ``` 15 | 16 | In a **SEPARATE** PowerShell/Terminal run: 17 | 18 | ``` 19 | dotnet run --project .\sample\Client\Client.csproj 20 | ``` 21 | 22 | When prompted in the _Client_ shell, enter a message. The _Server_ shell should then output the message you entered. -------------------------------------------------------------------------------- /sample/Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:19669", 7 | "sslPort": 44352 8 | } 9 | }, 10 | "profiles": { 11 | "Server": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7252;http://localhost:5067", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ### Expected behavior 14 | A clear and concise description of what you expected to happen. 15 | 16 | ### Steps to Reproduce 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | ### Screenshots 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ### Desktop (please complete the following information) 27 | - OS: [e.g. Windows, macOS, Linux, etc] 28 | - Version/Distro: [e.g. 11, Big Sur, Ubuntu 18.04] 29 | 30 | ### Additional context 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory) 5 | 6 | 7 | 8 | 3.5.109 9 | all 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/AqsSqsBindingTests.cs: -------------------------------------------------------------------------------- 1 | using AWS.CoreWCF.Extensions.SQS.Channels; 2 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace AWS.CoreWCF.Extensions.Tests; 7 | 8 | public class AqsSqsBindingTests 9 | { 10 | [Fact] 11 | public void PropertiesComeFromTransport() 12 | { 13 | // ARRANGE 14 | var fakeDispatch = new DispatchCallbacksCollection(); 15 | const long fakeMessageSize = 42L; 16 | 17 | var awsSqsBinding = new AwsSqsBinding { MaxMessageSize = 1 }; 18 | 19 | // ACT 20 | var transport = awsSqsBinding.CreateBindingElements().OfType().First(); 21 | 22 | transport.MaxReceivedMessageSize = fakeMessageSize; 23 | transport.DispatchCallbacksCollection = fakeDispatch; 24 | 25 | // ASSERT 26 | awsSqsBinding.DispatchCallbacksCollection.ShouldBe(fakeDispatch); 27 | awsSqsBinding.MaxMessageSize.ShouldBe(fakeMessageSize); 28 | awsSqsBinding.Scheme.ShouldNotBeNullOrEmpty(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nuspec.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Amazon Web Services 5 | Amazon Web Services 6 | Amazon Web Services 7 | Extensions library enabling AWS cloud service integration with CoreWCF 8 | en-US 9 | CoreWCF WCF AWS Server Client Extensions SQS 10 | https://github.com/aws/aws-corewcf-extensions 11 | Apache-2.0 12 | true 13 | true 14 | true 15 | true 16 | true 17 | snupkg 18 | https://sdk-for-net.amazonwebservices.com/images/AWSLogo128x128.png 19 | 20 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/IntegrationSetupTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Amazon; 3 | using Amazon.SQS; 4 | using Amazon.SQS.Model; 5 | using Shouldly; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace AWS.Extensions.IntegrationTests.SQS; 10 | 11 | public class IntegrationSetupTests 12 | { 13 | private readonly ITestOutputHelper _testOutput; 14 | 15 | public IntegrationSetupTests(ITestOutputHelper testOutput) 16 | { 17 | _testOutput = testOutput; 18 | 19 | AWSConfigs.InitializeCollections = true; 20 | } 21 | 22 | [Fact] 23 | public async Task CanAccessQueues() 24 | { 25 | var sqsClient = new AmazonSQSClient(); 26 | 27 | var response = await sqsClient.ListQueuesAsync(new ListQueuesRequest { MaxResults = 20 }); 28 | 29 | response.HttpStatusCode.ShouldBe(HttpStatusCode.OK); 30 | response.QueueUrls.Count.ShouldBeGreaterThan(1); 31 | 32 | foreach (var result in response.QueueUrls) 33 | { 34 | _testOutput.WriteLine(result); 35 | Console.WriteLine(result); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AWS.CoreWCF.Extensions.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | False 3 | KMS 4 | SQS 5 | WCF 6 | True 7 | True 8 | True 9 | True -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/AWS.WCF.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netstandard2.0 6 | enable 7 | enable 8 | 10.0 9 | icon.png 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/AWS.WCF.Extensions.Tests/SQSClientExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Net; 3 | using Amazon.Runtime; 4 | using AWS.WCF.Extensions.Common; 5 | using Shouldly; 6 | using Xunit; 7 | 8 | namespace AWS.WCF.Extensions.Tests; 9 | 10 | public class SQSClientExtensionTests 11 | { 12 | [Theory] 13 | [InlineData(HttpStatusCode.BadRequest)] 14 | [InlineData(HttpStatusCode.ServiceUnavailable)] 15 | [InlineData(HttpStatusCode.Processing)] 16 | [ExcludeFromCodeCoverage] 17 | public void ValidateThrowsException(HttpStatusCode errorCode) 18 | { 19 | // ARRANGE 20 | var fakeAmazonWebServiceResponse = new AmazonWebServiceResponse { HttpStatusCode = errorCode }; 21 | 22 | Exception? expectedException = null; 23 | 24 | // ACT 25 | try 26 | { 27 | fakeAmazonWebServiceResponse.Validate(); 28 | } 29 | catch (Exception e) 30 | { 31 | expectedException = e; 32 | } 33 | 34 | // ASSERT 35 | expectedException.ShouldNotBeNull(); 36 | expectedException.ShouldBeOfType(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System.ServiceModel; 2 | using Amazon.SQS; 3 | 4 | namespace Client 5 | { 6 | internal class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | Console.WriteLine("Hello, World!"); 11 | 12 | var queueName = "sample-sqs-queue"; 13 | 14 | var sqsClient = new AmazonSQSClient(); 15 | 16 | var sqsBinding = new AWS.WCF.Extensions.SQS.AwsSqsBinding(sqsClient, queueName); 17 | var endpointAddress = new EndpointAddress(new Uri(sqsBinding.QueueUrl)); 18 | var factory = new ChannelFactory(sqsBinding, endpointAddress); 19 | var channel = factory.CreateChannel(); 20 | ((System.ServiceModel.Channels.IChannel)channel).Open(); 21 | 22 | while (true) 23 | { 24 | Console.Write("Enter Message: "); 25 | var msg = Console.ReadLine() ?? ""; 26 | 27 | Console.WriteLine(); 28 | Console.WriteLine("Sending: " + msg); 29 | 30 | channel.LogMessage(msg); 31 | Console.WriteLine(); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/AWS.Extensions.PerformanceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Exe 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/Common/AmazonServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Amazon.Runtime; 3 | 4 | namespace AWS.WCF.Extensions.Common; 5 | 6 | public static class AmazonServiceExtensions 7 | { 8 | // Format is defined in SDK User Agent Header SEP 9 | private static readonly string UserAgentSuffix = 10 | $" ft/corewcf-sqs_{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}"; 11 | private const string UserAgentHeader = "User-Agent"; 12 | 13 | /// 14 | /// Modifies the User-Agent header for api requests made by the 15 | /// to indicate the call was bade via CoreWCF. 16 | /// 17 | public static void SetCustomUserAgentSuffix(this AmazonServiceClient amazonServiceClient) 18 | { 19 | amazonServiceClient.BeforeRequestEvent += (sender, e) => 20 | { 21 | if (e is not WebServiceRequestEventArgs args || !args.Headers.ContainsKey(UserAgentHeader)) 22 | return; 23 | 24 | if (args.Headers[UserAgentHeader].EndsWith(UserAgentSuffix)) 25 | return; 26 | 27 | args.Headers[UserAgentHeader] += UserAgentSuffix; 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/Common/XUnitLoggingProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.Extensions.Logging; 4 | using Xunit.Abstractions; 5 | 6 | namespace AWS.Extensions.IntegrationTests.Common; 7 | 8 | [ExcludeFromCodeCoverage] 9 | public class XUnitLoggingProvider : ILoggerProvider, ILogger 10 | { 11 | private readonly ITestOutputHelper _testOutputHelper; 12 | 13 | public XUnitLoggingProvider(ITestOutputHelper testOutputHelper) 14 | { 15 | _testOutputHelper = testOutputHelper; 16 | } 17 | 18 | public void Log( 19 | LogLevel logLevel, 20 | EventId eventId, 21 | TState state, 22 | Exception? exception, 23 | Func formatter 24 | ) 25 | { 26 | _testOutputHelper.WriteLine($"[{logLevel}]: {formatter(state, exception)}"); 27 | } 28 | 29 | [DebuggerStepThrough] 30 | public ILogger CreateLogger(string categoryName) => this; 31 | 32 | public bool IsEnabled(LogLevel logLevel) => true; 33 | 34 | public IDisposable? BeginScope(TState state) 35 | where TState : notnull => null; 36 | 37 | public void Dispose() { } 38 | } 39 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/Common/AmazonServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Amazon.Runtime; 3 | 4 | namespace AWS.CoreWCF.Extensions.Common; 5 | 6 | public static class AmazonServiceExtensions 7 | { 8 | // Format is defined in SDK User Agent Header SEP 9 | private static readonly string UserAgentSuffix = 10 | $" ft/corewcf-sqs_{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}"; 11 | private const string UserAgentHeader = "User-Agent"; 12 | 13 | /// 14 | /// Modifies the User-Agent header for api requests made by the 15 | /// to indicate the call was bade via CoreWCF. 16 | /// 17 | public static void SetCustomUserAgentSuffix(this AmazonServiceClient amazonServiceClient) 18 | { 19 | amazonServiceClient.BeforeRequestEvent += (sender, e) => 20 | { 21 | if (e is not WebServiceRequestEventArgs args || !args.Headers.ContainsKey(UserAgentHeader)) 22 | return; 23 | 24 | if (args.Headers[UserAgentHeader].EndsWith(UserAgentSuffix)) 25 | return; 26 | 27 | args.Headers[UserAgentHeader] += UserAgentSuffix; 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/AWS.CoreWCF.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | enable 6 | enable 7 | 10.0 8 | Library 9 | icon.png 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/Common/BasicPolicyTemplates.cs: -------------------------------------------------------------------------------- 1 | namespace AWS.CoreWCF.Extensions.Common; 2 | 3 | public class BasicPolicyTemplates 4 | { 5 | private const string AccountIdPlaceholder = "ACCOUNT_ID_PLACEHOLDER"; 6 | private const string SQSArnPlaceholder = "SQS_ARN_PLACEHOLDER"; 7 | 8 | /// 9 | /// Must use "sqs:*" to get around limit of 7 actions in IAM Policies. 10 | /// Currently, needs 8 actions. 11 | /// 12 | public const string BasicSQSPolicyTemplate = 13 | $@"{{ 14 | ""Version"": ""2008-10-17"", 15 | ""Id"": ""__default_policy_ID"", 16 | ""Statement"": [ 17 | {{ 18 | ""Sid"": ""__owner_statement"", 19 | ""Effect"": ""Allow"", 20 | ""Principal"": {{ 21 | ""AWS"": ""{AccountIdPlaceholder}"" 22 | }}, 23 | ""Action"": [ 24 | ""sqs:*"" 25 | ], 26 | ""Resource"": ""{SQSArnPlaceholder}"" 27 | }} 28 | ] 29 | }}"; 30 | 31 | public static string GetBasicSQSPolicy(string queueArn) 32 | { 33 | var accountId = GetAccountIdFromQueueArn(queueArn); 34 | return BasicSQSPolicyTemplate.Replace(AccountIdPlaceholder, accountId).Replace(SQSArnPlaceholder, queueArn); 35 | } 36 | 37 | private static string GetAccountIdFromQueueArn(string queueArn) 38 | { 39 | var arnParts = queueArn.Split(':'); 40 | // get 2nd from the end 41 | return arnParts.Reverse().Skip(1).First(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cdk/buildspecs/nuget-deploy.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | shell: bash 4 | phases: 5 | pre_build: 6 | commands: 7 | # assume nuget-deploy role to read nuget publish secrets 8 | - eval $(aws sts assume-role --role-arn $NUGET_PUBLISH_SECRET_ACCESS_ROLE_ARN --role-session-name signing --external-id CoreWCFExtensionsSigner | jq -r '.Credentials | "export AWS_ACCESS_KEY_ID=\(.AccessKeyId)\nexport AWS_SECRET_ACCESS_KEY=\(.SecretAccessKey)\nexport AWS_SESSION_TOKEN=\(.SessionToken)\n"') 9 | - export COREWCF_SECRET=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN_CORE_WCF_NUGET_PUBLISH_KEY | jq -r '.SecretString' | jq -r ".Key") 10 | - export WCF_SECRET=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN_WCF_NUGET_PUBLISH_KEY | jq -r '.SecretString' | jq -r ".Key") 11 | build: 12 | commands: 13 | - if test -d AWSCoreWCFServerExtensions; then cd AWSCoreWCFServerExtensions; fi 14 | - dotnet nuget push "AWS.CoreWCF.Extensions*.nupkg" --api-key ${COREWCF_SECRET} --source https://api.nuget.org/v3/index.json --skip-duplicate 15 | - dotnet nuget push "AWS.CoreWCF.Extensions*.snupkg" --api-key ${COREWCF_SECRET} --source https://api.nuget.org/v3/index.json --skip-duplicate 16 | - dotnet nuget push "AWS.WCF.Extensions*.nupkg" --api-key ${WCF_SECRET} --source https://api.nuget.org/v3/index.json --skip-duplicate 17 | - dotnet nuget push "AWS.WCF.Extensions*.snupkg" --api-key ${WCF_SECRET} --source https://api.nuget.org/v3/index.json --skip-duplicate -------------------------------------------------------------------------------- /cdk/AwsTicketingSystemAlarmAction.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Amazon.CDK.AWS.CloudWatch; 3 | using Amazon.JSII.Runtime.Deputy; 4 | using Constructs; 5 | 6 | namespace AWS.CoreWCF.ServerExtensions.Cdk; 7 | 8 | [ExcludeFromCodeCoverage] 9 | public class AwsTicketingSystemAlarmAction : DeputyBase, IAlarmAction 10 | { 11 | private readonly string _alarmActionArn; 12 | 13 | public AwsTicketingSystemAlarmAction(AwsTicketingSystemAlarmActionProps props) 14 | { 15 | _alarmActionArn = Encode( 16 | $"{props.ArnPrefix}:{props.Severity}:{props.Cti.Category}:{props.Cti.Type}:{props.Cti.Item}:{props.Cti.ResolverGroup}:{props.DedupeMessage}" 17 | ); 18 | } 19 | 20 | public IAlarmActionConfig Bind(Construct scope, IAlarm alarm) 21 | { 22 | return new AlarmActionConfig { AlarmActionArn = _alarmActionArn }; 23 | } 24 | 25 | private string Encode(string value) 26 | { 27 | return value.Replace(' ', '+'); 28 | } 29 | } 30 | 31 | [ExcludeFromCodeCoverage] 32 | public class AwsTicketingSystemAlarmActionProps 33 | { 34 | public string ArnPrefix { get; set; } 35 | public string Severity { get; set; } 36 | public Cti Cti { get; set; } 37 | public string DedupeMessage { get; set; } 38 | } 39 | 40 | [ExcludeFromCodeCoverage] 41 | public class Cti 42 | { 43 | public string Category { get; set; } 44 | public string Type { get; set; } 45 | public string Item { get; set; } 46 | public string ResolverGroup { get; set; } 47 | } 48 | -------------------------------------------------------------------------------- /test/AWS.WCF.Extensions.Tests/AWS.WCF.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/AWS.CoreWCF.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /cdk/AWS.CoreWCF.ServerExtensions.Cdk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | Major 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | Always 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /sample/Shared/LoggingService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Diagnostics; 3 | using System.Xml.Linq; 4 | using CoreWCF; 5 | 6 | internal static class Constants 7 | { 8 | public const string NS = "http://tempuri.org/"; 9 | public const string LOGGINGSERVICE_NAME = nameof(ILoggingService); 10 | public const string OPERATION_BASE = NS + LOGGINGSERVICE_NAME + "/"; 11 | } 12 | 13 | [System.ServiceModel.ServiceContract(Namespace = Constants.NS, Name = Constants.LOGGINGSERVICE_NAME)] 14 | [ServiceContract(Namespace = Constants.NS, Name = Constants.LOGGINGSERVICE_NAME)] 15 | public interface ILoggingService 16 | { 17 | [System.ServiceModel.OperationContract( 18 | Name = "LogMessage", 19 | Action = Constants.OPERATION_BASE + "LogMessage", 20 | IsOneWay = true 21 | )] 22 | [OperationContract(Name = "LogMessage", Action = Constants.OPERATION_BASE + "LogMessage", IsOneWay = true)] 23 | public void LogMessage(string toLog); 24 | 25 | [System.ServiceModel.OperationContract( 26 | Name = "CauseFailure", 27 | Action = Constants.OPERATION_BASE + "CauseFailure", 28 | IsOneWay = true 29 | )] 30 | [OperationContract(Name = "CauseFailure", Action = Constants.OPERATION_BASE + "CauseFailure", IsOneWay = true)] 31 | void CauseFailure(); 32 | } 33 | 34 | [ServiceBehavior(IncludeExceptionDetailInFaults = true)] 35 | public class LoggingService : ILoggingService 36 | { 37 | public void LogMessage(string toLog) 38 | { 39 | Console.WriteLine("Received " + toLog); 40 | Debug.WriteLine("Received " + toLog, category: "LoggingService"); 41 | } 42 | 43 | public void CauseFailure() 44 | { 45 | throw new Exception("Trigger Failure"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cdk/buildspecs/sign.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | shell: bash 4 | variables: 5 | WCF_EXTENSIONS_PATH: ./src/AWS.WCF.Extensions/bin/Release/netstandard2.0 6 | COREWCF_EXTENSIONS_PATH: ./src/AWS.CoreWCF.Extensions/bin/Release/netstandard2.0 7 | phases: 8 | install: 9 | commands: 10 | # .NET 8 is already installed in CodeBuild Standard 7 image. 11 | #- curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 12 | build: 13 | commands: 14 | - ls 15 | - if test -d AWSCoreWCFServerExtensions; then cd AWSCoreWCFServerExtensions; fi 16 | - # force codebuild to use latest SDK version, otherwise it will default to 6: 17 | # https://github.com/aws/aws-codebuild-docker-images/blob/master/ubuntu/standard/7.0/Dockerfile#L197C26-L197C126 18 | - dotnet new globaljson --force --sdk-version "8.0.0" --roll-forward latestMajor 19 | #assume signing role and save creds to env variables 20 | - eval $(aws sts assume-role --role-arn $SIGNING_ROLE_ARN --role-session-name signing --external-id CoreWCFExtensionsSigner | jq -r '.Credentials | "export AWS_ACCESS_KEY_ID=\(.AccessKeyId)\nexport AWS_SECRET_ACCESS_KEY=\(.SecretAccessKey)\nexport AWS_SESSION_TOKEN=\(.SessionToken)\n"') 21 | - dotnet build ./src/CodeSigningHelper 22 | # sign AWS.CoreWCF.Extensions and AWS.WCF.Extensions 23 | - ./src/CodeSigningHelper/bin/Debug/net6.0/CodeSigningHelper $UNSIGNED_BUCKET_NAME $SIGNED_BUCKET_NAME $WCF_EXTENSIONS_PATH $COREWCF_EXTENSIONS_PATH 24 | #create nuget packages 25 | - dotnet restore 26 | - dotnet pack -c Release --no-build ./src/AWS.WCF.Extensions --output . 27 | - dotnet pack -c Release --no-build ./src/AWS.CoreWCF.Extensions --output . 28 | artifacts: 29 | files: 30 | - '**/*.*nupkg' -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/ApplicationBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Amazon.SQS; 3 | using Amazon.SQS.Model; 4 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using NSubstitute; 8 | using Shouldly; 9 | using Xunit; 10 | 11 | namespace AWS.CoreWCF.Extensions.Tests; 12 | 13 | public class ApplicationBuilderExtensionsTests 14 | { 15 | [Fact] 16 | [ExcludeFromCodeCoverage] 17 | public void EnsureSqsQueueThrowsExceptionIfQueueIsNotInServiceProvider() 18 | { 19 | // ARRANGE 20 | const string fakeQueue = "fakeQueue"; 21 | 22 | var namedSqsClientCollection = new NamedSQSClientCollection( 23 | new NamedSQSClient { SQSClient = Substitute.For(), QueueName = "Not" + fakeQueue } 24 | ); 25 | 26 | var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection() 27 | .AddSingleton(namedSqsClientCollection) 28 | .BuildServiceProvider(); 29 | 30 | var fakeApplicationBuilder = Substitute.For(); 31 | fakeApplicationBuilder.ApplicationServices.Returns(services); 32 | 33 | Exception? expectedException = null; 34 | 35 | // ACT 36 | try 37 | { 38 | fakeApplicationBuilder.EnsureSqsQueue(fakeQueue, new CreateQueueRequest(fakeQueue)); 39 | } 40 | catch (Exception e) 41 | { 42 | expectedException = e; 43 | } 44 | 45 | // ASSERT 46 | expectedException.ShouldNotBeNull(); 47 | expectedException.ShouldBeOfType(); 48 | expectedException.Message.ShouldContain(fakeQueue); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sample/Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Amazon.Extensions.NETCore.Setup; 2 | using Amazon.SQS.Model; 3 | using AWS.CoreWCF.Extensions.Common; 4 | using AWS.CoreWCF.Extensions.SQS.Channels; 5 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 6 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 7 | using CoreWCF.Configuration; 8 | using CoreWCF.Queue.Common; 9 | using CoreWCF.Queue.Common.Configuration; 10 | 11 | namespace Server 12 | { 13 | public class Program 14 | { 15 | private static readonly string _queueName = "sample-sqs-queue"; 16 | 17 | public static void Main(string[] args) 18 | { 19 | var builder = WebApplication.CreateBuilder(args); 20 | 21 | // if needed, customize your aws credentials here, 22 | // otherwise it will default to searching ~\.aws 23 | var awsCredentials = new AWSOptions(); 24 | 25 | builder 26 | .Services.AddDefaultAWSOptions(awsCredentials) 27 | .AddServiceModelServices() 28 | .AddQueueTransport() 29 | .AddSQSClient(_queueName); 30 | 31 | var app = builder.Build(); 32 | 33 | var queueUrl = app.EnsureSqsQueue( 34 | _queueName, 35 | // optional callback for specifying how to create a queue 36 | // if it doesn't already exist 37 | createQueueRequest: new CreateQueueRequest().WithDeadLetterQueue() 38 | ); 39 | 40 | app.UseServiceModel(services => 41 | { 42 | services.AddService(); 43 | services.AddServiceEndpoint(new AwsSqsBinding(), queueUrl); 44 | }); 45 | 46 | app.Run(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/AWS.Extensions.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Always 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/TestService/ServiceContract/LoggingService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using CoreWCF; 3 | 4 | namespace AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 5 | 6 | internal static class Constants 7 | { 8 | public const string NS = "http://tempuri.org/"; 9 | public const string LOGGINGSERVICE_NAME = nameof(ILoggingService); 10 | public const string OPERATION_BASE = NS + LOGGINGSERVICE_NAME + "/"; 11 | } 12 | 13 | [System.ServiceModel.ServiceContract(Namespace = Constants.NS, Name = Constants.LOGGINGSERVICE_NAME)] 14 | [ServiceContract(Namespace = Constants.NS, Name = Constants.LOGGINGSERVICE_NAME)] 15 | public interface ILoggingService 16 | { 17 | [System.ServiceModel.OperationContract( 18 | Name = "LogMessage", 19 | Action = Constants.OPERATION_BASE + "LogMessage", 20 | IsOneWay = true 21 | )] 22 | [OperationContract(Name = "LogMessage", Action = Constants.OPERATION_BASE + "LogMessage", IsOneWay = true)] 23 | public void LogMessage(string toLog); 24 | 25 | [System.ServiceModel.OperationContract( 26 | Name = "CauseFailure", 27 | Action = Constants.OPERATION_BASE + "CauseFailure", 28 | IsOneWay = true 29 | )] 30 | [OperationContract(Name = "CauseFailure", Action = Constants.OPERATION_BASE + "CauseFailure", IsOneWay = true)] 31 | void CauseFailure(); 32 | } 33 | 34 | [ServiceBehavior(IncludeExceptionDetailInFaults = true)] 35 | [SimulateWorkBehavior] 36 | public class LoggingService : ILoggingService 37 | { 38 | public static readonly ConcurrentBag LogResults = new(); 39 | 40 | public void LogMessage(string toLog) 41 | { 42 | LogResults.Add(toLog); 43 | } 44 | 45 | public void CauseFailure() 46 | { 47 | throw new Exception("Trigger Failure"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/AwsSqsTransportTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using AWS.CoreWCF.Extensions.SQS.Channels; 3 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 4 | using CoreWCF.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using NSubstitute; 8 | using NSubstitute.ExceptionExtensions; 9 | using Shouldly; 10 | using Xunit; 11 | 12 | namespace AWS.CoreWCF.Extensions.Tests; 13 | 14 | /// 15 | /// Negative tests for 16 | /// 17 | public class AwsSqsTransportTests 18 | { 19 | [Fact] 20 | [ExcludeFromCodeCoverage] 21 | public async Task ReceiveQueueMessageContextRethrowsExceptions() 22 | { 23 | // ARRANGE 24 | var fakeException = new Exception("fake"); 25 | 26 | var mockSqsMessageProvider = Substitute.For(); 27 | 28 | mockSqsMessageProvider.ReceiveMessageAsync(Arg.Any()).ThrowsAsync(fakeException); 29 | 30 | var fakeServices = new ServiceCollection().AddSingleton(mockSqsMessageProvider).BuildServiceProvider(); 31 | 32 | var awsSqsTransport = new AwsSqsTransport( 33 | fakeServices, 34 | Substitute.For(), 35 | null, 36 | null, 37 | null, 38 | new Logger(Substitute.For()) 39 | ); 40 | 41 | Exception? expectedException = null; 42 | 43 | // ACT 44 | try 45 | { 46 | await awsSqsTransport.ReceiveQueueMessageContextAsync(CancellationToken.None); 47 | } 48 | catch (Exception e) 49 | { 50 | expectedException = e; 51 | } 52 | 53 | // ASSERT 54 | expectedException.ShouldNotBeNull(); 55 | expectedException.ShouldBe(fakeException); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/DispatchCallbacks/DispatchCallbacksCollection.cs: -------------------------------------------------------------------------------- 1 | using CoreWCF.Queue.Common; 2 | 3 | namespace AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 4 | 5 | public class DispatchCallbacksCollection : IDispatchCallbacksCollection 6 | { 7 | public NotificationDelegate NotificationDelegateForSuccessfulDispatch { get; set; } 8 | public NotificationDelegate NotificationDelegateForFailedDispatch { get; set; } 9 | 10 | public DispatchCallbacksCollection( 11 | Func? successfulDispatch = null, 12 | Func? failedDispatch = null 13 | ) 14 | { 15 | successfulDispatch ??= (_, _) => Task.CompletedTask; 16 | failedDispatch ??= (_, _) => Task.CompletedTask; 17 | 18 | NotificationDelegateForSuccessfulDispatch = new NotificationDelegate(successfulDispatch); 19 | NotificationDelegateForFailedDispatch = new NotificationDelegate(failedDispatch); 20 | } 21 | 22 | public DispatchCallbacksCollection( 23 | NotificationDelegate delegateForSuccessfulDispatch, 24 | NotificationDelegate delegateForFailedDispatch 25 | ) 26 | { 27 | NotificationDelegateForSuccessfulDispatch = delegateForSuccessfulDispatch; 28 | NotificationDelegateForFailedDispatch = delegateForFailedDispatch; 29 | } 30 | } 31 | 32 | public class DispatchCallbacksCollectionFactory 33 | { 34 | public static IDispatchCallbacksCollection GetDefaultCallbacksCollectionWithSns( 35 | string successTopicArn, 36 | string failureTopicArn 37 | ) 38 | { 39 | return new DispatchCallbacksCollection( 40 | DispatchCallbackFactory.GetDefaultSuccessNotificationCallbackWithSns(successTopicArn), 41 | DispatchCallbackFactory.GetDefaultFailureNotificationCallbackWithSns(failureTopicArn) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/Common/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Amazon.SQS; 3 | using AWS.CoreWCF.Extensions.Common; 4 | using Xunit; 5 | 6 | namespace AWS.Extensions.IntegrationTests.Common; 7 | 8 | [ExcludeFromCodeCoverage] 9 | public static class SqsAssert 10 | { 11 | public static async Task QueueIsEmpty( 12 | IAmazonSQS sqsClient, 13 | string queueName, 14 | int maxRetries = 3, 15 | int retryDelayInSeconds = 1 16 | ) 17 | { 18 | var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); 19 | var queueUrl = queueUrlResponse.QueueUrl; 20 | 21 | var queueIsEmpty = false; 22 | var attributesList = new List { "All" }; 23 | 24 | for (var attempt = 0; attempt < maxRetries; attempt++) 25 | { 26 | var response = await sqsClient.GetQueueAttributesAsync(queueUrl, attributesList); 27 | response.Validate(); 28 | 29 | var messageCounts = new List 30 | { 31 | response.ApproximateNumberOfMessages, 32 | response.ApproximateNumberOfMessagesNotVisible, 33 | response.ApproximateNumberOfMessagesDelayed 34 | }; 35 | queueIsEmpty = messageCounts.All(messageCount => messageCount == 0); 36 | if (queueIsEmpty) 37 | { 38 | break; 39 | } 40 | 41 | await Task.Delay(retryDelayInSeconds * 1000); 42 | } 43 | Assert.True(queueIsEmpty); 44 | } 45 | 46 | public static async Task ClearQueues(this IAmazonSQS sqsClient, params string[] queueNames) 47 | { 48 | foreach (var queue in queueNames) 49 | { 50 | var queueUrl = (await sqsClient.GetQueueUrlAsync(queue)).QueueUrl; 51 | await sqsClient.PurgeQueueAsync(queueUrl); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/AWS.WCF.Extensions.Tests/AwsSqsTransportBindingElementTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Xml; 3 | using System.Xml.Linq; 4 | using AWS.WCF.Extensions.SQS; 5 | using Shouldly; 6 | using Xunit; 7 | 8 | namespace AWS.WCF.Extensions.Tests 9 | { 10 | /// 11 | /// Negative tests for 12 | /// 13 | [ExcludeFromCodeCoverage] 14 | public class AwsSqsTransportBindingElementTests 15 | { 16 | [Fact] 17 | public void GetPropertyThrowsArgumentNullException() 18 | { 19 | // ARRANGE 20 | var element = new AWS.WCF.Extensions.SQS.AwsSqsTransportBindingElement(null, null); 21 | 22 | Exception? expectedException = null; 23 | 24 | // ACT 25 | try 26 | { 27 | element.GetProperty(null); 28 | } 29 | catch (Exception e) 30 | { 31 | expectedException = e; 32 | } 33 | 34 | // ASSERT 35 | expectedException.ShouldNotBeNull(); 36 | expectedException.ShouldBeOfType(); 37 | } 38 | 39 | [Fact] 40 | public void BuildChannelFactoryThrowsArgumentNullException() 41 | { 42 | // ARRANGE 43 | var element = new AWS.WCF.Extensions.SQS.AwsSqsTransportBindingElement(null, null); 44 | 45 | Exception? expectedException = null; 46 | 47 | // ACT 48 | try 49 | { 50 | element.BuildChannelFactory(null); 51 | } 52 | catch (Exception e) 53 | { 54 | expectedException = e; 55 | } 56 | 57 | // ASSERT 58 | expectedException.ShouldNotBeNull(); 59 | expectedException.ShouldBeOfType(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Channels/AwsSqsBinding.cs: -------------------------------------------------------------------------------- 1 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 2 | using CoreWCF.Channels; 3 | using CoreWCF.Configuration; 4 | 5 | namespace AWS.CoreWCF.Extensions.SQS.Channels; 6 | 7 | /// 8 | /// Constructs a new that uses Amazon SQS as a transport 9 | /// and can be registered in 10 | /// 11 | /// 12 | public class AwsSqsBinding : Binding 13 | { 14 | private readonly TextMessageEncodingBindingElement _encoding; 15 | private readonly AwsSqsTransportBindingElement _transport; 16 | 17 | /// 18 | /// Maximum number of workers polling the queue for messages 19 | public AwsSqsBinding(int concurrencyLevel = 1) 20 | { 21 | Name = nameof(AwsSqsBinding); 22 | Namespace = "https://schemas.aws.sqs.com/2007/sqs/"; 23 | 24 | _encoding = new TextMessageEncodingBindingElement(); 25 | 26 | _transport = new AwsSqsTransportBindingElement(concurrencyLevel); 27 | } 28 | 29 | public override BindingElementCollection CreateBindingElements() => new() { _encoding, _transport }; 30 | 31 | /// 32 | public long MaxMessageSize 33 | { 34 | get => _transport.MaxReceivedMessageSize; 35 | set => _transport.MaxReceivedMessageSize = value; 36 | } 37 | 38 | /// 39 | public IDispatchCallbacksCollection DispatchCallbacksCollection 40 | { 41 | get => _transport.DispatchCallbacksCollection; 42 | set => _transport.DispatchCallbacksCollection = value; 43 | } 44 | 45 | /// 46 | public override string Scheme => _transport.Scheme; 47 | } 48 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/Common/ServerFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using AWS.CoreWCF.Extensions.SQS.Channels; 3 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 4 | using AWS.Extensions.IntegrationTests.SQS.TestService; 5 | using CoreWCF.Configuration; 6 | using CoreWCF.Queue.Common.Configuration; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Logging.Abstractions; 11 | 12 | namespace AWS.Extensions.PerformanceTests.Common; 13 | 14 | [ExcludeFromCodeCoverage] 15 | public static class ServerFactory 16 | { 17 | public static IWebHost StartServer(string queueName, string queueUrl, AwsSqsBinding binding) 18 | where TService : class => StartServer((queueName, queueUrl, binding)); 19 | 20 | public static IWebHost StartServer( 21 | params (string queueName, string queueUrl, AwsSqsBinding binding)[] queueBindingPairs 22 | ) 23 | where TService : class 24 | { 25 | return ServiceHelper 26 | .CreateServiceHost( 27 | configureServices: services => 28 | { 29 | services.AddServiceModelServices().AddSingleton(NullLogger.Instance).AddQueueTransport(); 30 | 31 | foreach (var pair in queueBindingPairs) 32 | services.AddSQSClient(pair.queueName); 33 | }, 34 | configure: app => 35 | { 36 | app.UseServiceModel(svc => 37 | { 38 | svc.AddService(); 39 | 40 | foreach (var pair in queueBindingPairs) 41 | svc.AddServiceEndpoint(pair.binding, pair.queueUrl); 42 | }); 43 | } 44 | ) 45 | .Build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Channels/AwsSqsReceiveContext.cs: -------------------------------------------------------------------------------- 1 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 2 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 3 | using CoreWCF.Channels; 4 | 5 | namespace AWS.CoreWCF.Extensions.SQS.Channels; 6 | 7 | internal class AwsSqsReceiveContext : ReceiveContext 8 | { 9 | private readonly IServiceProvider _services; 10 | 11 | private readonly IDispatchCallbacksCollection? _dispatchCallbacksCollection; 12 | private readonly ISQSMessageProvider _sqsMessageProvider; 13 | private readonly string _queueName; 14 | private readonly AwsSqsMessageContext _sqsMessageContext; 15 | 16 | public AwsSqsReceiveContext( 17 | IServiceProvider services, 18 | IDispatchCallbacksCollection? dispatchCallbacksCollection, 19 | ISQSMessageProvider sqsMessageProvider, 20 | string queueName, 21 | AwsSqsMessageContext sqsMessageContext 22 | ) 23 | { 24 | _services = services; 25 | _dispatchCallbacksCollection = dispatchCallbacksCollection; 26 | _sqsMessageProvider = sqsMessageProvider; 27 | _queueName = queueName; 28 | _sqsMessageContext = sqsMessageContext; 29 | } 30 | 31 | protected override async Task OnCompleteAsync(CancellationToken token) 32 | { 33 | var notificationCallback = _dispatchCallbacksCollection?.NotificationDelegateForSuccessfulDispatch; 34 | 35 | if (null != notificationCallback) 36 | await notificationCallback.Invoke(_services, _sqsMessageContext); 37 | 38 | await _sqsMessageProvider.DeleteSqsMessageAsync(_queueName, _sqsMessageContext.MessageReceiptHandle); 39 | } 40 | 41 | protected override async Task OnAbandonAsync(CancellationToken token) 42 | { 43 | var notificationCallback = _dispatchCallbacksCollection?.NotificationDelegateForFailedDispatch; 44 | 45 | if (null != notificationCallback) 46 | await notificationCallback.Invoke(_services, _sqsMessageContext); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sample/AWSCoreWCFSample.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{6B151B0E-9352-4916-8E43-DFE29AD1D4C8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{BFBCD8CE-A3A1-4593-8A14-68BAC0C0270D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{A989A278-BD5E-4DEB-8298-A623E69D5C2D}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {6B151B0E-9352-4916-8E43-DFE29AD1D4C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {6B151B0E-9352-4916-8E43-DFE29AD1D4C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {6B151B0E-9352-4916-8E43-DFE29AD1D4C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {6B151B0E-9352-4916-8E43-DFE29AD1D4C8}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {BFBCD8CE-A3A1-4593-8A14-68BAC0C0270D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {BFBCD8CE-A3A1-4593-8A14-68BAC0C0270D}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {BFBCD8CE-A3A1-4593-8A14-68BAC0C0270D}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {BFBCD8CE-A3A1-4593-8A14-68BAC0C0270D}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {A989A278-BD5E-4DEB-8298-A623E69D5C2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A989A278-BD5E-4DEB-8298-A623E69D5C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A989A278-BD5E-4DEB-8298-A623E69D5C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A989A278-BD5E-4DEB-8298-A623E69D5C2D}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {89E24F5B-FB62-434B-A283-5A47C62C3CF3} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /test/AWS.WCF.Extensions.Tests/SqsChannelFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel.Channels; 3 | using AWS.WCF.Extensions.SQS; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace AWS.WCF.Extensions.Tests; 8 | 9 | /// 10 | /// Negative tests for 11 | /// 12 | public class SqsChannelFactoryTests 13 | { 14 | [Fact] 15 | [ExcludeFromCodeCoverage] 16 | public void ThrowsExceptionIfTooManyEncodingElements() 17 | { 18 | // ARRANGE 19 | var element = new AWS.WCF.Extensions.SQS.AwsSqsTransportBindingElement(null, null); 20 | 21 | var badBindingContext = new BindingContext( 22 | new CustomBinding("binding", "ns"), 23 | new BindingParameterCollection 24 | { 25 | new TextMessageEncodingBindingElement(), 26 | new BinaryMessageEncodingBindingElement() 27 | } 28 | ); 29 | 30 | Exception? expectedException = null; 31 | 32 | // ACT 33 | try 34 | { 35 | element.BuildChannelFactory(badBindingContext); 36 | } 37 | catch (Exception e) 38 | { 39 | expectedException = e; 40 | } 41 | 42 | // ASSERT 43 | expectedException.ShouldNotBeNull(); 44 | expectedException.ShouldBeOfType(); 45 | expectedException.Message.ShouldContain("More than one"); 46 | } 47 | 48 | [Fact] 49 | public void GetPropertyDefersToMessageEncoderFactoryForMessageVersion() 50 | { 51 | // ARRANGE 52 | var element = new AWS.WCF.Extensions.SQS.AwsSqsTransportBindingElement(null, null); 53 | 54 | var sqsChannelFactory = element.BuildChannelFactory( 55 | new BindingContext(new CustomBinding("binding", "ns"), new BindingParameterCollection()) 56 | ); 57 | 58 | // ACT 59 | var messageVersion = sqsChannelFactory.GetProperty(); 60 | 61 | // ASSERT 62 | messageVersion.ShouldNotBeNull(); 63 | messageVersion.Addressing?.ToString().ShouldNotBeNullOrEmpty(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | porting-assistant-nuget: 4 | type: nuget-feed 5 | url: https://s3-us-west-2.amazonaws.com/aws.portingassistant.dotnet.download/nuget/index.json 6 | nuget-org: 7 | type: nuget-feed 8 | url: https://api.nuget.org/v3/index.json 9 | updates: 10 | - package-ecosystem: "nuget" 11 | directory: "/src/PortingAssistant.Client.Analysis" 12 | registries: 13 | - porting-assistant-nuget 14 | - nuget-org 15 | schedule: 16 | interval: "weekly" 17 | - package-ecosystem: "nuget" 18 | directory: "/src/PortingAssistant.Client.Client" 19 | registries: 20 | - porting-assistant-nuget 21 | - nuget-org 22 | schedule: 23 | interval: "weekly" 24 | - package-ecosystem: "nuget" 25 | directory: "/src/PortingAssistant.Client.Common" 26 | registries: 27 | - porting-assistant-nuget 28 | - nuget-org 29 | schedule: 30 | interval: "weekly" 31 | - package-ecosystem: "nuget" 32 | directory: "/src/PortingAssistant.Client.NuGet" 33 | registries: 34 | - porting-assistant-nuget 35 | - nuget-org 36 | schedule: 37 | interval: "weekly" 38 | - package-ecosystem: "nuget" 39 | directory: "/src/PortingAssistant.Client.Porting" 40 | registries: 41 | - porting-assistant-nuget 42 | - nuget-org 43 | schedule: 44 | interval: "weekly" 45 | - package-ecosystem: "nuget" 46 | directory: "/src/PortingAssistant.Client.Telemetry" 47 | registries: 48 | - porting-assistant-nuget 49 | - nuget-org 50 | schedule: 51 | interval: "weekly" 52 | - package-ecosystem: "nuget" 53 | directory: "/src/PortingAssistant.Client" 54 | registries: 55 | - porting-assistant-nuget 56 | - nuget-org 57 | schedule: 58 | interval: "weekly" 59 | - package-ecosystem: "nuget" 60 | directory: "/tests/PortingAssistant.Client.IntegrationTests" 61 | registries: 62 | - porting-assistant-nuget 63 | - nuget-org 64 | schedule: 65 | interval: "weekly" 66 | - package-ecosystem: "nuget" 67 | directory: "/tests/PortingAssistant.Client.UnitTests" 68 | registries: 69 | - porting-assistant-nuget 70 | - nuget-org 71 | schedule: 72 | interval: "weekly" -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/AwsSqsBinding.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel.Channels; 3 | using Amazon.SQS; 4 | 5 | namespace AWS.WCF.Extensions.SQS; 6 | 7 | /// 8 | /// Creates a new a WCF Client 9 | /// can use to send messages to a CoreWCF Service using 10 | /// Amazon SQS as a transport. 11 | /// 12 | public class AwsSqsBinding : Binding 13 | { 14 | [ExcludeFromCodeCoverage] 15 | public override string Scheme => SqsConstants.Scheme; 16 | 17 | /// 18 | /// Url of the queue 19 | /// 20 | public string QueueUrl { get; } 21 | 22 | /// 23 | /// Gets the encoding binding element 24 | /// 25 | public TextMessageEncodingBindingElement? Encoding { get; } 26 | 27 | /// 28 | /// Gets the SQS transport binding element 29 | /// 30 | public AwsSqsTransportBindingElement? Transport { get; } 31 | 32 | /// 33 | /// 34 | /// A fully constructed client. 35 | /// For more details on how to construct an sqs client, see 36 | /// https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/sqs-apis-intro.html 37 | /// 38 | /// 39 | /// The name of the Amazon SQS Queue to use as a transport. 40 | /// 41 | /// 42 | /// 43 | public AwsSqsBinding( 44 | IAmazonSQS sqsClient, 45 | string queueName, 46 | long maxMessageSize = SqsDefaults.MaxSendMessageSize, 47 | long maxBufferPoolSize = SqsDefaults.MaxBufferPoolSize 48 | ) 49 | { 50 | QueueUrl = sqsClient.GetQueueUrlAsync(queueName).Result.QueueUrl; 51 | Transport = new AwsSqsTransportBindingElement(sqsClient, QueueUrl, maxMessageSize, maxBufferPoolSize); 52 | Encoding = new TextMessageEncodingBindingElement(); 53 | } 54 | 55 | public override BindingElementCollection CreateBindingElements() 56 | { 57 | var bindingElementCollection = new BindingElementCollection { Encoding, Transport }; 58 | return bindingElementCollection.Clone(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "dotnet run --project cdk/AWS.CoreWCF.ServerExtensions.Cdk.csproj", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "src/*/obj", 11 | "src/*/bin", 12 | "src/*.sln", 13 | "src/*/GlobalSuppressions.cs", 14 | "src/*/*.csproj" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/TestService/ServiceContract/SimulateWorkBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using AWS.CoreWCF.Extensions.SQS.Channels; 3 | using CoreWCF; 4 | using CoreWCF.Channels; 5 | using CoreWCF.Description; 6 | using CoreWCF.Dispatcher; 7 | 8 | namespace AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract 9 | { 10 | public class SimulateWorkMessageInspector : IDispatchMessageInspector 11 | { 12 | public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) 13 | { 14 | // simulate auth, routing, etc work being done 15 | Thread.Sleep(TimeSpan.FromMilliseconds(25)); 16 | 17 | return null; 18 | } 19 | 20 | public void BeforeSendReply(ref Message reply, object correlationState) { } 21 | } 22 | 23 | /// 24 | /// Simulates auth, routing, etc work that would be done in a real-world system. Importantly, 25 | /// this work is done on the Message Pump before the Message is dispatched onto a new thread 26 | /// by the Service Dispatch pipeline. 27 | /// 28 | /// Using this allows testing of the impact of setting 29 | /// . 30 | /// 31 | [AttributeUsage(AttributeTargets.Class)] 32 | public class SimulateWorkBehavior : Attribute, IServiceBehavior 33 | { 34 | public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) 35 | { 36 | var endpoints = serviceHostBase 37 | .ChannelDispatchers.OfType() 38 | .SelectMany(dispatcher => dispatcher.Endpoints); 39 | 40 | foreach (var endpointDispatcher in endpoints) 41 | { 42 | endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new SimulateWorkMessageInspector()); 43 | } 44 | } 45 | 46 | #region Unimplemented IServiceBevahior methods 47 | 48 | void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } 49 | 50 | void IServiceBehavior.AddBindingParameters( 51 | ServiceDescription serviceDescription, 52 | ServiceHostBase serviceHostBase, 53 | Collection endpoints, 54 | BindingParameterCollection bindingParameters 55 | ) { } 56 | 57 | #endregion 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/AwsSqsTransportBindingElement.cs: -------------------------------------------------------------------------------- 1 | using System.ServiceModel.Channels; 2 | using Amazon.SQS; 3 | 4 | namespace AWS.WCF.Extensions.SQS; 5 | 6 | public class AwsSqsTransportBindingElement : TransportBindingElement 7 | { 8 | public override string Scheme => SqsConstants.Scheme; 9 | 10 | public IAmazonSQS SqsClient { get; set; } 11 | 12 | public string QueueName { get; set; } 13 | 14 | public override long MaxReceivedMessageSize { get; set; } 15 | 16 | /// 17 | /// Creates a new instance of the AwsSqsTransportBindingElement class 18 | /// 19 | /// Client used for accessing the queue 20 | /// Name of the queue 21 | /// The maximum message size in bytes for messages in the queue 22 | /// The maximum buffer pool size 23 | public AwsSqsTransportBindingElement( 24 | IAmazonSQS sqsClient, 25 | string queueName, 26 | long maxMessageSize = SqsDefaults.MaxSendMessageSize, 27 | long maxBufferPoolSize = SqsDefaults.MaxBufferPoolSize 28 | ) 29 | { 30 | SqsClient = sqsClient; 31 | QueueName = queueName; 32 | MaxReceivedMessageSize = maxMessageSize; 33 | MaxBufferPoolSize = maxBufferPoolSize; 34 | } 35 | 36 | protected AwsSqsTransportBindingElement(AwsSqsTransportBindingElement other) 37 | { 38 | SqsClient = other.SqsClient; 39 | QueueName = other.QueueName; 40 | MaxReceivedMessageSize = other.MaxReceivedMessageSize; 41 | MaxBufferPoolSize = other.MaxBufferPoolSize; 42 | } 43 | 44 | public override BindingElement Clone() 45 | { 46 | return new AwsSqsTransportBindingElement(this); 47 | } 48 | 49 | public override T GetProperty(BindingContext context) 50 | { 51 | if (context == null) 52 | throw new ArgumentNullException(nameof(context)); 53 | 54 | return context.GetInnerProperty(); 55 | } 56 | 57 | public override IChannelFactory BuildChannelFactory(BindingContext context) 58 | { 59 | if (context == null) 60 | throw new ArgumentNullException(nameof(context)); 61 | 62 | return (IChannelFactory)(object)new SqsChannelFactory(this, context); 63 | } 64 | 65 | /// 66 | /// Used by higher layers to determine what types of channel factories this 67 | /// binding element supports. Which in this case is just IOutputChannel. 68 | /// 69 | public override bool CanBuildChannelFactory(BindingContext context) 70 | { 71 | return (typeof(TChannel) == typeof(IOutputChannel)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/Common/ClientMessageGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.ServiceModel; 3 | using Amazon.SQS; 4 | using Amazon.SQS.Model; 5 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 6 | using NSubstitute; 7 | 8 | namespace AWS.Extensions.PerformanceTests.Common; 9 | 10 | public static class ClientMessageGenerator 11 | { 12 | public static async Task SaturateQueue( 13 | IAmazonSQS setupSqsClient, 14 | string queueName, 15 | string queueUrl, 16 | int numMessages = 1000 17 | ) 18 | { 19 | var message = $"{queueName}-Message"; 20 | 21 | var rawMessage = BuildRawClientMessage( 22 | queueUrl, 23 | loggingClient => loggingClient.LogMessage(message) 24 | ); 25 | 26 | for (var j = 0; j < numMessages / 10; j++) 27 | { 28 | var batchMessages = Enumerable 29 | .Range(0, 10) 30 | .Select(_ => new SendMessageBatchRequestEntry(Guid.NewGuid().ToString(), rawMessage)) 31 | .ToList(); 32 | 33 | await setupSqsClient!.SendMessageBatchAsync(queueUrl, batchMessages); 34 | } 35 | 36 | Console.WriteLine("Queue Saturation Complete"); 37 | } 38 | 39 | public static string BuildRawClientMessage(string queueUrl, Action clientAction) 40 | where TContract : class 41 | { 42 | var fakeQueueName = "fake"; 43 | var mockSqs = Substitute.For(); 44 | 45 | // intercept the call the client will make to SendMessageAsync and capture the SendMessageRequest 46 | SendMessageRequest? capturedSendMessageRequest = null; 47 | 48 | mockSqs 49 | .SendMessageAsync( 50 | Arg.Do(r => 51 | { 52 | capturedSendMessageRequest = r; 53 | }), 54 | Arg.Any() 55 | ) 56 | .Returns(Task.FromResult(new SendMessageResponse { HttpStatusCode = HttpStatusCode.OK })); 57 | 58 | mockSqs 59 | .GetQueueUrlAsync(Arg.Any()) 60 | .Returns(Task.FromResult(new GetQueueUrlResponse { QueueUrl = queueUrl })); 61 | 62 | var sqsBinding = new WCF.Extensions.SQS.AwsSqsBinding(mockSqs, fakeQueueName); 63 | var endpointAddress = new EndpointAddress(new Uri(sqsBinding.QueueUrl)); 64 | var factory = new ChannelFactory(sqsBinding, endpointAddress); 65 | var channel = factory.CreateChannel(); 66 | ((System.ServiceModel.Channels.IChannel)channel).Open(); 67 | 68 | var client = (TContract)channel; 69 | 70 | clientAction.Invoke(client); 71 | 72 | return capturedSendMessageRequest?.MessageBody ?? ""; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/AWS.WCF.Extensions.Tests/AmazonServiceExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Amazon; 3 | using Amazon.Runtime; 4 | using Amazon.Runtime.Internal; 5 | using Amazon.SQS; 6 | using Amazon.SQS.Model; 7 | using AWS.WCF.Extensions.Common; 8 | using Shouldly; 9 | using Xunit; 10 | 11 | namespace AWS.WCF.Extensions.Tests 12 | { 13 | /// 14 | /// Negative tests for 15 | /// 16 | public class AmazonServiceExtensionTests 17 | { 18 | private readonly AmazonSQSClient _sqsClient; 19 | private readonly WebServiceRequestEventArgs? _webServiceRequestEventArgs; 20 | private readonly RequestEventHandler? _eventHandler; 21 | 22 | public AmazonServiceExtensionTests() 23 | { 24 | // ARRANGE 25 | _sqsClient = new AmazonSQSClient(new AnonymousAWSCredentials(), RegionEndpoint.USWest2); 26 | 27 | _sqsClient.SetCustomUserAgentSuffix(); 28 | 29 | var request = new DefaultRequest(new CreateQueueRequest("dummy"), _sqsClient.GetType().Name); 30 | 31 | _webServiceRequestEventArgs = 32 | typeof(WebServiceRequestEventArgs) 33 | .GetMethod("Create", BindingFlags.NonPublic | BindingFlags.Static)! 34 | .Invoke(null, new object[] { request }) as WebServiceRequestEventArgs; 35 | 36 | _eventHandler = 37 | typeof(AmazonServiceClient) 38 | .GetField( 39 | "mBeforeRequestEvent", 40 | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField 41 | )! 42 | .GetValue(_sqsClient) as RequestEventHandler; 43 | } 44 | 45 | [Fact] 46 | public void DoesNotErrorOutWhenHeadersIsMissing() 47 | { 48 | // ARRANGE 49 | // done in constructor 50 | 51 | // ACT 52 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 53 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 54 | 55 | // ASSERT 56 | // no exception is thrown 57 | } 58 | 59 | [Fact] 60 | public void HeaderAdditionAlgorithmIsIdempotent() 61 | { 62 | // ARRANGE 63 | _webServiceRequestEventArgs!.Headers["User-Agent"] = "fake"; 64 | 65 | // ACT 66 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 67 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 68 | 69 | // ASSERT 70 | _webServiceRequestEventArgs 71 | .Headers["User-Agent"] 72 | .Split(" ") 73 | .Count(s => s.StartsWith("ft/corewcf")) 74 | .ShouldBe(1); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Channels/AwsSqsTransportBindingElement.cs: -------------------------------------------------------------------------------- 1 | using Amazon.Runtime.Internal.Util; 2 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 3 | using CoreWCF.Channels; 4 | using CoreWCF.Configuration; 5 | using CoreWCF.Queue.Common; 6 | using CoreWCF.Queue.Common.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AWS.CoreWCF.Extensions.SQS.Channels; 11 | 12 | public sealed class AwsSqsTransportBindingElement : QueueBaseTransportBindingElement 13 | { 14 | public const int DefaultMaxMessageSize = 262144; // Max size for SQS message is 262144 (2^18) 15 | 16 | /// 17 | /// Creates a new instance of the AwsSqsTransportBindingElement class 18 | /// 19 | /// Maximum number of workers polling the queue for messages 20 | public AwsSqsTransportBindingElement(int concurrencyLevel = 1) 21 | { 22 | ConcurrencyLevel = concurrencyLevel; 23 | MaxReceivedMessageSize = DefaultMaxMessageSize; 24 | } 25 | 26 | private AwsSqsTransportBindingElement(AwsSqsTransportBindingElement other) 27 | { 28 | DispatchCallbacksCollection = other.DispatchCallbacksCollection; 29 | ConcurrencyLevel = other.ConcurrencyLevel; 30 | MaxReceivedMessageSize = other.MaxReceivedMessageSize; 31 | } 32 | 33 | public override QueueTransportPump BuildQueueTransportPump(BindingContext context) 34 | { 35 | var services = context.BindingParameters.Find(); 36 | var serviceDispatcher = context.BindingParameters.Find(); 37 | var messageEncoding = context.Binding.Elements.Find().WriteEncoding; 38 | var queueName = serviceDispatcher 39 | .BaseAddress.ToString() 40 | .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) 41 | .Last(); 42 | 43 | var transport = new AwsSqsTransport( 44 | services, 45 | serviceDispatcher, 46 | queueName, 47 | messageEncoding, 48 | DispatchCallbacksCollection, 49 | services.GetService>(), 50 | ConcurrencyLevel 51 | ); 52 | 53 | return QueueTransportPump.CreateDefaultPump(transport); 54 | } 55 | 56 | public override int ConcurrencyLevel { get; } 57 | 58 | /// 59 | /// Gets the scheme used by the binding, https 60 | /// 61 | public override string Scheme => "http"; 62 | 63 | /// 64 | /// Contains the collection of callbacks available to be called after a message is dispatched 65 | /// 66 | public IDispatchCallbacksCollection DispatchCallbacksCollection { get; set; } 67 | 68 | public override BindingElement Clone() => new AwsSqsTransportBindingElement(this); 69 | } 70 | -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/AmazonServiceExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Amazon; 8 | using Amazon.Runtime; 9 | using Amazon.Runtime.Internal; 10 | using Amazon.Runtime.Internal.Auth; 11 | using Amazon.SQS; 12 | using Amazon.SQS.Model; 13 | using AWS.CoreWCF.Extensions.Common; 14 | using Shouldly; 15 | using Xunit; 16 | 17 | namespace AWS.CoreWCF.Extensions.Tests 18 | { 19 | /// 20 | /// Negative tests for 21 | /// 22 | public class AmazonServiceExtensionTests 23 | { 24 | private readonly AmazonSQSClient _sqsClient; 25 | private readonly WebServiceRequestEventArgs? _webServiceRequestEventArgs; 26 | private readonly RequestEventHandler? _eventHandler; 27 | 28 | public AmazonServiceExtensionTests() 29 | { 30 | // ARRANGE 31 | _sqsClient = new AmazonSQSClient(new AnonymousAWSCredentials(), RegionEndpoint.USWest2); 32 | 33 | _sqsClient.SetCustomUserAgentSuffix(); 34 | 35 | var request = new DefaultRequest(new CreateQueueRequest("dummy"), _sqsClient.GetType().Name); 36 | 37 | _webServiceRequestEventArgs = 38 | typeof(WebServiceRequestEventArgs) 39 | .GetMethod("Create", BindingFlags.NonPublic | BindingFlags.Static)! 40 | .Invoke(null, new object[] { request }) as WebServiceRequestEventArgs; 41 | 42 | _eventHandler = 43 | typeof(AmazonServiceClient) 44 | .GetField( 45 | "mBeforeRequestEvent", 46 | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField 47 | )! 48 | .GetValue(_sqsClient) as RequestEventHandler; 49 | } 50 | 51 | [Fact] 52 | public void DoesNotErrorOutWhenHeadersIsMissing() 53 | { 54 | // ARRANGE 55 | // done in constructor 56 | 57 | // ACT 58 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 59 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 60 | 61 | // ASSERT 62 | // no exception is thrown 63 | } 64 | 65 | [Fact] 66 | public void HeaderAdditionAlgorithmIsIdempotent() 67 | { 68 | // ARRANGE 69 | _webServiceRequestEventArgs!.Headers["User-Agent"] = "fake"; 70 | 71 | // ACT 72 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 73 | _eventHandler!.Invoke(_sqsClient, _webServiceRequestEventArgs); 74 | 75 | // ASSERT 76 | _webServiceRequestEventArgs 77 | .Headers["User-Agent"] 78 | .Split(" ") 79 | .Count(s => s.StartsWith("ft/corewcf")) 80 | .ShouldBe(1); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/SqsChannelFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel; 3 | using System.ServiceModel.Channels; 4 | using AWS.WCF.Extensions.SQS.Runtime; 5 | 6 | namespace AWS.WCF.Extensions.SQS; 7 | 8 | public class SqsChannelFactory : ChannelFactoryBase 9 | { 10 | private readonly AwsSqsTransportBindingElement _bindingElement; 11 | public BufferManager BufferManager { get; } 12 | public MessageEncoderFactory MessageEncoderFactory { get; } 13 | 14 | internal SqsChannelFactory(AwsSqsTransportBindingElement bindingElement, BindingContext context) 15 | : base(context.Binding) 16 | { 17 | _bindingElement = bindingElement; 18 | BufferManager = BufferManager.CreateBufferManager(bindingElement.MaxBufferPoolSize, int.MaxValue); 19 | 20 | IEnumerable messageEncoderBindingElements = context 21 | .BindingParameters.OfType() 22 | .ToList(); 23 | 24 | if (messageEncoderBindingElements.Count() > 1) 25 | { 26 | throw new InvalidOperationException( 27 | "More than one MessageEncodingBindingElement was found in the BindingParameters of the BindingContext" 28 | ); 29 | } 30 | 31 | MessageEncoderFactory = messageEncoderBindingElements.Any() 32 | ? messageEncoderBindingElements.First().CreateMessageEncoderFactory() 33 | : SqsConstants.DefaultMessageEncoderFactory; 34 | } 35 | 36 | public override T GetProperty() 37 | { 38 | if (typeof(T) == typeof(MessageVersion)) 39 | return (T)(object)MessageEncoderFactory.Encoder.MessageVersion; 40 | 41 | return MessageEncoderFactory.Encoder.GetProperty() ?? base.GetProperty(); 42 | } 43 | 44 | /// 45 | /// Create a new Udp Channel. Supports IOutputChannel. 46 | /// 47 | /// The address of the remote endpoint 48 | /// 49 | protected override IOutputChannel OnCreateChannel(EndpointAddress queueUrl, Uri via) 50 | { 51 | return new SqsOutputChannel(this, _bindingElement.SqsClient, queueUrl, via, MessageEncoderFactory.Encoder); 52 | } 53 | 54 | /// 55 | /// Open the channel for use. We do not have any blocking work to perform so this is a no-op 56 | /// 57 | protected override void OnOpen(TimeSpan timeout) { } 58 | 59 | [ExcludeFromCodeCoverage] 60 | protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) 61 | { 62 | return Task.CompletedTask.ToApm(callback, state); 63 | } 64 | 65 | [ExcludeFromCodeCoverage] 66 | protected override void OnEndOpen(IAsyncResult result) 67 | { 68 | result.ToApmEnd(); 69 | } 70 | 71 | [ExcludeFromCodeCoverage] 72 | protected override void OnClosed() 73 | { 74 | base.OnClosed(); 75 | BufferManager.Clear(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Channels/AwsSqsTransport.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using System.Text; 3 | using Amazon.SQS.Model; 4 | using AWS.CoreWCF.Extensions.Common; 5 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 6 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 7 | using CoreWCF; 8 | using CoreWCF.Configuration; 9 | using CoreWCF.Queue.Common; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace AWS.CoreWCF.Extensions.SQS.Channels; 14 | 15 | internal class AwsSqsTransport : IQueueTransport 16 | { 17 | private readonly ILogger _logger; 18 | 19 | private readonly IServiceProvider _services; 20 | private readonly Uri _baseAddress; 21 | private readonly string _queueName; 22 | private readonly Encoding _encoding; 23 | private readonly IDispatchCallbacksCollection _dispatchCallbacksCollection; 24 | private readonly ISQSMessageProvider _sqsMessageProvider; 25 | 26 | public int ConcurrencyLevel { get; } 27 | 28 | public AwsSqsTransport( 29 | IServiceProvider services, 30 | IServiceDispatcher serviceDispatcher, 31 | string queueName, 32 | Encoding encoding, 33 | IDispatchCallbacksCollection dispatchCallbacksCollection, 34 | ILogger logger, 35 | int concurrencyLevel = 1 36 | ) 37 | { 38 | _services = services; 39 | _baseAddress = serviceDispatcher.BaseAddress; 40 | _queueName = queueName; 41 | _encoding = encoding; 42 | ConcurrencyLevel = concurrencyLevel; 43 | _dispatchCallbacksCollection = dispatchCallbacksCollection; 44 | _logger = logger; 45 | _sqsMessageProvider = _services.GetRequiredService(); 46 | } 47 | 48 | public async ValueTask ReceiveQueueMessageContextAsync(CancellationToken cancellationToken) 49 | { 50 | try 51 | { 52 | var sqsMessage = await _sqsMessageProvider.ReceiveMessageAsync(_queueName).ConfigureAwait(false); 53 | 54 | if (sqsMessage is null) 55 | { 56 | return null; 57 | } 58 | 59 | var queueMessageContext = GetContext(sqsMessage); 60 | return queueMessageContext; 61 | } 62 | catch (Exception e) 63 | { 64 | _logger.LogCritical(e, $"Failed starting Queue Message Context: {e.Message}"); 65 | 66 | throw; 67 | } 68 | } 69 | 70 | private QueueMessageContext GetContext(Message sqsMessage) 71 | { 72 | var reader = PipeReader.Create(sqsMessage.Body.ToStream(_encoding)); 73 | var receiptHandle = sqsMessage.ReceiptHandle; 74 | 75 | var context = new AwsSqsMessageContext 76 | { 77 | QueueMessageReader = reader, 78 | LocalAddress = new EndpointAddress(_baseAddress), 79 | MessageReceiptHandle = receiptHandle 80 | }; 81 | 82 | context.ReceiveContext = new AwsSqsReceiveContext( 83 | _services, 84 | _dispatchCallbacksCollection, 85 | _sqsMessageProvider, 86 | _queueName, 87 | context 88 | ); 89 | 90 | return context; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/TestService/ServiceHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Net; 4 | using AWS.Extensions.IntegrationTests.Common; 5 | using Microsoft.AspNetCore; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Xunit.Abstractions; 11 | 12 | namespace AWS.Extensions.IntegrationTests.SQS.TestService; 13 | 14 | [ExcludeFromCodeCoverage] 15 | public static class ServiceHelper 16 | { 17 | /// 18 | /// Takes care of the plumbing to initialize a Test WebServer 19 | /// for Integration Tests 20 | /// 21 | /// 22 | /// Equivalent to the Startup classes ConfigureServices( services) method. 23 | /// 24 | /// 25 | /// Equivalent to the Startup classes Configure( app) method. 26 | /// 27 | /// 28 | /// Pass this in to wire up a 29 | /// 30 | /// 31 | public static IWebHostBuilder CreateServiceHost( 32 | Action configureServices, 33 | Action configure, 34 | ITestOutputHelper? testOutputHelper = null 35 | ) 36 | { 37 | return WebHost 38 | .CreateDefaultBuilder(Array.Empty()) 39 | .ConfigureServices(services => 40 | { 41 | services.AddSingleton(new ConfigureStartup { Configure = configure }); 42 | 43 | if (null != testOutputHelper) 44 | services.AddLogging(builder => 45 | { 46 | builder.AddProvider(new XUnitLoggingProvider(testOutputHelper)); 47 | }); 48 | 49 | configureServices(services); 50 | }) 51 | .UseKestrel(options => 52 | { 53 | options.Limits.MaxRequestBufferSize = null; 54 | options.Limits.MaxRequestBodySize = null; 55 | options.Limits.MaxResponseBufferSize = null; 56 | options.AllowSynchronousIO = true; 57 | options.Listen( 58 | IPAddress.Any, 59 | 8088, 60 | listenOptions => 61 | { 62 | if (Debugger.IsAttached) 63 | { 64 | listenOptions.UseConnectionLogging(); 65 | } 66 | } 67 | ); 68 | }) 69 | .UseStartup(); 70 | } 71 | 72 | private class InfrastructureTestStartup 73 | { 74 | public void ConfigureServices(IServiceCollection services) { } 75 | 76 | public void Configure(IApplicationBuilder app) 77 | { 78 | var configureStartup = app.ApplicationServices.GetRequiredService(); 79 | configureStartup.Configure?.Invoke(app); 80 | } 81 | } 82 | 83 | private class ConfigureStartup 84 | { 85 | public Action? Configure { get; set; } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/ServerSingleClientPerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Amazon.SQS; 3 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 4 | using AWS.Extensions.PerformanceTests.Common; 5 | using BenchmarkDotNet.Attributes; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace AWS.Extensions.PerformanceTests 9 | { 10 | /// 11 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 12 | [SimpleJob(launchCount: 0, warmupCount: 0, iterationCount: 1)] 13 | [ExcludeFromCodeCoverage] 14 | public class ServerSingleClientPerformanceTests 15 | { 16 | private IWebHost? _host; 17 | private IAmazonSQS? _setupSqsClient; 18 | private readonly string _queueName = $"{nameof(ServerSingleClientPerformanceTests)}-{DateTime.Now.Ticks}"; 19 | private string _queueUrl = ""; 20 | 21 | [Params(1, 4, 8)] 22 | public int Threads { get; set; } 23 | 24 | [GlobalSetup] 25 | public async Task CreateInfrastructure() 26 | { 27 | _setupSqsClient = new AmazonSQSClient(); 28 | 29 | await _setupSqsClient.CreateQueueAsync(_queueName); 30 | _queueUrl = (await _setupSqsClient!.GetQueueUrlAsync(_queueName))?.QueueUrl ?? ""; 31 | 32 | Console.WriteLine($"QueueName: {_queueName}"); 33 | } 34 | 35 | [IterationSetup] 36 | public void Setup() 37 | { 38 | LoggingService.LogResults.Clear(); 39 | 40 | StartupHost().Wait(); 41 | } 42 | 43 | public async Task StartupHost() 44 | { 45 | Console.WriteLine($"Begin {nameof(StartupHost)}"); 46 | 47 | #region Configure Host 48 | 49 | _host = ServerFactory.StartServer( 50 | _queueName, 51 | _queueUrl, 52 | new AWS.CoreWCF.Extensions.SQS.Channels.AwsSqsBinding(concurrencyLevel: Threads) 53 | ); 54 | 55 | #endregion 56 | 57 | #region Pre Saturate Queue 58 | 59 | await ClientMessageGenerator.SaturateQueue(_setupSqsClient, _queueName, _queueUrl); 60 | 61 | #endregion 62 | } 63 | 64 | [IterationCleanup] 65 | public void CleanupHost() 66 | { 67 | _host?.Dispose(); 68 | } 69 | 70 | [GlobalCleanup] 71 | public async Task CleanUp() 72 | { 73 | await _setupSqsClient!.DeleteQueueAsync(_queueUrl); 74 | } 75 | 76 | [Benchmark] 77 | public async Task ServerCanProcess1000Messages() 78 | { 79 | var maxTime = TimeSpan.FromMinutes(5); 80 | 81 | var cancelToken = new CancellationTokenSource(maxTime).Token; 82 | 83 | // start the server 84 | await _host!.StartAsync(cancelToken); 85 | 86 | // wait for server to process all messages 87 | while (LoggingService.LogResults.Count < 1000) 88 | { 89 | Console.WriteLine($"Processed [{LoggingService.LogResults.Count}] Messages"); 90 | 91 | await Task.Delay(TimeSpan.FromMilliseconds(250), cancelToken); 92 | } 93 | 94 | Console.WriteLine($"Processed [{LoggingService.LogResults.Count}] messages"); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/DispatchCallbacks/DispatchCallbacks.cs: -------------------------------------------------------------------------------- 1 | using Amazon.SimpleNotificationService; 2 | using Amazon.SimpleNotificationService.Model; 3 | using AWS.CoreWCF.Extensions.Common; 4 | using AWS.CoreWCF.Extensions.SQS.Channels; 5 | using CoreWCF.Queue.Common; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 9 | 10 | public delegate Task NotificationDelegate(IServiceProvider services, QueueMessageContext context); 11 | 12 | public static class DispatchCallbackFactory 13 | { 14 | public static NotificationDelegate GetDefaultSuccessNotificationCallbackWithSns(string topicArn) 15 | { 16 | async Task DefaultSuccessNotificationCallbackWithSns(IServiceProvider services, QueueMessageContext context) 17 | { 18 | var subject = "Message Dispatch Successful"; 19 | var sqsContext = context as AwsSqsMessageContext; 20 | var message = sqsContext is null 21 | ? $"{nameof(QueueMessageContext)} of type {nameof(AwsSqsMessageContext)} was expected but type of {context.GetType()} was received." 22 | : $"Succeeded to dispatch message to {sqsContext.LocalAddress} with receipt {sqsContext.MessageReceiptHandle}"; 23 | 24 | var publishRequest = new PublishRequest 25 | { 26 | TargetArn = topicArn, 27 | Subject = subject, 28 | Message = message 29 | }; 30 | await SendNotificationToSns(services, publishRequest); 31 | } 32 | 33 | return DefaultSuccessNotificationCallbackWithSns; 34 | } 35 | 36 | public static NotificationDelegate GetDefaultFailureNotificationCallbackWithSns(string topicArn) 37 | { 38 | async Task DefaultFailureNotificationCallbackWithSns(IServiceProvider services, QueueMessageContext context) 39 | { 40 | var subject = "Message Dispatch Failed"; 41 | var sqsContext = context as AwsSqsMessageContext; 42 | var message = sqsContext is null 43 | ? $"{nameof(QueueMessageContext)} of type {nameof(AwsSqsMessageContext)} was expected but type of {context.GetType()} was received." 44 | : $"Failed to dispatch message to {sqsContext.LocalAddress} with receipt {sqsContext.MessageReceiptHandle}"; 45 | 46 | var publishRequest = new PublishRequest 47 | { 48 | TargetArn = topicArn, 49 | Subject = subject, 50 | Message = message 51 | }; 52 | await SendNotificationToSns(services, publishRequest); 53 | } 54 | 55 | return DefaultFailureNotificationCallbackWithSns; 56 | } 57 | 58 | private static bool HasAddedCustomUserAgentSuffix; 59 | 60 | private static async Task SendNotificationToSns(IServiceProvider services, PublishRequest publishRequest) 61 | { 62 | try 63 | { 64 | var snsClient = services.GetRequiredService(); 65 | 66 | if (!HasAddedCustomUserAgentSuffix) 67 | { 68 | (snsClient as AmazonSimpleNotificationServiceClient)?.SetCustomUserAgentSuffix(); 69 | HasAddedCustomUserAgentSuffix = true; 70 | } 71 | 72 | var response = await snsClient.PublishAsync(publishRequest); 73 | 74 | response.Validate(); 75 | } 76 | catch (Exception e) 77 | { 78 | Console.WriteLine(e); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. 4 | Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. 7 | 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check [existing open](https://github.com/aws/aws-corewcf-extensions/issues) and [closed](https://github.com/aws/aws-corewcf-extensions/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. 14 | Please try to include as much information as you can. 15 | Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. 25 | Before starting a pull request, please ensure that: 26 | 27 | 1. You open an issue first to discuss any significant work - we would hate for your time to be wasted. 28 | 2. You are working against the latest source on the *main* branch. 29 | 3. You check existing [open](https://github.com/aws/aws-corewcf-extensions/pulls) and [merged](https://github.com/aws/aws-corewcf-extensions/pulls?q=is%3Apr+is%3Aclosed) pull requests to make sure someone else hasn't addressed the problem already. 30 | 31 | To send us a pull request, please: 32 | 33 | 1. Fork the repository. 34 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat the code, it will be hard for us to focus on your change. 35 | 3. Ensure local tests pass. 36 | 4. Commit to your fork using clear commit messages. 37 | 5. Send us a pull request, answering any default questions in the pull request interface. 38 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 39 | 40 | GitHub provides additional documentation on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Code of Conduct 44 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 45 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. 46 | 47 | 48 | ## Security issue notifications 49 | We take all security reports seriously. 50 | When we receive such reports, 51 | we will investigate and subsequently address 52 | any potential vulnerabilities as quickly as possible. 53 | 54 | If you discover a potential security issue in this project, 55 | please notify AWS/Amazon Security via our 56 | [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) 57 | or directly via email to [AWS Security](mailto:aws-security@amazon.com). 58 | Please do *not* create a public GitHub issue in this project. 59 | 60 | 61 | ## Licensing 62 | 63 | See the [COPYRIGHT](COPYRIGHT) file for our project's licensing. 64 | We will ask you to confirm the licensing of your contribution. 65 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/SnsCallbackIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.SQS.Model; 3 | using AWS.CoreWCF.Extensions.Common; 4 | using AWS.Extensions.IntegrationTests.SQS.TestHelpers; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace AWS.Extensions.IntegrationTests.SQS; 9 | 10 | [Collection("ClientAndServer collection")] 11 | public class SnsCallbackIntegrationTests : IDisposable 12 | { 13 | private readonly ITestOutputHelper _output; 14 | private readonly ClientAndServerFixture _clientAndServerFixture; 15 | 16 | public SnsCallbackIntegrationTests(ITestOutputHelper output, ClientAndServerFixture clientAndServerFixture) 17 | { 18 | _output = output; 19 | _clientAndServerFixture = clientAndServerFixture; 20 | 21 | AWSConfigs.InitializeCollections = true; 22 | } 23 | 24 | /// 25 | /// Tests SNS Attach 26 | /// 27 | /// Data flow: 28 | /// Test.Client -> Server.OnSuccess -> Settings.SuccessTopicArn -> CoreWCF.Success-q 29 | /// 30 | /// This requires manual infrastructure setup: 31 | /// - Create a SNS Topic (and save arn to Settings file) 32 | /// - Create a new Queue (CoreWCF.Success-q) 33 | /// - Create a SNS Topic Subscription so that CoreWCF.Success-q is notified. 34 | /// 35 | [Fact] 36 | public async Task ServerEmitsDefaultSnsEvent() 37 | { 38 | var coreWcfQueueName = nameof(ServerEmitsDefaultSnsEvent) + Guid.NewGuid(); 39 | var logMessage = coreWcfQueueName + "-LogMessage"; 40 | 41 | // Fixture will automatically setup SNS callbacks 42 | // as long as the appsettings.test.json is populated with valid 43 | // sns topic arns 44 | _clientAndServerFixture.Start( 45 | _output, 46 | coreWcfQueueName, 47 | new CreateQueueRequest(coreWcfQueueName).SetDefaultValues() 48 | ); 49 | 50 | var clientService = _clientAndServerFixture.Channel!; 51 | 52 | var successQueueUrl = ( 53 | await _clientAndServerFixture.SqsClient!.GetQueueUrlAsync( 54 | ClientAndServerFixture.SnsNotificationSuccessQueue 55 | ) 56 | ).QueueUrl; 57 | 58 | Assert.NotEmpty(successQueueUrl); 59 | 60 | // ACT 61 | clientService.LogMessage(logMessage); 62 | 63 | var receivedSuccessNotification = false; 64 | // poll for success messages (up to 20 seconds) 65 | for (var polling = 0; polling < 40; polling++) 66 | { 67 | var messages = await _clientAndServerFixture.SqsClient.ReceiveMessageAsync(successQueueUrl); 68 | 69 | // sns message body contains a message of the corewcf queue that originally 70 | // received the message 71 | if (messages.Messages.Any(m => m.Body.Contains(coreWcfQueueName))) 72 | { 73 | receivedSuccessNotification = true; 74 | break; 75 | } 76 | 77 | await Task.Delay(TimeSpan.FromMilliseconds(500)); 78 | } 79 | 80 | // ASSERT 81 | try 82 | { 83 | Assert.True(receivedSuccessNotification); 84 | } 85 | finally 86 | { 87 | var queueUrlResponse = await _clientAndServerFixture.SqsClient.GetQueueUrlAsync(coreWcfQueueName); 88 | await _clientAndServerFixture.SqsClient.DeleteQueueAsync(queueUrlResponse.QueueUrl); 89 | } 90 | } 91 | 92 | public void Dispose() 93 | { 94 | _clientAndServerFixture.Dispose(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/AWS.WCF.Extensions.Tests/SqsOutputChannelTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel; 3 | using System.ServiceModel.Channels; 4 | using Amazon.SQS; 5 | using AWS.WCF.Extensions.SQS; 6 | using NSubstitute; 7 | using Shouldly; 8 | using Xunit; 9 | 10 | namespace AWS.WCF.Extensions.Tests; 11 | 12 | /// 13 | /// Negative tests for 14 | /// 15 | public class SqsOutputChannelTests 16 | { 17 | private readonly SqsChannelFactory _sqsChannelFactory; 18 | 19 | public SqsOutputChannelTests() 20 | { 21 | _sqsChannelFactory = 22 | new AwsSqsTransportBindingElement(null, null).BuildChannelFactory( 23 | new BindingContext(new CustomBinding("binding", "ns"), new BindingParameterCollection()) 24 | ) as SqsChannelFactory; 25 | } 26 | 27 | [Fact] 28 | [ExcludeFromCodeCoverage] 29 | public void ConstructorThrowsArgumentException() 30 | { 31 | // ARRANGE 32 | var badVia = new Uri("http://bad"); 33 | Exception? expectedException = null; 34 | 35 | // ACT 36 | try 37 | { 38 | new SqsOutputChannel( 39 | _sqsChannelFactory, 40 | Substitute.For(), 41 | new EndpointAddress("http://fake"), 42 | badVia, 43 | null 44 | ); 45 | } 46 | catch (Exception e) 47 | { 48 | expectedException = e; 49 | } 50 | 51 | // ASSERT 52 | expectedException.ShouldNotBeNull(); 53 | expectedException.ShouldBeOfType(); 54 | expectedException.Message.ShouldContain("scheme"); 55 | } 56 | 57 | [Fact] 58 | public void ViaPropertyIsSet() 59 | { 60 | // ARRANGE 61 | IOutputChannel outputChannel = new SqsOutputChannel( 62 | _sqsChannelFactory, 63 | Substitute.For(), 64 | new EndpointAddress("http://fake"), 65 | via: new Uri("https://fake"), 66 | null 67 | ); 68 | 69 | // ACT 70 | var via = outputChannel.Via; 71 | 72 | // ASSERT 73 | via.ShouldNotBeNull(); 74 | } 75 | 76 | [Fact] 77 | public void GetPropertyReturnsSqsOutputChannel() 78 | { 79 | // ARRANGE 80 | IOutputChannel outputChannel = new SqsOutputChannel( 81 | _sqsChannelFactory, 82 | Substitute.For(), 83 | new EndpointAddress("http://fake"), 84 | via: new Uri("https://fake"), 85 | null 86 | ); 87 | 88 | // ACT 89 | var property = outputChannel.GetProperty(); 90 | 91 | // ASSERT 92 | property.ShouldBe(outputChannel); 93 | } 94 | 95 | [Fact] 96 | public void GetPropertyFallsBackToEncoder() 97 | { 98 | // ARRANGE 99 | var mockEncoder = Substitute.For(); 100 | 101 | IOutputChannel outputChannel = new SqsOutputChannel( 102 | _sqsChannelFactory, 103 | NSubstitute.Substitute.For(), 104 | new EndpointAddress("http://fake"), 105 | via: new Uri("https://fake"), 106 | mockEncoder 107 | ); 108 | 109 | // ACT 110 | outputChannel.GetProperty(); 111 | 112 | // ASSERT 113 | mockEncoder.Received().GetProperty(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Infrastructure/SQSServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.SQS; 2 | using AWS.CoreWCF.Extensions.Common; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace AWS.CoreWCF.Extensions.SQS.Infrastructure; 6 | 7 | public static class SQSServiceCollectionExtensions 8 | { 9 | /// 10 | /// 11 | /// container. 12 | /// 13 | /// 14 | /// Names of the Amazon SQS Queues that this CoreWCF Server 15 | /// will listen to. 16 | /// 17 | /// 18 | /// Optional function to build the client. This is useful 19 | /// in cases where you need to provide specific credentials or otherwise customize 20 | /// the client object. 21 | /// 22 | public static IServiceCollection AddSQSClient( 23 | this IServiceCollection services, 24 | string queueName, 25 | Func? sqsClientBuilder = null 26 | ) 27 | { 28 | var queueNames = new List { queueName }; 29 | return AddSQSClient(services, queueNames, sqsClientBuilder); 30 | } 31 | 32 | /// 33 | /// Registers an client for use by a CoreWCF server and lists the 34 | /// queues the client will listen to. 35 | /// 36 | /// This method can be invoked multiple times to register multiple clients. See the README.md 37 | /// for more information on how multiple clients impact performance. 38 | /// 39 | /// 40 | /// container. 41 | /// 42 | /// 43 | /// The names of one or more Amazon SQS Queues that this CoreWCF Server 44 | /// will listen to. 45 | /// 46 | /// 47 | /// Optional function to build the client. This is useful 48 | /// in cases where you need to provide specific credentials or otherwise customize 49 | /// the client object. 50 | /// 51 | public static IServiceCollection AddSQSClient( 52 | this IServiceCollection services, 53 | IEnumerable queueNames, 54 | Func? sqsClientBuilder = null 55 | ) 56 | { 57 | if (null == sqsClientBuilder) 58 | { 59 | services.AddAWSService(); 60 | sqsClientBuilder = sp => sp.GetService(); 61 | } 62 | 63 | AddAmazonSQSClient(services, queueNames, sqsClientBuilder); 64 | return services; 65 | } 66 | 67 | private static void AddAmazonSQSClient( 68 | IServiceCollection services, 69 | IEnumerable queueNames, 70 | Func sqsClientBuilder 71 | ) 72 | { 73 | services.AddTransient(); 74 | 75 | services.AddSingleton(serviceProvider => 76 | { 77 | var sqsClient = sqsClientBuilder(serviceProvider); 78 | 79 | (sqsClient as AmazonSQSClient)?.SetCustomUserAgentSuffix(); 80 | 81 | var namedSqsClients = new NamedSQSClientCollection( 82 | queueNames.Select(queueName => new NamedSQSClient { SQSClient = sqsClient, QueueName = queueName }) 83 | ); 84 | 85 | return namedSqsClients; 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/ClientPerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel; 3 | using Amazon.SQS; 4 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 5 | using BenchmarkDotNet.Attributes; 6 | 7 | namespace AWS.Extensions.PerformanceTests 8 | { 9 | /// 10 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 11 | [SimpleJob(launchCount: 1, warmupCount: 0, iterationCount: 2)] 12 | [ExcludeFromCodeCoverage] 13 | public class ClientPerformanceTests 14 | { 15 | [Params(2, 4, 8)] 16 | public int Threads { get; set; } 17 | 18 | private ILoggingService[] _clients = Array.Empty(); 19 | private IAmazonSQS? _setupSqsClient; 20 | private readonly string _queueName = $"{nameof(ClientPerformanceTests)}-{DateTime.Now.Ticks}"; 21 | 22 | [GlobalSetup] 23 | public async Task CreateAllClients() 24 | { 25 | _setupSqsClient = new AmazonSQSClient(); 26 | 27 | await _setupSqsClient.CreateQueueAsync(_queueName); 28 | 29 | Console.WriteLine($"QueueName: {_queueName}"); 30 | 31 | _clients = Enumerable 32 | .Range(0, Threads) 33 | .AsParallel() 34 | .Select(x => 35 | { 36 | Console.WriteLine($"Creating Client [{x}]"); 37 | var sqsClient = new AmazonSQSClient(); 38 | 39 | var sqsBinding = new AWS.WCF.Extensions.SQS.AwsSqsBinding(sqsClient, _queueName); 40 | var endpointAddress = new EndpointAddress(new Uri(sqsBinding.QueueUrl)); 41 | var factory = new ChannelFactory(sqsBinding, endpointAddress); 42 | var channel = factory.CreateChannel(); 43 | ((System.ServiceModel.Channels.IChannel)channel).Open(); 44 | 45 | return (channel as ILoggingService); 46 | }) 47 | .ToArray(); 48 | 49 | Console.WriteLine("Created all Clients"); 50 | } 51 | 52 | [GlobalCleanup] 53 | public async Task CleanUp() 54 | { 55 | foreach (var client in _clients) 56 | { 57 | try 58 | { 59 | (client as System.ServiceModel.Channels.IChannel)?.Close(); 60 | } 61 | // ReSharper disable once EmptyGeneralCatchClause 62 | catch { } 63 | } 64 | 65 | var queueUrlDetails = await _setupSqsClient!.GetQueueUrlAsync(_queueName); 66 | await _setupSqsClient.DeleteQueueAsync(queueUrlDetails.QueueUrl); 67 | } 68 | 69 | [Benchmark] 70 | public async Task ClientCanWrite1000Messages() 71 | { 72 | var numberOfMessagesPerThread = 1000 / Threads; 73 | 74 | var constMessage = $"Client Perf Message: {DateTime.Now.Ticks}"; 75 | 76 | Console.WriteLine($"Begin sending message: [{constMessage}]"); 77 | 78 | var tasks = Enumerable 79 | .Range(0, Threads) 80 | .Select(id => 81 | Task.Factory.StartNew(() => 82 | { 83 | var client = _clients[id]; 84 | 85 | for (var i = 0; i < numberOfMessagesPerThread; i++) 86 | { 87 | client.LogMessage(constMessage); 88 | } 89 | 90 | Console.WriteLine($"Client [{id}] has completed sending all messages"); 91 | }) 92 | ); 93 | 94 | await Task.WhenAll(tasks); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Infrastructure/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.SQS; 2 | using Amazon.SQS.Model; 3 | using AWS.CoreWCF.Extensions.Common; 4 | using CoreWCF.Channels; 5 | using CoreWCF.Configuration; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace AWS.CoreWCF.Extensions.SQS.Infrastructure; 10 | 11 | public static class ApplicationBuilderExtensions 12 | { 13 | /// 14 | /// 15 | /// 16 | /// Name of the queue to create if it does not already exist. 17 | /// 18 | /// 19 | public static string EnsureSqsQueue( 20 | this IApplicationBuilder builder, 21 | string queueName, 22 | CreateQueueRequest? createQueueRequest = null 23 | ) 24 | { 25 | createQueueRequest ??= new CreateQueueRequest(queueName); 26 | 27 | return EnsureSqsQueue(builder, queueName, () => createQueueRequest); 28 | } 29 | 30 | /// 31 | /// Helper function that checks to see if exists in Amazon SQS, 32 | /// if not, uses to construct. 33 | /// 34 | /// This method returns the Queue Url for , which is required to invoke 35 | /// 36 | /// 37 | /// 43 | /// { 44 | /// services.AddService(); 45 | /// services.AddServiceEndpoint( 46 | /// new AwsSqsBinding(), 47 | /// queueUrl 48 | /// ); 49 | /// }); 50 | /// ]]> 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// Name of the queue to create if it does not already exist. 56 | /// 57 | /// 58 | /// Function for building a object that will be used to construct 59 | /// in the event it does not yet exist. 60 | /// is only invoked if does not exist. 61 | /// 62 | /// 63 | /// The url for . 64 | /// 65 | public static string EnsureSqsQueue( 66 | this IApplicationBuilder builder, 67 | string queueName, 68 | Func createQueueRequestBuilder 69 | ) 70 | { 71 | var sqsClient = builder 72 | .ApplicationServices.GetServices() 73 | .SelectMany(x => x) 74 | .FirstOrDefault(x => string.Equals(x.QueueName, queueName, StringComparison.InvariantCultureIgnoreCase)) 75 | ?.SQSClient; 76 | 77 | if (null == sqsClient) 78 | { 79 | throw new ArgumentException( 80 | $"Failed to find matching {nameof(IAmazonSQS)} for queue [{queueName}]. " 81 | + $"Ensure that you have first registered a SQS Client for this queue via " 82 | + $"{nameof(SQSServiceCollectionExtensions)}.{nameof(SQSServiceCollectionExtensions.AddSQSClient)}()", 83 | nameof(queueName) 84 | ); 85 | } 86 | 87 | return sqsClient.EnsureSQSQueue(queueName, createQueueRequestBuilder).Result; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cdk/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json; 3 | using Amazon.CDK; 4 | using Environment = System.Environment; 5 | 6 | namespace AWS.CoreWCF.ServerExtensions.Cdk 7 | { 8 | [ExcludeFromCodeCoverage] 9 | sealed class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | var app = new App(); 14 | 15 | new IntegrationTestsStack( 16 | app, 17 | "AWSCoreWCFServerExtensionsIntegrationTests", 18 | new StackProps 19 | { 20 | // creds are defined in .gitlab-ci.yml 21 | 22 | TerminationProtection = true 23 | } 24 | ); 25 | 26 | new CodeSigningAndDeployStack( 27 | app, 28 | "AWSCoreWCFServerExtensionsCodeSigning", 29 | new CodeSigningAndDeployStackProps 30 | { 31 | // creds are defined in .gitlab-ci.yml 32 | 33 | // env vars are defined in gitlab settings 34 | Signing = new CodeSigningAndDeployStackProps.SigningProps 35 | { 36 | SigningRoleArn = Environment.GetEnvironmentVariable("SIGNING_ROLE_ARN") ?? "signerRole", 37 | SignedBucketName = 38 | Environment.GetEnvironmentVariable("SIGNED_BUCKET_NAME") ?? "signedBucketArn", 39 | UnsignedBucketName = 40 | Environment.GetEnvironmentVariable("UNSIGNED_BUCKET_NAME") ?? "unsignedBucketArn", 41 | }, 42 | NugetPublishing = new CodeSigningAndDeployStackProps.NugetPublishingProps 43 | { 44 | SecretArnCoreWCFNugetPublishKey = 45 | Environment.GetEnvironmentVariable("SECRET_ARN_CORE_WCF_NUGET_PUBLISH_KEY") ?? "corewcf", 46 | SecretArnWCFNugetPublishKey = 47 | Environment.GetEnvironmentVariable("SECRET_ARN_WCF_NUGET_PUBLISH_KEY") ?? "wcf", 48 | NugetPublishSecretAccessRoleArn = 49 | Environment.GetEnvironmentVariable("NUGET_PUBLISH_SECRET_ACCESS_ROLE_ARN") 50 | ?? "nugetPublishRoleArn", 51 | }, 52 | TerminationProtection = true 53 | } 54 | ); 55 | 56 | new CanaryMonitoringStack( 57 | app, 58 | "AWSCoreWCFServerExtensionsCanaryMonitoring", 59 | new CanaryMonitoringStackProps 60 | { 61 | // creds are defined in .gitlab-ci.yml 62 | CloudWatchDashboardServicePrincipalName = 63 | Environment.GetEnvironmentVariable( 64 | "CANARY_MONITORING_CLOUDWATCH_DASHBOARD_SERVICE_PRINCIPAL_NAME" 65 | ) ?? "cloudWatchDashboardServicePrincipalName", 66 | CloudWatchDashboardPolicyStatementId = 67 | Environment.GetEnvironmentVariable("CANARY_MONITORING_CLOUDWATCH_DASHBOARD_POLICY_STATEMENT_ID") 68 | ?? "CloudWatchDashboardPolicyStatementId", 69 | TicketingArn = 70 | Environment.GetEnvironmentVariable("CANARY_MONITORING_AWS_TICKETING_ARN_PREFIX") 71 | ?? "ticketingArn", 72 | TicketingCti = JsonSerializer.Deserialize( 73 | Environment.GetEnvironmentVariable("CANARY_MONITORING_AWS_TICKETING_CTI_JSON") ?? "{}" 74 | ), 75 | TerminationProtection = true 76 | } 77 | ); 78 | 79 | app.Synth(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/AwsSdkPerformanceTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Amazon.SQS; 4 | using Amazon.SQS.Model; 5 | using AWS.Extensions.PerformanceTests.Common; 6 | using BenchmarkDotNet.Attributes; 7 | 8 | namespace AWS.Extensions.PerformanceTests 9 | { 10 | /// 11 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 12 | [SimpleJob(launchCount: 0, warmupCount: 0, iterationCount: 1)] 13 | [ExcludeFromCodeCoverage] 14 | public class AwsSdkPerformanceTest 15 | { 16 | private IAmazonSQS? _setupSqsClient; 17 | 18 | private readonly string _queueName = $"{nameof(AwsSdkPerformanceTest)}-{DateTime.Now.Ticks}"; 19 | private string _queueUrl = ""; 20 | 21 | private AmazonSQSClient[] _clientPool = Array.Empty(); 22 | 23 | private string _fakeQueueMessage = string.Empty; 24 | 25 | [Params(1, 2, 4)] 26 | public int Threads { get; set; } 27 | 28 | [GlobalSetup] 29 | public async Task CreateInfrastructure() 30 | { 31 | _setupSqsClient = new AmazonSQSClient(); 32 | 33 | await _setupSqsClient.CreateQueueAsync(_queueName); 34 | _queueUrl = (await _setupSqsClient!.GetQueueUrlAsync(_queueName))?.QueueUrl ?? ""; 35 | 36 | Console.WriteLine($"QueueName: {_queueName}"); 37 | 38 | _clientPool = Enumerable.Range(0, 4).Select(_ => new AmazonSQSClient()).ToArray(); 39 | 40 | _fakeQueueMessage = _queueName; 41 | 42 | // write an extra 5000 messages so the queue is nicely saturated 43 | // and ReadAndWriteMessages has extra messages it can read if necessary 44 | await ClientMessageGenerator.SaturateQueue(_setupSqsClient, _queueName, _queueUrl, numMessages: 5000); 45 | } 46 | 47 | [GlobalCleanup] 48 | public async Task CleanUp() 49 | { 50 | await _setupSqsClient!.DeleteQueueAsync(_queueUrl); 51 | } 52 | 53 | [Benchmark] 54 | public async Task ReadAndWrite100Messages() 55 | { 56 | var cancelToken = new CancellationTokenSource(TimeSpan.FromMinutes(2)).Token; 57 | 58 | var sw = Stopwatch.StartNew(); 59 | 60 | var tasks = Enumerable 61 | .Range(0, Threads) 62 | .Select(i => ReadAndWriteMessages(_clientPool[i], 100 / Threads, cancelToken)) 63 | .ToArray(); 64 | 65 | await Task.WhenAll(tasks); 66 | 67 | Console.WriteLine("======================="); 68 | Console.WriteLine($"Sent & Read 100 Messages in [{sw.Elapsed.TotalSeconds}] s"); 69 | Console.WriteLine("======================="); 70 | } 71 | 72 | private async Task ReadAndWriteMessages(IAmazonSQS client, int numberOfMessages, CancellationToken cancelToken) 73 | { 74 | Console.WriteLine($"[T {Thread.CurrentThread.ManagedThreadId}] Begin Writing Messages"); 75 | for (var i = 0; i < numberOfMessages; i++) 76 | { 77 | await client.SendMessageAsync(_queueUrl, _fakeQueueMessage, cancelToken); 78 | } 79 | 80 | Console.WriteLine($"[T {Thread.CurrentThread.ManagedThreadId}] Begin Reading Messages"); 81 | 82 | var messagesRead = 0; 83 | while (messagesRead < numberOfMessages) 84 | { 85 | var request = new ReceiveMessageRequest { QueueUrl = _queueUrl, MaxNumberOfMessages = 10 }; 86 | var response = await client.ReceiveMessageAsync(request, cancelToken); 87 | 88 | foreach (var msg in response.Messages) 89 | await client.DeleteMessageAsync(_queueUrl, msg.ReceiptHandle, cancelToken); 90 | 91 | messagesRead += response.Messages.Count; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/canary.yml: -------------------------------------------------------------------------------- 1 | name: Canary 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '10 */4 * * *' # At 10 minutes past the hour, every 4 hours 7 | env: 8 | AWS_REGION : "us-west-2" 9 | # permission can be added at job level or workflow level 10 | permissions: 11 | id-token: write # This is required for requesting the JWT 12 | contents: read # This is required for actions/checkout 13 | 14 | jobs: 15 | canary-runs-tests: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Setup .NET Versions 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: | 25 | 6.0.x 26 | 7.0.x 27 | 8.0.x 28 | - name: Install dependencies 29 | run: | 30 | dotnet restore 31 | dotnet tool restore 32 | - name: Build 33 | run: dotnet build --configuration Release --no-restore 34 | - name: configure aws credentials 35 | uses: aws-actions/configure-aws-credentials@v2 36 | with: 37 | role-to-assume: ${{ secrets.AWS_INTEGRATION_TEST_ROLE }} 38 | role-session-name: github-canary 39 | aws-region: ${{ env.AWS_REGION }} 40 | - name: Test 41 | # runs all automated tests 42 | run: dotnet test --configuration Release --no-restore --verbosity normal 43 | - name: Send metric success 44 | if: success() 45 | run: | 46 | aws cloudwatch put-metric-data --namespace TuxNetOps --metric-name corewcf-sqs-canary --value 1 47 | 48 | - name: Send metric failure 49 | if: ${{ !success() }} 50 | run: | 51 | aws cloudwatch put-metric-data --namespace TuxNetOps --metric-name corewcf-sqs-canary --value 0 52 | canary-benchmark: 53 | # disabled pending update to benachmark tool 54 | if: false 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | with: 59 | fetch-depth: 0 60 | - name: Setup .NET Versions 61 | uses: actions/setup-dotnet@v1 62 | with: 63 | dotnet-version: | 64 | 6.0.x 65 | 8.0.x 66 | - name: Install dependencies 67 | run: | 68 | dotnet restore 69 | dotnet tool restore 70 | dotnet tool install --add-source ${{ secrets.AWS_BENCHMARK_TOOL_PRIVATE_FEED }} BenchmarkDotnetCliTool 71 | - name: Prepare Benchmark AWS Config Json 72 | shell: pwsh 73 | run: | 74 | Get-Content benchmark-aws-config.json 75 | # update config file with values from secrets 76 | $awsConfigJson = Get-Content benchmark-aws-config.json 77 | $awsConfigJson = $awsConfigJson.Replace("BENCHMARK_EC2_INSTANCE_PROFILE_ARN", "${{ secrets.AWS_BENCHMARK_EC2_INSTANCE_PROFILE_ARN }}") 78 | $awsConfigJson = $awsConfigJson.Replace("BENCHMARK_BUCKET_NAME", "${{ secrets.AWS_BENCHMARK_BUCKET_NAME }}") 79 | $awsConfigJson = $awsConfigJson.Replace("BENCHMARK_TOOL_PRIVATE_FEED", "${{ secrets.AWS_BENCHMARK_TOOL_PRIVATE_FEED }}") 80 | $awsConfigJson | Out-File benchmark-aws-config.json 81 | - name: Configure AWS Credentials 82 | uses: aws-actions/configure-aws-credentials@v2 83 | with: 84 | role-to-assume: ${{ secrets.AWS_BENCHMARK_TEST_ROLE }} 85 | role-session-name: github-cicd 86 | aws-region: ${{ env.AWS_REGION }} 87 | - name: Run Benchmarking 88 | shell: pwsh 89 | run: | 90 | # baseline is the most recent git tag 91 | $baseline=(git tag -l --sort=-v:refname)[0] 92 | #performance must degrade by atleast 50% to trigger alarm 93 | $threshold=50.0 94 | echo "baseline: $baseline" 95 | echo "threshold: $threshold" 96 | dotnet benchmark run aws native ./test/AWS.Extensions.PerformanceTests/AWS.Extensions.PerformanceTests.csproj --targetApplicationRoot . -o . --tag vNext --baseline $baseline --threshold $threshold -ac ./benchmark-aws-config.json 97 | 98 | -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/DispatchCallbackFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Amazon.SimpleNotificationService; 3 | using Amazon.SimpleNotificationService.Model; 4 | using AWS.CoreWCF.Extensions.SQS.Channels; 5 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 6 | using CoreWCF; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using NSubstitute; 9 | using NSubstitute.ExceptionExtensions; 10 | using Xunit; 11 | 12 | namespace AWS.CoreWCF.Extensions.Tests 13 | { 14 | public class DispatchCallbackFactoryTests 15 | { 16 | const string FakeTopicArn = "fakeTopicArm"; 17 | 18 | [Fact] 19 | public void SendsNotificationOnFailure() 20 | { 21 | // ARRANGE 22 | var fakeMessageContext = new AwsSqsMessageContext 23 | { 24 | LocalAddress = new EndpointAddress("http://fake"), 25 | MessageReceiptHandle = "fakeHandle" 26 | }; 27 | 28 | var fakeSns = Substitute.For(); 29 | 30 | var fakeServices = new ServiceCollection() 31 | .AddSingleton(fakeSns) 32 | .BuildServiceProvider(); 33 | 34 | var failureNotification = DispatchCallbackFactory.GetDefaultFailureNotificationCallbackWithSns( 35 | FakeTopicArn 36 | ); 37 | 38 | // ACT 39 | failureNotification.Invoke(fakeServices, fakeMessageContext); 40 | 41 | // ASSERT 42 | // assert fake publish request 43 | fakeSns 44 | .Received() 45 | .PublishAsync( 46 | Arg.Is(req => 47 | // make sure we are sending a failure notification 48 | req.Message.Contains("Failed") 49 | && 50 | // make sure we are sending a notification about the correct queue 51 | req.Message.Contains(fakeMessageContext.MessageReceiptHandle) 52 | ), 53 | Arg.Any() 54 | ); 55 | } 56 | 57 | [Fact] 58 | [ExcludeFromCodeCoverage] 59 | public void NotificationCallbackSuppressesExceptions() 60 | { 61 | // ARRANGE 62 | var fakeMessageContext = new AwsSqsMessageContext 63 | { 64 | LocalAddress = new EndpointAddress("http://fake"), 65 | MessageReceiptHandle = "fakeHandle" 66 | }; 67 | 68 | var fakeSns = Substitute.For(); 69 | fakeSns 70 | .PublishAsync(Arg.Any(), Arg.Any()) 71 | .ThrowsAsync(new Exception("Fake Exception")); 72 | 73 | var fakeServices = new ServiceCollection() 74 | .AddSingleton(fakeSns) 75 | .BuildServiceProvider(); 76 | 77 | var failureNotification = DispatchCallbackFactory.GetDefaultFailureNotificationCallbackWithSns( 78 | FakeTopicArn 79 | ); 80 | 81 | // ACT 82 | // capture any exceptions thrown by the Invoke method 83 | Exception? capturedException = null; 84 | try 85 | { 86 | failureNotification.Invoke(fakeServices, fakeMessageContext); 87 | } 88 | catch (Exception e) 89 | { 90 | capturedException = e; 91 | } 92 | 93 | // ASSERT 94 | // make sure we didn't see any exceptions bubble up from Invoke() 95 | Assert.Null(capturedException); 96 | } 97 | 98 | [Fact] 99 | public async Task AllowNulls() 100 | { 101 | // ARRANGE 102 | var dispatch = new DispatchCallbacksCollection(); 103 | 104 | // ACT 105 | await dispatch.NotificationDelegateForFailedDispatch(null, null); 106 | await dispatch.NotificationDelegateForSuccessfulDispatch(null, null); 107 | 108 | // ASSERT 109 | // expect no exceptions have been thrown 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/ServerMultipleClientsPerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Amazon.SQS; 3 | using AWS.CoreWCF.Extensions.SQS.Channels; 4 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 5 | using AWS.Extensions.PerformanceTests.Common; 6 | using BenchmarkDotNet.Attributes; 7 | using Microsoft.AspNetCore.Hosting; 8 | 9 | namespace AWS.Extensions.PerformanceTests; 10 | 11 | /// 12 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 13 | [SimpleJob(launchCount: 0, warmupCount: 0, iterationCount: 1)] 14 | [ExcludeFromCodeCoverage] 15 | public class ServerMultipleClientsPerformanceTests 16 | { 17 | private IWebHost? _host; 18 | private IAmazonSQS? _setupSqsClient; 19 | 20 | private readonly string _queueName1 = $"{nameof(ServerMultipleClientsPerformanceTests)}-1-{DateTime.Now.Ticks}"; 21 | private readonly string _queueName2 = $"{nameof(ServerMultipleClientsPerformanceTests)}-2-{DateTime.Now.Ticks}"; 22 | private readonly string _queueName3 = $"{nameof(ServerMultipleClientsPerformanceTests)}-3-{DateTime.Now.Ticks}"; 23 | 24 | private string _queueUrl1 = ""; 25 | private string _queueUrl2 = ""; 26 | private string _queueUrl3 = ""; 27 | 28 | [Params(2, 3)] 29 | public int Clients { get; set; } 30 | 31 | [GlobalSetup] 32 | public async Task CreateInfrastructure() 33 | { 34 | _setupSqsClient = new AmazonSQSClient(); 35 | 36 | await _setupSqsClient.CreateQueueAsync(_queueName1); 37 | _queueUrl1 = (await _setupSqsClient!.GetQueueUrlAsync(_queueName1))?.QueueUrl ?? ""; 38 | 39 | await _setupSqsClient.CreateQueueAsync(_queueName2); 40 | _queueUrl2 = (await _setupSqsClient!.GetQueueUrlAsync(_queueName2))?.QueueUrl ?? ""; 41 | 42 | await _setupSqsClient.CreateQueueAsync(_queueName3); 43 | _queueUrl3 = (await _setupSqsClient!.GetQueueUrlAsync(_queueName3))?.QueueUrl ?? ""; 44 | 45 | Console.WriteLine($"QueueNames: {_queueName1}, {_queueName2}, {_queueName3}"); 46 | } 47 | 48 | [IterationSetup] 49 | public void Setup() 50 | { 51 | LoggingService.LogResults.Clear(); 52 | 53 | StartupHost().Wait(); 54 | } 55 | 56 | public async Task StartupHost() 57 | { 58 | Console.WriteLine($"Begin {nameof(StartupHost)}"); 59 | 60 | #region Configure Host 61 | 62 | var queueBindingPairs = new List<(string queueName, string queueUrl, AwsSqsBinding binding)> 63 | { 64 | (_queueName1, _queueUrl1, new AwsSqsBinding()), 65 | (_queueName2, _queueUrl2, new AwsSqsBinding()), 66 | (_queueName3, _queueUrl3, new AwsSqsBinding()) 67 | }; 68 | 69 | _host = ServerFactory.StartServer(queueBindingPairs.Take(Clients).ToArray()); 70 | 71 | #endregion 72 | 73 | #region Pre Saturate Queue 74 | 75 | await Task.WhenAll( 76 | ClientMessageGenerator.SaturateQueue(_setupSqsClient, _queueName1, _queueUrl1), 77 | ClientMessageGenerator.SaturateQueue(_setupSqsClient, _queueName2, _queueUrl2), 78 | ClientMessageGenerator.SaturateQueue(_setupSqsClient, _queueName3, _queueUrl3) 79 | ); 80 | 81 | #endregion 82 | } 83 | 84 | [IterationCleanup] 85 | public void CleanupHost() 86 | { 87 | _host?.Dispose(); 88 | } 89 | 90 | [GlobalCleanup] 91 | public async Task CleanUp() 92 | { 93 | await _setupSqsClient!.DeleteQueueAsync(_queueUrl1); 94 | await _setupSqsClient!.DeleteQueueAsync(_queueUrl2); 95 | await _setupSqsClient!.DeleteQueueAsync(_queueUrl3); 96 | } 97 | 98 | [Benchmark] 99 | public async Task ServerCanProcess1000Messages() 100 | { 101 | var maxTime = TimeSpan.FromMinutes(5); 102 | 103 | var cancelToken = new CancellationTokenSource(maxTime).Token; 104 | 105 | // start the server 106 | await _host!.StartAsync(cancelToken); 107 | 108 | // wait for server to process all messages 109 | while (LoggingService.LogResults.Count < 1000) 110 | { 111 | Console.WriteLine($"Processed [{LoggingService.LogResults.Count}] Messages"); 112 | 113 | await Task.Delay(TimeSpan.FromMilliseconds(250), cancelToken); 114 | } 115 | 116 | Console.WriteLine($"Processed [{LoggingService.LogResults.Count}] messages"); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages 2 | # Template: https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/lib/gitlab/ci/templates/dotNET-Core.gitlab-ci.yml 3 | 4 | image: alpine:latest 5 | 6 | variables: 7 | # 1) Name of directory where restore and build objects are stored. 8 | OBJECTS_DIRECTORY: 'obj' 9 | # 2) Name of directory used for keeping restored dependencies. 10 | NUGET_PACKAGES_DIRECTORY: '.nuget' 11 | # 3) A relative path to the source code from project repository root. 12 | SOURCE_CODE_PATH: 'src/*' 13 | 14 | cache: 15 | # Per-stage and per-branch caching. 16 | key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG" 17 | paths: 18 | # Specify three paths that should be cached: 19 | # 20 | # 1) Main JSON file holding information about package dependency tree, packages versions, 21 | # frameworks etc. It also holds information where to the dependencies were restored. 22 | - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json' 23 | # 2) Other NuGet and MSBuild related files. Also needed. 24 | - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*' 25 | # 3) Path to the directory where restored dependencies are kept. 26 | - '$NUGET_PACKAGES_DIRECTORY' 27 | 28 | before_script: 29 | - apk add dotnet8-sdk 30 | - apk add git jq nodejs npm 31 | - npm install -g aws-cdk 32 | - apk add --no-cache aws-cli 33 | - 'dotnet restore . --packages $NUGET_PACKAGES_DIRECTORY' 34 | - dotnet tool restore 35 | 36 | stages: # List of stages for jobs, and their order of execution 37 | - build 38 | - deploy-infra 39 | - test 40 | 41 | 42 | build-job: 43 | stage: build 44 | script: 45 | - dotnet build . --no-restore 46 | 47 | integration-tests-job: 48 | stage: test 49 | variables: 50 | AWS_CREDS_TARGET_ROLE: $AWS_CREDS_TARGET_ROLE_PROD 51 | AWS_DEFAULT_REGION: $CDK_DEFAULT_REGION 52 | AWS_REGION: $CDK_DEFAULT_REGION 53 | ACCOUNT_ID: $CDK_DEFAULT_ACCOUNT_PROD 54 | script: 55 | - echo "installing Junit Logger" 56 | - dotnet add ./test/AWS.Extensions.IntegrationTests/ package JUnitTestLogger 57 | - echo "Running automated tests..." 58 | #https://github.com/spekt/junit.testlogger/blob/master/docs/gitlab-recommendation.md https://stackoverflow.com/questions/57574782/how-to-capture-structured-xunit-test-output-in-gitlab-ci 59 | - 'dotnet test ./test/AWS.Extensions.IntegrationTests --test-adapter-path:. --logger:"junit;LogFilePath=../../artifacts/integration-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose"' 60 | artifacts: 61 | when: always 62 | paths: 63 | - ./artifacts/*test-result.xml 64 | reports: 65 | junit: 66 | - ./artifacts/*test-result.xml 67 | 68 | corewcf-unit-test-job: 69 | stage: test 70 | script: 71 | - echo "installing Junit Logger" 72 | - dotnet add ./test/AWS.CoreWCF.Extensions.Tests/ package JUnitTestLogger 73 | - echo "Running automated tests..." 74 | #https://github.com/spekt/junit.testlogger/blob/master/docs/gitlab-recommendation.md https://stackoverflow.com/questions/57574782/how-to-capture-structured-xunit-test-output-in-gitlab-ci 75 | - 'dotnet test ./test/AWS.CoreWCF.Extensions.Tests --test-adapter-path:. --logger:"junit;LogFilePath=../../artifacts/corewcf-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose"' 76 | artifacts: 77 | when: always 78 | paths: 79 | - ./artifacts/*test-result.xml 80 | reports: 81 | junit: 82 | - ./artifacts/*test-result.xml 83 | 84 | wcf-unit-test-job: 85 | stage: test 86 | script: 87 | - echo "installing Junit Logger" 88 | - dotnet add ./test/AWS.WCF.Extensions.Tests/ package JUnitTestLogger 89 | - echo "Running automated tests..." 90 | #https://github.com/spekt/junit.testlogger/blob/master/docs/gitlab-recommendation.md https://stackoverflow.com/questions/57574782/how-to-capture-structured-xunit-test-output-in-gitlab-ci 91 | - 'dotnet test ./test/AWS.WCF.Extensions.Tests --test-adapter-path:. --logger:"junit;LogFilePath=../../artifacts/wcf-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose"' 92 | artifacts: 93 | when: always 94 | paths: 95 | - ./artifacts/*test-result.xml 96 | reports: 97 | junit: 98 | - ./artifacts/*test-result.xml 99 | 100 | lint-test-job: 101 | stage: test 102 | script: 103 | - echo "🖥️ Using Csharpier to enforce code formating and style." 104 | - dotnet csharpier --check . 105 | 106 | deploy-cdk-job: # deploys cdk AND save output for integration tests 107 | stage: deploy-infra 108 | #environment: production 109 | rules: 110 | # only run on push to main 111 | - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH 112 | variables: 113 | AWS_CREDS_TARGET_ROLE: $AWS_CREDS_TARGET_ROLE_PROD 114 | AWS_DEFAULT_REGION: $CDK_DEFAULT_REGION 115 | ACCOUNT_ID: $CDK_DEFAULT_ACCOUNT_PROD 116 | # additional variables are defined in gitlab settings 117 | script: 118 | - cdk deploy --all --require-approval never 119 | 120 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | env: 9 | AWS_REGION : "us-west-2" 10 | # permission can be added at job level or workflow level 11 | permissions: 12 | id-token: write # This is required for requesting the JWT 13 | contents: read # This is required for actions/checkout 14 | 15 | jobs: 16 | build-and-test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - name: Setup .NET Versions 23 | uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: | 26 | 6.0.x 27 | 8.0.x 28 | - name: Install dependencies 29 | run: | 30 | dotnet restore 31 | dotnet tool restore 32 | - name: Build 33 | run: dotnet build --configuration Release --no-restore 34 | - name: configure aws credentials 35 | uses: aws-actions/configure-aws-credentials@v2 36 | with: 37 | role-to-assume: ${{ secrets.AWS_INTEGRATION_TEST_ROLE }} 38 | role-session-name: github-cicd 39 | aws-region: ${{ env.AWS_REGION }} 40 | - name: Test 41 | # runs all automated tests 42 | run: dotnet test --configuration Release --no-restore --verbosity normal 43 | 44 | benchmark: 45 | # disabled pending update to benachmark tool 46 | if: false 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | with: 51 | fetch-depth: 0 52 | - name: Setup .NET Versions 53 | uses: actions/setup-dotnet@v1 54 | with: 55 | dotnet-version: | 56 | 6.0.x 57 | 7.0.x 58 | 8.0.x 59 | - name: Install dependencies 60 | run: | 61 | dotnet restore 62 | dotnet tool restore 63 | dotnet tool install --add-source ${{ secrets.AWS_BENCHMARK_TOOL_PRIVATE_FEED }} BenchmarkDotnetCliTool 64 | - name: Prepare Benchmark AWS Config Json 65 | shell: pwsh 66 | run: | 67 | Get-Content benchmark-aws-config.json 68 | # update config file with values from secrets 69 | $awsConfigJson = Get-Content benchmark-aws-config.json 70 | $awsConfigJson = $awsConfigJson.Replace("BENCHMARK_EC2_INSTANCE_PROFILE_ARN", "${{ secrets.AWS_BENCHMARK_EC2_INSTANCE_PROFILE_ARN }}") 71 | $awsConfigJson = $awsConfigJson.Replace("BENCHMARK_BUCKET_NAME", "${{ secrets.AWS_BENCHMARK_BUCKET_NAME }}") 72 | $awsConfigJson = $awsConfigJson.Replace("BENCHMARK_TOOL_PRIVATE_FEED", "${{ secrets.AWS_BENCHMARK_TOOL_PRIVATE_FEED }}") 73 | $awsConfigJson | Out-File benchmark-aws-config.json 74 | - name: Configure AWS Credentials 75 | uses: aws-actions/configure-aws-credentials@v2 76 | with: 77 | role-to-assume: ${{ secrets.AWS_BENCHMARK_TEST_ROLE }} 78 | role-session-name: github-cicd 79 | aws-region: ${{ env.AWS_REGION }} 80 | - name: Run Benchmarking 81 | shell: pwsh 82 | run: | 83 | # current is the more recent git tag and baseline is the tag before that 84 | # this assumes that as part of the release process, the new tag has already 85 | # been created 86 | $current=(git tag -l --sort=-v:refname)[0] 87 | $baseline=(git tag -l --sort=-v:refname)[1] 88 | #performance must degrade by atleast 50% to trigger alarm 89 | $threshold=50.0 90 | echo "current: $current" 91 | echo "baseline: $baseline" 92 | echo "threshold: $threshold" 93 | dotnet benchmark run aws native ./test/AWS.Extensions.PerformanceTests/AWS.Extensions.PerformanceTests.csproj --targetApplicationRoot . -o . --tag $current --baseline $baseline --threshold $threshold -ac ./benchmark-aws-config.json 94 | 95 | trigger-deploy: 96 | runs-on: ubuntu-latest 97 | needs: 98 | - build-and-test 99 | # benchmark step is disabled 100 | #- benchmark 101 | steps: 102 | - uses: actions/checkout@v2 103 | # set fetch-depth to 0 to get full git history, needed for NerdBank GitVersion to caclulate version 104 | with: 105 | fetch-depth: 0 106 | - name: Zip Src 107 | run: | 108 | #cd ../ 109 | zip -r AWSCoreWCFServerExtensions.zip . 110 | - name: configure aws credentials 111 | uses: aws-actions/configure-aws-credentials@v2 112 | with: 113 | role-to-assume: ${{ secrets.AWS_DEPLOYMENT_ROLE }} 114 | role-session-name: github-cicd 115 | aws-region: ${{ env.AWS_REGION }} 116 | - name: Start Deployment Pipeline 117 | # uploading this zip archive will trigger a CodePipeline that will build, sign, and package 118 | # the dlls and then publish them to nuget.org 119 | run: aws s3 cp ./AWSCoreWCFServerExtensions.zip s3://${{ secrets.AWS_DEPLOYMENT_BUCKET_NAME }}/AWSCoreWCFServerExtensions.zip 120 | 121 | lint: 122 | runs-on: ubuntu-latest 123 | steps: 124 | - uses: actions/checkout@v2 125 | with: 126 | fetch-depth: 0 127 | - name: Setup .NET Versions 128 | uses: actions/setup-dotnet@v1 129 | with: 130 | dotnet-version: | 131 | 6.0.x 132 | 8.0.x 133 | - name: Install dependencies 134 | run: | 135 | dotnet restore 136 | dotnet tool restore 137 | - name: Lint 138 | run: dotnet csharpier . --check 139 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/Common/CreateQueueRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Amazon.SQS; 3 | using Amazon.SQS.Model; 4 | 5 | namespace AWS.CoreWCF.Extensions.Common; 6 | 7 | public static class CreateQueueRequestExtensions 8 | { 9 | private const string DefaultSQSTag = "CoreWCFExtensionsSQS"; 10 | 11 | public static CreateQueueRequest SetDefaultValues(this CreateQueueRequest request, string? queueName = null) 12 | { 13 | request.QueueName = queueName ?? request.QueueName; 14 | request.Attributes = GetDefaultAttributeValues(); 15 | request.Tags = new Dictionary { { DefaultSQSTag, DefaultSQSTag } }; 16 | return request; 17 | } 18 | 19 | public static CreateQueueRequest WithFIFO(this CreateQueueRequest request, bool useFIFO = true) 20 | { 21 | request.Attributes[QueueAttributeName.FifoQueue] = useFIFO.ToString(); 22 | if (useFIFO) 23 | { 24 | request.Attributes[QueueAttributeName.ContentBasedDeduplication] = true.ToString(); 25 | } 26 | 27 | return request; 28 | } 29 | 30 | public static CreateQueueRequest SetAttribute( 31 | this CreateQueueRequest request, 32 | QueueAttributeName attribute, 33 | string value 34 | ) 35 | { 36 | request.Attributes[attribute] = value; 37 | 38 | return request; 39 | } 40 | 41 | public static CreateQueueRequest WithDeadLetterQueue( 42 | this CreateQueueRequest request, 43 | int maxReceiveCount = 1, 44 | string? deadLetterTargetArn = null 45 | ) 46 | { 47 | var redrivePolicy = new Dictionary { { nameof(maxReceiveCount), maxReceiveCount.ToString() } }; 48 | 49 | if (!string.IsNullOrEmpty(deadLetterTargetArn)) 50 | { 51 | redrivePolicy[nameof(deadLetterTargetArn)] = deadLetterTargetArn; 52 | } 53 | 54 | request.Attributes[QueueAttributeName.RedrivePolicy] = JsonSerializer.Serialize(redrivePolicy); 55 | return request; 56 | } 57 | 58 | public static CreateQueueRequest WithManagedServerSideEncryption( 59 | this CreateQueueRequest request, 60 | bool useManagedServerSideEncryption = true 61 | ) 62 | { 63 | if (useManagedServerSideEncryption) 64 | { 65 | request.Attributes.Remove(QueueAttributeName.KmsMasterKeyId); 66 | request.Attributes.Remove(QueueAttributeName.KmsDataKeyReusePeriodSeconds); 67 | } 68 | 69 | request.Attributes[QueueAttributeName.SqsManagedSseEnabled] = useManagedServerSideEncryption.ToString(); 70 | return request; 71 | } 72 | 73 | public static CreateQueueRequest WithKMSEncryption( 74 | this CreateQueueRequest request, 75 | string kmsMasterKeyId, 76 | int kmsDataKeyReusePeriodInSeconds = 300 77 | ) 78 | { 79 | request.Attributes[QueueAttributeName.SqsManagedSseEnabled] = false.ToString(); 80 | request.Attributes[QueueAttributeName.KmsMasterKeyId] = kmsMasterKeyId; 81 | request.Attributes[QueueAttributeName.KmsDataKeyReusePeriodSeconds] = kmsDataKeyReusePeriodInSeconds.ToString(); 82 | 83 | return request; 84 | } 85 | 86 | private const int MaxSQSMessageSizeInBytes = 262144; // 2^18 87 | private const int MaxSQSMessageRetentionPeriodInSeconds = 345600; // 4 days 88 | private const int DefaultDelayInSeconds = 0; 89 | private const int DefaultReceiveMessageWaitTimeSeconds = 0; 90 | private const int DefaultVisibilityTimeoutInSeconds = 30; 91 | private const int DefaultKmsDataKeyReusePeriodInSeconds = 300; // 5 minutes 92 | 93 | private static Dictionary GetDefaultAttributeValues() 94 | { 95 | var defaultAttributes = new Dictionary 96 | { 97 | { QueueAttributeName.DelaySeconds, DefaultDelayInSeconds.ToString() }, 98 | { QueueAttributeName.MaximumMessageSize, MaxSQSMessageSizeInBytes.ToString() }, 99 | { QueueAttributeName.MessageRetentionPeriod, MaxSQSMessageRetentionPeriodInSeconds.ToString() }, 100 | { QueueAttributeName.ReceiveMessageWaitTimeSeconds, DefaultReceiveMessageWaitTimeSeconds.ToString() }, 101 | { QueueAttributeName.VisibilityTimeout, DefaultVisibilityTimeoutInSeconds.ToString() }, 102 | { QueueAttributeName.KmsDataKeyReusePeriodSeconds, DefaultKmsDataKeyReusePeriodInSeconds.ToString() }, 103 | { QueueAttributeName.SqsManagedSseEnabled, false.ToString() } 104 | }; 105 | return defaultAttributes; 106 | } 107 | 108 | public static bool IsFIFO(this CreateQueueRequest createQueueRequest) 109 | { 110 | return createQueueRequest.Attributes.TryGetValue(QueueAttributeName.FifoQueue, out var isFifoString) 111 | && isFifoString.Equals(true.ToString(), StringComparison.InvariantCultureIgnoreCase); 112 | } 113 | 114 | public static bool IsUsingDeadLetterQueue(this CreateQueueRequest createQueueRequest) 115 | { 116 | return createQueueRequest.GetRedrivePolicy() is not null; 117 | } 118 | 119 | public static Dictionary? GetRedrivePolicy(this CreateQueueRequest createQueueRequest) 120 | { 121 | if ( 122 | createQueueRequest.Attributes.TryGetValue(QueueAttributeName.RedrivePolicy, out var redrivePolicyString) 123 | && JsonSerializer.Deserialize>(redrivePolicyString) 124 | is Dictionary redrivePolicy 125 | ) 126 | { 127 | return redrivePolicy; 128 | } 129 | return null; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/CodeSigningHelper/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using Amazon.Runtime; 4 | using Amazon.S3; 5 | using Amazon.S3.Model; 6 | 7 | namespace CodeSigningHelper 8 | { 9 | [ExcludeFromCodeCoverage] 10 | internal class Program 11 | { 12 | private static IAmazonS3 _s3Client; 13 | private static string _signedBucketName; 14 | private static string _unsignedBucketName; 15 | 16 | private static readonly TimeSpan DefaultTimeOut = TimeSpan.FromMinutes(5); 17 | 18 | private const string Prefix = "CoreWCFExtensionsAuthenticodeSigner/AuthenticodeSigner-SHA256-RSA"; 19 | private const string SignerJobIdTag = "signer-job-id"; 20 | 21 | static async Task Main(string[] args) 22 | { 23 | try 24 | { 25 | Console.WriteLine("Starting Code Signing Helper"); 26 | 27 | _s3Client = new AmazonS3Client(new EnvironmentVariablesAWSCredentials()); 28 | 29 | Console.WriteLine("----------------"); 30 | 31 | _unsignedBucketName = args[0]; 32 | _signedBucketName = args[1]; 33 | 34 | var token = new CancellationTokenSource(DefaultTimeOut).Token; 35 | 36 | await Sign(args.Skip(2), token); 37 | } 38 | catch (Exception e) 39 | { 40 | Console.WriteLine(e.Message); 41 | Console.WriteLine(e.StackTrace); 42 | 43 | Console.WriteLine(Environment.NewLine); 44 | 45 | Console.WriteLine( 46 | $"Usage: {Assembly.GetExecutingAssembly().GetName().Name} " 47 | + $" ... " 48 | ); 49 | 50 | // signal failure to signing pipeline 51 | Environment.Exit(-25); 52 | } 53 | } 54 | 55 | static async Task Sign(IEnumerable workingDirectories, CancellationToken token) 56 | { 57 | workingDirectories ??= new List(); 58 | 59 | var tasks = workingDirectories.Select(dir => Sign(dir, token)).ToArray(); 60 | 61 | await Task.WhenAll(tasks); 62 | } 63 | 64 | static async Task Sign(string workingDirectory, CancellationToken token) 65 | { 66 | var files = Directory.GetFiles(workingDirectory, "*.dll", SearchOption.TopDirectoryOnly); 67 | 68 | foreach (var file in files) 69 | { 70 | var fileName = Path.GetFileName(file); 71 | Log($"Begin {fileName}"); 72 | 73 | var unsignedKey = Path.Join(Prefix, Path.GetFileName(file)); 74 | 75 | Log($"Putting Object [{unsignedKey}] to [{_unsignedBucketName}]"); 76 | 77 | await _s3Client.PutObjectAsync( 78 | new PutObjectRequest 79 | { 80 | FilePath = file, 81 | BucketName = _unsignedBucketName, 82 | Key = unsignedKey 83 | }, 84 | token 85 | ); 86 | 87 | Log($"Uploaded {fileName}. Waiting for SignerJobId Tag"); 88 | 89 | string? signerJob = null; 90 | while (true) 91 | { 92 | var tags = await _s3Client.GetObjectTaggingAsync( 93 | new GetObjectTaggingRequest { BucketName = _unsignedBucketName, Key = unsignedKey }, 94 | token 95 | ); 96 | 97 | signerJob = tags.Tagging.FirstOrDefault(t => t.Key == SignerJobIdTag)?.Value; 98 | 99 | if (!string.IsNullOrEmpty(signerJob)) 100 | break; 101 | 102 | await Task.Delay(TimeSpan.FromSeconds(1.5), token); 103 | } 104 | 105 | Log($"Found Signer Job Id for {fileName}: [{signerJob}]. Monitoring Signed Bucket"); 106 | 107 | var signedKey = Path.Join(Prefix, $"{fileName}-{signerJob}"); 108 | 109 | GetObjectResponse signedResult; 110 | while (true) 111 | { 112 | try 113 | { 114 | signedResult = await _s3Client.GetObjectAsync( 115 | new GetObjectRequest { BucketName = _signedBucketName, Key = signedKey }, 116 | token 117 | ); 118 | 119 | break; 120 | } 121 | catch (AmazonS3Exception e) 122 | { 123 | await Task.Delay(TimeSpan.FromSeconds(1.5), token); 124 | } 125 | } 126 | 127 | Log($"Found signed file for {fileName}. Downloading"); 128 | 129 | await signedResult.WriteResponseStreamToFileAsync(file, append: false, cancellationToken: token); 130 | 131 | Log($"Wrote signed file to {file}"); 132 | 133 | // Don't have permissions to cleanup _unsignedBucketName 134 | // Log($"Deleting {file} from [{_unsignedBucketName}]"); 135 | // 136 | // await _s3Client.DeleteObjectAsync( 137 | // new DeleteObjectRequest { BucketName = _unsignedBucketName, Key = unsignedKey }, 138 | // token 139 | // ); 140 | } 141 | } 142 | 143 | static void Log(string message) 144 | { 145 | Console.WriteLine( 146 | $"[{DateTime.Now:T} T-{Thread.CurrentThread.ManagedThreadId.ToString(format: "D2")}]: {message}" 147 | ); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/SqsOutputChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Globalization; 3 | using System.ServiceModel; 4 | using System.ServiceModel.Channels; 5 | using System.Text; 6 | using Amazon.SQS; 7 | using Amazon.SQS.Model; 8 | using AWS.WCF.Extensions.Common; 9 | using AWS.WCF.Extensions.SQS.Runtime; 10 | using Message = System.ServiceModel.Channels.Message; 11 | 12 | namespace AWS.WCF.Extensions.SQS; 13 | 14 | public class SqsOutputChannel : ChannelBase, IOutputChannel 15 | { 16 | private readonly EndpointAddress _queueUrl; 17 | private readonly Uri _via; 18 | private readonly MessageEncoder _encoder; 19 | private readonly SqsChannelFactory _parent; 20 | private readonly IAmazonSQS _sqsClient; 21 | 22 | internal SqsOutputChannel( 23 | SqsChannelFactory factory, 24 | IAmazonSQS sqsClient, 25 | EndpointAddress queueUrl, 26 | Uri via, 27 | MessageEncoder encoder 28 | ) 29 | : base(factory) 30 | { 31 | if (!string.Equals(via.Scheme, SqsConstants.Scheme, StringComparison.InvariantCultureIgnoreCase)) 32 | { 33 | throw new ArgumentException( 34 | string.Format( 35 | CultureInfo.CurrentCulture, 36 | "The scheme {0} specified in address is not supported.", 37 | via.Scheme 38 | ), 39 | nameof(via) 40 | ); 41 | } 42 | 43 | _parent = factory; 44 | _queueUrl = queueUrl; 45 | _via = via; 46 | _encoder = encoder; 47 | _sqsClient = sqsClient; 48 | 49 | (_sqsClient as AmazonSQSClient)?.SetCustomUserAgentSuffix(); 50 | } 51 | 52 | EndpointAddress IOutputChannel.RemoteAddress => _queueUrl; 53 | 54 | Uri IOutputChannel.Via => _via; 55 | 56 | public void Send(Message message) 57 | { 58 | var messageBuffer = EncodeMessage(message); 59 | 60 | try 61 | { 62 | var serializedMessage = Encoding.UTF8.GetString(messageBuffer.ToArray()); 63 | var sendMessageRequest = new SendMessageRequest 64 | { 65 | MessageBody = serializedMessage, 66 | QueueUrl = _queueUrl.ToString() 67 | }; 68 | if (_queueUrl.ToString().EndsWith(".fifo", StringComparison.InvariantCultureIgnoreCase)) 69 | { 70 | sendMessageRequest.MessageGroupId = _queueUrl.ToString(); 71 | } 72 | var response = _sqsClient.SendMessageAsync(sendMessageRequest).Result; 73 | response.Validate(); 74 | } 75 | finally 76 | { 77 | // Make sure buffers are always returned to the BufferManager 78 | _parent.BufferManager.ReturnBuffer(messageBuffer.Array); 79 | } 80 | } 81 | 82 | public void Send(Message message, TimeSpan timeout) 83 | { 84 | Send(message); 85 | } 86 | 87 | public override T GetProperty() 88 | { 89 | if (typeof(T) == typeof(IOutputChannel)) 90 | return (T)(object)this; 91 | 92 | return _encoder.GetProperty() ?? base.GetProperty(); 93 | } 94 | 95 | /// 96 | /// Address the Message and serialize it into a byte array. 97 | /// 98 | internal ArraySegment EncodeMessage(Message message) 99 | { 100 | try 101 | { 102 | _queueUrl.ApplyTo(message); 103 | return _encoder.WriteMessage(message, int.MaxValue, _parent.BufferManager); 104 | } 105 | finally 106 | { 107 | // We have consumed the message by serializing it, so clean up 108 | message.Close(); 109 | } 110 | } 111 | 112 | #region Events 113 | /// 114 | /// Open the channel for use. We do not have any blocking work to perform so this is a no-op 115 | /// 116 | [ExcludeFromCodeCoverage] 117 | protected override void OnOpen(TimeSpan timeout) { } 118 | 119 | /// 120 | /// no-op 121 | /// 122 | [ExcludeFromCodeCoverage] 123 | protected override void OnAbort() { } 124 | 125 | /// 126 | /// no-op 127 | /// 128 | [ExcludeFromCodeCoverage] 129 | protected override void OnClose(TimeSpan timeout) { } 130 | #endregion 131 | 132 | #region OnBegin/OnEnd methods 133 | 134 | [ExcludeFromCodeCoverage] 135 | protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) 136 | { 137 | return Task.CompletedTask.ToApm(callback, state); 138 | } 139 | 140 | [ExcludeFromCodeCoverage] 141 | protected override void OnEndOpen(IAsyncResult result) 142 | { 143 | result.ToApmEnd(); 144 | } 145 | 146 | [ExcludeFromCodeCoverage] 147 | protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) 148 | { 149 | return Task.CompletedTask.ToApm(callback, state); 150 | } 151 | 152 | [ExcludeFromCodeCoverage] 153 | protected override void OnEndClose(IAsyncResult result) 154 | { 155 | result.ToApmEnd(); 156 | } 157 | 158 | [ExcludeFromCodeCoverage] 159 | public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state) 160 | { 161 | var task = Task.Run(() => Send(message)); 162 | 163 | return task.ToApm(callback, state); 164 | } 165 | 166 | [ExcludeFromCodeCoverage] 167 | public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state) 168 | { 169 | return BeginSend(message, callback, state); 170 | } 171 | 172 | [ExcludeFromCodeCoverage] 173 | public void EndSend(IAsyncResult result) 174 | { 175 | result.ToApmEnd(); 176 | } 177 | 178 | #endregion 179 | } 180 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/NegativeIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel; 3 | using Amazon; 4 | using Amazon.IdentityManagement; 5 | using Amazon.IdentityManagement.Model; 6 | using Amazon.SecurityToken.Model; 7 | using Amazon.SQS; 8 | using Amazon.SQS.Model; 9 | using AWS.Extensions.IntegrationTests.SQS.TestHelpers; 10 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 11 | using Shouldly; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | 15 | namespace AWS.Extensions.IntegrationTests.SQS; 16 | 17 | [Collection("ClientAndServer collection")] 18 | [ExcludeFromCodeCoverage] 19 | public class NegativeIntegrationTests : IDisposable 20 | { 21 | public const string SqsReadOnlyRoleName = "IntegrationTestsSqsReadOnlyRole"; 22 | 23 | private readonly ITestOutputHelper _output; 24 | private readonly ClientAndServerFixture _clientAndServerFixture; 25 | 26 | public NegativeIntegrationTests(ITestOutputHelper output, ClientAndServerFixture clientAndServerFixture) 27 | { 28 | _output = output; 29 | _clientAndServerFixture = clientAndServerFixture; 30 | 31 | AWSConfigs.InitializeCollections = true; 32 | } 33 | 34 | [Fact] 35 | [ExcludeFromCodeCoverage] 36 | public async Task ClientWithInsufficientQueuePermissionsThrowsException() 37 | { 38 | // ARRANGE 39 | string queueName = ClientAndServerFixture.QueueWithDefaultSettings; 40 | 41 | _clientAndServerFixture.Start(_output, queueName: queueName); 42 | 43 | var roleArn = await FindSqsReadOnlyRoleArn(_clientAndServerFixture.IamClient!); 44 | 45 | var assumedSqsReadOnlyCreds = await _clientAndServerFixture.StsClient!.AssumeRoleAsync( 46 | new AssumeRoleRequest 47 | { 48 | RoleArn = roleArn, 49 | RoleSessionName = nameof(ClientWithInsufficientQueuePermissionsThrowsException) 50 | } 51 | ); 52 | 53 | // Create SQS Client with read-only perms 54 | var limitedSqsClient = new AmazonSQSClient(assumedSqsReadOnlyCreds.Credentials); 55 | 56 | // Start WCF Client with read-only perms 57 | var sqsBinding = new AWS.WCF.Extensions.SQS.AwsSqsBinding(limitedSqsClient, queueName); 58 | var endpointAddress = new EndpointAddress(new Uri(sqsBinding.QueueUrl)); 59 | var factory = new ChannelFactory(sqsBinding, endpointAddress); 60 | var channel = factory.CreateChannel(); 61 | ((System.ServiceModel.Channels.IChannel)channel).Open(); 62 | 63 | Exception? expectedException = null; 64 | 65 | // ACT 66 | try 67 | { 68 | channel.LogMessage("Expect this to fail - client doesn't have write permissions"); 69 | } 70 | catch (Exception e) 71 | { 72 | expectedException = e; 73 | } 74 | 75 | // ASSERT 76 | expectedException.ShouldNotBeNull(); 77 | expectedException.ShouldBeOfType(); 78 | expectedException.InnerException.ShouldNotBeNull(); 79 | expectedException.InnerException.ShouldBeOfType(); 80 | expectedException.InnerException.Message.ShouldContain( 81 | "no identity-based policy allows the sqs:sendmessage action" 82 | ); 83 | } 84 | 85 | [Fact] 86 | [ExcludeFromCodeCoverage] 87 | public async Task ServerIgnoresMalformedMessage() 88 | { 89 | // ARRANGE 90 | string queueName = nameof(ServerIgnoresMalformedMessage) + DateTime.Now.Ticks; 91 | 92 | _clientAndServerFixture.Start(_output, queueName, new CreateQueueRequest(queueName)); 93 | 94 | var sqsClient = _clientAndServerFixture.SqsClient!; 95 | 96 | var queueUrl = (await sqsClient.GetQueueUrlAsync(queueName)).QueueUrl; 97 | 98 | // ACT 99 | 100 | // send 5 good messages, then 1 bad message, then 5 good messages 101 | for (var i = 0; i < 5; i++) 102 | _clientAndServerFixture.Channel!.LogMessage($"{nameof(ServerIgnoresMalformedMessage)}-1. " + i); 103 | 104 | // send a bad message 105 | await sqsClient.SendMessageAsync(queueUrl, $"{nameof(ServerIgnoresMalformedMessage)} - Not a Soap Message"); 106 | 107 | for (var i = 0; i < 5; i++) 108 | _clientAndServerFixture.Channel!.LogMessage($"{nameof(ServerIgnoresMalformedMessage)}-2. " + i); 109 | 110 | var serverReceivedAllMessages = false; 111 | 112 | // poll for up to 20 seconds 113 | for (var polling = 0; polling < 40; polling++) 114 | { 115 | if (10 == LoggingService.LogResults.Count(m => m.Contains(nameof(ServerIgnoresMalformedMessage)))) 116 | { 117 | serverReceivedAllMessages = true; 118 | break; 119 | } 120 | 121 | await Task.Delay(TimeSpan.FromMilliseconds(500)); 122 | } 123 | 124 | _output.WriteLine($"Received: {string.Join(",", LoggingService.LogResults.ToArray())}"); 125 | 126 | // ASSERT 127 | try 128 | { 129 | Assert.True(serverReceivedAllMessages); 130 | } 131 | finally 132 | { 133 | // cleanup 134 | await sqsClient.DeleteQueueAsync(queueUrl); 135 | } 136 | } 137 | 138 | private async Task FindSqsReadOnlyRoleArn(IAmazonIdentityManagementService iamClient) 139 | { 140 | await foreach (var role in iamClient.Paginators.ListRoles(new ListRolesRequest()).Roles) 141 | { 142 | if (role.RoleName.StartsWith(SqsReadOnlyRoleName)) 143 | return role.Arn; 144 | } 145 | 146 | throw new Exception("Failed to find Role needed for Testing. Was CDK Run?"); 147 | } 148 | 149 | public void Dispose() 150 | { 151 | _clientAndServerFixture.Dispose(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/AWS.CoreWCF.Extensions.Tests/SQSClientExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Net; 3 | using Amazon.Runtime; 4 | using Amazon.Runtime.Internal.Transform; 5 | using Amazon.SQS; 6 | using Amazon.SQS.Model; 7 | using AWS.CoreWCF.Extensions.Common; 8 | using Microsoft.Extensions.Logging; 9 | using NSubstitute; 10 | using NSubstitute.Core; 11 | using NSubstitute.ExceptionExtensions; 12 | using Shouldly; 13 | using Xunit; 14 | 15 | namespace AWS.CoreWCF.Extensions.Tests; 16 | 17 | public class SQSClientExtensionTests 18 | { 19 | [Theory] 20 | [InlineData(HttpStatusCode.BadRequest)] 21 | [InlineData(HttpStatusCode.ServiceUnavailable)] 22 | [InlineData(HttpStatusCode.Processing)] 23 | [ExcludeFromCodeCoverage] 24 | public void ValidateThrowsException(HttpStatusCode errorCode) 25 | { 26 | // ARRANGE 27 | var fakeAmazonWebServiceResponse = new AmazonWebServiceResponse { HttpStatusCode = errorCode }; 28 | 29 | Exception? expectedException = null; 30 | 31 | // ACT 32 | try 33 | { 34 | fakeAmazonWebServiceResponse.Validate(); 35 | } 36 | catch (Exception e) 37 | { 38 | expectedException = e; 39 | } 40 | 41 | // ASSERT 42 | expectedException.ShouldNotBeNull(); 43 | expectedException.ShouldBeOfType(); 44 | } 45 | 46 | [Fact] 47 | public async Task DeleteMessageSwallowsExceptions() 48 | { 49 | // ARRANGE 50 | var fakeSqsClient = Substitute.For(); 51 | fakeSqsClient 52 | .DeleteMessageAsync(Arg.Any(), Arg.Any()) 53 | .ThrowsAsync(new Exception("Testing")); 54 | 55 | // ACT 56 | await fakeSqsClient.DeleteMessageAsync("queueUrl", "recipeHandle", Substitute.For()); 57 | 58 | // ASSERT 59 | // expect no exception to be thrown 60 | } 61 | 62 | [Fact] 63 | public async Task ReceiveMessagesSwallowsExceptions() 64 | { 65 | // ARRANGE 66 | var fakeSqsClient = Substitute.For(); 67 | fakeSqsClient 68 | .ReceiveMessageAsync(Arg.Any(), Arg.Any()) 69 | .ThrowsAsync(new Exception("Testing")); 70 | 71 | // ACT 72 | await fakeSqsClient.ReceiveMessagesAsync("queueUrl", Substitute.For()); 73 | 74 | // ASSERT 75 | // expect no exception to be thrown 76 | } 77 | 78 | [Fact] 79 | [ExcludeFromCodeCoverage] 80 | public async Task EnsureSQSQueueWrapsExceptions() 81 | { 82 | // ARRANGE 83 | const string fakeQueue = "fakeQueue"; 84 | 85 | var fakeCreateQueueRequest = new CreateQueueRequest(fakeQueue); 86 | 87 | var fakeSqsClient = Substitute.For(); 88 | fakeSqsClient 89 | .GetQueueUrlAsync(Arg.Is(fakeQueue), Arg.Any()) 90 | .ThrowsAsync(new QueueDoesNotExistException("")); 91 | 92 | fakeSqsClient 93 | .CreateQueueAsync(Arg.Is(fakeQueue), Arg.Any()) 94 | .ThrowsAsync(new Exception("Testing")); 95 | 96 | Exception? expectedException = null; 97 | 98 | // ACT 99 | try 100 | { 101 | await fakeSqsClient.EnsureSQSQueue(fakeCreateQueueRequest); 102 | } 103 | catch (Exception e) 104 | { 105 | expectedException = e; 106 | } 107 | 108 | // ASSERT 109 | expectedException.ShouldNotBeNull(); 110 | expectedException.Message.ShouldContain(fakeQueue); 111 | expectedException.Message.ShouldContain("Failed"); 112 | } 113 | 114 | [Fact] 115 | public async Task EnsureQueueDoesNotTryToCreateDeadLetterQueueIfItAlreadyExists() 116 | { 117 | // ARRANGE 118 | const string fakeDlqArn = "dlq"; 119 | 120 | const string fakeQueue = "fakeQueue"; 121 | 122 | var fakeCreateQueueRequest = new CreateQueueRequest() 123 | .SetDefaultValues(fakeQueue) 124 | .WithDeadLetterQueue(deadLetterTargetArn: fakeDlqArn); 125 | 126 | var fakeSqsClient = Substitute.For(); 127 | fakeSqsClient 128 | .GetQueueUrlAsync(Arg.Is(fakeQueue), Arg.Any()) 129 | .Returns( 130 | // first pretend queue hasn't been created 131 | x => throw new QueueDoesNotExistException(""), 132 | // on second call, return a fake url 133 | x => 134 | Task.FromResult( 135 | new GetQueueUrlResponse { HttpStatusCode = HttpStatusCode.OK, QueueUrl = "fakeUrl" } 136 | ) 137 | ); 138 | 139 | fakeSqsClient 140 | .GetQueueAttributesAsync(Arg.Any(), Arg.Any()) 141 | .Returns( 142 | Task.FromResult( 143 | new GetQueueAttributesResponse 144 | { 145 | HttpStatusCode = HttpStatusCode.OK, 146 | Attributes = new Dictionary { { "QueueArn", "fake:arn:with:parts" } } 147 | } 148 | ) 149 | ); 150 | 151 | fakeSqsClient 152 | .SetQueueAttributesAsync(Arg.Any(), Arg.Any()) 153 | .Returns(Task.FromResult(new SetQueueAttributesResponse { HttpStatusCode = HttpStatusCode.OK })); 154 | 155 | // ok to create fakeQueue 156 | fakeSqsClient 157 | .CreateQueueAsync(Arg.Is(req => req.QueueName == fakeQueue)) 158 | .Returns(Task.FromResult(new CreateQueueResponse { HttpStatusCode = HttpStatusCode.OK })); 159 | 160 | // fail if attempting to create dlq 161 | fakeSqsClient 162 | .CreateQueueAsync(Arg.Is(req => req.QueueName.Contains("DLQ"))) 163 | .ThrowsAsync(new Exception("Fail: Should not try and create dlq")); 164 | 165 | // ACT 166 | await fakeSqsClient.EnsureSQSQueue(fakeCreateQueueRequest); 167 | 168 | // ASSERT 169 | // expect no exception to be thrown 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/AWS.CoreWCF.Extensions/SQS/Infrastructure/SQSMessageProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Amazon.SQS; 3 | using Amazon.SQS.Model; 4 | using AWS.CoreWCF.Extensions.Common; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace AWS.CoreWCF.Extensions.SQS.Infrastructure; 8 | 9 | /// 10 | /// Requires a custom DI Factory. See 11 | /// 12 | public interface ISQSMessageProvider 13 | { 14 | Task ReceiveMessageAsync(string queueName); 15 | Task DeleteSqsMessageAsync(string queueName, string receiptHandle); 16 | } 17 | 18 | internal class SQSMessageProvider : ISQSMessageProvider 19 | { 20 | private readonly ILogger _logger; 21 | 22 | private readonly ConcurrentDictionary _cache = 23 | new(StringComparer.InvariantCultureIgnoreCase); 24 | 25 | public SQSMessageProvider( 26 | IEnumerable namedSQSClientCollections, 27 | ILogger logger 28 | ) 29 | { 30 | _logger = logger; 31 | 32 | var namedSQSClients = namedSQSClientCollections.SelectMany(x => x).ToList(); 33 | 34 | foreach (var namedSQSClient in namedSQSClients) 35 | { 36 | if (null == namedSQSClient?.SQSClient || string.IsNullOrEmpty(namedSQSClient.QueueName)) 37 | throw new ArgumentException($"Invalid [{nameof(NamedSQSClient)}]", nameof(namedSQSClientCollections)); 38 | 39 | string queueUrl; 40 | TimeSpan messageVisibilityTimeout; 41 | try 42 | { 43 | queueUrl = namedSQSClient.SQSClient.GetQueueUrlAsync(namedSQSClient.QueueName).Result.QueueUrl; 44 | 45 | messageVisibilityTimeout = TimeSpan.FromSeconds( 46 | namedSQSClient 47 | .SQSClient.GetQueueAttributesAsync( 48 | queueUrl, 49 | new List { QueueAttributeName.VisibilityTimeout } 50 | ) 51 | .Result.VisibilityTimeout 52 | ); 53 | } 54 | catch (Exception e) 55 | { 56 | throw new ArgumentException( 57 | $"Exception loading Queue details for [{namedSQSClient.QueueName}]. " 58 | + $"Make sure it has been created.", 59 | nameof(namedSQSClientCollections), 60 | e 61 | ); 62 | } 63 | 64 | var entry = new SQSMessageProviderQueueCacheEntry 65 | { 66 | QueueName = namedSQSClient.QueueName!, 67 | QueueUrl = queueUrl, 68 | SQSClient = namedSQSClient.SQSClient, 69 | MessageVisibilityTimeout = messageVisibilityTimeout 70 | }; 71 | 72 | _cache.AddOrUpdate(namedSQSClient.QueueName!, _ => entry, (_, _) => entry); 73 | } 74 | } 75 | 76 | public async Task ReceiveMessageAsync(string queueName) 77 | { 78 | if (!_cache.TryGetValue(queueName, out var cacheEntry)) 79 | throw new ArgumentException( 80 | $"QueueName [{queueName}] was not found. Was it registered in the Constructor?", 81 | nameof(queueName) 82 | ); 83 | 84 | var cachedMessages = cacheEntry.QueueMessages; 85 | var sqsClient = cacheEntry.SQSClient; 86 | var queueUrl = cacheEntry.QueueUrl; 87 | var mutex = cacheEntry.Mutex; 88 | 89 | if (cachedMessages.IsEmpty || cacheEntry.IsExpired()) 90 | { 91 | await mutex.WaitAsync().ConfigureAwait(false); 92 | 93 | try 94 | { 95 | if (cacheEntry.IsExpired()) 96 | // clear the cache as SQS may have started giving these messages 97 | // to other workers. we'll need to reacquire a new batch of messages 98 | while (cachedMessages.TryDequeue(out _)) { } 99 | 100 | if (cachedMessages.IsEmpty) 101 | { 102 | // reset cache expiration. this value is conservative, as we're 103 | // setting expiration _before_ the ReceiveMessage call is sent to SQS. 104 | cacheEntry.RefreshExpirationTime(); 105 | 106 | var newMessages = await sqsClient.ReceiveMessagesAsync(queueUrl, _logger); 107 | foreach (var newMessage in newMessages) 108 | { 109 | cachedMessages.Enqueue(newMessage); 110 | } 111 | } 112 | } 113 | finally 114 | { 115 | mutex.Release(); 116 | } 117 | } 118 | 119 | if (cachedMessages.TryDequeue(out var message)) 120 | { 121 | return message; 122 | } 123 | 124 | return null; 125 | } 126 | 127 | public async Task DeleteSqsMessageAsync(string queueName, string receiptHandle) 128 | { 129 | if (!_cache.TryGetValue(queueName, out var cacheEntry)) 130 | throw new ArgumentException( 131 | $"QueueName [{queueName}] was not found. Was it registered in the Constructor?", 132 | nameof(queueName) 133 | ); 134 | 135 | await cacheEntry.SQSClient.DeleteMessageAsync(cacheEntry.QueueUrl, receiptHandle, _logger); 136 | } 137 | 138 | private class SQSMessageProviderQueueCacheEntry 139 | { 140 | public ConcurrentQueue QueueMessages { get; } = new(); 141 | public SemaphoreSlim Mutex { get; } = new SemaphoreSlim(1, 1); 142 | public string QueueName { get; set; } 143 | public IAmazonSQS SQSClient { get; set; } 144 | public string QueueUrl { get; set; } 145 | public TimeSpan MessageVisibilityTimeout { get; set; } 146 | public DateTimeOffset CacheEntryExpiration { get; set; } 147 | 148 | public bool IsExpired() 149 | { 150 | return DateTimeOffset.Now > CacheEntryExpiration; 151 | } 152 | 153 | public void RefreshExpirationTime() 154 | { 155 | CacheEntryExpiration = DateTimeOffset.Now.Add(MessageVisibilityTimeout); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/AWS.Extensions.PerformanceTests/ServerPerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Net; 3 | using System.ServiceModel; 4 | using Amazon.SQS; 5 | using Amazon.SQS.Model; 6 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 7 | using AWS.Extensions.PerformanceTests.Common; 8 | using BenchmarkDotNet.Attributes; 9 | using Microsoft.AspNetCore.Hosting; 10 | using NSubstitute; 11 | 12 | namespace AWS.Extensions.PerformanceTests 13 | { 14 | /// 15 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 16 | [SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 1)] 17 | [ExcludeFromCodeCoverage] 18 | public class ServerPerformanceTests 19 | { 20 | private IWebHost? _host; 21 | private IAmazonSQS? _setupSqsClient; 22 | private readonly string _queueName = $"{nameof(ServerPerformanceTests)}-{DateTime.Now.Ticks}"; 23 | private string _queueUrl = ""; 24 | 25 | [Params(1, 4)] 26 | public int Threads { get; set; } 27 | 28 | [GlobalSetup] 29 | public async Task CreateInfrastructure() 30 | { 31 | _setupSqsClient = new AmazonSQSClient(); 32 | 33 | await _setupSqsClient.CreateQueueAsync(_queueName); 34 | _queueUrl = (await _setupSqsClient!.GetQueueUrlAsync(_queueName))?.QueueUrl ?? ""; 35 | 36 | Console.WriteLine($"QueueName: {_queueName}"); 37 | } 38 | 39 | [IterationSetup] 40 | public void Setup() 41 | { 42 | LoggingService.LogResults.Clear(); 43 | 44 | StartupHost().Wait(); 45 | } 46 | 47 | public async Task StartupHost() 48 | { 49 | Console.WriteLine(); 50 | Console.WriteLine("================================"); 51 | Console.WriteLine("================================"); 52 | Console.WriteLine($"Begin {nameof(StartupHost)}"); 53 | 54 | #region Configure Host 55 | 56 | _host = ServerFactory.StartServer( 57 | _queueName, 58 | _queueUrl, 59 | new AWS.CoreWCF.Extensions.SQS.Channels.AwsSqsBinding(concurrencyLevel: Threads) 60 | ); 61 | 62 | #endregion 63 | 64 | #region Pre Saturate Queue 65 | 66 | await ClientMessageGenerator.SaturateQueue(_setupSqsClient, _queueName, _queueUrl); 67 | 68 | #endregion 69 | } 70 | 71 | [IterationCleanup] 72 | public void CleanupHost() 73 | { 74 | _host?.Dispose(); 75 | } 76 | 77 | [GlobalCleanup] 78 | public async Task CleanUp() 79 | { 80 | await _setupSqsClient!.DeleteQueueAsync(_queueUrl); 81 | } 82 | 83 | [Benchmark] 84 | public async Task ServerCanProcess1000Messages() 85 | { 86 | var maxTime = TimeSpan.FromMinutes(5); 87 | 88 | var cancelToken = new CancellationTokenSource(maxTime).Token; 89 | 90 | // start the server 91 | await _host!.StartAsync(cancelToken); 92 | 93 | // wait for server to process all messages 94 | while (LoggingService.LogResults.Count < 1000) 95 | { 96 | Console.WriteLine($"Processed [{LoggingService.LogResults.Count}] Messages"); 97 | 98 | await Task.Delay(TimeSpan.FromMilliseconds(250), cancelToken); 99 | } 100 | 101 | Console.WriteLine($"Processed [{LoggingService.LogResults.Count}] messages"); 102 | } 103 | } 104 | 105 | public static class ClientMessageGenerator 106 | { 107 | public static async Task SaturateQueue( 108 | IAmazonSQS setupSqsClient, 109 | string queueName, 110 | string queueUrl, 111 | int numMessages = 1000 112 | ) 113 | { 114 | var message = $"{queueName}-Message"; 115 | 116 | var rawMessage = ClientMessageGenerator.BuildRawClientMessage( 117 | queueUrl, 118 | loggingClient => loggingClient.LogMessage(message) 119 | ); 120 | 121 | for (var j = 0; j < numMessages / 10; j++) 122 | { 123 | var batchMessages = Enumerable 124 | .Range(0, 10) 125 | .Select(_ => new SendMessageBatchRequestEntry(Guid.NewGuid().ToString(), rawMessage)) 126 | .ToList(); 127 | 128 | await setupSqsClient!.SendMessageBatchAsync(queueUrl, batchMessages); 129 | } 130 | 131 | Console.WriteLine("Queue Saturation Complete"); 132 | } 133 | 134 | public static string BuildRawClientMessage(string queueUrl, Action clientAction) 135 | where TContract : class 136 | { 137 | var fakeQueueName = "fake"; 138 | var mockSqs = Substitute.For(); 139 | 140 | // intercept the call the client will make to SendMessageAsync and capture the SendMessageRequest 141 | SendMessageRequest? capturedSendMessageRequest = null; 142 | 143 | mockSqs 144 | .SendMessageAsync( 145 | Arg.Do(r => 146 | { 147 | capturedSendMessageRequest = r; 148 | }), 149 | Arg.Any() 150 | ) 151 | .Returns(Task.FromResult(new SendMessageResponse { HttpStatusCode = HttpStatusCode.OK })); 152 | 153 | mockSqs 154 | .GetQueueUrlAsync(Arg.Any()) 155 | .Returns(Task.FromResult(new GetQueueUrlResponse { QueueUrl = queueUrl })); 156 | 157 | var sqsBinding = new AWS.WCF.Extensions.SQS.AwsSqsBinding(mockSqs, fakeQueueName); 158 | var endpointAddress = new EndpointAddress(new Uri(sqsBinding.QueueUrl)); 159 | var factory = new ChannelFactory(sqsBinding, endpointAddress); 160 | var channel = factory.CreateChannel(); 161 | ((System.ServiceModel.Channels.IChannel)channel).Open(); 162 | 163 | var client = (TContract)channel; 164 | 165 | clientAction.Invoke(client); 166 | 167 | return capturedSendMessageRequest?.MessageBody ?? ""; 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/AWS.WCF.Extensions/SQS/Runtime/TaskHelper.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace AWS.WCF.Extensions.SQS.Runtime 7 | { 8 | /// 9 | /// Clone of CoreWCF.Runtime.TaskHelper (in CoreWCF.RabbitMQ.Client nuget package) 10 | /// 11 | [ExcludeFromCodeCoverage] 12 | internal static class TaskHelpers 13 | { 14 | // Helper method when implementing an APM wrapper around a Task based async method which returns a result. 15 | // In the BeginMethod method, you would call use ToApm to wrap a call to MethodAsync: 16 | // return MethodAsync(params).ToApm(callback, state); 17 | // In the EndMethod, you would use ToApmEnd to ensure the correct exception handling 18 | // This will handle throwing exceptions in the correct place and ensure the IAsyncResult contains the provided 19 | // state object 20 | public static IAsyncResult ToApm(this Task task, AsyncCallback callback, object state) => 21 | ToApm(new ValueTask(task), callback, state); 22 | 23 | /// 24 | /// Helper method to convert from Task async method to "APM" (IAsyncResult with Begin/End calls) 25 | /// 26 | public static IAsyncResult ToApm(this ValueTask valueTask, AsyncCallback callback, object state) 27 | { 28 | var result = new AsyncResult(valueTask, callback, state); 29 | if (result.CompletedSynchronously) 30 | { 31 | result.ExecuteCallback(); 32 | } 33 | else if (callback != null) 34 | { 35 | // We use OnCompleted rather than ContinueWith in order to avoid running synchronously 36 | // if the task has already completed by the time we get here. 37 | // This will allocate a delegate and some extra data to add it as a TaskContinuation 38 | valueTask.ConfigureAwait(false).GetAwaiter().OnCompleted(result.ExecuteCallback); 39 | } 40 | 41 | return result; 42 | } 43 | 44 | /// 45 | /// Helper method to convert from Task async method to "APM" (IAsyncResult with Begin/End calls) 46 | /// 47 | public static IAsyncResult ToApm(this Task task, AsyncCallback callback, object state) 48 | { 49 | var result = new AsyncResult(task, callback, state); 50 | if (result.CompletedSynchronously) 51 | { 52 | result.ExecuteCallback(); 53 | } 54 | else if (callback != null) 55 | { 56 | // We use OnCompleted rather than ContinueWith in order to avoid running synchronously 57 | // if the task has already completed by the time we get here. 58 | // This will allocate a delegate and some extra data to add it as a TaskContinuation 59 | task.ConfigureAwait(false).GetAwaiter().OnCompleted(result.ExecuteCallback); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | public static T ToApmEnd(this IAsyncResult asyncResult) 66 | { 67 | if (asyncResult is AsyncResult asyncResultInstance) 68 | { 69 | return asyncResultInstance.GetResult(); 70 | } 71 | else 72 | { 73 | // throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( 74 | // new ArgumentException(SRCommon.SFxInvalidCallbackIAsyncResult)); 75 | throw new ArgumentException(nameof(asyncResult)); 76 | } 77 | } 78 | 79 | public static void ToApmEnd(this IAsyncResult asyncResult) 80 | { 81 | if (asyncResult is AsyncResult asyncResultInstance) 82 | { 83 | asyncResultInstance.GetResult(); 84 | } 85 | else 86 | { 87 | // throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( 88 | // new ArgumentException(SRCommon.SFxInvalidCallbackIAsyncResult)); 89 | throw new ArgumentException(nameof(asyncResult)); 90 | } 91 | } 92 | 93 | private class AsyncResult : IAsyncResult 94 | { 95 | private readonly Task _task; 96 | private readonly AsyncCallback _asyncCallback; 97 | 98 | public AsyncResult(Task task, AsyncCallback asyncCallback, object asyncState) 99 | { 100 | _task = task; 101 | _asyncCallback = asyncCallback; 102 | AsyncState = asyncState; 103 | CompletedSynchronously = task.IsCompleted; 104 | } 105 | 106 | public void GetResult() => _task.GetAwaiter().GetResult(); 107 | 108 | public void ExecuteCallback() => _asyncCallback?.Invoke(this); 109 | 110 | public object AsyncState { get; } 111 | WaitHandle IAsyncResult.AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; 112 | 113 | public bool CompletedSynchronously { get; } 114 | public bool IsCompleted => _task.IsCompleted; 115 | } 116 | 117 | internal class AsyncResult : IAsyncResult 118 | { 119 | private readonly ValueTask _task; 120 | private readonly AsyncCallback _asyncCallback; 121 | 122 | public AsyncResult(ValueTask task, AsyncCallback asyncCallback, object asyncState) 123 | { 124 | _task = task; 125 | _asyncCallback = asyncCallback; 126 | AsyncState = asyncState; 127 | CompletedSynchronously = task.IsCompleted; 128 | } 129 | 130 | public T GetResult() => _task.GetAwaiter().GetResult(); 131 | 132 | public bool IsFaulted => _task.IsFaulted; 133 | public AggregateException Exception => _task.AsTask().Exception; 134 | 135 | // Calls the async callback with this as parameter 136 | public void ExecuteCallback() => _asyncCallback?.Invoke(this); 137 | 138 | public object AsyncState { get; } 139 | 140 | WaitHandle IAsyncResult.AsyncWaitHandle => 141 | !CompletedSynchronously 142 | ? ((IAsyncResult)_task.AsTask()).AsyncWaitHandle 143 | : throw new NotImplementedException(); 144 | 145 | public bool CompletedSynchronously { get; } 146 | public bool IsCompleted => _task.IsCompleted; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/AWS.Extensions.IntegrationTests/SQS/TestHelpers/ClientAndServerFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.ServiceModel; 3 | using Amazon; 4 | using Amazon.Extensions.NETCore.Setup; 5 | using Amazon.IdentityManagement; 6 | using Amazon.Runtime; 7 | using Amazon.SecurityToken; 8 | using Amazon.SimpleNotificationService; 9 | using Amazon.SimpleNotificationService.Model; 10 | using Amazon.SQS; 11 | using Amazon.SQS.Model; 12 | using AWS.CoreWCF.Extensions.Common; 13 | using AWS.CoreWCF.Extensions.SQS.Channels; 14 | using AWS.CoreWCF.Extensions.SQS.DispatchCallbacks; 15 | using AWS.CoreWCF.Extensions.SQS.Infrastructure; 16 | using AWS.Extensions.IntegrationTests.Common; 17 | using AWS.Extensions.IntegrationTests.SQS.TestService; 18 | using AWS.Extensions.IntegrationTests.SQS.TestService.ServiceContract; 19 | using CoreWCF.Configuration; 20 | using CoreWCF.Queue.Common.Configuration; 21 | using Microsoft.AspNetCore.Hosting; 22 | using Microsoft.Extensions.Configuration; 23 | using Microsoft.Extensions.DependencyInjection; 24 | using Microsoft.Extensions.Logging; 25 | using Microsoft.Extensions.Logging.Abstractions; 26 | using Xunit; 27 | using Xunit.Abstractions; 28 | using JsonSerializer = System.Text.Json.JsonSerializer; 29 | 30 | namespace AWS.Extensions.IntegrationTests.SQS.TestHelpers; 31 | 32 | [CollectionDefinition("ClientAndServer collection")] 33 | public class ClientAndServerCollectionFixture : ICollectionFixture 34 | { 35 | // This class has no code, and is never created. Its purpose is simply 36 | // to be the place to apply [CollectionDefinition] and all the 37 | // ICollectionFixture<> interfaces. 38 | } 39 | 40 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 41 | public class ClientAndServerFixture : IDisposable 42 | { 43 | private ChannelFactory? _factory; 44 | 45 | public const string QueueWithDefaultSettings = "CoreWCFExtensionsDefaultSettingsQueue"; 46 | public const string FifoQueueName = "CoreWCFExtensionsTest.fifo"; 47 | public const string SnsNotificationSuccessQueue = "CoreWCF-SNSSuccessQueue"; 48 | 49 | public const string SuccessTopicName = "CoreWCF-Success"; 50 | public const string FailureTopicName = "CoreWCF-Failure"; 51 | 52 | public IWebHost? Host { get; private set; } 53 | public ILoggingService? Channel { get; private set; } 54 | public IAmazonSQS? SqsClient { get; private set; } 55 | public IAmazonSecurityTokenService? StsClient { get; set; } 56 | public IAmazonIdentityManagementService? IamClient { get; set; } 57 | public string? QueueName { get; private set; } 58 | 59 | public Settings? Settings { get; private set; } 60 | 61 | public void Start( 62 | ITestOutputHelper testOutputHelper, 63 | string queueName, 64 | CreateQueueRequest? createQueue = null, 65 | IDispatchCallbacksCollection? dispatchCallbacks = null 66 | ) 67 | { 68 | QueueName = queueName; 69 | 70 | var config = new ConfigurationBuilder() 71 | .AddJsonFile(Path.Combine("SQS", "appsettings.test.json")) 72 | .AddEnvironmentVariables() 73 | .Build(); 74 | 75 | Settings = config.Get(); 76 | 77 | var defaultAwsOptions = !string.IsNullOrEmpty(Settings?.AWS?.AWS_ACCESS_KEY_ID) 78 | ? new AWSOptions 79 | { 80 | Credentials = new BasicAWSCredentials( 81 | Settings.AWS.AWS_ACCESS_KEY_ID, 82 | Settings.AWS.AWS_SECRET_ACCESS_KEY 83 | ), 84 | Region = RegionEndpoint.GetBySystemName(Settings.AWS.AWS_REGION) 85 | } 86 | : config.GetAWSOptions(); 87 | 88 | // bootstrap helper aws services 89 | var serviceProvider = new ServiceCollection() 90 | .AddAWSService() 91 | .AddAWSService() 92 | .AddAWSService() 93 | .AddAWSService() 94 | .AddDefaultAWSOptions(defaultAwsOptions) 95 | .BuildServiceProvider(); 96 | 97 | SqsClient = serviceProvider.GetService(); 98 | StsClient = serviceProvider.GetService(); 99 | IamClient = serviceProvider.GetService(); 100 | 101 | var snsClient = serviceProvider.GetService()!; 102 | 103 | var successTopicArn = snsClient.FindTopicAsync(SuccessTopicName).Result.TopicArn; 104 | var failureTopicArn = snsClient.FindTopicAsync(FailureTopicName).Result.TopicArn; 105 | 106 | dispatchCallbacks ??= DispatchCallbacksCollectionFactory.GetDefaultCallbacksCollectionWithSns( 107 | successTopicArn, 108 | failureTopicArn 109 | ); 110 | 111 | Host = ServiceHelper 112 | .CreateServiceHost( 113 | configureServices: services => 114 | services 115 | .AddDefaultAWSOptions(defaultAwsOptions) 116 | .AddSingleton(NullLogger.Instance) 117 | .AddAWSService() 118 | .AddServiceModelServices() 119 | .AddQueueTransport() 120 | .AddSQSClient(queueName), 121 | configure: app => 122 | { 123 | var queueUrl = app.EnsureSqsQueue(queueName, createQueue); 124 | 125 | app.UseServiceModel(services => 126 | { 127 | services.AddService(); 128 | services.AddServiceEndpoint( 129 | new AwsSqsBinding { DispatchCallbacksCollection = dispatchCallbacks }, 130 | queueUrl 131 | ); 132 | }); 133 | }, 134 | testOutputHelper: testOutputHelper 135 | ) 136 | .Build(); 137 | 138 | Host.Start(); 139 | 140 | // Start Client 141 | var sqsBinding = new AWS.WCF.Extensions.SQS.AwsSqsBinding(SqsClient, QueueName); 142 | var endpointAddress = new EndpointAddress(new Uri(sqsBinding.QueueUrl)); 143 | _factory = new ChannelFactory(sqsBinding, endpointAddress); 144 | Channel = _factory.CreateChannel(); 145 | ((System.ServiceModel.Channels.IChannel)Channel).Open(); 146 | } 147 | 148 | public void Dispose() 149 | { 150 | if (Host != null) 151 | { 152 | Host.Dispose(); 153 | } 154 | 155 | if (Channel != null) 156 | { 157 | ((System.ServiceModel.Channels.IChannel)Channel).Close(); 158 | } 159 | } 160 | } 161 | --------------------------------------------------------------------------------