├── .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 |
--------------------------------------------------------------------------------