@@ -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.NATS", "src\DotNetCore.CAP.NATS\DotNetCore.CAP.NATS.csproj", "{25A1B3A1-DD74-436C-9956-17E04FE7643D}" | |||||
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 | ||||
{25A1B3A1-DD74-436C-9956-17E04FE7643D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{25A1B3A1-DD74-436C-9956-17E04FE7643D}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{25A1B3A1-DD74-436C-9956-17E04FE7643D}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{25A1B3A1-DD74-436C-9956-17E04FE7643D}.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} | ||||
{25A1B3A1-DD74-436C-9956-17E04FE7643D} = {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,6 @@ | |||||
<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/=NATS/@EntryIndexedValue">NATS</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/=NATS/@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,32 @@ | |||||
// 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.NATS; | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
internal sealed class NATSCapOptionsExtension : ICapOptionsExtension | |||||
{ | |||||
private readonly Action<NATSOptions> _configure; | |||||
public NATSCapOptionsExtension(Action<NATSOptions> configure) | |||||
{ | |||||
_configure = configure; | |||||
} | |||||
public void AddServices(IServiceCollection services) | |||||
{ | |||||
services.AddSingleton<CapMessageQueueMakerService>(); | |||||
services.Configure(_configure); | |||||
services.AddSingleton<ITransport, NATSTransport>(); | |||||
services.AddSingleton<IConsumerClientFactory, NATSConsumerClientFactory>(); | |||||
services.AddSingleton<IConnectionPool, ConnectionPool>(); | |||||
} | |||||
} | |||||
} |
@@ -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 NATS.Client; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
/// <summary> | |||||
/// Provides programmatic configuration for the CAP NATS project. | |||||
/// </summary> | |||||
public class NATSOptions | |||||
{ | |||||
/// <summary> | |||||
/// Gets or sets the server url/urls used to connect to the NATs server. | |||||
/// </summary> | |||||
/// <remarks>This may contain username/password information.</remarks> | |||||
public string Servers { get; set; } | |||||
/// <summary> | |||||
/// connection pool size, default is 10 | |||||
/// </summary> | |||||
public int ConnectionPoolSize { get; set; } = 10; | |||||
/// <summary> | |||||
/// Used to setup all NATs client options | |||||
/// </summary> | |||||
public Options Options { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,40 @@ | |||||
// 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; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace Microsoft.Extensions.DependencyInjection | |||||
{ | |||||
public static class CapOptionsExtensions | |||||
{ | |||||
/// <summary> | |||||
/// Configuration to use NATS in CAP. | |||||
/// </summary> | |||||
/// <param name="options">CAP configuration options</param> | |||||
/// <param name="bootstrapServers">NATS bootstrap server urls.</param> | |||||
public static CapOptions UseNATS(this CapOptions options, string bootstrapServers) | |||||
{ | |||||
return options.UseNATS(opt => { opt.Servers = bootstrapServers; }); | |||||
} | |||||
/// <summary> | |||||
/// Configuration to use NATS in CAP. | |||||
/// </summary> | |||||
/// <param name="options">CAP configuration options</param> | |||||
/// <param name="configure">Provides programmatic configuration for the NATS.</param> | |||||
/// <returns></returns> | |||||
public static CapOptions UseNATS(this CapOptions options, Action<NATSOptions> configure) | |||||
{ | |||||
if (configure == null) | |||||
{ | |||||
throw new ArgumentNullException(nameof(configure)); | |||||
} | |||||
options.RegisterExtension(new NATSCapOptionsExtension(configure)); | |||||
return options; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,23 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netstandard2.0</TargetFramework> | |||||
<AssemblyName>DotNetCore.CAP.NATS</AssemblyName> | |||||
<PackageTags>$(PackageTags);NATS</PackageTags> | |||||
</PropertyGroup> | |||||
<PropertyGroup> | |||||
<WarningsAsErrors>NU1605;NU1701</WarningsAsErrors> | |||||
<NoWarn>NU1701;CS1591</NoWarn> | |||||
<DocumentationFile>bin\$(Configuration)\netstandard2.0\DotNetCore.CAP.NATS.xml</DocumentationFile> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="NATS.Client" Version="0.10.1" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,83 @@ | |||||
// 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.Concurrent; | |||||
using System.Threading; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using NATS.Client; | |||||
namespace DotNetCore.CAP.NATS | |||||
{ | |||||
public class ConnectionPool : IConnectionPool, IDisposable | |||||
{ | |||||
private readonly NATSOptions _options; | |||||
private readonly ConcurrentQueue<IConnection> _connectionPool; | |||||
private readonly ConnectionFactory _connectionFactory; | |||||
private int _pCount; | |||||
private int _maxSize; | |||||
public ConnectionPool(ILogger<ConnectionPool> logger, IOptions<NATSOptions> options) | |||||
{ | |||||
_options = options.Value; | |||||
_connectionPool = new ConcurrentQueue<IConnection>(); | |||||
_connectionFactory = new ConnectionFactory(); | |||||
_maxSize = _options.ConnectionPoolSize; | |||||
logger.LogDebug("NATS configuration: {0}", options.Value.Options); | |||||
} | |||||
public string ServersAddress => _options.Servers; | |||||
public IConnection RentConnection() | |||||
{ | |||||
if (_connectionPool.TryDequeue(out var connection)) | |||||
{ | |||||
Interlocked.Decrement(ref _pCount); | |||||
return connection; | |||||
} | |||||
if (_options.Options != null) | |||||
{ | |||||
if (_options.Servers != null) | |||||
{ | |||||
_options.Options.Url = _options.Servers; | |||||
} | |||||
connection = _connectionFactory.CreateConnection(_options.Options); | |||||
} | |||||
else | |||||
{ | |||||
connection = _connectionFactory.CreateConnection(_options.Servers); | |||||
} | |||||
return connection; | |||||
} | |||||
public bool Return(IConnection connection) | |||||
{ | |||||
if (Interlocked.Increment(ref _pCount) <= _maxSize) | |||||
{ | |||||
_connectionPool.Enqueue(connection); | |||||
return true; | |||||
} | |||||
Interlocked.Decrement(ref _pCount); | |||||
return false; | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_maxSize = 0; | |||||
while (_connectionPool.TryDequeue(out var context)) | |||||
{ | |||||
context.Dispose(); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,16 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | |||||
// Licensed under the MIT License. See License.txt in the project root for license information. | |||||
using NATS.Client; | |||||
namespace DotNetCore.CAP.NATS | |||||
{ | |||||
public interface IConnectionPool | |||||
{ | |||||
string ServersAddress { get; } | |||||
IConnection RentConnection(); | |||||
bool Return(IConnection connection); | |||||
} | |||||
} |
@@ -0,0 +1,60 @@ | |||||
// 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.IO; | |||||
using System.Runtime.Serialization.Formatters.Binary; | |||||
using System.Threading.Tasks; | |||||
using DotNetCore.CAP.Internal; | |||||
using DotNetCore.CAP.Messages; | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.Logging; | |||||
namespace DotNetCore.CAP.NATS | |||||
{ | |||||
internal class NATSTransport : ITransport | |||||
{ | |||||
private readonly IConnectionPool _connectionPool; | |||||
private readonly ILogger _logger; | |||||
public NATSTransport(ILogger<NATSTransport> logger, IConnectionPool connectionPool) | |||||
{ | |||||
_logger = logger; | |||||
_connectionPool = connectionPool; | |||||
} | |||||
public BrokerAddress BrokerAddress => new BrokerAddress("NATS", _connectionPool.ServersAddress); | |||||
public Task<OperateResult> SendAsync(TransportMessage message) | |||||
{ | |||||
var connection = _connectionPool.RentConnection(); | |||||
try | |||||
{ | |||||
var binFormatter = new BinaryFormatter(); | |||||
using var mStream = new MemoryStream(); | |||||
binFormatter.Serialize(mStream, message); | |||||
connection.Publish(message.GetName(), mStream.ToArray()); | |||||
_logger.LogDebug($"kafka topic message [{message.GetName()}] has been published."); | |||||
return Task.FromResult(OperateResult.Success); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
var warpEx = new PublisherSentFailedException(ex.Message, ex); | |||||
return Task.FromResult(OperateResult.Failed(warpEx)); | |||||
} | |||||
finally | |||||
{ | |||||
var returned = _connectionPool.Return(connection); | |||||
if (!returned) | |||||
{ | |||||
connection.Dispose(); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,147 @@ | |||||
// 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.IO; | |||||
using System.Runtime.Serialization.Formatters.Binary; | |||||
using System.Threading; | |||||
using DotNetCore.CAP.Messages; | |||||
using DotNetCore.CAP.Transport; | |||||
using Microsoft.Extensions.Options; | |||||
using NATS.Client; | |||||
namespace DotNetCore.CAP.NATS | |||||
{ | |||||
internal sealed class NATSConsumerClient : IConsumerClient | |||||
{ | |||||
private static readonly SemaphoreSlim ConnectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); | |||||
private readonly string _groupId; | |||||
private readonly NATSOptions _natsOptions; | |||||
private readonly IList<IAsyncSubscription> _asyncSubscriptions; | |||||
private IConnection _consumerClient; | |||||
public NATSConsumerClient(string groupId, IOptions<NATSOptions> options) | |||||
{ | |||||
_groupId = groupId; | |||||
_asyncSubscriptions = new List<IAsyncSubscription>(); | |||||
_natsOptions = options.Value ?? throw new ArgumentNullException(nameof(options)); | |||||
} | |||||
public event EventHandler<TransportMessage> OnMessageReceived; | |||||
public event EventHandler<LogMessageEventArgs> OnLog; | |||||
public BrokerAddress BrokerAddress => new BrokerAddress("NATS", _natsOptions.Servers); | |||||
public void Subscribe(IEnumerable<string> topics) | |||||
{ | |||||
if (topics == null) | |||||
{ | |||||
throw new ArgumentNullException(nameof(topics)); | |||||
} | |||||
Connect(); | |||||
foreach (var topic in topics) | |||||
{ | |||||
_asyncSubscriptions.Add(_consumerClient.SubscribeAsync(topic, _groupId)); | |||||
} | |||||
} | |||||
public void Listening(TimeSpan timeout, CancellationToken cancellationToken) | |||||
{ | |||||
Connect(); | |||||
foreach (var subscription in _asyncSubscriptions) | |||||
{ | |||||
subscription.MessageHandler += Subscription_MessageHandler; | |||||
subscription.Start(); | |||||
} | |||||
while (true) | |||||
{ | |||||
cancellationToken.ThrowIfCancellationRequested(); | |||||
cancellationToken.WaitHandle.WaitOne(timeout); | |||||
} | |||||
// ReSharper disable once FunctionNeverReturns | |||||
} | |||||
private void Subscription_MessageHandler(object sender, MsgHandlerEventArgs e) | |||||
{ | |||||
using var mStream = new MemoryStream(); | |||||
var binFormatter = new BinaryFormatter(); | |||||
mStream.Write(e.Message.Data, 0, e.Message.Data.Length); | |||||
mStream.Position = 0; | |||||
var message = binFormatter.Deserialize(mStream) as TransportMessage; | |||||
OnMessageReceived?.Invoke(sender, message); | |||||
} | |||||
public void Commit(object sender) | |||||
{ | |||||
//TODO : Only NATS Streaming Server Support | |||||
} | |||||
public void Reject(object sender) | |||||
{ | |||||
//TODO : Only NATS Streaming Server Support | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_consumerClient?.Dispose(); | |||||
} | |||||
public void Connect() | |||||
{ | |||||
if (_consumerClient != null) | |||||
{ | |||||
return; | |||||
} | |||||
ConnectionLock.Wait(); | |||||
try | |||||
{ | |||||
if (_consumerClient == null) | |||||
{ | |||||
var opts = _natsOptions.Options ?? ConnectionFactory.GetDefaultOptions(); | |||||
opts.Url = _natsOptions.Servers ?? opts.Url; | |||||
opts.ClosedEventHandler = ConnectedEventHandler; | |||||
opts.DisconnectedEventHandler = ConnectedEventHandler; | |||||
opts.AsyncErrorEventHandler = AsyncErrorEventHandler; | |||||
_consumerClient = new ConnectionFactory().CreateConnection(opts); | |||||
} | |||||
} | |||||
finally | |||||
{ | |||||
ConnectionLock.Release(); | |||||
} | |||||
} | |||||
private void ConnectedEventHandler(object sender, ConnEventArgs e) | |||||
{ | |||||
var logArgs = new LogMessageEventArgs | |||||
{ | |||||
LogType = MqLogType.ServerConnError, | |||||
Reason = $"An error occurred during connect NATS --> {e.Error}" | |||||
}; | |||||
OnLog?.Invoke(null, logArgs); | |||||
} | |||||
private void AsyncErrorEventHandler(object sender, ErrEventArgs e) | |||||
{ | |||||
var logArgs = new LogMessageEventArgs | |||||
{ | |||||
LogType = MqLogType.AsyncErrorEvent, | |||||
Reason = $"An error occurred out of band --> {e.Error}" | |||||
}; | |||||
OnLog?.Invoke(null, logArgs); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,32 @@ | |||||
// 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.NATS | |||||
{ | |||||
internal sealed class NATSConsumerClientFactory : IConsumerClientFactory | |||||
{ | |||||
private readonly IOptions<NATSOptions> _natsOptions; | |||||
public NATSConsumerClientFactory(IOptions<NATSOptions> natsOptions) | |||||
{ | |||||
_natsOptions = natsOptions; | |||||
} | |||||
public IConsumerClient Create(string groupId) | |||||
{ | |||||
try | |||||
{ | |||||
var client = new NATSConsumerClient(groupId, _natsOptions); | |||||
client.Connect(); | |||||
return client; | |||||
} | |||||
catch (System.Exception e) | |||||
{ | |||||
throw new BrokerConnectionException(e); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -271,6 +271,9 @@ namespace DotNetCore.CAP.Internal | |||||
case MqLogType.ExceptionReceived: | case MqLogType.ExceptionReceived: | ||||
_logger.LogError("AzureServiceBus subscriber received an error. --> " + logmsg.Reason); | _logger.LogError("AzureServiceBus subscriber received an error. --> " + logmsg.Reason); | ||||
break; | break; | ||||
case MqLogType.AsyncErrorEvent: | |||||
_logger.LogError("NATS subscriber received an error. --> " + logmsg.Reason); | |||||
break; | |||||
default: | default: | ||||
throw new ArgumentOutOfRangeException(); | throw new ArgumentOutOfRangeException(); | ||||
} | } | ||||
@@ -18,7 +18,10 @@ namespace DotNetCore.CAP.Transport | |||||
ServerConnError, | ServerConnError, | ||||
//AzureServiceBus | //AzureServiceBus | ||||
ExceptionReceived | |||||
ExceptionReceived, | |||||
//NATS | |||||
AsyncErrorEvent | |||||
} | } | ||||
public class LogMessageEventArgs : EventArgs | public class LogMessageEventArgs : EventArgs | ||||