@@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.Test", "test | |||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.ConsoleApp", "samples\Sample.ConsoleApp\Sample.ConsoleApp.csproj", "{2B0F467E-ABBD-4A51-BF38-D4F609DB6266}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.ConsoleApp", "samples\Sample.ConsoleApp\Sample.ConsoleApp.csproj", "{2B0F467E-ABBD-4A51-BF38-D4F609DB6266}" | ||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.AmazonSQS", "src\DotNetCore.CAP.AmazonSQS\DotNetCore.CAP.AmazonSQS.csproj", "{43475E00-51B7-443D-BC2D-FC21F9D8A0B4}" | |||||
EndProject | |||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | Debug|Any CPU = Debug|Any CPU | ||||
@@ -147,6 +149,10 @@ Global | |||||
{2B0F467E-ABBD-4A51-BF38-D4F609DB6266}.Debug|Any CPU.Build.0 = Debug|Any CPU | {2B0F467E-ABBD-4A51-BF38-D4F609DB6266}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
{2B0F467E-ABBD-4A51-BF38-D4F609DB6266}.Release|Any CPU.ActiveCfg = Release|Any CPU | {2B0F467E-ABBD-4A51-BF38-D4F609DB6266}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
{2B0F467E-ABBD-4A51-BF38-D4F609DB6266}.Release|Any CPU.Build.0 = Release|Any CPU | {2B0F467E-ABBD-4A51-BF38-D4F609DB6266}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
@@ -171,6 +177,7 @@ Global | |||||
{93176BAE-914B-4BED-9DE3-01FFB4F27FC5} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} | {93176BAE-914B-4BED-9DE3-01FFB4F27FC5} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} | ||||
{75CC45E6-BF06-40F4-977D-10DCC05B2EFA} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} | {75CC45E6-BF06-40F4-977D-10DCC05B2EFA} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} | ||||
{2B0F467E-ABBD-4A51-BF38-D4F609DB6266} = {3A6B6931-A123-477A-9469-8B468B5385AF} | {2B0F467E-ABBD-4A51-BF38-D4F609DB6266} = {3A6B6931-A123-477A-9469-8B468B5385AF} | ||||
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(ExtensibilityGlobals) = postSolution | GlobalSection(ExtensibilityGlobals) = postSolution | ||||
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} | SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} | ||||
@@ -1,4 +1,5 @@ | |||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SNS/@EntryIndexedValue">SNS</s:String> | |||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mongo/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Mongo/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> |
@@ -0,0 +1,166 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading; | |||||
using Amazon.SimpleNotificationService; | |||||
using Amazon.SimpleNotificationService.Model; | |||||
using Amazon.SQS; | |||||
using Amazon.SQS.Model; | |||||
using DotNetCore.CAP.Messages; | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.Options; | |||||
using Newtonsoft.Json; | |||||
using Headers = DotNetCore.CAP.Messages.Headers; | |||||
namespace DotNetCore.CAP.AmazonSQS | |||||
{ | |||||
internal sealed class AmazonSQSConsumerClient : IConsumerClient | |||||
{ | |||||
private static readonly SemaphoreSlim ConnectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); | |||||
private readonly string _groupId; | |||||
private readonly AmazonSQSOptions _amazonSQSOptions; | |||||
private IAmazonSimpleNotificationService _snsClient; | |||||
private IAmazonSQS _sqsClient; | |||||
private string _queueUrl = string.Empty; | |||||
public AmazonSQSConsumerClient(string groupId, IOptions<AmazonSQSOptions> options) | |||||
{ | |||||
_groupId = groupId; | |||||
_amazonSQSOptions = options.Value; | |||||
} | |||||
public event EventHandler<TransportMessage> OnMessageReceived; | |||||
public event EventHandler<LogMessageEventArgs> OnLog; | |||||
public BrokerAddress BrokerAddress => new BrokerAddress("AmazonSQS", _queueUrl); | |||||
public void Subscribe(IEnumerable<string> topics) | |||||
{ | |||||
if (topics == null) | |||||
{ | |||||
throw new ArgumentNullException(nameof(topics)); | |||||
} | |||||
Connect(initSNS: true, initSQS: false); | |||||
var topicArns = new List<string>(); | |||||
foreach (var topic in topics) | |||||
{ | |||||
var createTopicRequest = new CreateTopicRequest(topic.NormalizeForAws()); | |||||
var createTopicResponse = _snsClient.CreateTopicAsync(createTopicRequest).GetAwaiter().GetResult(); | |||||
topicArns.Add(createTopicResponse.TopicArn); | |||||
} | |||||
Connect(initSNS: false, initSQS: true); | |||||
_snsClient.SubscribeQueueToTopicsAsync(topicArns, _sqsClient, _queueUrl) | |||||
.GetAwaiter().GetResult(); | |||||
} | |||||
public void Listening(TimeSpan timeout, CancellationToken cancellationToken) | |||||
{ | |||||
Connect(); | |||||
var request = new ReceiveMessageRequest(_queueUrl) | |||||
{ | |||||
WaitTimeSeconds = 5, | |||||
MaxNumberOfMessages = 1 | |||||
}; | |||||
while (true) | |||||
{ | |||||
var response = _sqsClient.ReceiveMessageAsync(request, cancellationToken).GetAwaiter().GetResult(); | |||||
if (response.Messages.Count == 1) | |||||
{ | |||||
var messageObj = JsonConvert.DeserializeObject<SQSReceivedMessage>(response.Messages[0].Body); | |||||
var header = messageObj.MessageAttributes.ToDictionary(x => x.Key, x => x.Value.Value); | |||||
var body = messageObj.Message; | |||||
var message = new TransportMessage(header, body != null ? Encoding.UTF8.GetBytes(body) : null); | |||||
message.Headers.Add(Headers.Group, _groupId); | |||||
OnMessageReceived?.Invoke(response.Messages[0].ReceiptHandle, message); | |||||
} | |||||
else | |||||
{ | |||||
cancellationToken.ThrowIfCancellationRequested(); | |||||
cancellationToken.WaitHandle.WaitOne(timeout); | |||||
} | |||||
} | |||||
} | |||||
public void Commit(object sender) | |||||
{ | |||||
_sqsClient.DeleteMessageAsync(_queueUrl, (string)sender); | |||||
} | |||||
public void Reject(object sender) | |||||
{ | |||||
_sqsClient.ChangeMessageVisibilityAsync(_queueUrl, (string)sender, 3000); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_sqsClient?.Dispose(); | |||||
_snsClient?.Dispose(); | |||||
} | |||||
public void Connect(bool initSNS = true, bool initSQS = true) | |||||
{ | |||||
if (_snsClient != null && _sqsClient != null) | |||||
{ | |||||
return; | |||||
} | |||||
if (_snsClient == null && initSNS) | |||||
{ | |||||
ConnectionLock.Wait(); | |||||
try | |||||
{ | |||||
_snsClient = _amazonSQSOptions.Credentials != null | |||||
? new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Credentials, _amazonSQSOptions.Region) | |||||
: new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Region); | |||||
} | |||||
finally | |||||
{ | |||||
ConnectionLock.Release(); | |||||
} | |||||
} | |||||
if (_sqsClient == null && initSQS) | |||||
{ | |||||
ConnectionLock.Wait(); | |||||
try | |||||
{ | |||||
_sqsClient = _amazonSQSOptions.Credentials != null | |||||
? new AmazonSQSClient(_amazonSQSOptions.Credentials, _amazonSQSOptions.Region) | |||||
: new AmazonSQSClient(_amazonSQSOptions.Region); | |||||
// If provide the name of an existing queue along with the exact names and values | |||||
// of all the queue's attributes, <code>CreateQueue</code> returns the queue URL for | |||||
// the existing queue. | |||||
_queueUrl = _sqsClient.CreateQueueAsync(_groupId.NormalizeForAws()).GetAwaiter().GetResult().QueueUrl; | |||||
} | |||||
finally | |||||
{ | |||||
ConnectionLock.Release(); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,31 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.Options; | |||||
namespace DotNetCore.CAP.AmazonSQS | |||||
{ | |||||
internal sealed class AmazonSQSConsumerClientFactory : IConsumerClientFactory | |||||
{ | |||||
private readonly IOptions<AmazonSQSOptions> _amazonSQSOptions; | |||||
public AmazonSQSConsumerClientFactory(IOptions<AmazonSQSOptions> amazonSQSOptions) | |||||
{ | |||||
_amazonSQSOptions = amazonSQSOptions; | |||||
} | |||||
public IConsumerClient Create(string groupId) | |||||
{ | |||||
try | |||||
{ | |||||
var client = new AmazonSQSConsumerClient(groupId, _amazonSQSOptions); | |||||
return client; | |||||
} | |||||
catch (System.Exception e) | |||||
{ | |||||
throw new BrokerConnectionException(e); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,17 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using Amazon; | |||||
using Amazon.Runtime; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
// ReSharper disable once InconsistentNaming | |||||
public class AmazonSQSOptions | |||||
{ | |||||
public RegionEndpoint Region { get; set; } | |||||
public AWSCredentials Credentials { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,30 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using System; | |||||
using DotNetCore.CAP.AmazonSQS; | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
internal sealed class AmazonSQSOptionsExtension : ICapOptionsExtension | |||||
{ | |||||
private readonly Action<AmazonSQSOptions> _configure; | |||||
public AmazonSQSOptionsExtension(Action<AmazonSQSOptions> configure) | |||||
{ | |||||
_configure = configure; | |||||
} | |||||
public void AddServices(IServiceCollection services) | |||||
{ | |||||
services.AddSingleton<CapMessageQueueMakerService>(); | |||||
services.Configure(_configure); | |||||
services.AddSingleton<ITransport, AmazonSQSTransport>(); | |||||
services.AddSingleton<IConsumerClientFactory, AmazonSQSConsumerClientFactory>(); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,30 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using System; | |||||
using Amazon; | |||||
using DotNetCore.CAP; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace Microsoft.Extensions.DependencyInjection | |||||
{ | |||||
public static class CapOptionsExtensions | |||||
{ | |||||
public static CapOptions UseAmazonSQS(this CapOptions options, RegionEndpoint region) | |||||
{ | |||||
return options.UseAmazonSQS(opt => { opt.Region = region; }); | |||||
} | |||||
public static CapOptions UseAmazonSQS(this CapOptions options, Action<AmazonSQSOptions> configure) | |||||
{ | |||||
if (configure == null) | |||||
{ | |||||
throw new ArgumentNullException(nameof(configure)); | |||||
} | |||||
options.RegisterExtension(new AmazonSQSOptionsExtension(configure)); | |||||
return options; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,23 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netstandard2.0</TargetFramework> | |||||
<AssemblyName>DotNetCore.CAP.AmazonSQS</AssemblyName> | |||||
<PackageTags>$(PackageTags);AmazonSQS;SQS</PackageTags> | |||||
</PropertyGroup> | |||||
<PropertyGroup> | |||||
<DocumentationFile>bin\$(Configuration)\netstandard2.0\DotNetCore.CAP.AmazonSQS.xml</DocumentationFile> | |||||
<NoWarn>1701;1702;1705;CS1591</NoWarn> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="AWSSDK.SimpleNotificationService" Version="3.3.101.182" /> | |||||
<PackageReference Include="AWSSDK.SQS" Version="3.3.102.125" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,125 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using Amazon.SimpleNotificationService; | |||||
using Amazon.SimpleNotificationService.Model; | |||||
using DotNetCore.CAP.Internal; | |||||
using DotNetCore.CAP.Messages; | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
namespace DotNetCore.CAP.AmazonSQS | |||||
{ | |||||
internal sealed class AmazonSQSTransport : ITransport | |||||
{ | |||||
private readonly ILogger _logger; | |||||
private readonly IOptions<AmazonSQSOptions> _sqsOptions; | |||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); | |||||
private IAmazonSimpleNotificationService _snsClient; | |||||
private IDictionary<string, string> _topicArnMaps; | |||||
public AmazonSQSTransport(ILogger<AmazonSQSTransport> logger, IOptions<AmazonSQSOptions> sqsOptions) | |||||
{ | |||||
_logger = logger; | |||||
_sqsOptions = sqsOptions; | |||||
} | |||||
public BrokerAddress BrokerAddress => new BrokerAddress("RabbitMQ", string.Empty); | |||||
public async Task<OperateResult> SendAsync(TransportMessage message) | |||||
{ | |||||
try | |||||
{ | |||||
await TryAddTopicArns(); | |||||
if (_topicArnMaps.TryGetValue(message.GetName().NormalizeForAws(), out var arn)) | |||||
{ | |||||
string bodyJson = null; | |||||
if (message.Body != null) | |||||
{ | |||||
bodyJson = Encoding.UTF8.GetString(message.Body); | |||||
} | |||||
var attributes = message.Headers.Where(x => x.Value != null).ToDictionary(x => x.Key, | |||||
x => new MessageAttributeValue | |||||
{ | |||||
StringValue = x.Value, | |||||
DataType = "String" | |||||
}); | |||||
var request = new PublishRequest(arn, bodyJson) | |||||
{ | |||||
MessageAttributes = attributes | |||||
}; | |||||
await _snsClient.PublishAsync(request); | |||||
_logger.LogDebug($"SNS topic message [{message.GetName().NormalizeForAws()}] has been published."); | |||||
} | |||||
else | |||||
{ | |||||
_logger.LogWarning($"Can't be found SNS topics for [{message.GetName().NormalizeForAws()}]"); | |||||
} | |||||
return OperateResult.Success; | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
var wrapperEx = new PublisherSentFailedException(ex.Message, ex); | |||||
var errors = new OperateError | |||||
{ | |||||
Code = ex.HResult.ToString(), | |||||
Description = ex.Message | |||||
}; | |||||
return OperateResult.Failed(wrapperEx, errors); | |||||
} | |||||
} | |||||
public async Task<bool> TryAddTopicArns() | |||||
{ | |||||
if (_topicArnMaps != null) | |||||
{ | |||||
return true; | |||||
} | |||||
await _semaphore.WaitAsync(); | |||||
try | |||||
{ | |||||
_snsClient = _sqsOptions.Value.Credentials != null | |||||
? new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Credentials, _sqsOptions.Value.Region) | |||||
: new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Region); | |||||
if (_topicArnMaps == null) | |||||
{ | |||||
_topicArnMaps = new Dictionary<string, string>(); | |||||
var topics = await _snsClient.ListTopicsAsync(); | |||||
topics.Topics.ForEach(x => | |||||
{ | |||||
var name = x.TopicArn.Split(':').Last(); | |||||
_topicArnMaps.Add(name, x.TopicArn); | |||||
}); | |||||
return true; | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
_logger.LogError(e, "Init topics from aws sns error!"); | |||||
} | |||||
finally | |||||
{ | |||||
_semaphore.Release(); | |||||
} | |||||
return false; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,18 @@ | |||||
using System.Collections.Generic; | |||||
namespace DotNetCore.CAP.AmazonSQS | |||||
{ | |||||
class SQSReceivedMessage | |||||
{ | |||||
public string Message { get; set; } | |||||
public Dictionary<string, SQSReceivedMessageAttributes> MessageAttributes { get; set; } | |||||
} | |||||
class SQSReceivedMessageAttributes | |||||
{ | |||||
public string Type { get; set; } | |||||
public string Value { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
using System; | |||||
namespace DotNetCore.CAP.AmazonSQS | |||||
{ | |||||
public static class TopicNormalizer | |||||
{ | |||||
public static string NormalizeForAws(this string origin) | |||||
{ | |||||
if (origin.Length > 256) | |||||
{ | |||||
throw new ArgumentOutOfRangeException(nameof(origin) + " character string length must between 1~256!"); | |||||
} | |||||
return origin.Replace(".", "-").Replace(":", "_"); | |||||
} | |||||
public static string DeNormalizeForAws(this string origin) | |||||
{ | |||||
return origin.Replace("-", ".").Replace("_", ":"); | |||||
} | |||||
} | |||||
} |