From c5e3f1647c4aa556be34588f16fb2f986964db00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Tue, 2 Dec 2025 11:48:21 +0300 Subject: [PATCH 01/13] add net9.0, net10.0 support. - update Newtonsoft.Json --- .github/workflows/netcorelibrary.yml | 2 +- .../RabbitMQCoreClient.ConsoleClient.csproj | 2 +- .../RabbitMQCoreClient.WebApp.csproj | 2 +- .../RabbitMQCoreClient.Tests.csproj | 8 ++++---- src/RabbitMQCoreClient/RabbitMQCoreClient.csproj | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/netcorelibrary.yml b/.github/workflows/netcorelibrary.yml index f4eeb11..071667a 100644 --- a/.github/workflows/netcorelibrary.yml +++ b/.github/workflows/netcorelibrary.yml @@ -27,7 +27,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj b/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj index 9eef8e2..cba8f14 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj +++ b/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj b/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj index b5f0cc0..e054fb4 100644 --- a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj +++ b/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 diff --git a/src/RabbitMQCoreClient.Tests/RabbitMQCoreClient.Tests.csproj b/src/RabbitMQCoreClient.Tests/RabbitMQCoreClient.Tests.csproj index 91bf779..24cd752 100644 --- a/src/RabbitMQCoreClient.Tests/RabbitMQCoreClient.Tests.csproj +++ b/src/RabbitMQCoreClient.Tests/RabbitMQCoreClient.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 @@ -9,12 +9,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj index 9d55267..675e482 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj +++ b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj @@ -1,11 +1,11 @@ - 6.1.1 + 6.1.2 $(VersionSuffix) $(Version)-$(VersionSuffix) true - net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0;net9.0;net10.0 Sergey Pismennyi MONQ Digital lab RabbitMQCoreClient @@ -27,7 +27,7 @@ - + From 71e047c0038a48e561f5524904e6090fd1833590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Mon, 8 Dec 2025 15:32:43 +0300 Subject: [PATCH 02/13] Update to RabbitMQ 7.2.0 --- README.md | 12 +- .../ServiceCollectionExtensions.cs | 123 ++-- .../Exceptions/PersistingException.cs | 59 +- .../IQueueEventsBufferEngine.cs | 59 +- .../BatchQueueSender/IQueueEventsWriter.cs | 25 +- .../QueueBatchSenderOptions.cs | 29 +- .../BatchQueueSender/QueueEventItem.cs | 31 +- .../QueueEventsBufferEngine.cs | 261 ++++---- .../BatchQueueSender/QueueEventsWriter.cs | 91 ++- .../Configuration/AppConstants.cs | 19 +- .../ConfigFormats/JsonV1Binder.cs | 171 +++-- .../ConfigFormats/JsonV2Binder.cs | 175 +++-- .../ConfigModels/QueueConfig.cs | 105 ++- .../ConfigModels/SubscriptionConfig.cs | 55 +- .../ApplicationBuilderExtentions.cs | 31 +- .../Extensions/BuilderExtensions.cs | 411 ++++++------ .../Extensions/ServiceCollectionExtensions.cs | 159 +++-- .../DependencyInjection/IQueueConsumer.cs | 48 +- .../IRabbitMQCoreClientBuilder.cs | 41 +- .../IRabbitMQCoreClientConsumerBuilder.cs | 41 +- .../Options/ConsumerHandlerOptions.cs | 29 +- .../Options/ErrorHandlingOptions.cs | 27 +- .../Options/ExchangeOptions.cs | 77 ++- .../Options/RabbitMQCoreClientOptions.cs | 255 ++++---- .../RabbitMQCoreClientBuilder.cs | 39 +- .../RabbitMQCoreClientConsumerBuilder.cs | 45 +- src/RabbitMQCoreClient/ErrorMessageRouting.cs | 67 +- .../Events/ReconnectEventArgs.cs | 7 + .../ClientConfigurationException.cs | 27 +- .../Exceptions/NotConnectedException.cs | 28 +- .../Exceptions/QueueBindException.cs | 27 +- .../ReconnectAttemptsExceededException.cs | 28 +- .../NewtonSoftJsonBuilderExtentions.cs | 27 - .../NewtonSoftJsonQueueServiceExtentions.cs | 98 --- .../SystemTextJsonBuilderExtentions.cs | 37 +- .../SystemTextJsonQueueServiceExtentions.cs | 306 +++++---- src/RabbitMQCoreClient/IMessageHandler.cs | 49 +- src/RabbitMQCoreClient/IQueueService.cs | 381 ++++++----- src/RabbitMQCoreClient/MessageHandlerJson.cs | 113 ++-- src/RabbitMQCoreClient/Models/Exchange.cs | 78 +-- src/RabbitMQCoreClient/Models/Queue.cs | 56 +- src/RabbitMQCoreClient/Models/QueueBase.cs | 212 +++--- .../Models/RabbitMessageEventArgs.cs | 44 +- src/RabbitMQCoreClient/Models/Routes.cs | 23 +- src/RabbitMQCoreClient/Models/Subscription.cs | 41 +- src/RabbitMQCoreClient/Models/TtlActions.cs | 27 +- src/RabbitMQCoreClient/QueueService.cs | 601 ++++++++++++++++++ src/RabbitMQCoreClient/QueueServiceImpl.cs | 597 ----------------- .../RabbitMQCoreClient.csproj | 8 +- .../RabbitMQCoreClientConsumer.cs | 482 +++++++------- .../Serializers/IMessageSerializer.cs | 39 +- .../NewtonsoftJArrayConverter.cs | 26 - .../NewtonsoftJObjectConverter.cs | 26 - .../NewtonsoftJTokenConverter.cs | 26 - .../NewtonsoftJsonMessageSerializer.cs | 47 -- .../SystemTextJsonMessageSerializer.cs | 63 +- 56 files changed, 2851 insertions(+), 3158 deletions(-) create mode 100644 src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs delete mode 100644 src/RabbitMQCoreClient/Extentions/NewtonSoftJsonBuilderExtentions.cs delete mode 100644 src/RabbitMQCoreClient/Extentions/NewtonSoftJsonQueueServiceExtentions.cs create mode 100644 src/RabbitMQCoreClient/QueueService.cs delete mode 100644 src/RabbitMQCoreClient/QueueServiceImpl.cs delete mode 100644 src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJArrayConverter.cs delete mode 100644 src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJObjectConverter.cs delete mode 100644 src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJTokenConverter.cs delete mode 100644 src/RabbitMQCoreClient/Serializers/NewtonsoftJsonMessageSerializer.cs diff --git a/README.md b/README.md index 80a7771..5b9cf02 100644 --- a/README.md +++ b/README.md @@ -88,14 +88,14 @@ In order to send a message, it is enough to get the interface `RabbitMQCoreClien and use one of the following methods ```csharp -ValueTask SendAsync(T obj, string routingKey, string exchange = default, bool decreaseTtl = true, string correlationId = default); -ValueTask SendJsonAsync(string json, string routingKey, string exchange = default, bool decreaseTtl = true, string correlationId = default); -ValueTask SendAsync(byte[] obj, IBasicProperties props, string routingKey, string exchange, bool decreaseTtl = true, string correlationId = default); +ValueTask SendAsync(T obj, string routingKey, string exchange = default, bool decreaseTtl = true); +ValueTask SendJsonAsync(string json, string routingKey, string exchange = default, bool decreaseTtl = true); +ValueTask SendAsync(byte[] obj, IBasicProperties props, string routingKey, string exchange, bool decreaseTtl = true); // Batch sending -ValueTask SendBatchAsync(IEnumerable objs, string routingKey, string exchange = default, bool decreaseTtl = true, string correlationId = default); -ValueTask SendJsonBatchAsync(IEnumerable serializedJsonList, string routingKey, string exchange = default, bool decreaseTtl = true, string correlationId = default); -ValueTask SendBatchAsync(IEnumerable<(byte[] Body, IBasicProperties Props)> objs, string routingKey, string exchange, bool decreaseTtl = true, string correlationId = default); +ValueTask SendBatchAsync(IEnumerable objs, string routingKey, string exchange = default, bool decreaseTtl = true); +ValueTask SendJsonBatchAsync(IEnumerable serializedJsonList, string routingKey, string exchange = default, bool decreaseTtl = true); +ValueTask SendBatchAsync(IEnumerable<(byte[] Body, IBasicProperties Props)> objs, string routingKey, string exchange, bool decreaseTtl = true); ``` In this case, if you do not specify `exchange`, then the default exchange will be used (from the configuration), diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index fa43f62..ab31581 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,77 +1,76 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System; -namespace RabbitMQCoreClient.BatchQueueSender.DependencyInjection +namespace RabbitMQCoreClient.BatchQueueSender.DependencyInjection; + +/// +/// Class containing extension methods for registering the BatchQueueSender services at DI. +/// +public static class ServiceCollectionExtensions { + static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(); + return services; + } + /// - /// Class containing extension methods for registering the BatchQueueSender services at DI. + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages + /// to the inmemory queue and process them at the separate thread. /// - public static class ServiceCollectionExtensions + /// List of services registered in DI. + /// Configuration section containing fields for configuring the message processing service. + /// Use this method if you need to override the configuration. + public static IServiceCollection AddBatchQueueSender( + this IServiceCollection services, + IConfiguration configuration, + Action? setupAction = null) { - static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) - { - services.AddTransient(); - services.AddSingleton(); - return services; - } + RegisterOptions(services, configuration, setupAction); - /// - /// Registers an instance of the interface in the DI. - /// Injected service can be used to send messages - /// to the inmemory queue and process them at the separate thread. - /// - /// List of services registered in DI. - /// Configuration section containing fields for configuring the message processing service. - /// Use this method if you need to override the configuration. - public static IServiceCollection AddBatchQueueSender( - this IServiceCollection services, - IConfiguration configuration, - Action? setupAction = null) - { - RegisterOptions(services, configuration, setupAction); - - return services.AddBatchQueueSenderCore(); - } + return services.AddBatchQueueSenderCore(); + } - /// - /// Registers an instance of the interface in the DI. - /// Injected service can be used to send messages - /// to the inmemory queue and process them at the separate thread. - /// - /// List of services registered in DI. - /// Use this method if you need to override the configuration. - /// - public static IServiceCollection AddBatchQueueSender(this IServiceCollection services, - Action? setupAction) - { - services.Configure(setupAction); - return services.AddBatchQueueSenderCore(); - } + /// + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages + /// to the inmemory queue and process them at the separate thread. + /// + /// List of services registered in DI. + /// Use this method if you need to override the configuration. + /// + public static IServiceCollection AddBatchQueueSender(this IServiceCollection services, + Action? setupAction) + { + services.Configure(setupAction); + return services.AddBatchQueueSenderCore(); + } - /// - /// Registers an instance of the interface in the DI. - /// Injected service can be used to send messages - /// to the inmemory queue and process them at the separate thread. - /// - /// List of services registered in DI. - /// - public static IServiceCollection AddBatchQueueSender(this IServiceCollection services) - { - services.AddSingleton((x) => Options.Create(new QueueBatchSenderOptions())); - return services.AddBatchQueueSenderCore(); - } + /// + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages + /// to the inmemory queue and process them at the separate thread. + /// + /// List of services registered in DI. + /// + public static IServiceCollection AddBatchQueueSender(this IServiceCollection services) + { + services.AddSingleton((x) => Options.Create(new QueueBatchSenderOptions())); + return services.AddBatchQueueSenderCore(); + } - static void RegisterOptions(IServiceCollection services, - IConfiguration configuration, - Action? setupAction) - { - var instance = configuration.Get(); - setupAction?.Invoke(instance); - var options = Options.Create(instance); + static void RegisterOptions(IServiceCollection services, + IConfiguration configuration, + Action? setupAction) + { + var instance = configuration.Get() ?? new QueueBatchSenderOptions(); + setupAction?.Invoke(instance); + var options = Options.Create(instance); - services.AddSingleton((x) => options); - } + services.AddSingleton((x) => options); } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs index 6f64bb6..042c22a 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs @@ -1,36 +1,35 @@ -using System; +using System; -namespace RabbitMQCoreClient.BatchQueueSender.Exceptions +namespace RabbitMQCoreClient.BatchQueueSender.Exceptions; + +public class PersistingException : Exception { - public class PersistingException : Exception - { - /// - /// The data items. - /// - public object[] Items { get; } + /// + /// The data items. + /// + public object[] Items { get; } - /// - /// The routing key of the queue bus. - /// - public string RoutingKey { get; } + /// + /// The routing key of the queue bus. + /// + public string RoutingKey { get; } - /// - /// Initializes a new instance of the - /// class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, - /// or a null reference ( in Visual Basic) if no inner exception is specified. - /// The data items. - /// The routing key of the queue bus. - /// - public PersistingException(string message, - object[] items, - string routingKey, - Exception innerException) : base(message, innerException) - { - Items = items; - RoutingKey = routingKey; - } + /// + /// Initializes a new instance of the + /// class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, + /// or a null reference ( in Visual Basic) if no inner exception is specified. + /// The data items. + /// The routing key of the queue bus. + /// + public PersistingException(string message, + object[] items, + string routingKey, + Exception innerException) : base(message, innerException) + { + Items = items; + RoutingKey = routingKey; } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs index 0b2ee28..a4e0d24 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs @@ -1,39 +1,38 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace RabbitMQCoreClient.BatchQueueSender +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Event buffer interface. +/// +public interface IQueueEventsBufferEngine { /// - /// Event buffer interface. + /// Add an event to send to the data bus. /// - public interface IQueueEventsBufferEngine - { - /// - /// Add an event to send to the data bus. - /// - /// The object to send to the data bus. - /// The name of the route key with which you want to send events to the data bus. - /// showing the completion of the operation. - Task AddEvent(T @event, string routingKey); + /// The object to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// showing the completion of the operation. + Task AddEvent(T @event, string routingKey); - /// - /// Add events to send to the data bus. - /// - /// The type of list item of the property. - /// The list of objects to send to the data bus. - /// The name of the route key with which you want to send events to the data bus. - /// - [Obsolete("Use AddEvents instead.")] - Task AddEvent(IEnumerable events, string routingKey); + /// + /// Add events to send to the data bus. + /// + /// The type of list item of the property. + /// The list of objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// + [Obsolete("Use AddEvents instead.")] + Task AddEvent(IEnumerable events, string routingKey); - /// - /// Add events to send to the data bus. - /// - /// The type of list item of the property. - /// The list of objects to send to the data bus. - /// The name of the route key with which you want to send events to the data bus. - /// - Task AddEvents(IEnumerable events, string routingKey); - } + /// + /// Add events to send to the data bus. + /// + /// The type of list item of the property. + /// The list of objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// + Task AddEvents(IEnumerable events, string routingKey); } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs index 805eb3a..ee4264b 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs @@ -1,18 +1,17 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; -namespace RabbitMQCoreClient.BatchQueueSender +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// The service interface that represents methods to work with queue bus. +/// +public interface IQueueEventsWriter { /// - /// The service interface that represents methods to work with queue bus. + /// Send events to the queue. /// - public interface IQueueEventsWriter - { - /// - /// Send events to the queue. - /// - /// The item objects to send to the queue. - /// The routing key to send. - /// when the operation completes. - Task Write(object[] items, string routingKey); - } + /// The item objects to send to the queue. + /// The routing key to send. + /// when the operation completes. + Task Write(object[] items, string routingKey); } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs index fb2f8f5..37bdd5f 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs @@ -1,20 +1,19 @@ -namespace RabbitMQCoreClient.BatchQueueSender +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Queue bus buffer options. +/// +public sealed class QueueBatchSenderOptions { /// - /// Queue bus buffer options. + /// The period for resetting (writing) events in RabbitMQ. + /// Default: 2 sec. /// - public sealed class QueueBatchSenderOptions - { - /// - /// The period for resetting (writing) events in RabbitMQ. - /// Default: 2 sec. - /// - public int EventsFlushPeriodSec { get; set; } = 1; + public int EventsFlushPeriodSec { get; set; } = 1; - /// - /// The number of events upon reaching which to reset (write) to the database. - /// Default: 500. - /// - public int EventsFlushCount { get; set; } = 500; - } + /// + /// The number of events upon reaching which to reset (write) to the database. + /// Default: 500. + /// + public int EventsFlushCount { get; set; } = 500; } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs index bd68fea..22dd70c 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs @@ -1,21 +1,20 @@ -namespace RabbitMQCoreClient.BatchQueueSender +namespace RabbitMQCoreClient.BatchQueueSender; + +public struct QueueEventItem { - public struct QueueEventItem - { - /// - /// The event to be written to the database. - /// - public object Event { get; } + /// + /// The event to be written to the database. + /// + public object Event { get; } - /// - /// The routing key to which you want to send the event. - /// - public string RoutingKey { get; } + /// + /// The routing key to which you want to send the event. + /// + public string RoutingKey { get; } - public QueueEventItem(object @event, string tableName) - { - Event = @event; - RoutingKey = tableName; - } + public QueueEventItem(object @event, string tableName) + { + Event = @event; + RoutingKey = tableName; } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs index 2387463..9d705ae 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs @@ -7,175 +7,174 @@ using System.Threading; using System.Threading.Tasks; -namespace RabbitMQCoreClient.BatchQueueSender +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Implementation of the stream data event store buffer. +/// +public sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable { + readonly Timer _flushTimer; + readonly List _events = new List(); + readonly IQueueEventsWriter _eventsWriter; + readonly QueueBatchSenderOptions _engineOptions; + readonly ILogger _logger; + + static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private bool _disposedValue; + /// - /// Implementation of the stream data event store buffer. + /// Event storage buffer implementation constructor. + /// Creates a new instance of the class. /// - public sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable + public QueueEventsBufferEngine( + IOptions engineOptions, + IQueueEventsWriter eventsWriter, + ILogger logger) { - readonly Timer _flushTimer; - readonly List _events = new List(); - readonly IQueueEventsWriter _eventsWriter; - readonly QueueBatchSenderOptions _engineOptions; - readonly ILogger _logger; - - static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private bool _disposedValue; - - /// - /// Event storage buffer implementation constructor. - /// Creates a new instance of the class. - /// - public QueueEventsBufferEngine( - IOptions engineOptions, - IQueueEventsWriter eventsWriter, - ILogger logger) - { - if (engineOptions?.Value == null) - throw new ArgumentNullException(nameof(engineOptions), $"{nameof(engineOptions)} is null."); + if (engineOptions?.Value == null) + throw new ArgumentNullException(nameof(engineOptions), $"{nameof(engineOptions)} is null."); - _engineOptions = engineOptions.Value; - _eventsWriter = eventsWriter; - _logger = logger; + _engineOptions = engineOptions.Value; + _eventsWriter = eventsWriter; + _logger = logger; - _flushTimer = new Timer(async obj => await FlushTimerDelegate(obj), null, - _engineOptions.EventsFlushPeriodSec * 1000, - _engineOptions.EventsFlushPeriodSec * 1000); - } + _flushTimer = new Timer(async obj => await FlushTimerDelegate(obj), null, + _engineOptions.EventsFlushPeriodSec * 1000, + _engineOptions.EventsFlushPeriodSec * 1000); + } - /// - public async Task AddEvent(T @event, string routingKey) - { - if (@event is null) - return; + /// + public async Task AddEvent(T @event, string routingKey) + { + if (@event is null) + return; - await _semaphore.WaitAsync(); + await _semaphore.WaitAsync(); - try - { - _events.Add(new QueueEventItem(@event, routingKey)); - if (_events.Count < _engineOptions.EventsFlushCount) - return; + try + { + _events.Add(new QueueEventItem(@event, routingKey)); + if (_events.Count < _engineOptions.EventsFlushCount) + return; - await Flush(); - } - finally - { - _semaphore.Release(); - } + await Flush(); } - - /// - public Task AddEvent(IEnumerable events, string routingKey) - => AddEvents(events, routingKey); - - /// - public async Task AddEvents(IEnumerable events, string routingKey) + finally { - await _semaphore.WaitAsync(); + _semaphore.Release(); + } + } - try - { - foreach (var @event in events) - { - if (@event is not null) - _events.Add(new QueueEventItem(@event, routingKey)); - } + /// + public Task AddEvent(IEnumerable events, string routingKey) + => AddEvents(events, routingKey); - if (_events.Count < _engineOptions.EventsFlushCount) - return; + /// + public async Task AddEvents(IEnumerable events, string routingKey) + { + await _semaphore.WaitAsync(); - await Flush(); - } - finally + try + { + foreach (var @event in events) { - _semaphore.Release(); + if (@event is not null) + _events.Add(new QueueEventItem(@event, routingKey)); } - } - async Task FlushTimerDelegate(object? _) + if (_events.Count < _engineOptions.EventsFlushCount) + return; + + await Flush(); + } + finally { - await _semaphore.WaitAsync(); - try - { - await Flush(); - } - finally - { - _semaphore.Release(); - } + _semaphore.Release(); } + } - Task Flush() + async Task FlushTimerDelegate(object? _) + { + await _semaphore.WaitAsync(); + try { - if (_events.Count == 0) - return Task.CompletedTask; + await Flush(); + } + finally + { + _semaphore.Release(); + } + } - var eventsCache = _events.ToArray(); - _events.Clear(); + Task Flush() + { + if (_events.Count == 0) + return Task.CompletedTask; - return HandleEvents(eventsCache); - } + var eventsCache = _events.ToArray(); + _events.Clear(); - async Task HandleEvents(IEnumerable streamDataEvents) - { - var routingGroups = streamDataEvents.GroupBy(x => x.RoutingKey); + return HandleEvents(eventsCache); + } - var tasks = new List(); + async Task HandleEvents(IEnumerable streamDataEvents) + { + var routingGroups = streamDataEvents.GroupBy(x => x.RoutingKey); - foreach (var routingGroup in routingGroups) - { - var itemsToSend = routingGroup.Select(val => val.Event).ToArray(); - tasks.Add(_eventsWriter.Write(itemsToSend, routingGroup.Key)); - } - try - { - await Task.WhenAll(tasks); - } - catch (Exception e) - { - _logger.LogError(e, "Error while trying to write batch of data to Storage."); + var tasks = new List(); - var exceptions = tasks - .Where(t => t.Exception != null) - .Select(t => t.Exception) - .ToList(); - foreach (var aggregateException in exceptions) + foreach (var routingGroup in routingGroups) + { + var itemsToSend = routingGroup.Select(val => val.Event).ToArray(); + tasks.Add(_eventsWriter.Write(itemsToSend, routingGroup.Key)); + } + try + { + await Task.WhenAll(tasks); + } + catch (Exception e) + { + _logger.LogError(e, "Error while trying to write batch of data to Storage."); + + var exceptions = tasks + .Where(t => t.Exception != null) + .Select(t => t.Exception) + .ToList(); + foreach (var aggregateException in exceptions) + { + var persistException = aggregateException?.InnerExceptions?.First() as PersistingException; + if (persistException != null) { - var persistException = aggregateException?.InnerExceptions?.First() as PersistingException; - if (persistException != null) + var extendedError = string.Join(Environment.NewLine, new[] { - var extendedError = string.Join(Environment.NewLine, new[] - { - $"Routing key: {persistException.RoutingKey}. Source: ", - System.Text.Json.JsonSerializer.Serialize(persistException.Items) - }); - _logger.LogDebug(extendedError); - } - // Unrecorded events are not sent anywhere. For the current implementation, this is not fatal. + $"Routing key: {persistException.RoutingKey}. Source: ", + System.Text.Json.JsonSerializer.Serialize(persistException.Items) + }); + _logger.LogDebug(extendedError); } + // Unrecorded events are not sent anywhere. For the current implementation, this is not fatal. } } + } - void Dispose(bool disposing) + void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - _flushTimer?.Dispose(); - } - - _disposedValue = true; + _flushTimer?.Dispose(); } - } - public void Dispose() - { - // Do not change this code. Place the cleanup code in the "Dispose(bool disposing)" method. - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } } + + public void Dispose() + { + // Do not change this code. Place the cleanup code in the "Dispose(bool disposing)" method. + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs index aa335c5..39ccca2 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs @@ -1,65 +1,64 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using RabbitMQCoreClient.BatchQueueSender.Exceptions; using System; using System.Diagnostics; using System.Threading.Tasks; -namespace RabbitMQCoreClient.BatchQueueSender +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Implementation of the service for sending events to the data bus. +/// +public class QueueEventsWriter : IQueueEventsWriter { - /// - /// Implementation of the service for sending events to the data bus. - /// - public class QueueEventsWriter : IQueueEventsWriter - { - readonly ILogger _logger; - readonly IQueueService _queueService; + readonly ILogger _logger; + readonly IQueueService _queueService; - long _writtenCount; - long _avgWriteTimeMs; + long _writtenCount; + long _avgWriteTimeMs; - public QueueEventsWriter( - IQueueService queueService, - ILogger logger) - { - _queueService = queueService; - _logger = logger; - } + public QueueEventsWriter( + IQueueService queueService, + ILogger logger) + { + _queueService = queueService; + _logger = logger; + } - /// - public async Task Write(object[] items, string routingKey) - { - if (items.Length == 0) - return; + /// + public async Task Write(object[] items, string routingKey) + { + if (items.Length == 0) + return; - _logger.LogInformation("Start writing {rowsCount} data rows from the buffer.", items.Length); + _logger.LogInformation("Start writing {RowsCount} data rows from the buffer.", items.Length); - var sw = new Stopwatch(); - sw.Start(); + var sw = new Stopwatch(); + sw.Start(); - try - { - await _queueService.SendBatchAsync(items, routingKey); - } - catch (Exception e) - { - throw new PersistingException("Error while persisting data", items, routingKey, e); - } + try + { + await _queueService.SendBatchAsync(items, routingKey); + } + catch (Exception e) + { + throw new PersistingException("Error while persisting data", items, routingKey, e); + } - sw.Stop(); + sw.Stop(); - _logger.LogInformation("Buffer has sent {rowsCount} rows to the queue bus at {elapsedMilliseconds} ms.", - items.Length, sw.ElapsedMilliseconds); + _logger.LogInformation("Buffer has sent {RowsCount} rows to the queue bus at {ElapsedMilliseconds} ms.", + items.Length, sw.ElapsedMilliseconds); - _writtenCount += items.Length; + _writtenCount += items.Length; - if (_avgWriteTimeMs == 0) - _avgWriteTimeMs = sw.ElapsedMilliseconds; - else - _avgWriteTimeMs = (sw.ElapsedMilliseconds + _avgWriteTimeMs) / 2; + if (_avgWriteTimeMs == 0) + _avgWriteTimeMs = sw.ElapsedMilliseconds; + else + _avgWriteTimeMs = (sw.ElapsedMilliseconds + _avgWriteTimeMs) / 2; - _logger.LogInformation("From start of the service {rowsCount} total rows has been sent to the queue bus. " + - "The average sending time per row is {avgWriteTime} ms.", - _writtenCount, _avgWriteTimeMs); - } + _logger.LogInformation("From start of the service {RowsCount} total rows has been sent to the queue bus. " + + "The average sending time per row is {AvgWriteTime} ms.", + _writtenCount, _avgWriteTimeMs); } } diff --git a/src/RabbitMQCoreClient/Configuration/AppConstants.cs b/src/RabbitMQCoreClient/Configuration/AppConstants.cs index 8802154..6cf7f1e 100644 --- a/src/RabbitMQCoreClient/Configuration/AppConstants.cs +++ b/src/RabbitMQCoreClient/Configuration/AppConstants.cs @@ -1,14 +1,13 @@ -namespace RabbitMQCoreClient.Configuration -{ +namespace RabbitMQCoreClient.Configuration; + #pragma warning disable CS1591 - internal static class AppConstants +internal static class AppConstants +{ + public static class RabbitMQHeaders { - public static class RabbitMQHeaders - { - public const string TtlHeader = "x-message-ttl"; - public const string DeadLetterExchangeHeader = "x-dead-letter-exchange"; - public const string QueueTypeHeader = "x-queue-type"; - public const string QueueExpiresHeader = "x-expires"; - } + public const string TtlHeader = "x-message-ttl"; + public const string DeadLetterExchangeHeader = "x-dead-letter-exchange"; + public const string QueueTypeHeader = "x-queue-type"; + public const string QueueExpiresHeader = "x-expires"; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs index 72b63a1..f440267 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; @@ -7,108 +7,107 @@ using System; using System.Linq; -namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats +namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; + +public static class JsonV1Binder { - public static class JsonV1Binder + const string QueueSection = "Queue"; + const string QueueName = "Queue:QueueName"; + const string ExchangeName = "Exchange:Name"; + const string SubscriptionSection = "Subscription"; + const string UseQuorumQueuesName = "UseQuorumQueues"; + + public static IRabbitMQCoreClientBuilder RegisterV1Configuration(this IRabbitMQCoreClientBuilder builder, + IConfiguration? configuration) { - const string QueueSection = "Queue"; - const string QueueName = "Queue:QueueName"; - const string ExchangeName = "Exchange:Name"; - const string SubscriptionSection = "Subscription"; - const string UseQuorumQueuesName = "UseQuorumQueues"; - - public static IRabbitMQCoreClientBuilder RegisterV1Configuration(this IRabbitMQCoreClientBuilder builder, - IConfiguration? configuration) - { - if (configuration is null) - return builder; + if (configuration is null) + return builder; - // The exchange point will be the default point. - var oldExchangeName = configuration[ExchangeName]; - if (!string.IsNullOrEmpty(oldExchangeName)) - builder.AddExchange(oldExchangeName, options: new ExchangeOptions { Name = oldExchangeName, IsDefault = true }); + // The exchange point will be the default point. + var oldExchangeName = configuration[ExchangeName]; + if (!string.IsNullOrEmpty(oldExchangeName)) + builder.AddExchange(oldExchangeName, options: new ExchangeOptions { Name = oldExchangeName, IsDefault = true }); + return builder; + } + + public static IRabbitMQCoreClientConsumerBuilder RegisterV1Configuration(this IRabbitMQCoreClientConsumerBuilder builder, + IConfiguration? configuration) + { + if (configuration is null) return builder; - } - public static IRabbitMQCoreClientConsumerBuilder RegisterV1Configuration(this IRabbitMQCoreClientConsumerBuilder builder, - IConfiguration? configuration) + // Try to detect old configuration format. + // The exchange point will be the default point. + var oldExchangeName = configuration[ExchangeName]; + if (string.IsNullOrEmpty(oldExchangeName)) + return builder; + + // Old queue format detected. + var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == oldExchangeName); + if (exchange is null) + throw new ClientConfigurationException($"The exchange {oldExchangeName} is " + + "not found in \"Exchange\" section."); + + var useQuorumQueues = configuration.GetValue(UseQuorumQueuesName); + + if (configuration.GetSection(QueueSection).Exists()) { - if (configuration is null) - return builder; - - // Try to detect old configuration format. - // The exchange point will be the default point. - var oldExchangeName = configuration[ExchangeName]; - if (string.IsNullOrEmpty(oldExchangeName)) - return builder; - - // Old queue format detected. - var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == oldExchangeName); - if (exchange is null) - throw new ClientConfigurationException($"The exchange {oldExchangeName} is " + - "not found in \"Exchange\" section."); - - var useQuorumQueues = configuration.GetValue(UseQuorumQueuesName); - - if (configuration.GetSection(QueueSection).Exists()) - { - // Register a queue and bind it to exchange points. - var queueName = configuration[QueueName]; - if (!string.IsNullOrEmpty(queueName)) - RegisterQueue(builder, - configuration.GetSection(QueueSection), - exchange, - (qConfig) => Queue.Create(qConfig)); - } - - if (configuration.GetSection(SubscriptionSection).Exists()) - { - // Register a subscription and link it to exchange points. - RegisterQueue(builder, - configuration.GetSection(SubscriptionSection), + // Register a queue and bind it to exchange points. + var queueName = configuration[QueueName]; + if (!string.IsNullOrEmpty(queueName)) + RegisterQueue(builder, + configuration.GetSection(QueueSection), exchange, - (qConfig) => Subscription.Create(qConfig)); - } - - return builder; + (qConfig) => Queue.Create(qConfig)); } - static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, - IConfigurationSection? queueConfig, - Exchange exchange, - Func createQueue) - where TConfig : new() - where TQueue : QueueBase + if (configuration.GetSection(SubscriptionSection).Exists()) { - if (queueConfig is null) - return; + // Register a subscription and link it to exchange points. + RegisterQueue(builder, + configuration.GetSection(SubscriptionSection), + exchange, + (qConfig) => Subscription.Create(qConfig)); + } - var q = BindConfig(queueConfig); + return builder; + } - var queue = createQueue(q); - queue.Exchanges.Add(exchange.Name); + static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, + IConfigurationSection? queueConfig, + Exchange exchange, + Func createQueue) + where TConfig : new() + where TQueue : QueueBase + { + if (queueConfig is null) + return; - AddQueue(builder, queue); - } + var q = BindConfig(queueConfig); - static TConfig BindConfig(IConfigurationSection queueConfig) - where TConfig : new() - { - var q = new TConfig(); - queueConfig.Bind(q); - return q; - } + var queue = createQueue(q); + queue.Exchanges.Add(exchange.Name); - static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) - where T : QueueBase + AddQueue(builder, queue); + } + + static TConfig BindConfig(IConfigurationSection queueConfig) + where TConfig : new() + { + var q = new TConfig(); + queueConfig.Bind(q); + return q; + } + + static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) + where T : QueueBase + { + // So-so solution, but without dubbing. + switch (queue) { - // So-so solution, but without dubbing. - switch (queue) - { - case Queue q: builder.AddQueue(q); break; - case Subscription q: builder.AddSubscription(q); break; - } + case Queue q: builder.AddQueue(q); break; + case Subscription q: builder.AddSubscription(q); break; } } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs index a580555..5d4063b 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigModels; @@ -6,117 +6,116 @@ using System; using System.Linq; -namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats +namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; + +public static class JsonV2Binder { - public static class JsonV2Binder - { - const string ExhangesSection = "Exchanges"; - const string QueuesSection = "Queues"; - const string SubscriptionsSection = "Subscriptions"; + const string ExhangesSection = "Exchanges"; + const string QueuesSection = "Queues"; + const string SubscriptionsSection = "Subscriptions"; - public static IRabbitMQCoreClientBuilder RegisterV2Configuration(this IRabbitMQCoreClientBuilder builder, - IConfiguration configuration) - { - if (configuration is null) - return builder; + public static IRabbitMQCoreClientBuilder RegisterV2Configuration(this IRabbitMQCoreClientBuilder builder, + IConfiguration configuration) + { + if (configuration is null) + return builder; - // Register exchange points. - RegisterExchanges(builder, configuration); + // Register exchange points. + RegisterExchanges(builder, configuration); - return builder; - } + return builder; + } - static void RegisterExchanges(IRabbitMQCoreClientBuilder builder, IConfiguration configuration) + static void RegisterExchanges(IRabbitMQCoreClientBuilder builder, IConfiguration configuration) + { + var exchanges = configuration.GetSection(ExhangesSection); + foreach (var exchangeConfig in exchanges.GetChildren()) { - var exchanges = configuration.GetSection(ExhangesSection); - foreach (var exchangeConfig in exchanges.GetChildren()) - { - var options = new ExchangeOptions(); - exchangeConfig.Bind(options); - builder.AddExchange(options.Name, options: options); - } + var options = new ExchangeOptions(); + exchangeConfig.Bind(options); + builder.AddExchange(options.Name, options: options); } + } - public static IRabbitMQCoreClientConsumerBuilder RegisterV2Configuration(this IRabbitMQCoreClientConsumerBuilder builder, - IConfiguration configuration) - { - if (configuration is null) - return builder; + public static IRabbitMQCoreClientConsumerBuilder RegisterV2Configuration(this IRabbitMQCoreClientConsumerBuilder builder, + IConfiguration configuration) + { + if (configuration is null) + return builder; - // Register queues and link them to exchange points. - RegisterQueues(builder, - configuration.GetSection(QueuesSection), - (qConfig) => Queue.Create(qConfig)); + // Register queues and link them to exchange points. + RegisterQueues(builder, + configuration.GetSection(QueuesSection), + (qConfig) => Queue.Create(qConfig)); - // Register subscriptions and link them to exchange points. - RegisterQueues(builder, - configuration.GetSection(SubscriptionsSection), - (qConfig) => Subscription.Create(qConfig)); + // Register subscriptions and link them to exchange points. + RegisterQueues(builder, + configuration.GetSection(SubscriptionsSection), + (qConfig) => Subscription.Create(qConfig)); - return builder; - } + return builder; + } - static void RegisterQueues(IRabbitMQCoreClientConsumerBuilder builder, - IConfigurationSection? queuesConfiguration, - Func createQueue) - where TConfig : new() - where TQueue : QueueBase - { - if (queuesConfiguration is null) - return; + static void RegisterQueues(IRabbitMQCoreClientConsumerBuilder builder, + IConfigurationSection? queuesConfiguration, + Func createQueue) + where TConfig : new() + where TQueue : QueueBase + { + if (queuesConfiguration is null) + return; - foreach (var queueConfig in queuesConfiguration.GetChildren()) - { - var q = BindConfig(queueConfig); + foreach (var queueConfig in queuesConfiguration.GetChildren()) + { + var q = BindConfig(queueConfig); - var queue = createQueue(q); + var queue = createQueue(q); - RegisterQueue(builder, queue); - } + RegisterQueue(builder, queue); } + } - static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, TQueue queue) - where TQueue : QueueBase + static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, TQueue queue) + where TQueue : QueueBase + { + if (!queue.Exchanges.Any()) { - if (!queue.Exchanges.Any()) - { - var defaultExchange = builder.Builder.DefaultExchange; - if (defaultExchange is null) - throw new QueueBindException($"Queue {queue.Name} has no configured exchanges and the Default Exchange not found."); - queue.Exchanges.Add(defaultExchange.Name); - } - else + var defaultExchange = builder.Builder.DefaultExchange; + if (defaultExchange is null) + throw new QueueBindException($"Queue {queue.Name} has no configured exchanges and the Default Exchange not found."); + queue.Exchanges.Add(defaultExchange.Name); + } + else + { + // Checking echange points declared in queue in configured "Exchanges". + foreach (var exchangeName in queue.Exchanges) { - // Checking echange points declared in queue in configured "Exchanges". - foreach (var exchangeName in queue.Exchanges) - { - var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == exchangeName); - if (exchange is null) - throw new ClientConfigurationException($"The exchange {exchangeName} configured in queue {queue.Name} " + - $"not found in Exchanges section."); - } + var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == exchangeName); + if (exchange is null) + throw new ClientConfigurationException($"The exchange {exchangeName} configured in queue {queue.Name} " + + $"not found in Exchanges section."); } - - AddQueue(builder, queue); } - static TConfig BindConfig(IConfigurationSection queueConfig) - where TConfig : new() - { - var q = new TConfig(); - queueConfig.Bind(q); - return q; - } + AddQueue(builder, queue); + } - static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) - where T : QueueBase + static TConfig BindConfig(IConfigurationSection queueConfig) + where TConfig : new() + { + var q = new TConfig(); + queueConfig.Bind(q); + return q; + } + + static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) + where T : QueueBase + { + // So-so solution, but without dubbing. + switch (queue) { - // So-so solution, but without dubbing. - switch (queue) - { - case Queue q: builder.AddQueue(q); break; - case Subscription q: builder.AddSubscription(q); break; - } + case Queue q: builder.AddQueue(q); break; + case Subscription q: builder.AddSubscription(q); break; } } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs index 39255ed..8c00890 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs @@ -1,68 +1,67 @@ -using System; +using System; using System.Collections.Generic; -namespace RabbitMQCoreClient.DependencyInjection.ConfigModels +namespace RabbitMQCoreClient.DependencyInjection.ConfigModels; + +/// +/// Simple custom message queue. +/// +public class QueueConfig { + string? _name; + /// - /// Simple custom message queue. + /// The name of the message queue. /// - public class QueueConfig - { - string? _name; - - /// - /// The name of the message queue. - /// - [Obsolete("It is worth switching to using Name.")] - public string QueueName { get; set; } = default!; + [Obsolete("It is worth switching to using Name.")] + public string QueueName { get; set; } = default!; - /// - /// The name of the message queue. - /// - public string Name - { - get => string.IsNullOrEmpty(_name) ? QueueName : _name; - set => _name = value; - } + /// + /// The name of the message queue. + /// + public string Name + { + get => string.IsNullOrEmpty(_name) ? QueueName : _name; + set => _name = value; + } - /// - /// If true, the queue will be saved on disc. - /// - public bool Durable { get; set; } = true; + /// + /// If true, the queue will be saved on disc. + /// + public bool Durable { get; set; } = true; - /// - /// If true, then the queue will be used by single service and will be deleted after client will disconnect. - /// - public bool Exclusive { get; set; } = false; + /// + /// If true, then the queue will be used by single service and will be deleted after client will disconnect. + /// + public bool Exclusive { get; set; } = false; - /// - /// If true, the queue will be automaticly deleted on client disconnect. - /// - public bool AutoDelete { get; set; } = false; + /// + /// If true, the queue will be automatically deleted on client disconnect. + /// + public bool AutoDelete { get; set; } = false; - /// - /// The name of the exchange point that will receive messages for which a reject or nack was received. - /// - public string? DeadLetterExchange { get; set; } + /// + /// The name of the exchange point that will receive messages for which a reject or nack was received. + /// + public string? DeadLetterExchange { get; set; } - /// - /// While creating the queue use parameter "x-queue-type": "quorum". - /// - public bool UseQuorum { get; set; } = false; + /// + /// While creating the queue use parameter "x-queue-type": "quorum". + /// + public bool UseQuorum { get; set; } = false; - /// - /// List of additional parameters that will be used when initializing the queue. - /// - public IDictionary Arguments { get; set; } = new Dictionary(); + /// + /// List of additional parameters that will be used when initializing the queue. + /// + public IDictionary Arguments { get; set; } = new Dictionary(); - /// - /// List of routing keys for the queue. - /// - public HashSet RoutingKeys { get; set; } = new HashSet(); + /// + /// List of routing keys for the queue. + /// + public HashSet RoutingKeys { get; set; } = new HashSet(); - /// - /// The list of exchange points to which the queue is bound. - /// - public HashSet Exchanges { get; set; } = new HashSet(); - } + /// + /// The list of exchange points to which the queue is bound. + /// + public HashSet Exchanges { get; set; } = new HashSet(); } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs index 7a88a0c..910ec29 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs @@ -1,37 +1,36 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace RabbitMQCoreClient.DependencyInjection.ConfigModels +namespace RabbitMQCoreClient.DependencyInjection.ConfigModels; + +/// +/// Message queue for subscribing to events. +/// The queue is automatically named. +/// When the client disconnects from the server, the queue is automatically deleted. +/// +public class SubscriptionConfig { /// - /// Message queue for subscribing to events. - /// The queue is automatically named. - /// When the client disconnects from the server, the queue is automatically deleted. + /// The name of the exchange point that will receive messages for which a reject or nack was received. /// - public class SubscriptionConfig - { - /// - /// The name of the exchange point that will receive messages for which a reject or nack was received. - /// - public string? DeadLetterExchange { get; set; } + public string? DeadLetterExchange { get; set; } - /// - /// While creating the queue use parameter "x-queue-type": "quorum". - /// - public bool UseQuorum { get; set; } = false; + /// + /// While creating the queue use parameter "x-queue-type": "quorum". + /// + public bool UseQuorum { get; set; } = false; - /// - /// List of additional parameters that will be used when initializing the queue. - /// - public IDictionary Arguments { get; set; } = new Dictionary(); + /// + /// List of additional parameters that will be used when initializing the queue. + /// + public IDictionary Arguments { get; set; } = new Dictionary(); - /// - /// List of routing keys for the queue. - /// - public HashSet RoutingKeys { get; set; } = new HashSet(); + /// + /// List of routing keys for the queue. + /// + public HashSet RoutingKeys { get; set; } = new HashSet(); - /// - /// The list of exchange points to which the queue is bound. - /// - public HashSet Exchanges { get; set; } = new HashSet(); - } + /// + /// The list of exchange points to which the queue is bound. + /// + public HashSet Exchanges { get; set; } = new HashSet(); } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs index b6205ea..ef2d473 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs @@ -1,26 +1,25 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; using RabbitMQCoreClient.Exceptions; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class ApplicationBuilderExtentions { - public static class ApplicationBuilderExtentions + /// Starts the rabbit mq core client consuming queues. + /// The application. + /// The lifetime. + public static IApplicationBuilder StartRabbitMqCore(this IApplicationBuilder app, IHostApplicationLifetime lifetime) { - /// Starts the rabbit mq core client consuming queues. - /// The application. - /// The lifetime. - public static IApplicationBuilder StartRabbitMqCore(this IApplicationBuilder app, IHostApplicationLifetime lifetime) - { - var consumer = app.ApplicationServices.GetService(); + var consumer = app.ApplicationServices.GetService(); - if (consumer is null) - throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + - "Add services.AddRabbitMQCoreClient(...).AddConsumer(); to the DI."); + if (consumer is null) + throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + + "Add services.AddRabbitMQCoreClient(...).AddConsumer(); to the DI."); - lifetime.ApplicationStarted.Register(() => consumer.Start()); - lifetime.ApplicationStopping.Register(() => consumer.Shutdown()); + lifetime.ApplicationStarted.Register(() => consumer.Start()); + lifetime.ApplicationStopping.Register(() => consumer.Shutdown()); - return app; - } + return app; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index 9870f41..81d7228 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using RabbitMQCoreClient; @@ -10,252 +10,251 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class BuilderExtensions { - public static class BuilderExtensions + /// + /// Adds the required platform services. + /// + /// The builder. + public static IRabbitMQCoreClientBuilder AddRequiredPlatformServices(this IRabbitMQCoreClientBuilder builder) { - /// - /// Adds the required platform services. - /// - /// The builder. - public static IRabbitMQCoreClientBuilder AddRequiredPlatformServices(this IRabbitMQCoreClientBuilder builder) - { - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); - builder.Services.AddOptions(); - builder.Services.AddLogging(); + builder.Services.AddOptions(); + builder.Services.AddLogging(); - builder.Services.TryAddSingleton( - resolver => resolver.GetRequiredService>().Value); + builder.Services.TryAddSingleton( + resolver => resolver.GetRequiredService>().Value); - return builder; - } + return builder; + } - /// - /// Use default serializer as NewtonsoftJson. - /// - public static IRabbitMQCoreClientBuilder AddDefaultSerializer(this IRabbitMQCoreClientBuilder builder) => - builder.AddSystemTextJson(); - - /// - /// Add exchange connection to RabbitMQ. - /// - /// The builder. - /// Name of the exchange. - /// The options. - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientBuilder AddExchange( - this IRabbitMQCoreClientBuilder builder, - string exchangeName, - ExchangeOptions? options = default) - { - if (builder.Exchanges.Any(x => x.Name == exchangeName)) - throw new ArgumentException("The exchange with same name was added earlier."); + /// + /// Use default serializer as NewtonsoftJson. + /// + public static IRabbitMQCoreClientBuilder AddDefaultSerializer(this IRabbitMQCoreClientBuilder builder) => + builder.AddSystemTextJson(); + + /// + /// Add exchange connection to RabbitMQ. + /// + /// The builder. + /// Name of the exchange. + /// The options. + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientBuilder AddExchange( + this IRabbitMQCoreClientBuilder builder, + string exchangeName, + ExchangeOptions? options = default) + { + if (builder.Exchanges.Any(x => x.Name == exchangeName)) + throw new ArgumentException("The exchange with same name was added earlier."); - var exchange = new Exchange(options ?? new ExchangeOptions { Name = exchangeName }); - builder.Exchanges.Add(exchange); + var exchange = new Exchange(options ?? new ExchangeOptions { Name = exchangeName }); + builder.Exchanges.Add(exchange); - return builder; - } + return builder; + } - /// - /// Adds the consumer builder. - /// - /// The rabbit MQ builder. - public static IRabbitMQCoreClientConsumerBuilder AddConsumer( - this IRabbitMQCoreClientBuilder builder) - { - var consumerBuilder = new RabbitMQCoreClientConsumerBuilder(builder); + /// + /// Adds the consumer builder. + /// + /// The rabbit MQ builder. + public static IRabbitMQCoreClientConsumerBuilder AddConsumer( + this IRabbitMQCoreClientBuilder builder) + { + var consumerBuilder = new RabbitMQCoreClientConsumerBuilder(builder); - builder.Services.TryAddSingleton(consumerBuilder); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(consumerBuilder); + builder.Services.TryAddSingleton(); - return consumerBuilder; - } + return consumerBuilder; + } - /// - /// Add default queue to RabbitMQ consumer. - /// - /// The builder. - /// Name of the queue. - /// Name of the exchange. If null - will try to bind to default exchange. - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientConsumerBuilder AddQueue( - this IRabbitMQCoreClientConsumerBuilder builder, - string queueName, string? exchangeName = default) - { - var queue = new Queue(queueName); - if (!string.IsNullOrEmpty(exchangeName)) - queue.Exchanges.Add(exchangeName); + /// + /// Add default queue to RabbitMQ consumer. + /// + /// The builder. + /// Name of the queue. + /// Name of the exchange. If null - will try to bind to default exchange. + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientConsumerBuilder AddQueue( + this IRabbitMQCoreClientConsumerBuilder builder, + string queueName, string? exchangeName = default) + { + var queue = new Queue(queueName); + if (!string.IsNullOrEmpty(exchangeName)) + queue.Exchanges.Add(exchangeName); - builder.AddQueue(queue); + builder.AddQueue(queue); - return builder; - } + return builder; + } - /// - /// Add default queue to RabbitMQ consumer. - /// - /// The builder. - /// The queue configuration. - /// queue - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientConsumerBuilder AddQueue( - this IRabbitMQCoreClientConsumerBuilder builder, - IConfiguration queueConfig) - { - if (queueConfig is null) - throw new ArgumentNullException(nameof(queueConfig), $"{nameof(queueConfig)} is null."); + /// + /// Add default queue to RabbitMQ consumer. + /// + /// The builder. + /// The queue configuration. + /// queue + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientConsumerBuilder AddQueue( + this IRabbitMQCoreClientConsumerBuilder builder, + IConfiguration queueConfig) + { + if (queueConfig is null) + throw new ArgumentNullException(nameof(queueConfig), $"{nameof(queueConfig)} is null."); - var q = new QueueConfig(); - queueConfig.Bind(q); + var q = new QueueConfig(); + queueConfig.Bind(q); - var queue = Queue.Create(q); + var queue = Queue.Create(q); - builder.AddQueue(queue); + builder.AddQueue(queue); - return builder; - } + return builder; + } - /// - /// Add default queue to RabbitMQ consumer. - /// - /// The builder. - /// The queue. - /// queue - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientConsumerBuilder AddQueue( - this IRabbitMQCoreClientConsumerBuilder builder, - Queue queue) - { - if (queue is null) - throw new ArgumentNullException(nameof(queue), $"{nameof(queue)} is null."); + /// + /// Add default queue to RabbitMQ consumer. + /// + /// The builder. + /// The queue. + /// queue + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientConsumerBuilder AddQueue( + this IRabbitMQCoreClientConsumerBuilder builder, + Queue queue) + { + if (queue is null) + throw new ArgumentNullException(nameof(queue), $"{nameof(queue)} is null."); - if (builder.Queues.Any(x => (x is Queue) && x.Name == queue.Name)) - throw new ArgumentException("The queue with same name was added earlier."); + if (builder.Queues.Any(x => (x is Queue) && x.Name == queue.Name)) + throw new ArgumentException("The queue with same name was added earlier."); - builder.Queues.Add(queue); + builder.Queues.Add(queue); - return builder; - } + return builder; + } - /// - /// Add subscription queue to RabbitMQ consumer. - /// - /// The builder. - /// The queue configuration. - /// queue - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientConsumerBuilder AddSubscription( - this IRabbitMQCoreClientConsumerBuilder builder, - IConfiguration subscriptionConfig) - { - if (subscriptionConfig is null) - throw new ArgumentNullException(nameof(subscriptionConfig), $"{nameof(subscriptionConfig)} is null."); + /// + /// Add subscription queue to RabbitMQ consumer. + /// + /// The builder. + /// The queue configuration. + /// queue + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientConsumerBuilder AddSubscription( + this IRabbitMQCoreClientConsumerBuilder builder, + IConfiguration subscriptionConfig) + { + if (subscriptionConfig is null) + throw new ArgumentNullException(nameof(subscriptionConfig), $"{nameof(subscriptionConfig)} is null."); - var s = new SubscriptionConfig(); - subscriptionConfig.Bind(s); + var s = new SubscriptionConfig(); + subscriptionConfig.Bind(s); - var subscription = Subscription.Create(s); + var subscription = Subscription.Create(s); - builder.AddSubscription(subscription); + builder.AddSubscription(subscription); - return builder; - } + return builder; + } - /// - /// Add subscription queue to RabbitMQ consumer. - /// - /// The builder. - /// Name of the exchange. If null - will try to bind to default exchange. - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientConsumerBuilder AddSubscription( - this IRabbitMQCoreClientConsumerBuilder builder, - string? exchangeName = default) - { - var subscription = new Subscription(); - if (!string.IsNullOrEmpty(exchangeName)) - subscription.Exchanges.Add(exchangeName); + /// + /// Add subscription queue to RabbitMQ consumer. + /// + /// The builder. + /// Name of the exchange. If null - will try to bind to default exchange. + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientConsumerBuilder AddSubscription( + this IRabbitMQCoreClientConsumerBuilder builder, + string? exchangeName = default) + { + var subscription = new Subscription(); + if (!string.IsNullOrEmpty(exchangeName)) + subscription.Exchanges.Add(exchangeName); - builder.AddSubscription(subscription); + builder.AddSubscription(subscription); - return builder; - } + return builder; + } - /// - /// Add subscription queue to RabbitMQ consumer. - /// - /// The builder. - /// The subscription queue. - /// The exchange with same name was added earlier. - public static IRabbitMQCoreClientConsumerBuilder AddSubscription( - this IRabbitMQCoreClientConsumerBuilder builder, - Subscription subscription) - { - if (subscription is null) - throw new ArgumentNullException(nameof(subscription), $"{nameof(subscription)} is null."); + /// + /// Add subscription queue to RabbitMQ consumer. + /// + /// The builder. + /// The subscription queue. + /// The exchange with same name was added earlier. + public static IRabbitMQCoreClientConsumerBuilder AddSubscription( + this IRabbitMQCoreClientConsumerBuilder builder, + Subscription subscription) + { + if (subscription is null) + throw new ArgumentNullException(nameof(subscription), $"{nameof(subscription)} is null."); - builder.Queues.Add(subscription); + builder.Queues.Add(subscription); - return builder; - } + return builder; + } - /// - /// Add the message handler that will be trigger on messages with desired routing keys. - /// - /// RabbitMQ Handler type. - /// IRabbitMQCoreClientConsumerBuilder instance. - /// Routing keys binded to the queue. - /// The handler needs to set at least one routing key. - public static IRabbitMQCoreClientConsumerBuilder AddHandler(this IRabbitMQCoreClientConsumerBuilder builder, - params string[] routingKeys) - where TMessageHandler : class, IMessageHandler - { - CheckRoutingKeysParam(routingKeys); + /// + /// Add the message handler that will be trigger on messages with desired routing keys. + /// + /// RabbitMQ Handler type. + /// IRabbitMQCoreClientConsumerBuilder instance. + /// Routing keys bound to the queue. + /// The handler needs to set at least one routing key. + public static IRabbitMQCoreClientConsumerBuilder AddHandler(this IRabbitMQCoreClientConsumerBuilder builder, + params string[] routingKeys) + where TMessageHandler : class, IMessageHandler + { + CheckRoutingKeysParam(routingKeys); - AddHandlerToBuilder(builder, typeof(TMessageHandler), default!, routingKeys); - builder.Services.TryAddTransient(); - return builder; - } + AddHandlerToBuilder(builder, typeof(TMessageHandler), default!, routingKeys); + builder.Services.TryAddTransient(); + return builder; + } - /// - /// Add the message handler that will be trigger on messages with desired routing keys. - /// - /// RabbitMQ Handler type. - /// IRabbitMQCoreClientConsumerBuilder instance. - /// The options that can change consumer handler default behavior. - /// Routing keys binded to the queue. - public static IRabbitMQCoreClientConsumerBuilder AddHandler(this IRabbitMQCoreClientConsumerBuilder builder, - IList routingKeys, - ConsumerHandlerOptions? options = default) - where TMessageHandler : class, IMessageHandler - { - CheckRoutingKeysParam(routingKeys); + /// + /// Add the message handler that will be trigger on messages with desired routing keys. + /// + /// RabbitMQ Handler type. + /// IRabbitMQCoreClientConsumerBuilder instance. + /// The options that can change consumer handler default behavior. + /// Routing keys bound to the queue. + public static IRabbitMQCoreClientConsumerBuilder AddHandler(this IRabbitMQCoreClientConsumerBuilder builder, + IList routingKeys, + ConsumerHandlerOptions? options = default) + where TMessageHandler : class, IMessageHandler + { + CheckRoutingKeysParam(routingKeys); - AddHandlerToBuilder(builder, typeof(TMessageHandler), options ?? new ConsumerHandlerOptions(), routingKeys); - builder.Services.TryAddTransient(); - return builder; - } + AddHandlerToBuilder(builder, typeof(TMessageHandler), options ?? new ConsumerHandlerOptions(), routingKeys); + builder.Services.TryAddTransient(); + return builder; + } - static void AddHandlerToBuilder(IRabbitMQCoreClientConsumerBuilder builder, - Type handlerType, - ConsumerHandlerOptions options, - IList routingKeys) + static void AddHandlerToBuilder(IRabbitMQCoreClientConsumerBuilder builder, + Type handlerType, + ConsumerHandlerOptions options, + IList routingKeys) + { + foreach (var routingKey in routingKeys) { - foreach (var routingKey in routingKeys) - { - if (builder.RoutingHandlerTypes.ContainsKey(routingKey)) - throw new ClientConfigurationException("The routing key is already being processed by a handler like " + - $"{builder.RoutingHandlerTypes[routingKey].Type.FullName}."); - - builder.RoutingHandlerTypes.Add(routingKey, (handlerType, options)); - } - } + if (builder.RoutingHandlerTypes.ContainsKey(routingKey)) + throw new ClientConfigurationException("The routing key is already being processed by a handler like " + + $"{builder.RoutingHandlerTypes[routingKey].Type.FullName}."); - static void CheckRoutingKeysParam(IEnumerable routingKeys) - { - if (routingKeys is null || !routingKeys.Any()) - throw new ClientConfigurationException("The handler needs to set at least one routing key."); + builder.RoutingHandlerTypes.Add(routingKey, (handlerType, options)); } } + + static void CheckRoutingKeysParam(IEnumerable routingKeys) + { + if (routingKeys is null || !routingKeys.Any()) + throw new ClientConfigurationException("The handler needs to set at least one routing key."); + } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs index 5f9c414..cde0ade 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs @@ -1,92 +1,91 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigFormats; using System; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Class containing extension methods for creating a RabbitMQ message handler configuration interface. +/// . +/// +public static class ServiceCollectionExtensions { + static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services) + { + var builder = new RabbitMQCoreClientBuilder(services); + + builder.AddRequiredPlatformServices(); + + builder.AddDefaultSerializer(); + + services.TryAddSingleton(builder); + + return builder; + } + + /// + /// Create an instance of the RabbitMQ message handler configuration class. + /// + /// List of services registered in DI. + /// Configuration section containing fields for configuring the message processing service. + /// Use this method if you need to override the configuration. + public static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient( + this IServiceCollection services, + IConfiguration configuration, + Action? setupAction = null) + { + RegisterOptions(services, configuration, setupAction); + + // We perform auto-tuning of queues from IConfiguration settings. + var builder = services.AddRabbitMQCoreClient(); + + // Support for the old queue registration format. + builder.RegisterV1Configuration(configuration); + builder.RegisterV2Configuration(configuration); + + return builder; + } + + /// + /// Create an instance of the RabbitMQ message handler configuration class. + /// + public static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services, + Action? setupAction) + { + services.Configure(setupAction); + return services.AddRabbitMQCoreClient(); + } + /// - /// Class containing extension methods for creating a RabbitMQ message handler configuration interface. - /// . + /// Create an instance of the RabbitMQ message handler configuration class. /// - public static class ServiceCollectionExtensions + /// List of services registered in DI. + /// Configuration section containing fields for configuring the message processing service. + /// Use this method if you need to override the configuration. + public static IRabbitMQCoreClientConsumerBuilder AddRabbitMQCoreClientConsumer(this IServiceCollection services, + IConfiguration configuration, Action? setupAction = null) { - static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services) - { - var builder = new RabbitMQCoreClientBuilder(services); - - builder.AddRequiredPlatformServices(); - - builder.AddDefaultSerializer(); - - services.TryAddSingleton(builder); - - return builder; - } - - /// - /// Create an instance of the RabbitMQ message handler configuration class. - /// - /// List of services registered in DI. - /// Configuration section containing fields for configuring the message processing service. - /// Use this method if you need to override the configuration. - public static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient( - this IServiceCollection services, - IConfiguration configuration, - Action? setupAction = null) - { - RegisterOptions(services, configuration, setupAction); - - // We perform auto-tuning of queues from IConfiguration settings. - var builder = services.AddRabbitMQCoreClient(); - - // Support for the old queue registration format. - builder.RegisterV1Configuration(configuration); - builder.RegisterV2Configuration(configuration); - - return builder; - } - - /// - /// Create an instance of the RabbitMQ message handler configuration class. - /// - public static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services, - Action? setupAction) - { - services.Configure(setupAction); - return services.AddRabbitMQCoreClient(); - } - - /// - /// Create an instance of the RabbitMQ message handler configuration class. - /// - /// List of services registered in DI. - /// Configuration section containing fields for configuring the message processing service. - /// Use this method if you need to override the configuration. - public static IRabbitMQCoreClientConsumerBuilder AddRabbitMQCoreClientConsumer(this IServiceCollection services, - IConfiguration configuration, Action? setupAction = null) - { - // We perform auto-tuning of queues from IConfiguration settings. - var builder = services.AddRabbitMQCoreClient(configuration, setupAction); - - var consumerBuilder = builder.AddConsumer(); - - // Support for the old queue registration format. - consumerBuilder.RegisterV1Configuration(configuration); - consumerBuilder.RegisterV2Configuration(configuration); - - return consumerBuilder; - } - - static void RegisterOptions(IServiceCollection services, IConfiguration configuration, Action? setupAction) - { - var instance = configuration.Get(); - setupAction?.Invoke(instance); - var options = Options.Options.Create(instance); - - services.AddSingleton((x) => options); - } + // We perform auto-tuning of queues from IConfiguration settings. + var builder = services.AddRabbitMQCoreClient(configuration, setupAction); + + var consumerBuilder = builder.AddConsumer(); + + // Support for the old queue registration format. + consumerBuilder.RegisterV1Configuration(configuration); + consumerBuilder.RegisterV2Configuration(configuration); + + return consumerBuilder; + } + + static void RegisterOptions(IServiceCollection services, IConfiguration configuration, Action? setupAction) + { + var instance = configuration.Get() ?? new RabbitMQCoreClientOptions(); + setupAction?.Invoke(instance); + var options = Options.Options.Create(instance); + + services.AddSingleton((x) => options); } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs index b646b7e..da2f6d6 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs @@ -1,34 +1,34 @@ -using RabbitMQ.Client; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using System; +using System.Threading.Tasks; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// The Consumer interface uses for starting and stopping manipulations. +/// +public interface IQueueConsumer : IAsyncDisposable { /// - /// The Consumer interface uses for starting and stopping manipulations. + /// Connect to all queues and start receiving messages. /// - public interface IQueueConsumer : IDisposable - { - /// - /// Connect to all queues and start receiving messages. - /// - /// - void Start(); + /// + Task Start(); - /// - /// Stops listening Queues. - /// - void Shutdown(); + /// + /// Stops listening Queues. + /// + Task Shutdown(); - /// - /// The channel that consume messages from the RabbitMQ Instance. - /// Can be Null, if method was not called. - /// - IModel? ConsumeChannel { get; } + /// + /// The channel that consume messages from the RabbitMQ Instance. + /// Can be Null, if method was not called. + /// + IChannel? ConsumeChannel { get; } - /// - /// The Async consumer, with default consume method configurated. - /// - AsyncEventingBasicConsumer? Consumer { get; } - } + /// + /// The Async consumer, with default consume method configurated. + /// + AsyncEventingBasicConsumer? Consumer { get; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs index 1053b2c..a4b6130 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs @@ -1,29 +1,28 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection; +using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Serializers; using System.Collections.Generic; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public interface IRabbitMQCoreClientBuilder { - public interface IRabbitMQCoreClientBuilder - { - /// - /// List of services registered in DI. - /// - IServiceCollection Services { get; } + /// + /// List of services registered in DI. + /// + IServiceCollection Services { get; } - /// - /// List of configured exchange points. - /// - IList Exchanges { get; } + /// + /// List of configured exchange points. + /// + IList Exchanges { get; } - /// - /// Gets the default exchange. - /// - Exchange? DefaultExchange { get; } + /// + /// Gets the default exchange. + /// + Exchange? DefaultExchange { get; } - /// - /// The default JSON serializer. - /// - IMessageSerializer Serializer { get; set; } - } + /// + /// The default JSON serializer. + /// + IMessageSerializer Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs index 307ce89..ba49832 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs @@ -1,29 +1,28 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using System; using System.Collections.Generic; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public interface IRabbitMQCoreClientConsumerBuilder { - public interface IRabbitMQCoreClientConsumerBuilder - { - /// - /// RabbitMQCoreClientBuilder - /// - IRabbitMQCoreClientBuilder Builder { get; } + /// + /// RabbitMQCoreClientBuilder + /// + IRabbitMQCoreClientBuilder Builder { get; } - /// - /// List of services registered in DI. - /// - IServiceCollection Services { get; } + /// + /// List of services registered in DI. + /// + IServiceCollection Services { get; } - /// - /// List of configured queues. - /// - IList Queues { get; } + /// + /// List of configured queues. + /// + IList Queues { get; } - /// - /// List of registered event handlers by routing key. - /// - Dictionary RoutingHandlerTypes { get; } - } + /// + /// List of registered event handlers by routing key. + /// + Dictionary RoutingHandlerTypes { get; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs index 0e04016..8c61e8c 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs @@ -1,21 +1,20 @@ -using RabbitMQCoreClient.Serializers; +using RabbitMQCoreClient.Serializers; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + +/// +/// Consumer Handler Options. +/// +public class ConsumerHandlerOptions { /// - /// Consumer Handler Options. + /// Gets or sets the json serializer that overrides default serializer for the following massage handler. /// - public class ConsumerHandlerOptions - { - /// - /// Gets or sets the json serializer that overides default serializer for the following massage handler. - /// - public IMessageSerializer? CustomSerializer { get; set; } = default; + public IMessageSerializer? CustomSerializer { get; set; } = default; - /// - /// The routing key that will mark the message on the exception handling stage. - /// If configured, the message will return to the exchange with this key instead of . - /// - public string? RetryKey { get; set; } = default; - } + /// + /// The routing key that will mark the message on the exception handling stage. + /// If configured, the message will return to the exchange with this key instead of original Key. + /// + public string? RetryKey { get; set; } = default; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs index de73f43..5a43330 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs @@ -1,21 +1,20 @@ -using RabbitMQ.Client.Events; +using RabbitMQ.Client.Events; using System; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + +/// +/// Parameters that allow you to organize your own client exception handling mechanisms. +/// +public class ErrorHandlingOptions { /// - /// Parameters that allow you to organize your own client exception handling mechanisms. + /// Internal library call exception handler event. null to use default handlers. /// - public class ErrorHandlingOptions - { - /// - /// Internal library call exception handler event. null to use default handlers. - /// - public EventHandler? CallbackExceptionHandler { get; set; } = null; + public EventHandler? CallbackExceptionHandler { get; set; } = null; - /// - /// An exception handler event when the connection cannot be reestablished. null to use default handlers. - /// - public EventHandler? ConnectionRecoveryErrorHandler { get; set; } = null; - } + /// + /// An exception handler event when the connection cannot be reestablished. null to use default handlers. + /// + public EventHandler? ConnectionRecoveryErrorHandler { get; set; } = null; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs index 5aa32a0..927551b 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs @@ -1,49 +1,48 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + +/// +/// Exchange point settings. +/// +public class ExchangeOptions { /// - /// Exchange point settings. + /// Exchange point name. /// - public class ExchangeOptions - { - /// - /// Exchange point name. - /// - public string Name { get; set; } = default!; + public string Name { get; set; } = default!; - /// - /// Exchange point type. - /// Possible values: "direct", "topic", "fanout", "headers" - /// - public string Type { get; set; } = "direct"; + /// + /// Exchange point type. + /// Possible values: "direct", "topic", "fanout", "headers" + /// + public string Type { get; set; } = "direct"; - /// - /// Gets or sets a value indicating whether this is durable. - /// - /// - /// true if durable; otherwise, false. - /// - public bool Durable { get; set; } = true; + /// + /// Gets or sets a value indicating whether this is durable. + /// + /// + /// true if durable; otherwise, false. + /// + public bool Durable { get; set; } = true; - /// - /// If yes, the exchange will delete itself after at least one queue or exchange - /// has been bound to this one, and then all queues or exchanges have been unbound. - /// - public bool AutoDelete { get; set; } = false; + /// + /// If yes, the exchange will delete itself after at least one queue or exchange + /// has been bound to this one, and then all queues or exchanges have been unbound. + /// + public bool AutoDelete { get; set; } = false; - /// - /// A set of additional settings for the exchange point. - /// - public IDictionary Arguments { get; set; } = new Dictionary(); + /// + /// A set of additional settings for the exchange point. + /// + public IDictionary Arguments { get; set; } = new Dictionary(); - /// - /// Sets the Default Interchange Point value. - /// Default: false. - /// - /// - /// true if the exchange point is the default point; otherwise, false. - /// - public bool IsDefault { get; set; } = false; - } + /// + /// Sets the Default Interchange Point value. + /// Default: false. + /// + /// + /// true if the exchange point is the default point; otherwise, false. + /// + public bool IsDefault { get; set; } = false; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs index 7b25deb..f3f20bd 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs @@ -4,133 +4,132 @@ using System.Security.Authentication; using System.Text.Json.Serialization; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options -{ +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + - public class RabbitMQCoreClientOptions - { - /// - /// RabbitMQ server address. - /// - public string HostName { get; set; } = "127.0.0.1"; - - /// - /// Password of the user who has rights to connect to the server . - /// - public string Password { get; set; } = "guest"; - - /// - /// Service access port. - /// - public int Port { get; set; } = 5672; - - /// - /// Timeout setting for connection attempts (in milliseconds). - /// Default: 30000. - /// - public int RequestedConnectionTimeout { get; set; } = 30000; - - public ushort RequestedHeartbeat { get; set; } = 60; - - /// - /// Timeout setting between reconnection attempts (in milliseconds). - /// Default: 3000. - /// - public int ReconnectionTimeout { get; set; } = 3000; - - /// - /// Reconnection attempts count. - /// Default: null. - /// So it will try to establish a connection every 3 seconds an unlimited number of times. - /// In other case ConnectionException will occur when the limit is reached. - /// - public int? ReconnectionAttemptsCount { get; set; } - - /// - /// User who has rights to connect to the server . - /// - public string UserName { get; set; } = "guest"; - - /// - /// Gets or sets the virtual host. - /// - public string VirtualHost { get; set; } = "/"; - - /// - /// The number of times the message was attempted to be processed during which an exception was thrown. - /// - public int DefaultTtl { get; set; } = 5; - - /// - /// Number of messages to be pre-loaded into the handler. - /// - public ushort PrefetchCount { get; set; } = 1; - - /// - /// Internal library call exception handler event. - /// - public EventHandler? ConnectionCallbackExceptionHandler { get; set; } - - /// - /// While creating queues use parameter "x-queue-type": "quorum" on the whole client. - /// - public bool UseQuorumQueues { get; set; } = false; - - /// - /// Controls if TLS should indeed be used. Set to false to disable TLS - /// on the connection. - /// - public bool SslEnabled { get; set; } = false; - - /// - /// Retrieve or set the set of TLS policy (peer verification) errors that are deemed acceptable. - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public SslPolicyErrors SslAcceptablePolicyErrors { get; set; } = SslPolicyErrors.None; - - /// - /// Retrieve or set the TLS protocol version. - /// The client will let the OS pick a suitable version by using . - /// If this option is disabled, e.g.see via app context, the client will attempt to fall back - /// to TLSv1.2. - /// - /// - /// - /// - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public SslProtocols SslVersion { get; set; } = SslProtocols.None; - - /// - /// Retrieve or set server's expected name. - /// This MUST match the Subject Alternative Name (SAN) or CN on the peer's (server's) leaf certificate, - /// otherwise the TLS connection will fail. - /// - public string? SslServerName { get; set; } - - /// - /// Attempts to check certificate revocation status. Default is false. - /// Set to true to check peer certificate for revocation. - /// - /// - /// Uses the built-in .NET TLS implementation machinery for checking a certificate against - /// certificate revocation lists. - /// - public bool SslCheckCertificateRevocation { get; set; } = false; - - /// - /// Retrieve or set the client certificate passphrase. - /// - public string? SslCertPassphrase { get; set; } - - /// - /// Retrieve or set the path to client certificate. - /// - public string? SslCertPath { get; set; } - - /// - /// The maximum message body size limit. Default is 16MBi. - /// - public int MaxBodySize { get; set; } = 16 * 1024 * 1024; // 16 МБи - } +public class RabbitMQCoreClientOptions +{ + /// + /// RabbitMQ server address. + /// + public string HostName { get; set; } = "127.0.0.1"; + + /// + /// Password of the user who has rights to connect to the server . + /// + public string Password { get; set; } = "guest"; + + /// + /// Service access port. + /// + public int Port { get; set; } = 5672; + + /// + /// Timeout setting for connection attempts (in milliseconds). + /// Default: 30000. + /// + public int RequestedConnectionTimeout { get; set; } = 30000; + + public ushort RequestedHeartbeat { get; set; } = 60; + + /// + /// Timeout setting between reconnection attempts (in milliseconds). + /// Default: 3000. + /// + public int ReconnectionTimeout { get; set; } = 3000; + + /// + /// Reconnection attempts count. + /// Default: null. + /// So it will try to establish a connection every 3 seconds an unlimited number of times. + /// In other case ConnectionException will occur when the limit is reached. + /// + public int? ReconnectionAttemptsCount { get; set; } + + /// + /// User who has rights to connect to the server . + /// + public string UserName { get; set; } = "guest"; + + /// + /// Gets or sets the virtual host. + /// + public string VirtualHost { get; set; } = "/"; + + /// + /// The number of times the message was attempted to be processed during which an exception was thrown. + /// + public int DefaultTtl { get; set; } = 5; + + /// + /// Number of messages to be pre-loaded into the handler. + /// + public ushort PrefetchCount { get; set; } = 1; + + /// + /// Internal library call exception handler event. + /// + public AsyncEventHandler? ConnectionCallbackExceptionHandler { get; set; } + + /// + /// While creating queues use parameter "x-queue-type": "quorum" on the whole client. + /// + public bool UseQuorumQueues { get; set; } = false; + + /// + /// Controls if TLS should indeed be used. Set to false to disable TLS + /// on the connection. + /// + public bool SslEnabled { get; set; } = false; + + /// + /// Retrieve or set the set of TLS policy (peer verification) errors that are deemed acceptable. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public SslPolicyErrors SslAcceptablePolicyErrors { get; set; } = SslPolicyErrors.None; + + /// + /// Retrieve or set the TLS protocol version. + /// The client will let the OS pick a suitable version by using . + /// If this option is disabled, e.g.see via app context, the client will attempt to fall back + /// to TLSv1.2. + /// + /// + /// + /// + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public SslProtocols SslVersion { get; set; } = SslProtocols.None; + + /// + /// Retrieve or set server's expected name. + /// This MUST match the Subject Alternative Name (SAN) or CN on the peer's (server's) leaf certificate, + /// otherwise the TLS connection will fail. + /// + public string? SslServerName { get; set; } + + /// + /// Attempts to check certificate revocation status. Default is false. + /// Set to true to check peer certificate for revocation. + /// + /// + /// Uses the built-in .NET TLS implementation machinery for checking a certificate against + /// certificate revocation lists. + /// + public bool SslCheckCertificateRevocation { get; set; } = false; + + /// + /// Retrieve or set the client certificate passphrase. + /// + public string? SslCertPassphrase { get; set; } + + /// + /// Retrieve or set the path to client certificate. + /// + public string? SslCertPath { get; set; } + + /// + /// The maximum message body size limit. Default is 16MBi. + /// + public int MaxBodySize { get; set; } = 16 * 1024 * 1024; // 16 МБи } diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs index 4e55bfa..4e109a4 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs @@ -1,31 +1,30 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Serializers; using System; using System.Collections.Generic; using System.Linq; -namespace RabbitMQCoreClient.Configuration.DependencyInjection +namespace RabbitMQCoreClient.Configuration.DependencyInjection; + +public sealed class RabbitMQCoreClientBuilder : IRabbitMQCoreClientBuilder { - public class RabbitMQCoreClientBuilder : IRabbitMQCoreClientBuilder - { - /// - /// Initializes a new instance of the class. - /// - /// The services. - /// services - public RabbitMQCoreClientBuilder(IServiceCollection? services) => - Services = services ?? throw new ArgumentNullException(nameof(services)); + /// + /// Initializes a new instance of the class. + /// + /// The services. + /// services + public RabbitMQCoreClientBuilder(IServiceCollection services) => + Services = services ?? throw new ArgumentNullException(nameof(services)); - /// - public IServiceCollection Services { get; } + /// + public IServiceCollection Services { get; } - /// - public IList Exchanges { get; } = new List(); + /// + public IList Exchanges { get; } = new List(); - /// - public Exchange? DefaultExchange => Exchanges.FirstOrDefault(x => x.Options.IsDefault); + /// + public Exchange? DefaultExchange => Exchanges.FirstOrDefault(x => x.Options.IsDefault); - /// - public IMessageSerializer Serializer { get; set; } - } + /// + public IMessageSerializer Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs index ed16fa1..2d886d5 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs @@ -1,34 +1,33 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using System; using System.Collections.Generic; -namespace RabbitMQCoreClient.Configuration.DependencyInjection +namespace RabbitMQCoreClient.Configuration.DependencyInjection; + +public sealed class RabbitMQCoreClientConsumerBuilder : IRabbitMQCoreClientConsumerBuilder { - public class RabbitMQCoreClientConsumerBuilder : IRabbitMQCoreClientConsumerBuilder + /// + /// Initializes a new instance of the class. + /// + /// The builder. + /// services + public RabbitMQCoreClientConsumerBuilder(IRabbitMQCoreClientBuilder builder) { - /// - /// Initializes a new instance of the class. - /// - /// The builder. - /// services - public RabbitMQCoreClientConsumerBuilder(IRabbitMQCoreClientBuilder builder) - { - Builder = builder ?? throw new ArgumentNullException(nameof(builder)); - Services = builder.Services ?? throw new ArgumentException($"{nameof(builder.Services)} is null"); - } + Builder = builder ?? throw new ArgumentNullException(nameof(builder)); + Services = builder.Services ?? throw new ArgumentException($"{nameof(builder.Services)} is null"); + } - /// - public IRabbitMQCoreClientBuilder Builder { get; } + /// + public IRabbitMQCoreClientBuilder Builder { get; } - /// - public IServiceCollection Services { get; } + /// + public IServiceCollection Services { get; } - /// - public IList Queues { get; } = new List(); + /// + public IList Queues { get; } = new List(); - /// - public Dictionary RoutingHandlerTypes { get; } = - new Dictionary(); - } + /// + public Dictionary RoutingHandlerTypes { get; } = + new Dictionary(); } diff --git a/src/RabbitMQCoreClient/ErrorMessageRouting.cs b/src/RabbitMQCoreClient/ErrorMessageRouting.cs index 0fe648f..8eb7788 100644 --- a/src/RabbitMQCoreClient/ErrorMessageRouting.cs +++ b/src/RabbitMQCoreClient/ErrorMessageRouting.cs @@ -1,43 +1,42 @@ -using RabbitMQCoreClient.Models; +using RabbitMQCoreClient.Models; -namespace RabbitMQCoreClient +namespace RabbitMQCoreClient; + +/// +/// Incoming message routing methods. +/// +public sealed class ErrorMessageRouting { /// - /// Incoming message routing methods. + /// Selected processing route. + /// Default: Routes.SourceQueue. /// - public sealed class ErrorMessageRouting - { - /// - /// Selected processing route. - /// Default: Routes.SourceQueue. - /// - public Routes Route { get; private set; } = Routes.SourceQueue; + public Routes Route { get; private set; } = Routes.SourceQueue; - /// - /// Actions to change Ttl. - /// Default: TtlActions.Decrease. - /// - public TtlActions TtlAction { get; set; } = TtlActions.Decrease; - - /// - /// Select the route for sending the message. - /// - /// Маршрут. - public void MoveTo(Routes route) => Route = route; + /// + /// Actions to change Ttl. + /// Default: TtlActions.Decrease. + /// + public TtlActions TtlAction { get; set; } = TtlActions.Decrease; - /// - /// Send the message back to the queue. - /// - /// If true then ttl messages will be minified. - public void MoveBackToQueue(bool decreaseTtl = true) - { - Route = Routes.SourceQueue; - TtlAction = decreaseTtl ? TtlActions.Decrease : TtlActions.DoNotChange; - } + /// + /// Select the route for sending the message. + /// + /// Маршрут. + public void MoveTo(Routes route) => Route = route; - /// - /// Send a message to dead letter exchange. - /// - public void MoveToDeadLetter() => Route = Routes.DeadLetter; + /// + /// Send the message back to the queue. + /// + /// If true then ttl messages will be minified. + public void MoveBackToQueue(bool decreaseTtl = true) + { + Route = Routes.SourceQueue; + TtlAction = decreaseTtl ? TtlActions.Decrease : TtlActions.DoNotChange; } + + /// + /// Send a message to dead letter exchange. + /// + public void MoveToDeadLetter() => Route = Routes.DeadLetter; } diff --git a/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs b/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs new file mode 100644 index 0000000..f0ce632 --- /dev/null +++ b/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs @@ -0,0 +1,7 @@ +using RabbitMQ.Client.Events; + +namespace RabbitMQCoreClient.Events; + +public class ReconnectEventArgs : AsyncEventArgs +{ +} diff --git a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs index 85644b2..34673f9 100644 --- a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs @@ -1,22 +1,21 @@ -using System; +using System; -namespace RabbitMQCoreClient.Exceptions +namespace RabbitMQCoreClient.Exceptions; + +public class ClientConfigurationException : Exception { - public class ClientConfigurationException : Exception + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public ClientConfigurationException(string message) : base(message) { - /// Initializes a new instance of the class with a specified error message. - /// The message that describes the error. - public ClientConfigurationException(string message) : base(message) - { - } + } - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public ClientConfigurationException(string message, Exception innerException) : base(message, innerException) - { + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public ClientConfigurationException(string message, Exception innerException) : base(message, innerException) + { - } } } diff --git a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs index cfc93af..fff38db 100644 --- a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs +++ b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs @@ -1,23 +1,21 @@ -using System; +using System; -namespace RabbitMQCoreClient.Exceptions +namespace RabbitMQCoreClient.Exceptions; + +public class NotConnectedException : Exception { - [Serializable] - public class NotConnectedException : Exception + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public NotConnectedException(string message) : base(message) { - /// Initializes a new instance of the class with a specified error message. - /// The message that describes the error. - public NotConnectedException(string message) : base(message) - { - } + } - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotConnectedException(string message, Exception innerException) : base(message, innerException) - { + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public NotConnectedException(string message, Exception innerException) : base(message, innerException) + { - } } } diff --git a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs index dac2f0d..d1f53cf 100644 --- a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs +++ b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs @@ -1,22 +1,21 @@ -using System; +using System; -namespace RabbitMQCoreClient.Exceptions +namespace RabbitMQCoreClient.Exceptions; + +public class QueueBindException : Exception { - public class QueueBindException : Exception + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public QueueBindException(string message) : base(message) { - /// Initializes a new instance of the class with a specified error message. - /// The message that describes the error. - public QueueBindException(string message) : base(message) - { - } + } - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public QueueBindException(string message, Exception innerException) : base(message, innerException) - { + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public QueueBindException(string message, Exception innerException) : base(message, innerException) + { - } } } diff --git a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs index 08eb6f2..7e75729 100644 --- a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs @@ -1,23 +1,21 @@ -using System; +using System; -namespace RabbitMQCoreClient.Exceptions +namespace RabbitMQCoreClient.Exceptions; + +public class ReconnectAttemptsExceededException : Exception { - [Serializable] - public class ReconnectAttemptsExceededException : Exception + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public ReconnectAttemptsExceededException(string message) : base(message) { - /// Initializes a new instance of the class with a specified error message. - /// The message that describes the error. - public ReconnectAttemptsExceededException(string message) : base(message) - { - } + } - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public ReconnectAttemptsExceededException(string message, Exception innerException) : base(message, innerException) - { + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public ReconnectAttemptsExceededException(string message, Exception innerException) : base(message, innerException) + { - } } } diff --git a/src/RabbitMQCoreClient/Extentions/NewtonSoftJsonBuilderExtentions.cs b/src/RabbitMQCoreClient/Extentions/NewtonSoftJsonBuilderExtentions.cs deleted file mode 100644 index 1eda371..0000000 --- a/src/RabbitMQCoreClient/Extentions/NewtonSoftJsonBuilderExtentions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Newtonsoft.Json; -using RabbitMQCoreClient.Serializers; -using System; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class NewtonSoftJsonBuilderExtentions - { - /// - /// Use NewtonsoftJson serializer as default serializer for the RabbitMQ messages. - /// - public static IRabbitMQCoreClientBuilder AddNewtonsoftJson(this IRabbitMQCoreClientBuilder builder, Action? setupAction = null) - { - builder.Serializer = new NewtonsoftJsonMessageSerializer(setupAction); - return builder; - } - - /// - /// Use NewtonsoftJson serializer as default serializer for the RabbitMQ messages. - /// - public static IRabbitMQCoreClientConsumerBuilder AddNewtonsoftJson(this IRabbitMQCoreClientConsumerBuilder builder, Action? setupAction = null) - { - builder.Builder.AddNewtonsoftJson(setupAction); - return builder; - } - } -} diff --git a/src/RabbitMQCoreClient/Extentions/NewtonSoftJsonQueueServiceExtentions.cs b/src/RabbitMQCoreClient/Extentions/NewtonSoftJsonQueueServiceExtentions.cs deleted file mode 100644 index c6bb6a6..0000000 --- a/src/RabbitMQCoreClient/Extentions/NewtonSoftJsonQueueServiceExtentions.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -namespace RabbitMQCoreClient -{ - public static class NewtonSoftJsonQueueServiceExtentions - { - [NotNull] - public static JsonSerializerSettings DefaultSerializerSettings => - new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; - - /// - /// Send the message to the queue (thread safe method). will be serialized to Json. - /// - /// The class type of the message. - /// The service object. - /// An instance of the class that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// The json serializer settings. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - public static ValueTask SendAsync( - this IQueueService queueService, - T obj, - string routingKey, - JsonSerializerSettings? jsonSerializerSettings, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default - ) - { - // Checking for Null without boxing. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) - throw new ArgumentNullException(nameof(obj)); - - var serializedObj = JsonConvert.SerializeObject(obj, jsonSerializerSettings ?? DefaultSerializerSettings); - return queueService.SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - /// Send messages pack to the queue (thred safe method). will be serialized to Json. - /// - /// The class type of the message. - /// The service object. - /// A list of objects that are instances of the class - /// that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// The json serializer settings. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - public static ValueTask SendBatchAsync( - this IQueueService queueService, - IEnumerable objs, - string routingKey, - JsonSerializerSettings? jsonSerializerSettings, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - var messages = new List(); - var serializeSettings = jsonSerializerSettings ?? DefaultSerializerSettings; - foreach (var obj in objs) - { - messages.Add(JsonConvert.SerializeObject(obj, serializeSettings)); - } - - return queueService.SendJsonBatchAsync( - serializedJsonList: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - } -} diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs index 90142d5..490b2bb 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs @@ -1,27 +1,26 @@ -using RabbitMQCoreClient.Serializers; +using RabbitMQCoreClient.Serializers; using System; using System.Text.Json; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class SystemTextJsonBuilderExtentions { - public static class SystemTextJsonBuilderExtentions + /// + /// Use System.Text.Json serializer as default serializer for the RabbitMQ messages. + /// + public static IRabbitMQCoreClientBuilder AddSystemTextJson(this IRabbitMQCoreClientBuilder builder, Action? setupAction = null) { - /// - /// Use System.Text.Json serializer as default serializer for the RabbitMQ messages. - /// - public static IRabbitMQCoreClientBuilder AddSystemTextJson(this IRabbitMQCoreClientBuilder builder, Action? setupAction = null) - { - builder.Serializer = new SystemTextJsonMessageSerializer(setupAction); - return builder; - } + builder.Serializer = new SystemTextJsonMessageSerializer(setupAction); + return builder; + } - /// - /// Use System.Text.Json serializer as default serializer for the RabbitMQ messages. - /// - public static IRabbitMQCoreClientConsumerBuilder AddSystemTextJson(this IRabbitMQCoreClientConsumerBuilder builder, Action? setupAction = null) - { - builder.Builder.AddSystemTextJson(setupAction); - return builder; - } + /// + /// Use System.Text.Json serializer as default serializer for the RabbitMQ messages. + /// + public static IRabbitMQCoreClientConsumerBuilder AddSystemTextJson(this IRabbitMQCoreClientConsumerBuilder builder, Action? setupAction = null) + { + builder.Builder.AddSystemTextJson(setupAction); + return builder; } } diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs index c44951c..138a098 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs @@ -1,182 +1,162 @@ -using RabbitMQCoreClient.Serializers; +using RabbitMQCoreClient.Serializers; using System; using System.Collections.Generic; -using System.IO; using System.Text.Json; -#if NET6_0_OR_GREATER using System.Text.Json.Serialization.Metadata; -#endif using System.Threading.Tasks; -namespace RabbitMQCoreClient +namespace RabbitMQCoreClient; + +public static class SystemTextJsonQueueServiceExtentions { - public static class SystemTextJsonQueueServiceExtentions + /// + /// Send the message to the queue (thread safe method). will be serialized to Json. + /// + /// The class type of the message. + /// The service object. + /// An instance of the class that will be serialized to JSON and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// if set to true [decrease TTL]. + /// The json serializer settings. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + public static ValueTask SendAsync( + this IQueueService queueService, + T obj, + string routingKey, + JsonSerializerOptions? jsonSerializerSettings, + string? exchange = default, + bool decreaseTtl = true + ) { - /// - /// Send the message to the queue (thread safe method). will be serialized to Json. - /// - /// The class type of the message. - /// The service object. - /// An instance of the class that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// The json serializer settings. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - public static ValueTask SendAsync( - this IQueueService queueService, - T obj, - string routingKey, - JsonSerializerOptions? jsonSerializerSettings, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default - ) - { - // Checking for Null without boxing. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) - throw new ArgumentNullException(nameof(obj)); + // Checking for Null without boxing. // https://stackoverflow.com/a/864860 + if (EqualityComparer.Default.Equals(obj, default)) + throw new ArgumentNullException(nameof(obj)); - var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions); - return queueService.SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } + var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions); + return queueService.SendJsonAsync( + serializedObj, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } -#if NET6_0_OR_GREATER - /// - /// Send the message to the queue (thread safe method). will be serialized to Json with source generator. - /// - /// The class type of the message. - /// The service object. - /// An instance of the class that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// Metadata about the type to convert. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - public static ValueTask SendAsync( - this IQueueService queueService, - T obj, - string routingKey, - JsonTypeInfo jsonTypeInfo, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default - ) - { - // Checking for Null without boxing. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) - throw new ArgumentNullException(nameof(obj)); + /// + /// Send the message to the queue (thread safe method). will be serialized to Json with source generator. + /// + /// The class type of the message. + /// The service object. + /// An instance of the class that will be serialized to JSON and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// if set to true [decrease TTL]. + /// Metadata about the type to convert. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + public static ValueTask SendAsync( + this IQueueService queueService, + T obj, + string routingKey, + JsonTypeInfo jsonTypeInfo, + string? exchange = default, + bool decreaseTtl = true + ) + { + // Checking for Null without boxing. // https://stackoverflow.com/a/864860 + if (EqualityComparer.Default.Equals(obj, default)) + throw new ArgumentNullException(nameof(obj)); - var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo); - return queueService.SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } -#endif + var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo); + return queueService.SendJsonAsync( + serializedObj, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } - /// - /// Send pack of messages to the queue (thread safe method). will be serialized to Json. - /// - /// The class type of the message. - /// The service object. - /// A list of objects that are instances of the class - /// that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// The json serializer settings. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - public static ValueTask SendBatchAsync( - this IQueueService queueService, - IEnumerable objs, - string routingKey, - JsonSerializerOptions? jsonSerializerSettings, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) + /// + /// Send pack of messages to the queue (thread safe method). will be serialized to Json. + /// + /// The class type of the message. + /// The service object. + /// A list of objects that are instances of the class + /// that will be serialized to JSON and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If set to true [decrease TTL]. + /// The json serializer settings. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + public static ValueTask SendBatchAsync( + this IQueueService queueService, + IEnumerable objs, + string routingKey, + JsonSerializerOptions? jsonSerializerSettings, + string? exchange = default, + bool decreaseTtl = true) + { + var messages = new List>(); + var serializeSettings = jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions; + foreach (var obj in objs) { - var messages = new List>(); - var serializeSettings = jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions; - foreach (var obj in objs) - { - messages.Add(JsonSerializer.SerializeToUtf8Bytes(obj, serializeSettings)); - } - - return queueService.SendJsonBatchAsync( - serializedJsonList: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); + messages.Add(JsonSerializer.SerializeToUtf8Bytes(obj, serializeSettings)); } -#if NET6_0_OR_GREATER - /// - /// Send pack of messages to the queue (thread safe method). will be serialized to Json with source generator. - /// - /// The class type of the message. - /// The service object. - /// A list of objects that are instances of the class - /// that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// Metadata about the type to convert. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - public static ValueTask SendBatchAsync( - this IQueueService queueService, - IEnumerable objs, - string routingKey, - JsonTypeInfo jsonTypeInfo, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - var messages = new List>(); - foreach (var obj in objs) - { - messages.Add(JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo)); - } + return queueService.SendJsonBatchAsync( + serializedJsonList: messages, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } - return queueService.SendJsonBatchAsync( - serializedJsonList: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); + /// + /// Send pack of messages to the queue (thread safe method). will be serialized to Json with source generator. + /// + /// The class type of the message. + /// The service object. + /// A list of objects that are instances of the class + /// that will be serialized to JSON and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If set to true [decrease TTL]. + /// Metadata about the type to convert. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + public static ValueTask SendBatchAsync( + this IQueueService queueService, + IEnumerable objs, + string routingKey, + JsonTypeInfo jsonTypeInfo, + string? exchange = default, + bool decreaseTtl = true) + { + var messages = new List>(); + foreach (var obj in objs) + { + messages.Add(JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo)); } -#endif + + return queueService.SendJsonBatchAsync( + serializedJsonList: messages, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); } } diff --git a/src/RabbitMQCoreClient/IMessageHandler.cs b/src/RabbitMQCoreClient/IMessageHandler.cs index 5c4a6cb..1502970 100644 --- a/src/RabbitMQCoreClient/IMessageHandler.cs +++ b/src/RabbitMQCoreClient/IMessageHandler.cs @@ -1,36 +1,35 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; using System; using System.Threading.Tasks; -namespace RabbitMQCoreClient +namespace RabbitMQCoreClient; + +/// +/// The interface for the handler received from the message queue. +/// +public interface IMessageHandler { /// - /// The interface for the handler received from the message queue. + /// Process the message asynchronously. /// - public interface IMessageHandler - { - /// - /// Process the message asynchronously. - /// - /// Input byte array from condumed by RabbitMQ queue. - /// The instance containing the message data. - Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args); + /// Input byte array from consumed queue. + /// The instance containing the message data. + Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args); - /// - /// Instructions to the router in case of an exception while processing a message. - /// - ErrorMessageRouting ErrorMessageRouter { get; } + /// + /// Instructions to the router in case of an exception while processing a message. + /// + ErrorMessageRouting ErrorMessageRouter { get; } - /// - /// Consumer handler options, that was used during configuration. - /// - ConsumerHandlerOptions? Options { get; set; } + /// + /// Consumer handler options, that was used during configuration. + /// + ConsumerHandlerOptions? Options { get; set; } - /// - /// The default json serializer. - /// - IMessageSerializer Serializer { get; set; } - } -} \ No newline at end of file + /// + /// The default json serializer. + /// + IMessageSerializer Serializer { get; set; } +} diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index 8f70c92..113471f 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -1,220 +1,203 @@ -using RabbitMQ.Client; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.Events; using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace RabbitMQCoreClient +namespace RabbitMQCoreClient; + +/// +/// The interface describes the basic set of methods required to implement a RabbitMQ message queue handler. +/// +public interface IQueueService : IAsyncDisposable { /// - /// The interface describes the basic set of methods required to implement a RabbitMQ message queue handler. + /// RabbitMQ connection interface. /// - public interface IQueueService : IDisposable - { - /// - /// RabbitMQ connection interface. - /// - IConnection Connection { get; } + IConnection Connection { get; } - /// - /// A channel for sending RabbitMQ data. - /// - IModel SendChannel { get; } + /// + /// A channel for sending RabbitMQ data. + /// + IChannel SendChannel { get; } - /// - /// MQ service settings. - /// - RabbitMQCoreClientOptions Options { get; } + /// + /// MQ service settings. + /// + RabbitMQCoreClientOptions Options { get; } - /// - /// Occurs when connection restored after reconnect. - /// - event Action OnReconnected; + /// + /// Occurs when connection restored after reconnect. + /// + event AsyncEventHandler ReconnectedAsync; - /// - /// Occurs when connection is shuted down on any reason. - /// - event Action OnConnectionShutdown; + /// + /// Occurs when connection is interrupted for some reason. + /// + event AsyncEventHandler ConnectionShutdownAsync; - /// - /// Send a message to the queue (thread safe method). - /// - /// The json. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonAsync( - string json, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send a message to the queue (thread safe method). + /// + /// The json. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendJsonAsync( + string json, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send a message to the queue (thread safe method). - /// - /// The json converted to UTF-8 bytes array. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonAsync( - ReadOnlyMemory jsonBytes, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send a message to the queue (thread safe method). + /// + /// The json converted to UTF-8 bytes array. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendJsonAsync( + ReadOnlyMemory jsonBytes, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send the message to the queue (thread safe method). will be serialized to Json. - /// - /// The class type of the message. - /// An instance of the class that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - ValueTask SendAsync( - T obj, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default - ); + /// + /// Send the message to the queue (thread safe method). will be serialized to Json. + /// + /// The class type of the message. + /// An instance of the class that will be serialized to JSON and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If set to true [decrease TTL]. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + ValueTask SendAsync( + T obj, + string routingKey, + string? exchange = default, + bool decreaseTtl = true + ); - /// - /// Send a raw message to the queue with the specified properties (thread safe). - /// - /// An array of bytes to be sent to the queue as the body of the message. - /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendAsync( - byte[] obj, - IBasicProperties props, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send a raw message to the queue with the specified properties (thread safe). + /// + /// An array of bytes to be sent to the queue as the body of the message. + /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendAsync( + byte[] obj, + BasicProperties props, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send a raw message to the queue with the specified properties (thread safe). - /// - /// An array of bytes to be sent to the queue as the body of the message. - /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendAsync( - ReadOnlyMemory obj, - IBasicProperties props, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send a raw message to the queue with the specified properties (thread safe). + /// + /// An array of bytes to be sent to the queue as the body of the message. + /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendAsync( + ReadOnlyMemory obj, + BasicProperties props, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send messages pack to the queue (thred safe method). will be serialized to Json. - /// - /// The class type of the message. - /// A list of objects that are instances of the class - /// that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - ValueTask SendBatchAsync( - IEnumerable objs, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send messages pack to the queue (thread safe method). will be serialized to Json. + /// + /// The class type of the message. + /// A list of objects that are instances of the class + /// that will be serialized to JSON and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// if set to true [decrease TTL]. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + ValueTask SendBatchAsync( + IEnumerable objs, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send a batch raw message to the queue with the specified properties (thread safe). - /// - /// List of objects and settings that will be sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendBatchAsync( - IEnumerable<(ReadOnlyMemory Body, IBasicProperties Props)> objs, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send a batch raw message to the queue with the specified properties (thread safe). + /// + /// List of objects and settings that will be sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendBatchAsync( + IEnumerable<(ReadOnlyMemory Body, BasicProperties Props)> objs, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send batch messages to the queue (thread safe method). - /// - /// A list of serialized json to be sent to the queue in batch. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonBatchAsync( - IEnumerable serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); + /// + /// Send batch messages to the queue (thread safe method). + /// + /// A list of serialized json to be sent to the queue in batch. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendJsonBatchAsync( + IEnumerable serializedJsonList, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); - /// - /// Send batch messages to the queue (thread safe method). - /// - /// A list of serialized json to be sent to the queue in batch. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// Correlation Id, which is used to log messages. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonBatchAsync( - IEnumerable> serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default); - } -} \ No newline at end of file + /// + /// Send batch messages to the queue (thread safe method). + /// + /// A list of serialized json to be sent to the queue in batch. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// If true then decrease TTL. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendJsonBatchAsync( + IEnumerable> serializedJsonList, + string routingKey, + string? exchange = default, + bool decreaseTtl = true); +} diff --git a/src/RabbitMQCoreClient/MessageHandlerJson.cs b/src/RabbitMQCoreClient/MessageHandlerJson.cs index bd6aab2..88442a9 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -1,76 +1,75 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; using System; using System.Text; using System.Threading.Tasks; -namespace RabbitMQCoreClient +namespace RabbitMQCoreClient; + +/// +/// Handler for the message received from the queue. +/// +/// The type of model that will be deserialized into. +/// +public abstract class MessageHandlerJson : IMessageHandler { /// - /// Handler for the message received from the queue. + /// Incoming message routing methods. /// - /// The type of model that will be deserialized into. - /// - public abstract class MessageHandlerJson : IMessageHandler - { - /// - /// Incoming message routing methods. - /// - public ErrorMessageRouting ErrorMessageRouter { get; } = new ErrorMessageRouting(); + public ErrorMessageRouting ErrorMessageRouter { get; } = new ErrorMessageRouting(); - /// - /// The method will be called when there is an error parsing Json into the model. - /// - /// The json. - /// The exception. - /// The instance containing the event data. - /// - protected virtual ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) => default; + /// + /// The method will be called when there is an error parsing Json into the model. + /// + /// The json. + /// The exception. + /// The instance containing the event data. + /// + protected virtual ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) => default; - /// - /// Process json message. - /// - /// The message deserialized into an object. - /// The instance containing the event data. - /// - protected abstract Task HandleMessage(TModel message, RabbitMessageEventArgs args); + /// + /// Process json message. + /// + /// The message deserialized into an object. + /// The instance containing the event data. + /// + protected abstract Task HandleMessage(TModel message, RabbitMessageEventArgs args); - /// - /// Raw Json formatted message. - /// - protected string? RawJson { get; set; } + /// + /// Raw Json formatted message. + /// + protected string? RawJson { get; set; } - /// - /// Gets the options. - /// - public ConsumerHandlerOptions? Options { get; set; } + /// + /// Gets the options. + /// + public ConsumerHandlerOptions? Options { get; set; } - /// - /// The default json serializer. - /// - public IMessageSerializer Serializer { get; set; } = new SystemTextJsonMessageSerializer(); + /// + /// The default json serializer. + /// + public IMessageSerializer Serializer { get; set; } = new SystemTextJsonMessageSerializer(); - /// - public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + /// + public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + { + RawJson = Encoding.UTF8.GetString(message.ToArray()); + TModel messageModel; + try { - RawJson = Encoding.UTF8.GetString(message.ToArray()); - TModel messageModel; - try - { - var obj = Serializer.Deserialize(message); - if (obj is null) - throw new InvalidOperationException("The json parser returns null."); - messageModel = obj; - } - catch (Exception e) - { - await OnParseError(RawJson, e, args); - // Fall to the top-level exception handler. - throw; - } - - await HandleMessage(messageModel, args); + var obj = Serializer.Deserialize(message); + if (obj is null) + throw new InvalidOperationException("The json parser returns null."); + messageModel = obj; + } + catch (Exception e) + { + await OnParseError(RawJson, e, args); + // Fall to the top-level exception handler. + throw; } + + await HandleMessage(messageModel, args); } } diff --git a/src/RabbitMQCoreClient/Models/Exchange.cs b/src/RabbitMQCoreClient/Models/Exchange.cs index 8648c93..f902670 100644 --- a/src/RabbitMQCoreClient/Models/Exchange.cs +++ b/src/RabbitMQCoreClient/Models/Exchange.cs @@ -1,50 +1,50 @@ -using RabbitMQ.Client; +using RabbitMQ.Client; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using System; +using System.Threading.Tasks; -namespace RabbitMQCoreClient.Configuration.DependencyInjection +namespace RabbitMQCoreClient.Configuration.DependencyInjection; + +/// +/// The RabbitMQ Exchange +/// +public class Exchange { /// - /// The RabbitMQ Exchange + /// Exchange point name. /// - public class Exchange - { - /// - /// Exchange point name. - /// - public string Name => Options.Name; - - /// - /// Exchange point configuration settings. - /// - public ExchangeOptions Options { get; } = new ExchangeOptions(); + public string Name => Options.Name; - /// - /// Initializes a new instance of the class. - /// - /// The options. - /// options - /// exchangeName - /// or - /// services - public Exchange(ExchangeOptions options) - { - Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); + /// + /// Exchange point configuration settings. + /// + public ExchangeOptions Options { get; } = new ExchangeOptions(); - if (string.IsNullOrEmpty(options.Name)) - throw new ArgumentException($"{nameof(options.Name)} is null or empty.", nameof(options.Name)); - } + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// options + /// exchangeName + /// or + /// services + public Exchange(ExchangeOptions options) + { + Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); - /// - /// Starts the exchange. - /// - /// The channel. - public void StartExchange(IModel _channel) => _channel.ExchangeDeclare( - exchange: Name, - type: Options.Type, - durable: Options.Durable, - autoDelete: Options.AutoDelete, - arguments: Options.Arguments - ); + if (string.IsNullOrEmpty(options.Name)) + throw new ArgumentException($"{nameof(options.Name)} is null or empty.", nameof(options.Name)); } + + /// + /// Starts the exchange. + /// + /// The channel. + public Task StartExchange(IChannel _channel) => _channel.ExchangeDeclareAsync( + exchange: Name, + type: Options.Type, + durable: Options.Durable, + autoDelete: Options.AutoDelete, + arguments: Options.Arguments + ); } diff --git a/src/RabbitMQCoreClient/Models/Queue.cs b/src/RabbitMQCoreClient/Models/Queue.cs index 373ec6b..58d2db8 100644 --- a/src/RabbitMQCoreClient/Models/Queue.cs +++ b/src/RabbitMQCoreClient/Models/Queue.cs @@ -1,35 +1,37 @@ -using RabbitMQCoreClient.DependencyInjection.ConfigModels; +using RabbitMQCoreClient.DependencyInjection.ConfigModels; using System; using System.Collections.Generic; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + +/// +/// Simple custom message queue. +/// +public sealed class Queue : QueueBase { - /// - /// Simple custom message queue. - /// - public sealed class Queue : QueueBase + public Queue(string name, bool durable = true, bool exclusive = false, bool autoDelete = false, bool useQuorum = false) + : base(name, durable, exclusive, autoDelete, useQuorum) { - public Queue(string name, bool durable = true, bool exclusive = false, bool autoDelete = false, bool useQuorum = false) - : base(name, durable, exclusive, autoDelete, useQuorum) - { - if (string.IsNullOrEmpty(name)) - throw new ArgumentException($"{nameof(name)} is null or empty.", nameof(name)); - } + if (string.IsNullOrEmpty(name)) + throw new ArgumentException($"{nameof(name)} is null or empty.", nameof(name)); + } - public static Queue Create(QueueConfig queueConfig) + /// + /// Create new queue from configuration. + /// + /// Queue model from IConfiguration. + /// + public static Queue Create(QueueConfig queueConfig) => + new Queue(name: queueConfig.Name, + durable: queueConfig.Durable, + exclusive: queueConfig.Exclusive, + autoDelete: queueConfig.AutoDelete, + useQuorum: queueConfig.UseQuorum) { - return new Queue(name: queueConfig.Name, - durable: queueConfig.Durable, - exclusive: queueConfig.Exclusive, - autoDelete: queueConfig.AutoDelete, - useQuorum: queueConfig.UseQuorum) - { - Arguments = queueConfig.Arguments ?? new Dictionary(), - DeadLetterExchange = queueConfig.DeadLetterExchange, - UseQuorum = queueConfig.UseQuorum, - Exchanges = queueConfig.Exchanges ?? new HashSet(), - RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() - }; - } - } + Arguments = queueConfig.Arguments ?? new Dictionary(), + DeadLetterExchange = queueConfig.DeadLetterExchange, + UseQuorum = queueConfig.UseQuorum, + Exchanges = queueConfig.Exchanges ?? new HashSet(), + RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() + }; } diff --git a/src/RabbitMQCoreClient/Models/QueueBase.cs b/src/RabbitMQCoreClient/Models/QueueBase.cs index 882993a..76c8e5b 100644 --- a/src/RabbitMQCoreClient/Models/QueueBase.cs +++ b/src/RabbitMQCoreClient/Models/QueueBase.cs @@ -1,117 +1,117 @@ -using RabbitMQ.Client; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQCoreClient.Exceptions; using System; using System.Collections.Generic; +using System.Threading.Tasks; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + +/// +/// Options to be applied to the message queue. +/// +public abstract class QueueBase { + protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, bool useQuorum) + { + Name = name; + Durable = durable; + Exclusive = exclusive; + AutoDelete = autoDelete; + UseQuorum = useQuorum; + } + + /// + /// The queue Name. If null, then the name will be automatically chosen. + /// + public virtual string? Name { get; protected set; } + + /// + /// If true, the queue will be saved on disc. + /// + public virtual bool Durable { get; protected set; } + + /// + /// If true, then the queue will be used by single service and will be deleted after client will disconnect. + /// + public virtual bool Exclusive { get; protected set; } + + /// + /// If true, the queue will be automatically deleted on client disconnect. + /// + public virtual bool AutoDelete { get; protected set; } + + /// + /// The name of the exchange point that will receive messages for which a reject or nack was received. + /// + public virtual string? DeadLetterExchange { get; set; } + + /// + /// While creating the queue use parameter "x-queue-type": "quorum". + /// + public virtual bool UseQuorum { get; set; } = false; + /// - /// Options to be applied to the message queue. + /// List of additional parameters that will be used when initializing the queue. /// - public abstract class QueueBase + public virtual IDictionary Arguments { get; set; } = new Dictionary(); + + /// + /// ist of routing keys for the queue. + /// + public virtual HashSet RoutingKeys { get; set; } = new HashSet(); + + /// + /// The list of exchange points to which the queue is bound. + /// + public virtual HashSet Exchanges { get; set; } = new HashSet(); + + /// + /// Declare the queue on and start consuming messages. + /// + /// + /// + public virtual async Task StartQueue(IChannel channel, AsyncEventingBasicConsumer consumer) + { + if (!string.IsNullOrWhiteSpace(DeadLetterExchange) + && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.DeadLetterExchangeHeader)) + Arguments.Add(AppConstants.RabbitMQHeaders.DeadLetterExchangeHeader, DeadLetterExchange); + + if (UseQuorum && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.QueueTypeHeader)) + Arguments.Add(AppConstants.RabbitMQHeaders.QueueTypeHeader, "quorum"); + + if (UseQuorum && AutoDelete && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.QueueExpiresHeader)) + Arguments.Add(AppConstants.RabbitMQHeaders.QueueExpiresHeader, 10000); + + var declaredQueue = await channel.QueueDeclareAsync(queue: Name ?? string.Empty, + durable: UseQuorum || Durable, + exclusive: !UseQuorum && Exclusive, + autoDelete: !UseQuorum && AutoDelete, + arguments: Arguments); + + if (declaredQueue is null) + throw new QueueBindException("Queue is not properly bind."); + + if (RoutingKeys.Count > 0) + foreach (var exchangeName in Exchanges) + { + await BindToExchange(channel, declaredQueue, exchangeName); + } + + await channel.BasicConsumeAsync(queue: declaredQueue.QueueName, + autoAck: false, + consumer: consumer, + consumerTag: $"amq.{declaredQueue.QueueName}.{Guid.NewGuid()}" + ); + } + + async Task BindToExchange(IChannel channel, QueueDeclareOk declaredQueue, string exchangeName) { - protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, bool useQuorum) - { - Name = name; - Durable = durable; - Exclusive = exclusive; - AutoDelete = autoDelete; - UseQuorum = useQuorum; - } - - /// - /// The queue Name. If null, then the name will be automaticly choosen. - /// - public virtual string? Name { get; protected set; } - - /// - /// If true, the queue will be saved on disc. - /// - public virtual bool Durable { get; protected set; } - - /// - /// If true, then the queue will be used by single service and will be deleted after client will disconnect. - /// - public virtual bool Exclusive { get; protected set; } - - /// - /// If true, the queue will be automaticly deleted on client disconnect. - /// - public virtual bool AutoDelete { get; protected set; } - - /// - /// The name of the exchange point that will receive messages for which a reject or nack was received. - /// - public virtual string? DeadLetterExchange { get; set; } - - /// - /// While creating the queue use parameter "x-queue-type": "quorum". - /// - public virtual bool UseQuorum { get; set; } = false; - - /// - /// List of additional parameters that will be used when initializing the queue. - /// - public virtual IDictionary Arguments { get; set; } = new Dictionary(); - - /// - /// ist of routing keys for the queue. - /// - public virtual HashSet RoutingKeys { get; set; } = new HashSet(); - - /// - /// The list of exchange points to which the queue is bound. - /// - public virtual HashSet Exchanges { get; set; } = new HashSet(); - - /// - /// Declare the queue on and start consuming messages. - /// - /// - /// - public virtual void StartQueue(IModel channel, AsyncEventingBasicConsumer consumer) - { - if (!string.IsNullOrWhiteSpace(DeadLetterExchange) - && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.DeadLetterExchangeHeader)) - Arguments.Add(AppConstants.RabbitMQHeaders.DeadLetterExchangeHeader, DeadLetterExchange); - - if (UseQuorum && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.QueueTypeHeader)) - Arguments.Add(AppConstants.RabbitMQHeaders.QueueTypeHeader, "quorum"); - - if (UseQuorum && AutoDelete && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.QueueExpiresHeader)) - Arguments.Add(AppConstants.RabbitMQHeaders.QueueExpiresHeader, 10000); - - var declaredQueue = channel.QueueDeclare(queue: Name ?? string.Empty, - durable: UseQuorum || Durable, - exclusive: !UseQuorum && Exclusive, - autoDelete: !UseQuorum && AutoDelete, - arguments: Arguments); - - if (declaredQueue is null) - throw new QueueBindException("Queue is not properly binded."); - - if (RoutingKeys.Count > 0) - foreach (var exchangeName in Exchanges) - { - BindToExchange(channel, declaredQueue, exchangeName); - } - - channel.BasicConsume(queue: declaredQueue.QueueName, - autoAck: false, - consumer: consumer, - consumerTag: $"amq.{declaredQueue.QueueName}.{Guid.NewGuid()}" - ); - } - - void BindToExchange(IModel channel, QueueDeclareOk declaredQueue, string exchangeName) - { - foreach (var route in RoutingKeys) - channel.QueueBind( - queue: declaredQueue.QueueName, - exchange: exchangeName, - routingKey: route - ); - } + foreach (var route in RoutingKeys) + await channel.QueueBindAsync( + queue: declaredQueue.QueueName, + exchange: exchangeName, + routingKey: route + ); } } diff --git a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs index 9d0199f..ca37de5 100644 --- a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs +++ b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs @@ -1,27 +1,29 @@ -using System; +using System; -namespace RabbitMQCoreClient.Models -{ - public class RabbitMessageEventArgs : EventArgs - { - /// - /// The routing key used when the message was originally published. - /// - public string RoutingKey { get; private set; } +namespace RabbitMQCoreClient.Models; - /// - /// Correlation Id, which is forwarded along with the message and can be used to identify log chains. - /// - public string? CorrelationId { get; set; } +public class RabbitMessageEventArgs : EventArgs +{ + /// + /// The routing key used when the message was originally published. + /// + public string RoutingKey { get; private set; } - /// - /// The consumer tag, which receive the message. Typically is generated by the server, but can be set in the queue declaration. - /// - public string ConsumerTag { get; set; } + /// + /// The consumer tag, which receive the message. + /// Typically is generated by the server, but can be set in the queue declaration. + /// + public string ConsumerTag { get; set; } - public RabbitMessageEventArgs(string routingKey) - { - RoutingKey = routingKey; - } + /// + /// Create new object of . + /// + /// The routing key used when the message was originally published. + /// The consumer tag, which receive the message. + /// Typically is generated by the server, but can be set in the queue declaration. + public RabbitMessageEventArgs(string routingKey, string consumerTag) + { + RoutingKey = routingKey; + ConsumerTag = consumerTag; } } diff --git a/src/RabbitMQCoreClient/Models/Routes.cs b/src/RabbitMQCoreClient/Models/Routes.cs index 08676ca..a15dc8c 100644 --- a/src/RabbitMQCoreClient/Models/Routes.cs +++ b/src/RabbitMQCoreClient/Models/Routes.cs @@ -1,14 +1,13 @@ -namespace RabbitMQCoreClient.Models +namespace RabbitMQCoreClient.Models; + +public enum Routes { - public enum Routes - { - /// - /// The dead letter queue. - /// - DeadLetter, - /// - /// The source queue. - /// - SourceQueue - } + /// + /// The dead letter queue. + /// + DeadLetter, + /// + /// The source queue. + /// + SourceQueue } diff --git a/src/RabbitMQCoreClient/Models/Subscription.cs b/src/RabbitMQCoreClient/Models/Subscription.cs index abae1ae..cb697b8 100644 --- a/src/RabbitMQCoreClient/Models/Subscription.cs +++ b/src/RabbitMQCoreClient/Models/Subscription.cs @@ -1,29 +1,28 @@ -using RabbitMQCoreClient.DependencyInjection.ConfigModels; +using RabbitMQCoreClient.DependencyInjection.ConfigModels; using System; using System.Collections.Generic; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; + +/// +/// Message queue for subscribing to events. +/// The queue is automatically named. When the client disconnects from the server, the queue is automatically deleted. +/// +public sealed class Subscription : QueueBase { - /// - /// Message queue for subscribing to events. - /// The queue is automatically named. When the client disconnects from the server, the queue is automatically deleted. - /// - public sealed class Subscription : QueueBase - { - public Subscription(bool useQuorum = false) - : base($"sub_{Guid.NewGuid().ToString()}", false, true, true, useQuorum) - { } + public Subscription(bool useQuorum = false) + : base($"sub_{Guid.NewGuid().ToString()}", false, true, true, useQuorum) + { } - public static Subscription Create(SubscriptionConfig queueConfig) + public static Subscription Create(SubscriptionConfig queueConfig) + { + return new Subscription { - return new Subscription - { - Arguments = queueConfig.Arguments ?? new Dictionary(), - DeadLetterExchange = queueConfig.DeadLetterExchange, - UseQuorum = queueConfig.UseQuorum, - Exchanges = queueConfig.Exchanges ?? new HashSet(), - RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() - }; - } + Arguments = queueConfig.Arguments ?? new Dictionary(), + DeadLetterExchange = queueConfig.DeadLetterExchange, + UseQuorum = queueConfig.UseQuorum, + Exchanges = queueConfig.Exchanges ?? new HashSet(), + RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() + }; } } diff --git a/src/RabbitMQCoreClient/Models/TtlActions.cs b/src/RabbitMQCoreClient/Models/TtlActions.cs index 4ef749f..0932601 100644 --- a/src/RabbitMQCoreClient/Models/TtlActions.cs +++ b/src/RabbitMQCoreClient/Models/TtlActions.cs @@ -1,17 +1,16 @@ -namespace RabbitMQCoreClient.Models +namespace RabbitMQCoreClient.Models; + +/// +/// Actions performed with ttl. +/// +public enum TtlActions { /// - /// Actions performed with ttl. + /// Reduce ttl messages. /// - public enum TtlActions - { - /// - /// Reduce ttl messages. - /// - Decrease, - /// - /// Don't change ttl messages. - /// - DoNotChange - } -} \ No newline at end of file + Decrease, + /// + /// Don't change ttl messages. + /// + DoNotChange +} diff --git a/src/RabbitMQCoreClient/QueueService.cs b/src/RabbitMQCoreClient/QueueService.cs new file mode 100644 index 0000000..f8ad522 --- /dev/null +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -0,0 +1,601 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQCoreClient.Configuration.DependencyInjection; +using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.Events; +using RabbitMQCoreClient.Exceptions; +using RabbitMQCoreClient.Serializers; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static RabbitMQCoreClient.Configuration.AppConstants.RabbitMQHeaders; + +namespace RabbitMQCoreClient; + +/// +/// Implementations of the . +/// +/// +public sealed class QueueService : IQueueService +{ + readonly ILogger _log; + readonly IList _exchanges; + readonly IMessageSerializer _serializer; + bool _connectionBlocked = false; + IConnection? _connection; + IChannel? _publishChannel; + + /// + public RabbitMQCoreClientOptions Options { get; } + + /// + public IConnection Connection => _connection!; // So far, that's it. The property is completely initialized in the constructor. + + /// + public IChannel SendChannel => _publishChannel!; // So far, that's it. The property is completely initialized in the constructor. + + /// + public event AsyncEventHandler ReconnectedAsync = default!; + + /// + public event AsyncEventHandler ConnectionShutdownAsync = default!; + + string? GetDefaultExchange() => _exchanges.FirstOrDefault(x => x.Options.IsDefault)?.Name; + + long _reconnectAttemptsCount; + + /// + /// Initializes a new instance of the class . + /// + /// The options. + /// The logger factory. + /// The builder. + /// options + public QueueService(RabbitMQCoreClientOptions options, ILoggerFactory loggerFactory, IRabbitMQCoreClientBuilder builder) + { + Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); + _log = loggerFactory.CreateLogger(); + _exchanges = builder.Exchanges; + _serializer = builder.Serializer; + } + + /// + /// Connects this instance to RabbitMQ. + /// + public async Task Connect() + { + // Protection against repeated calls to the `Connect()` method. + if (_connection?.IsOpen == true) + { + _log.LogWarning("Connection already open."); + return; + } + + _log.LogInformation("Connecting to RabbitMQ endpoint {HostName}.", Options.HostName); + + var factory = new ConnectionFactory + { + HostName = Options.HostName, + UserName = Options.UserName, + Password = Options.Password, + RequestedHeartbeat = TimeSpan.FromSeconds(Options.RequestedHeartbeat), + RequestedConnectionTimeout = TimeSpan.FromMilliseconds(Options.RequestedConnectionTimeout), + AutomaticRecoveryEnabled = false, + TopologyRecoveryEnabled = false, + Port = Options.Port, + VirtualHost = Options.VirtualHost + }; + if (Options.SslEnabled) + { + var ssl = new SslOption + { + Enabled = true, + AcceptablePolicyErrors = Options.SslAcceptablePolicyErrors, + Version = Options.SslVersion, + ServerName = Options.SslServerName ?? string.Empty, + CheckCertificateRevocation = Options.SslCheckCertificateRevocation, + CertPassphrase = Options.SslCertPassphrase ?? string.Empty, + CertPath = Options.SslCertPath ?? string.Empty + }; + factory.Ssl = ssl; + } + _connection = await factory.CreateConnectionAsync(); + + _connection.ConnectionShutdownAsync += Connection_ConnectionShutdown; + _connection.ConnectionBlockedAsync += Connection_ConnectionBlocked; + _connection.CallbackExceptionAsync += Connection_CallbackException; + if (Options.ConnectionCallbackExceptionHandler != null) + _connection.CallbackExceptionAsync += Options.ConnectionCallbackExceptionHandler; + _log.LogDebug("Connection opened."); + + _publishChannel = await Connection.CreateChannelAsync(); + _publishChannel.CallbackExceptionAsync += Channel_CallbackException; + await _publishChannel.BasicQosAsync(0, Options.PrefetchCount, false); // Per consumer limit + + foreach (var exchange in _exchanges) + { + await exchange.StartExchange(_publishChannel); + } + _connectionBlocked = false; + + CheckSendChannelOpened(); + _log.LogInformation("Connected to RabbitMQ endpoint {HostName}", Options.HostName); + } + + /// + /// Close all connections and Cleans up. + /// + public async Task Cleanup() + { + _log.LogInformation("Closing and cleaning up old connection and channels."); + try + { + if (_connection != null) + { + _connection.ConnectionShutdownAsync -= Connection_ConnectionShutdown; + _connection.CallbackExceptionAsync -= Connection_CallbackException; + _connection.ConnectionBlockedAsync -= Connection_ConnectionBlocked; + if (Options.ConnectionCallbackExceptionHandler != null) + _connection.CallbackExceptionAsync -= Options.ConnectionCallbackExceptionHandler; + } + // Closing send channel. + if (_publishChannel != null) + _publishChannel.CallbackExceptionAsync -= Channel_CallbackException; + + // Closing connection. + if (_connection?.IsOpen == true) + await _connection.CloseAsync(TimeSpan.FromSeconds(1)); + + if (ConnectionShutdownAsync != null) + await ConnectionShutdownAsync.Invoke(this, new ShutdownEventArgs(ShutdownInitiator.Application, + 0, "Closed gracefully", 0, 0, null, default)); + } + catch (Exception e) + { + _log.LogError(e, "Error closing connection."); + // Close() may throw an IOException if connection + // dies - but that's ok (handled by reconnect) + } + } + + /// + /// Reconnects this instance to RabbitMQ. + /// + async Task Reconnect() + { + _log.LogInformation("Reconnect requested"); + await Cleanup(); + + // TODO: возможно нужно заменить эту конструкцию из-за Task. + var mres = new ManualResetEventSlim(false); // state is initially false + + while (!mres.Wait(Options.ReconnectionTimeout)) // loop until state is true, checking every Options.ReconnectionTimeout + { + if (Options.ReconnectionAttemptsCount is not null && _reconnectAttemptsCount > Options.ReconnectionAttemptsCount) + throw new ReconnectAttemptsExceededException($"Max reconnect attempts {Options.ReconnectionAttemptsCount} reached."); + + try + { + _log.LogInformation("Trying to connect with reconnect attempt {ReconnectAttempt}", _reconnectAttemptsCount); + await Connect(); + _reconnectAttemptsCount = 0; + + if (ReconnectedAsync != null) + await ReconnectedAsync.Invoke(this, new ReconnectEventArgs()); + + break; + //mres.Set(); // state set to true - breaks out of loop + } + catch (Exception e) + { + _reconnectAttemptsCount++; + await Task.Delay(Options.ReconnectionTimeout); + string? innerExceptionMessage = null; + if (e.InnerException != null) + innerExceptionMessage = e.InnerException.Message; + _log.LogCritical(e, "Connection failed. Details: {ErrorMessage}. Reconnect attempts: {ReconnectAttempt}", + e.Message + innerExceptionMessage, _reconnectAttemptsCount); + } + } + } + + /// + public async ValueTask AsyncDispose() => await Cleanup(); + + /// + public ValueTask SendJsonAsync( + string json, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + if (string.IsNullOrEmpty(json)) + throw new ArgumentException($"{nameof(json)} is null or empty.", nameof(json)); + + var body = Encoding.UTF8.GetBytes(json); + var properties = CreateBasicJsonProperties(); + + _log.LogDebug("Sending json message {Message} to exchange {Exchange} " + + "with routing key {RoutingKey}.", json, exchange, routingKey); + + return SendAsync(body, + props: properties, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + /// + public ValueTask SendJsonAsync( + ReadOnlyMemory jsonBytes, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + if (jsonBytes.Length == 0) + throw new ArgumentException($"{nameof(jsonBytes)} is null or empty.", nameof(jsonBytes)); + + var properties = CreateBasicJsonProperties(); + + _log.LogDebug("Sending json message to exchange {Exchange} " + + "with routing key {RoutingKey}.", exchange, routingKey); + + return SendAsync(jsonBytes, + props: properties, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + /// + public ValueTask SendAsync( + T obj, + string routingKey, + string? exchange = default, + bool decreaseTtl = true + ) + { + // Проверка на Null без боксинга. // https://stackoverflow.com/a/864860 + if (EqualityComparer.Default.Equals(obj, default)) + throw new ArgumentNullException(nameof(obj)); + + var serializedObj = _serializer.Serialize(obj); + return SendJsonAsync( + serializedObj, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + /// + public async ValueTask SendAsync( + byte[] obj, + BasicProperties props, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + await SendAsync(new ReadOnlyMemory(obj), + props: props, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + /// + public ValueTask SendBatchAsync( + IEnumerable objs, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + var messages = new List>(); + foreach (var obj in objs) + messages.Add(_serializer.Serialize(obj)); + + return SendJsonBatchAsync( + serializedJsonList: messages, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + /// + public ValueTask SendJsonBatchAsync( + IEnumerable serializedJsonList, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + var messages = new List<(byte[] Body, IBasicProperties Props)>(); + foreach (var json in serializedJsonList) + { + var props = CreateBasicJsonProperties(); + AddTtl(props, decreaseTtl); + + var body = Encoding.UTF8.GetBytes(json); + messages.Add((body, props)); + } + + _log.LogDebug("Sending json messages batch to exchange {Exchange} " + + "with routing key {RoutingKey}.", exchange, routingKey); + + return SendBatchAsync( + objs: messages, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + /// + public ValueTask SendJsonBatchAsync( + IEnumerable> serializedJsonList, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + var messages = new List<(ReadOnlyMemory Body, IBasicProperties Props)>(); + foreach (var json in serializedJsonList) + { + var props = CreateBasicJsonProperties(); + AddTtl(props, decreaseTtl); + + messages.Add((json, props)); + } + + _log.LogDebug("Sending json messages batch to exchange {Exchange} " + + "with routing key {RoutingKey}.", exchange, routingKey); + + return SendBatchAsync( + objs: messages, + exchange: exchange, + routingKey: routingKey, + decreaseTtl: decreaseTtl + ); + } + + #region Base Publish methods + + /// + public async ValueTask SendAsync( + ReadOnlyMemory obj, + BasicProperties props, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + if (string.IsNullOrEmpty(exchange)) + exchange = GetDefaultExchange(); + + if (string.IsNullOrEmpty(exchange)) + throw new ArgumentException($"{nameof(exchange)} is null or empty.", nameof(exchange)); + + if (obj.Length > Options.MaxBodySize) + { + string decodedString = DecodeMessageAsString(obj); + throw new BadMessageException($"The message size \"{obj.Length}\" exceeds max body limit of \"{Options.MaxBodySize}\" " + + $"on routing key \"{routingKey}\" (exchange: \"{exchange}\"). Decoded message part: {decodedString}"); + } + + CheckSendChannelOpened(); + + AddTtl(props, decreaseTtl); + + await SendChannel.BasicPublishAsync(exchange: exchange, + routingKey: routingKey, + mandatory: false, // Just not reacting when no queue is subscribed for key. + basicProperties: props, + body: obj); + _log.LogDebug("Sent raw message to exchange {Exchange} with routing key {RoutingKey}.", + exchange, routingKey); + } + + /// + public async ValueTask SendBatchAsync( + IEnumerable<(ReadOnlyMemory Body, BasicProperties Props)> objs, + string routingKey, + string? exchange = default, + bool decreaseTtl = true) + { + if (string.IsNullOrEmpty(exchange)) + exchange = GetDefaultExchange(); + + if (string.IsNullOrEmpty(exchange)) + throw new ArgumentException($"{nameof(exchange)} is null or empty.", nameof(exchange)); + + CheckSendChannelOpened(); + + const ushort MAX_OUTSTANDING_CONFIRMS = 256; + + int batchSize = Math.Max(1, MAX_OUTSTANDING_CONFIRMS / 2); + + var publishTasks = new List(); + + foreach (var (body, props) in objs) + { + if (body.Length > Options.MaxBodySize) + { + string decodedString = DecodeMessageAsString(body); + + _log.LogError("Skipped message due to message size '{MessageSize}' exceeds max body limit of '{MaxBodySize}' " + + "on routing key '{RoutingKey}' (exchange: '{Exchange}'. Decoded message part: {DecodedString})", + body.Length, + Options.MaxBodySize, + routingKey, + exchange, + decodedString); + continue; + } + + AddTtl(props, decreaseTtl); + + var publishTask = _publishChannel.BasicPublishAsync( + exchange: exchange, + routingKey: routingKey, + body: body, + mandatory: false, // Just not reacting when no queue is subscribed for key. + basicProperties: props); + publishTasks.Add(publishTask); + + await MaybeAwaitPublishes(publishTasks, batchSize); + } + + // Await any remaining tasks in case message count was not + // evenly divisible by batch size. + await MaybeAwaitPublishes(publishTasks, 0); + + _log.LogDebug("Sent raw messages batch to exchange {Exchange} " + + "with routing key {RoutingKey}.", exchange, routingKey); + } + + async Task MaybeAwaitPublishes(List publishTasks, int batchSize) + { + if (publishTasks.Count >= batchSize) + { + foreach (ValueTask pt in publishTasks) + { + try + { + await pt; + } + catch (Exception e) + { + _log.LogError(e, "[ERROR] saw nack or return, ex: '{ErrorMessage}'", e.Message); + } + } + publishTasks.Clear(); + } + } + #endregion + + #region Actions implementation + Task Connection_CallbackException(object? sender, CallbackExceptionEventArgs e) + { + if (e != null) + _log.LogError(e.Exception, e.Exception.Message); + + return Task.CompletedTask; + } + + Task Channel_CallbackException(object? sender, CallbackExceptionEventArgs e) + { + if (e != null) + { + var message = string.Join(Environment.NewLine, e.Detail.Select(x => $"{x.Key} - {x.Value}")); + _log.LogError(e.Exception, message); + } + + return Task.CompletedTask; + } + + async Task Connection_ConnectionShutdown(object? sender, ShutdownEventArgs args) + { + if (args != null) + _log.LogError("Connection broke! Reason: {BrokeReason}", args.ReplyText); + + if (ConnectionShutdownAsync != null) + await ConnectionShutdownAsync.Invoke(sender ?? this, args); + + await Reconnect(); + } + + async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventArgs e) + { + if (e != null) + _log.LogError("Connection blocked! Reason: {Reason}", e.Reason); + _connectionBlocked = true; + await Reconnect(); + } + #endregion + + static BasicProperties CreateBasicJsonProperties() + { + var properties = new BasicProperties + { + Persistent = true, + ContentType = "application/json" + }; + return properties; + } + + void AddTtl(BasicProperties props, bool decreaseTtl) + { + if (decreaseTtl) + { + if (props.Headers == null) + props.Headers = new Dictionary(); + + if (props.Headers.ContainsKey(TtlHeader)) + props.Headers[TtlHeader] = (int)props.Headers[TtlHeader] - 1; + else + props.Headers.Add(TtlHeader, Options.DefaultTtl); + } + } + + [MemberNotNull(nameof(_publishChannel))] + void CheckSendChannelOpened() + { + if (_publishChannel is null || _publishChannel.IsClosed) + throw new NotConnectedException("Channel not opened."); + + if (_connectionBlocked) + throw new NotConnectedException("Connection is blocked."); + } + + /// + /// Try decode bytes array to string 1024 length or write as hex. + /// + /// + /// + static string DecodeMessageAsString(ReadOnlyMemory obj) + { + int bufferSize = 1024; // We need ~1 KB of text to log. + int decodedStringLength = Math.Min(obj.Length, bufferSize); + ReadOnlySpan slice = obj.Span.Slice(0, decodedStringLength); + + // Find the index of the last complete character + int lastValidIndex = slice.Length - 1; + while (lastValidIndex >= 0 && (slice[lastValidIndex] & 0b11000000) == 0b10000000) + { + // If a byte is a "continuation" of a UTF-8 character (starts with 10xxxxxx), + // it means that it is part of the previous character and needs to be discarded. + lastValidIndex--; + } + + // Truncating to the last valid character + slice = slice.Slice(0, lastValidIndex + 1); + + // Checking the string is UTF8 + var decoder = Encoding.UTF8.GetDecoder(); + char[] buffer = new char[decodedStringLength]; + decoder.Convert(slice, buffer, flush: true, out _, out int charsUsed, out bool completed); + if (completed) + return new string(buffer, 0, charsUsed); + else + { + // Generating bytes as string. + return BitConverter.ToString(obj.Span.ToArray(), 0, decodedStringLength); + } + } + + public async ValueTask DisposeAsync() + { + if (_publishChannel != null) + await _publishChannel.DisposeAsync(); + + if (_connection != null) + await _connection.DisposeAsync(); + } +} diff --git a/src/RabbitMQCoreClient/QueueServiceImpl.cs b/src/RabbitMQCoreClient/QueueServiceImpl.cs deleted file mode 100644 index 8db818b..0000000 --- a/src/RabbitMQCoreClient/QueueServiceImpl.cs +++ /dev/null @@ -1,597 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; -using RabbitMQCoreClient.Configuration.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using RabbitMQCoreClient.Exceptions; -using RabbitMQCoreClient.Serializers; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using static RabbitMQCoreClient.Configuration.AppConstants.RabbitMQHeaders; - -namespace RabbitMQCoreClient -{ - /// - /// Implementations of the . - /// - /// - public sealed class QueueServiceImpl : IQueueService - { - readonly ILogger _log; - static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); - readonly IList _exchanges; - readonly IMessageSerializer _serializer; - bool _connectionBlocked = false; - IConnection? _connection; - IModel? _sendChannel; - - /// - /// Client Options. - /// - public RabbitMQCoreClientOptions Options { get; } - - /// - /// RabbitMQ connection interface. - /// - public IConnection Connection => _connection!; // So far, that's it. The property is completely initialized in the constructor. - - /// - /// Sending channel. - /// - public IModel SendChannel => _sendChannel!; // So far, that's it. The property is completely initialized in the constructor. - - /// - /// Occurs when connection restored after reconnect. - /// - public event Action? OnReconnected; - - /// - /// Occurs when connection is shuted down on any reason. - /// - public event Action? OnConnectionShutdown; - - string? GetDefaultExchange() => _exchanges.FirstOrDefault(x => x.Options.IsDefault)?.Name; - - long _reconnectAttemptsCount; - - /// - /// Initializes a new instance of the class . - /// - /// The options. - /// The logger factory. - /// The builder. - /// options - public QueueServiceImpl(RabbitMQCoreClientOptions options, ILoggerFactory loggerFactory, IRabbitMQCoreClientBuilder builder) - { - Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); - _log = loggerFactory.CreateLogger(); - _exchanges = builder.Exchanges; - _serializer = builder.Serializer; - - Reconnect(); // Start connection cycle. - } - - /// - /// Connects this instance to RabbitMQ. - /// - public void Connect() - { - // Protection against repeated calls to the `Connect()` method. - if (_connection?.IsOpen == true) - { - _log.LogWarning("Connection already open."); - return; - } - - _log.LogInformation("Connecting to RabbitMQ endpoint {RabbitMQEndpoint}.", Options.HostName); - - var factory = new ConnectionFactory - { - HostName = Options.HostName, - UserName = Options.UserName, - Password = Options.Password, - RequestedHeartbeat = TimeSpan.FromSeconds(Options.RequestedHeartbeat), - RequestedConnectionTimeout = TimeSpan.FromMilliseconds(Options.RequestedConnectionTimeout), - AutomaticRecoveryEnabled = false, - TopologyRecoveryEnabled = false, - Port = Options.Port, - VirtualHost = Options.VirtualHost, - DispatchConsumersAsync = true, - }; - if (Options.SslEnabled) - { - var ssl = new SslOption - { - Enabled = true, - AcceptablePolicyErrors = Options.SslAcceptablePolicyErrors, - Version = Options.SslVersion, - ServerName = Options.SslServerName ?? string.Empty, - CheckCertificateRevocation = Options.SslCheckCertificateRevocation, - CertPassphrase = Options.SslCertPassphrase ?? string.Empty, - CertPath = Options.SslCertPath ?? string.Empty - }; - factory.Ssl = ssl; - } - _connection = factory.CreateConnection(); - - _connection.ConnectionShutdown += Connection_ConnectionShutdown; - _connection.ConnectionBlocked += Connection_ConnectionBlocked; - _connection.CallbackException += Connection_CallbackException; - if (Options.ConnectionCallbackExceptionHandler != null) - _connection.CallbackException += Options.ConnectionCallbackExceptionHandler; - _log.LogDebug("Connection opened."); - - _sendChannel = Connection.CreateModel(); - _sendChannel.CallbackException += Channel_CallbackException; - _sendChannel.BasicQos(0, Options.PrefetchCount, false); // Per consumer limit - - foreach (var exchange in _exchanges) - { - exchange.StartExchange(_sendChannel); - } - _connectionBlocked = false; - - CheckSendChannelOpened(); - _log.LogInformation("Connected to RabbitMQ endpoint {RabbitMQEndpoint}", Options.HostName); - } - - /// - /// Close all connections and Cleans up. - /// - public void Cleanup() - { - _log.LogInformation("Closing and cleaning up old connection and channels."); - try - { - if (_connection != null) - { - _connection.ConnectionShutdown -= Connection_ConnectionShutdown; - _connection.CallbackException -= Connection_CallbackException; - _connection.ConnectionBlocked -= Connection_ConnectionBlocked; - if (Options.ConnectionCallbackExceptionHandler != null) - _connection.CallbackException -= Options.ConnectionCallbackExceptionHandler; - } - // Closing send channel. - if (_sendChannel != null) - { - _sendChannel.CallbackException -= Channel_CallbackException; - } - - OnConnectionShutdown?.Invoke(); - // Closing connection. - if (_connection?.IsOpen == true) - _connection.Close(TimeSpan.FromSeconds(1)); - } - catch (Exception e) - { - _log.LogError(e, "Error closing connection."); - // Close() may throw an IOException if connection - // dies - but that's ok (handled by reconnect) - } - } - - /// - /// Reconnects this instance to RabbitMQ. - /// - void Reconnect() - { - _log.LogInformation("Reconnect requested"); - Cleanup(); - - var mres = new ManualResetEventSlim(false); // state is initially false - - while (!mres.Wait(Options.ReconnectionTimeout)) // loop until state is true, checking every Options.ReconnectionTimeout - { - if (Options.ReconnectionAttemptsCount is not null && _reconnectAttemptsCount > Options.ReconnectionAttemptsCount) - throw new ReconnectAttemptsExceededException($"Max reconnect attempts {Options.ReconnectionAttemptsCount} reached."); - - try - { - _log.LogInformation($"Trying to connect with reconnect attempt {_reconnectAttemptsCount}"); - Connect(); - _reconnectAttemptsCount = 0; - OnReconnected?.Invoke(); - break; - //mres.Set(); // state set to true - breaks out of loop - } - catch (Exception e) - { - _reconnectAttemptsCount++; - Thread.Sleep(Options.ReconnectionTimeout); - string? innerExceptionMessage = null; - if (e.InnerException != null) - innerExceptionMessage = e.InnerException.Message; - _log.LogCritical(e, $"Connection failed. Details: {e.Message} {innerExceptionMessage}. Reconnect attempts: {_reconnectAttemptsCount}", e); - } - } - } - - /// - public void Dispose() => Cleanup(); - - /// - public ValueTask SendJsonAsync( - string json, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - if (string.IsNullOrEmpty(json)) - throw new ArgumentException($"{nameof(json)} is null or empty.", nameof(json)); - - var body = Encoding.UTF8.GetBytes(json); - var properties = CreateBasicJsonProperties(); - - _log.LogDebug("Sending json message {message} to exchange {exchange} with routing key {routingKey}.", json, exchange, routingKey); - - return SendAsync(body, - props: properties, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - public ValueTask SendJsonAsync( - ReadOnlyMemory jsonBytes, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - if (jsonBytes.Length == 0) - throw new ArgumentException($"{nameof(jsonBytes)} is null or empty.", nameof(jsonBytes)); - - var properties = CreateBasicJsonProperties(); - - _log.LogDebug("Sending json message to exchange {exchange} with routing key {routingKey}.", exchange, routingKey); - - return SendAsync(jsonBytes, - props: properties, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - public ValueTask SendAsync( - T obj, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default - ) - { - // Проверка на Null без боксинга. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) - throw new ArgumentNullException(nameof(obj)); - - var serializedObj = _serializer.Serialize(obj); - return SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - public async ValueTask SendAsync( - byte[] obj, - IBasicProperties props, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - await SendAsync(new ReadOnlyMemory(obj), - props: props, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - public async ValueTask SendAsync( - ReadOnlyMemory obj, - IBasicProperties props, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - if (string.IsNullOrEmpty(exchange)) - exchange = GetDefaultExchange(); - - if (string.IsNullOrEmpty(exchange)) - throw new ArgumentException($"{nameof(exchange)} is null or empty.", nameof(exchange)); - - if (obj.Length > Options.MaxBodySize) - { - string decodedString = DecodeMessageAsString(obj); - throw new BadMessageException($"The message size \"{obj.Length}\" exceeds max body limit of \"{Options.MaxBodySize}\" " + - $"on routing key \"{routingKey}\" (exchange: \"{exchange}\"). Decoded message part: {decodedString}"); - } - - CheckSendChannelOpened(); - - await _semaphoreSlim.WaitAsync(); - try - { - AddTtl(props, decreaseTtl); - AddCorrelationId(props, correlationId); - SendChannel.BasicPublish(exchange: exchange, - routingKey: routingKey, - basicProperties: props, - body: obj); - _log.LogDebug("Sent raw message to exchange {exchange} with routing key {routingKey}.", exchange, routingKey); - } - catch - { - throw; - } - finally - { - _semaphoreSlim.Release(); - } - } - - /// - public ValueTask SendBatchAsync( - IEnumerable objs, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - var messages = new List>(); - foreach (var obj in objs) - messages.Add(_serializer.Serialize(obj)); - - return SendJsonBatchAsync( - serializedJsonList: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - public async ValueTask SendBatchAsync( - IEnumerable<(ReadOnlyMemory Body, IBasicProperties Props)> objs, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - if (string.IsNullOrEmpty(exchange)) - exchange = GetDefaultExchange(); - - if (string.IsNullOrEmpty(exchange)) - throw new ArgumentException($"{nameof(exchange)} is null or empty.", nameof(exchange)); - - CheckSendChannelOpened(); - - await _semaphoreSlim.WaitAsync(); - try - { - var batchOperation = CreateBasicJsonBatchProperties(); - - foreach (var (body, props) in objs) - { - if (body.Length > Options.MaxBodySize) - { - string decodedString = DecodeMessageAsString(body); - - _log.LogError("Skipped message due to message size \"{messageSize}\" exceeds max body limit of \"{maxBodySize}\" " + - "on routing key \"{routingKey}\" (exchange: \"{exchange}\". Decoded message part: {DecodedString})", - body.Length, - Options.MaxBodySize, - routingKey, - exchange, - decodedString); - continue; - } - - AddTtl(props, decreaseTtl); - AddCorrelationId(props, correlationId); - - batchOperation?.Add(exchange, routingKey, false, props, body); - } - batchOperation?.Publish(); - _log.LogDebug("Sent raw messages batch to exchange {exchange} with routing key {routingKey}.", exchange, routingKey); - } - catch - { - throw; - } - finally - { - _semaphoreSlim.Release(); - } - } - - /// - public ValueTask SendJsonBatchAsync( - IEnumerable serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - var messages = new List<(byte[] Body, IBasicProperties Props)>(); - foreach (var json in serializedJsonList) - { - var props = CreateBasicJsonProperties(); - AddTtl(props, decreaseTtl); - AddCorrelationId(props, correlationId); - - var body = Encoding.UTF8.GetBytes(json); - messages.Add((body, props)); - } - - _log.LogDebug("Sending json messages batch to exchange {exchange} with routing key {routingKey}.", exchange, routingKey); - - return SendBatchAsync( - objs: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - /// - public ValueTask SendJsonBatchAsync( - IEnumerable> serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true, - string? correlationId = default) - { - var messages = new List<(ReadOnlyMemory Body, IBasicProperties Props)>(); - foreach (var json in serializedJsonList) - { - var props = CreateBasicJsonProperties(); - AddTtl(props, decreaseTtl); - AddCorrelationId(props, correlationId); - - messages.Add((json, props)); - } - - _log.LogDebug("Sending json messages batch to exchange {exchange} with routing key {routingKey}.", exchange, routingKey); - - return SendBatchAsync( - objs: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - - void Connection_CallbackException(object? sender, CallbackExceptionEventArgs e) - { - if (e != null) - _log.LogError(e.Exception, e.Exception.Message); - } - - void Channel_CallbackException(object? sender, CallbackExceptionEventArgs e) - { - if (e != null) - _log.LogError(e.Exception, string.Join(Environment.NewLine, e.Detail.Select(x => $"{x.Key} - {x.Value}"))); - } - - void Connection_ConnectionShutdown(object? sender, ShutdownEventArgs e) - { - if (e != null) - _log.LogError($"Connection broke! Reason: {e.ReplyText}"); - - Reconnect(); - } - - void Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventArgs e) - { - if (e != null) - _log.LogError($"Connection blocked! Reason: {e.Reason}"); - _connectionBlocked = true; - Reconnect(); - } - - [return: NotNull] - IBasicProperties CreateBasicJsonProperties() - { - CheckSendChannelOpened(); - - var properties = SendChannel.CreateBasicProperties(); - - properties.Persistent = true; - properties.ContentType = "application/json"; - return properties!; - } - - void AddTtl(IBasicProperties props, bool decreaseTtl) - { - if (props.Headers == null) - props.Headers = new Dictionary(); - - if (decreaseTtl) - { - if (props.Headers.ContainsKey(TtlHeader)) - props.Headers[TtlHeader] = (int)props.Headers[TtlHeader] - 1; - else - props.Headers.Add(TtlHeader, Options.DefaultTtl); - } - } - - static void AddCorrelationId(IBasicProperties props, string? correlationId) - { - if (!string.IsNullOrEmpty(correlationId)) - props.CorrelationId = correlationId; - } - - [return: NotNull] - IBasicPublishBatch CreateBasicJsonBatchProperties() => SendChannel.CreateBasicPublishBatch(); - - void CheckSendChannelOpened() - { - if (_sendChannel is null || _sendChannel.IsClosed) - throw new NotConnectedException("Channel not opened."); - - if (_connectionBlocked) - throw new NotConnectedException("Connection is blocked."); - } - - /// - /// Try decode bytes array to string 1024 length or write as hex. - /// - /// - /// - static string DecodeMessageAsString(ReadOnlyMemory obj) - { - int bufferSize = 1024; // We need ~1 KB of text to log. - int decodedStringLength = Math.Min(obj.Length, bufferSize); - ReadOnlySpan slice = obj.Span.Slice(0, decodedStringLength); - - // Find the index of the last complete character - int lastValidIndex = slice.Length - 1; - while (lastValidIndex >= 0 && (slice[lastValidIndex] & 0b11000000) == 0b10000000) - { - // If a byte is a "continuation" of a UTF-8 character (starts with 10xxxxxx), - // it means that it is part of the previous character and needs to be discarded. - lastValidIndex--; - } - - // Truncating to the last valid character - slice = slice.Slice(0, lastValidIndex + 1); - - // Checking the string is UTF8 - var decoder = Encoding.UTF8.GetDecoder(); - char[] buffer = new char[decodedStringLength]; - decoder.Convert(slice, buffer, flush: true, out _, out int charsUsed, out bool completed); - if (completed) - return new string(buffer, 0, charsUsed); - else - { - // Generating bytes as string. - return BitConverter.ToString(obj.Span.ToArray(), 0, decodedStringLength); - } - } - } -} diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj index 675e482..40675ff 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj +++ b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj @@ -1,11 +1,11 @@ - + 6.1.2 $(VersionSuffix) $(Version)-$(VersionSuffix) true - net6.0;net7.0;net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0 Sergey Pismennyi MONQ Digital lab RabbitMQCoreClient @@ -20,6 +20,7 @@ true snupkg enable + enable @@ -27,8 +28,7 @@ - - + diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index 23d7e48..ce48ece 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -1,293 +1,301 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQCoreClient.Configuration; +using RabbitMQCoreClient.Events; using RabbitMQCoreClient.Exceptions; using RabbitMQCoreClient.Models; using System; using System.Linq; using System.Net; -using System.Text; using System.Threading.Tasks; -namespace RabbitMQCoreClient +namespace RabbitMQCoreClient; + +public sealed class RabbitMQCoreClientConsumer : IQueueConsumer { - public sealed class RabbitMQCoreClientConsumer : IQueueConsumer + readonly IQueueService _queueService; + readonly IServiceScopeFactory _scopeFactory; + readonly IRabbitMQCoreClientConsumerBuilder _builder; + readonly ILogger _log; + + /// + /// The RabbitMQ consume messages channel. + /// + public IChannel? ConsumeChannel { get; private set; } + + AsyncEventingBasicConsumer? _consumer; + + /// + /// The Async consumer, with default consume method configurated. + /// + public AsyncEventingBasicConsumer? Consumer => _consumer; + + bool _wasSubscribed = false; + + /// + /// Initializes a new instance of the class. + /// + /// The builder. + /// The log. + /// The queue service. + /// The scope factory. + /// + /// scopeFactory + /// or + /// queueService + /// or + /// log + /// or + /// builder + /// + public RabbitMQCoreClientConsumer( + IRabbitMQCoreClientConsumerBuilder builder, + ILogger log, + IQueueService queueService, + IServiceScopeFactory scopeFactory) { - readonly IQueueService _queueService; - readonly IServiceScopeFactory _scopeFactory; - readonly IRabbitMQCoreClientConsumerBuilder _builder; - readonly ILogger _log; - - /// - /// The RabbitMQ consume messages channel. - /// - public IModel? ConsumeChannel { get; private set; } - - AsyncEventingBasicConsumer? _consumer; - - /// - /// The Async consumer, with default consume method configurated. - /// - public AsyncEventingBasicConsumer? Consumer => _consumer; - - bool _wasSubscribed = false; - - /// - /// Initializes a new instance of the class. - /// - /// The builder. - /// The log. - /// The queue service. - /// The scope factory. - /// - /// scopeFactory - /// or - /// queueService - /// or - /// log - /// or - /// builder - /// - public RabbitMQCoreClientConsumer( - IRabbitMQCoreClientConsumerBuilder builder, - ILogger log, - IQueueService queueService, - IServiceScopeFactory scopeFactory) - { - _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory), $"{nameof(scopeFactory)} is null."); - _queueService = queueService ?? throw new ArgumentNullException(nameof(queueService), $"{nameof(queueService)} is null."); - _log = log ?? throw new ArgumentNullException(nameof(log), $"{nameof(log)} is null."); - _builder = builder ?? throw new ArgumentNullException(nameof(builder), $"{nameof(builder)} is null."); - } - - /// inheritdoc /> - public void Start() - { - if (_consumer != null && _consumer.IsRunning) - return; - - if (_queueService.Connection is null || !_queueService.Connection.IsOpen) - throw new NotConnectedException("Connection is not opened."); - - if (_queueService.SendChannel is null || _queueService.SendChannel.IsClosed) - throw new NotConnectedException("Send channel is not opened."); - - if (!_wasSubscribed) - { - _queueService.OnReconnected += QueueService_OnReconnected; - _queueService.OnConnectionShutdown += QueueService_OnConnectionShutdown; - _wasSubscribed = true; - } + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory), $"{nameof(scopeFactory)} is null."); + _queueService = queueService ?? throw new ArgumentNullException(nameof(queueService), $"{nameof(queueService)} is null."); + _log = log ?? throw new ArgumentNullException(nameof(log), $"{nameof(log)} is null."); + _builder = builder ?? throw new ArgumentNullException(nameof(builder), $"{nameof(builder)} is null."); + } - ConsumeChannel = _queueService.Connection.CreateModel(); - ConsumeChannel.CallbackException += Channel_CallbackException; - ConsumeChannel.BasicQos(0, _queueService.Options.PrefetchCount, false); // Per consumer limit + /// inheritdoc /> + public async Task Start() + { + if (_consumer != null && _consumer.IsRunning) + return; - ConnectToAllQueues(); - } + if (_queueService.Connection is null || !_queueService.Connection.IsOpen) + throw new NotConnectedException("Connection is not opened."); - /// inheritdoc /> - public void Shutdown() => StopAndClearConsumer(); + if (_queueService.SendChannel is null || _queueService.SendChannel.IsClosed) + throw new NotConnectedException("Send channel is not opened."); - void ConnectToAllQueues() + if (!_wasSubscribed) { - if (ConsumeChannel is null) - throw new NotConnectedException("The consumer Channel is null."); + _queueService.ReconnectedAsync += QueueService_OnReconnected; + _queueService.ConnectionShutdownAsync += QueueService_OnConnectionShutdown; + _wasSubscribed = true; + } - _consumer = new AsyncEventingBasicConsumer(ConsumeChannel); + ConsumeChannel = await _queueService.Connection.CreateChannelAsync(); + ConsumeChannel.CallbackExceptionAsync += Channel_CallbackException; + await ConsumeChannel.BasicQosAsync(0, _queueService.Options.PrefetchCount, false); // Per consumer limit - _consumer.Received += Consumer_Received; + await ConnectToAllQueues(); + } - // DeadLetterExchange configuration. - if (_builder.Queues.Any(x => !string.IsNullOrEmpty(x.DeadLetterExchange))) - ConfigureDeadLetterExchange(); + /// inheritdoc /> + public async Task Shutdown() => await StopAndClearConsumer(); - foreach (var queue in _builder.Queues) - { - // Set queue parameters from main configuration. - if (_queueService.Options.UseQuorumQueues) - queue.UseQuorum = true; - queue.StartQueue(ConsumeChannel, _consumer); - } - _log.LogInformation($"Consumer connected to {_builder.Queues.Count} queues."); - } + async Task ConnectToAllQueues() + { + if (ConsumeChannel is null) + throw new NotConnectedException("The consumer Channel is null."); - async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) - { - if (ConsumeChannel is null) - throw new NotConnectedException("ConsumeChannel is null"); + _consumer = new AsyncEventingBasicConsumer(ConsumeChannel); - var rabbitArgs = new RabbitMessageEventArgs(@event.RoutingKey) - { - CorrelationId = @event.BasicProperties.CorrelationId, - ConsumerTag = @event.ConsumerTag - }; + _consumer.ReceivedAsync += Consumer_Received; - _log.LogDebug("New message received with deliveryTag={deliveryTag}.", @event.DeliveryTag); + // DeadLetterExchange configuration. + if (_builder.Queues.Any(x => !string.IsNullOrEmpty(x.DeadLetterExchange))) + await ConfigureDeadLetterExchange(); - // Send a message to the death queue if ttl is over. - if (@event.BasicProperties.Headers?.ContainsKey(AppConstants.RabbitMQHeaders.TtlHeader) == true - && (int)@event.BasicProperties.Headers[AppConstants.RabbitMQHeaders.TtlHeader] <= 0) - { - ConsumeChannel.BasicNack(@event.DeliveryTag, false, false); - _log.LogDebug("Message was rejected due to low ttl."); - return; - } + foreach (var queue in _builder.Queues) + { + // Set queue parameters from main configuration. + if (_queueService.Options.UseQuorumQueues) + queue.UseQuorum = true; + await queue.StartQueue(ConsumeChannel, _consumer); + } + _log.LogInformation("Consumer connected to {QueuesCount} queues.", _builder.Queues.Count); + } - if (!_builder.RoutingHandlerTypes.ContainsKey(@event.RoutingKey)) - { - RejectDueToNoHandler(@event); - return; - } - var handlerType = _builder.RoutingHandlerTypes[@event.RoutingKey].Type; - var handlerOptions = _builder.RoutingHandlerTypes[@event.RoutingKey].Options; + async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) + { + if (ConsumeChannel is null) + throw new NotConnectedException("ConsumeChannel is null"); - // Get the message handler service. - using var scope = _scopeFactory.CreateScope(); - var handler = (IMessageHandler)scope.ServiceProvider.GetRequiredService(handlerType); - if (handler is null) - { - RejectDueToNoHandler(@event); - return; - } + var rabbitArgs = new RabbitMessageEventArgs(@event.RoutingKey, @event.ConsumerTag); - handler.Options = handlerOptions ?? new(); - // If user overides the default serializer then the custom serializer will be used for the handler. - handler.Serializer = handler.Options.CustomSerializer ?? _builder.Builder.Serializer; + _log.LogDebug("New message received with deliveryTag={DeliveryTag}.", @event.DeliveryTag); - _log.LogDebug($"Created scope for handler type {handler.GetType().Name}. Start processing message."); - try - { - await handler.HandleMessage(@event.Body, rabbitArgs); - ConsumeChannel.BasicAck(@event.DeliveryTag, false); - _log.LogDebug($"Message successfully processed by handler type {handler?.GetType().Name} " + - $"with deliveryTag={{deliveryTag}}.", @event.DeliveryTag); - } - catch (Exception e) - { - // Process the message depending on the given route. - switch (handler.ErrorMessageRouter.Route) - { - case Routes.DeadLetter: - ConsumeChannel.BasicNack(@event.DeliveryTag, false, false); - _log.LogError(e, "Error message with deliveryTag={deliveryTag}. " + - "Sent to dead letter exchange.", @event.DeliveryTag); - break; - case Routes.SourceQueue: - var decreaseTtl = handler.ErrorMessageRouter.TtlAction == TtlActions.Decrease; - ConsumeChannel.BasicAck(@event.DeliveryTag, false); - - // Forward the message back to the queue, while the TTL of the message is reduced by 1, - // depending on the settings of handler.ErrorMessageRouter.TtlAction. - // The message is sent back to the queue using the `handlerOptions?.RetryKey` key, - // if specified, otherwise it is sent to the queue with the original key. - await _queueService.SendAsync( - @event.Body, - @event.BasicProperties, - exchange: @event.Exchange, - routingKey: !string.IsNullOrEmpty(handlerOptions?.RetryKey) ? handlerOptions.RetryKey : @event.RoutingKey, - decreaseTtl: decreaseTtl, - correlationId: @event.BasicProperties.CorrelationId); - _log.LogError(e, "Error message with deliveryTag={deliveryTag}. Requeue.", @event.DeliveryTag); - break; - } - } + // Send a message to the death queue if ttl is over. + if (@event.BasicProperties.Headers?.TryGetValue(AppConstants.RabbitMQHeaders.TtlHeader, out var ttl) == true + && ttl is int ttlInt + && ttlInt <= 0) + { + await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false); + _log.LogDebug("Message was rejected due to low ttl."); + return; } - void QueueService_OnReconnected() + if (!_builder.RoutingHandlerTypes.ContainsKey(@event.RoutingKey)) { - StopAndClearConsumer(); - Start(); + await RejectDueToNoHandler(@event); + return; } + var handlerType = _builder.RoutingHandlerTypes[@event.RoutingKey].Type; + var handlerOptions = _builder.RoutingHandlerTypes[@event.RoutingKey].Options; - void QueueService_OnConnectionShutdown() + // Get the message handler service. + using var scope = _scopeFactory.CreateScope(); + var handler = (IMessageHandler)scope.ServiceProvider.GetRequiredService(handlerType); + if (handler is null) { - StopAndClearConsumer(); + await RejectDueToNoHandler(@event); + return; } - void ConfigureDeadLetterExchange() + handler.Options = handlerOptions ?? new(); + // If user overrides the default serializer then the custom serializer will be used for the handler. + handler.Serializer = handler.Options.CustomSerializer ?? _builder.Builder.Serializer; + + _log.LogDebug("Created scope for handler type {TypeName}. Start processing message.", + handler.GetType().Name); + try + { + await handler.HandleMessage(@event.Body, rabbitArgs); + await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false); + _log.LogDebug("Message successfully processed by handler type {TypeName} " + + "with deliveryTag={DeliveryTag}.", handler?.GetType().Name, @event.DeliveryTag); + } + catch (Exception e) { - if (ConsumeChannel is null) - throw new NotConnectedException("ConsumeChannel is null"); - - // Declaring DeadLetterExchange. - var deadLetterExchanges = _builder.Queues - .Where(x => !string.IsNullOrWhiteSpace(x.DeadLetterExchange)) - .Select(x => x.DeadLetterExchange) - .Distinct(); - - // TODO: Redo the configuration of the dead message queue in the future. So far, hardcode. - const string deadLetterQueueName = "dead_letter"; - - // We register the queue where the "rejected" messages will be stored. - ConsumeChannel.QueueDeclare(queue: "dead_letter", - durable: true, - exclusive: false, - autoDelete: false, - arguments: null); - var allRoutingKeys = _builder.Queues.SelectMany(x => x.RoutingKeys).Distinct(); - - foreach (var deadLetterEx in deadLetterExchanges) + // Process the message depending on the given route. + switch (handler.ErrorMessageRouter.Route) { - ConsumeChannel.ExchangeDeclare( - exchange: deadLetterEx, - type: "direct", - durable: true, - autoDelete: false, - arguments: null - ); - - if (allRoutingKeys.Any()) - foreach (var route in allRoutingKeys) - ConsumeChannel.QueueBind( - queue: deadLetterQueueName, - exchange: deadLetterEx, - routingKey: route, - arguments: null - ); + case Routes.DeadLetter: + await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false); + _log.LogError(e, "Error message with deliveryTag={DeliveryTag}. " + + "Sent to dead letter exchange.", @event.DeliveryTag); + break; + case Routes.SourceQueue: + var decreaseTtl = handler.ErrorMessageRouter.TtlAction == TtlActions.Decrease; + await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false); + + // Forward the message back to the queue, while the TTL of the message is reduced by 1, + // depending on the settings of handler.ErrorMessageRouter.TtlAction. + // The message is sent back to the queue using the `handlerOptions?.RetryKey` key, + // if specified, otherwise it is sent to the queue with the original key. + await _queueService.SendAsync( + @event.Body, + new BasicProperties(@event.BasicProperties), + exchange: @event.Exchange, + routingKey: !string.IsNullOrEmpty(handlerOptions?.RetryKey) ? handlerOptions.RetryKey : @event.RoutingKey, + decreaseTtl: decreaseTtl); + _log.LogError(e, "Error message with deliveryTag={DeliveryTag}. Requeue.", @event.DeliveryTag); + break; } } + } - void RejectDueToNoHandler(BasicDeliverEventArgs ea) + async Task QueueService_OnReconnected(object? sender, ReconnectEventArgs args) + { + await StopAndClearConsumer(); + await Start(); + } + + async Task QueueService_OnConnectionShutdown(object? sender, ShutdownEventArgs args) + { + await StopAndClearConsumer(); + } + + async Task ConfigureDeadLetterExchange() + { + if (ConsumeChannel is null) + throw new NotConnectedException("ConsumeChannel is null"); + + // Declaring DeadLetterExchange. + var deadLetterExchanges = _builder.Queues + .Where(x => !string.IsNullOrWhiteSpace(x.DeadLetterExchange)) + .Select(x => x.DeadLetterExchange!) + .Distinct(); + + // TODO: Redo the configuration of the dead message queue in the future. So far, hardcode. + const string deadLetterQueueName = "dead_letter"; + + // We register the queue where the "rejected" messages will be stored. + await ConsumeChannel.QueueDeclareAsync(queue: deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null); + var allRoutingKeys = _builder + .Queues + .SelectMany(x => x.RoutingKeys) + .Distinct() + .ToArray(); + + foreach (var deadLetterEx in deadLetterExchanges) { - _log.LogDebug($"Message was rejected due to no handler configured for the routing key {ea.RoutingKey}."); - ConsumeChannel?.BasicNack(ea.DeliveryTag, false, false); + await ConsumeChannel.ExchangeDeclareAsync( + exchange: deadLetterEx, + type: "direct", + durable: true, + autoDelete: false, + arguments: null + ); + + foreach (var route in allRoutingKeys) + await ConsumeChannel.QueueBindAsync( + queue: deadLetterQueueName, + exchange: deadLetterEx, + routingKey: route, + arguments: null + ); } + } + + async Task RejectDueToNoHandler(BasicDeliverEventArgs ea) + { + _log.LogDebug("Message was rejected due to no handler configured for the routing key {RoutingKey}.", + ea.RoutingKey); + + if (ConsumeChannel != null) + await ConsumeChannel.BasicNackAsync(ea.DeliveryTag, false, false); + } - void StopAndClearConsumer() + async Task StopAndClearConsumer() + { + try { - try - { - if (_consumer != null) - { - _consumer.Received -= Consumer_Received; - _consumer = null; - } - - // Closing consuming channel. - if (ConsumeChannel != null) - { - ConsumeChannel.CallbackException -= Channel_CallbackException; - } - if (ConsumeChannel?.IsOpen == true) - ConsumeChannel.Close((int)HttpStatusCode.OK, "Goodbye"); - } - catch (Exception e) + if (_consumer != null) { - _log.LogError(e, "Error closing consumer channel."); - // Close() may throw an IOException if connection - // dies - but that's ok (handled by reconnect) + _consumer.ReceivedAsync -= Consumer_Received; + _consumer = null; } + + // Closing consuming channel. + if (ConsumeChannel != null) + ConsumeChannel.CallbackExceptionAsync -= Channel_CallbackException; + + if (ConsumeChannel?.IsOpen == true) + await ConsumeChannel.CloseAsync((int)HttpStatusCode.OK, "Goodbye"); + } + catch (Exception e) + { + _log.LogError(e, "Error closing consumer channel."); + // Close() may throw an IOException if connection + // dies - but that's ok (handled by reconnect) } + } - void Channel_CallbackException(object? sender, CallbackExceptionEventArgs? e) + Task Channel_CallbackException(object? sender, CallbackExceptionEventArgs? e) + { + if (e != null) { - if (e != null) - _log.LogError(e.Exception, string.Join(Environment.NewLine, e.Detail.Select(x => $"{x.Key} - {x.Value}"))); + var message = string.Join(Environment.NewLine, e.Detail.Select(x => $"{x.Key} - {x.Value}")); + + _log.LogError(e.Exception, message); } - public void Dispose() => StopAndClearConsumer(); + return Task.CompletedTask; } + + /// + public async ValueTask DisposeAsync() => await StopAndClearConsumer(); } diff --git a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs index 2ec18e0..0e6fd4a 100644 --- a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs @@ -1,26 +1,25 @@ -using System; +using System; -namespace RabbitMQCoreClient.Serializers +namespace RabbitMQCoreClient.Serializers; + +/// +/// The serialization factory that uses be the RabbitMQCoreClient to serialize/deserialize messages. +/// +public interface IMessageSerializer { /// - /// The serialization factory that uses be the RabbitMQCoreClient to serialize/deserialize messages. + /// Serialize the value of type to string. /// - public interface IMessageSerializer - { - /// - /// Serialize the value of type to string. - /// - /// The value type. - /// The object to serialize. - /// Serialized string. - ReadOnlyMemory Serialize(TValue value); + /// The value type. + /// The object to serialize. + /// Serialized string. + ReadOnlyMemory Serialize(TValue value); - /// - /// Deserialize the value from string to type. - /// - /// The result type. - /// The byte array of the message from the provider as ReadOnlyMemory <byte>. - /// The object of type or null. - TResult? Deserialize(ReadOnlyMemory value); - } + /// + /// Deserialize the value from string to type. + /// + /// The result type. + /// The byte array of the message from the provider as ReadOnlyMemory <byte>. + /// The object of type or null. + TResult? Deserialize(ReadOnlyMemory value); } diff --git a/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJArrayConverter.cs b/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJArrayConverter.cs deleted file mode 100644 index b4d159f..0000000 --- a/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJArrayConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace RabbitMQCoreClient.Serializers.JsonConverters -{ - public class NewtonsoftJArrayConverter : JsonConverter - { - public override Newtonsoft.Json.Linq.JArray? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var jsonDoc = JsonDocument.ParseValue(ref reader); - var objStr = jsonDoc.RootElement.GetRawText(); - return Newtonsoft.Json.Linq.JArray.Parse(objStr); - } - - public override void Write(Utf8JsonWriter writer, Newtonsoft.Json.Linq.JArray value, JsonSerializerOptions options) - { -#if NET5_0 - using var doc = JsonDocument.Parse(value.ToString()); - doc.WriteTo(writer); -#else - writer.WriteRawValue(value.ToString()); -#endif - } - } -} diff --git a/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJObjectConverter.cs b/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJObjectConverter.cs deleted file mode 100644 index ec49eab..0000000 --- a/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJObjectConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace RabbitMQCoreClient.Serializers.JsonConverters -{ - public class NewtonsoftJObjectConverter : JsonConverter - { - public override Newtonsoft.Json.Linq.JObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var jsonDoc = JsonDocument.ParseValue(ref reader); - var objStr = jsonDoc.RootElement.GetRawText(); - return Newtonsoft.Json.Linq.JObject.Parse(objStr); - } - - public override void Write(Utf8JsonWriter writer, Newtonsoft.Json.Linq.JObject value, JsonSerializerOptions options) - { -#if NET5_0 - using var doc = JsonDocument.Parse(value.ToString()); - doc.WriteTo(writer); -#else - writer.WriteRawValue(value.ToString()); -#endif - } - } -} diff --git a/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJTokenConverter.cs b/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJTokenConverter.cs deleted file mode 100644 index eefdda4..0000000 --- a/src/RabbitMQCoreClient/Serializers/JsonConverters/NewtonsoftJTokenConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace RabbitMQCoreClient.Serializers.JsonConverters -{ - public class NewtonsoftJTokenConverter : JsonConverter - { - public override Newtonsoft.Json.Linq.JToken? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var jsonDoc = JsonDocument.ParseValue(ref reader); - var objStr = jsonDoc.RootElement.GetRawText(); - return Newtonsoft.Json.Linq.JToken.Parse(objStr); - } - - public override void Write(Utf8JsonWriter writer, Newtonsoft.Json.Linq.JToken value, JsonSerializerOptions options) - { -#if NET5_0 - using var doc = JsonDocument.Parse(value.ToString()); - doc.WriteTo(writer); -#else - writer.WriteRawValue(value.ToString()); -#endif - } - } -} diff --git a/src/RabbitMQCoreClient/Serializers/NewtonsoftJsonMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/NewtonsoftJsonMessageSerializer.cs deleted file mode 100644 index 5026a85..0000000 --- a/src/RabbitMQCoreClient/Serializers/NewtonsoftJsonMessageSerializer.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Text; - -namespace RabbitMQCoreClient.Serializers -{ - public class NewtonsoftJsonMessageSerializer : IMessageSerializer - { - public Newtonsoft.Json.JsonSerializerSettings Options { get; } - - static readonly Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver JsonResolver = - new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver - { - NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy - { - ProcessDictionaryKeys = true - } - }; - - public NewtonsoftJsonMessageSerializer(Action? setupAction = null) - { - if (setupAction is null) - { - Options = new Newtonsoft.Json.JsonSerializerSettings() { ContractResolver = JsonResolver }; - } - else - { - Options = new Newtonsoft.Json.JsonSerializerSettings(); - setupAction(Options); - } - } - - /// - public ReadOnlyMemory Serialize(TValue value) - { - var serializedValue = Newtonsoft.Json.JsonConvert.SerializeObject(value, Options); - return Encoding.UTF8.GetBytes(serializedValue); - } - - /// - public TResult? Deserialize(ReadOnlyMemory value) - { - var message = Encoding.UTF8.GetString(value.ToArray()) ?? string.Empty; - return Newtonsoft.Json.JsonConvert.DeserializeObject(message, Options); - } - } -} diff --git a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs index 577dd20..f34ae2d 100644 --- a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs @@ -1,48 +1,43 @@ -using RabbitMQCoreClient.Serializers.JsonConverters; using System; using System.Text.Json.Serialization; -namespace RabbitMQCoreClient.Serializers +namespace RabbitMQCoreClient.Serializers; + +public class SystemTextJsonMessageSerializer : IMessageSerializer { - public class SystemTextJsonMessageSerializer : IMessageSerializer + static readonly System.Text.Json.JsonSerializerOptions _defaultOptions = new System.Text.Json.JsonSerializerOptions { - static readonly System.Text.Json.JsonSerializerOptions _defaultOptions = new System.Text.Json.JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase - }; + PropertyNameCaseInsensitive = true, + DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; - public static System.Text.Json.JsonSerializerOptions DefaultOptions => _defaultOptions; + public static System.Text.Json.JsonSerializerOptions DefaultOptions => _defaultOptions; - static SystemTextJsonMessageSerializer() - { - _defaultOptions.Converters.Add(new JsonStringEnumConverter()); - _defaultOptions.Converters.Add(new NewtonsoftJObjectConverter()); - _defaultOptions.Converters.Add(new NewtonsoftJArrayConverter()); - _defaultOptions.Converters.Add(new NewtonsoftJTokenConverter()); - } + static SystemTextJsonMessageSerializer() + { + _defaultOptions.Converters.Add(new JsonStringEnumConverter()); + } - public System.Text.Json.JsonSerializerOptions Options { get; } + public System.Text.Json.JsonSerializerOptions Options { get; } - public SystemTextJsonMessageSerializer(Action? setupAction = null) + public SystemTextJsonMessageSerializer(Action? setupAction = null) + { + if (setupAction is null) + { + Options = _defaultOptions; + } + else { - if (setupAction is null) - { - Options = _defaultOptions; - } - else - { - Options = new System.Text.Json.JsonSerializerOptions(); - setupAction(Options); - } + Options = new System.Text.Json.JsonSerializerOptions(); + setupAction(Options); } + } - /// - public ReadOnlyMemory Serialize(TValue value) => - System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, Options); + /// + public ReadOnlyMemory Serialize(TValue value) => + System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, Options); - /// - public TResult? Deserialize(ReadOnlyMemory value) => - System.Text.Json.JsonSerializer.Deserialize(value.Span, Options); - } + /// + public TResult? Deserialize(ReadOnlyMemory value) => + System.Text.Json.JsonSerializer.Deserialize(value.Span, Options); } From 3f017f7cea0688505615ec252480bbafad4e0ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Mon, 8 Dec 2025 16:13:02 +0300 Subject: [PATCH 03/13] Fix some warnings. Peoject code clear --- .../Program.cs | 3 +- .../ServiceCollectionExtensions.cs | 4 +- .../Exceptions/PersistingException.cs | 2 - .../IQueueEventsBufferEngine.cs | 14 ---- .../BatchQueueSender/IQueueEventsWriter.cs | 2 - .../BatchQueueSender/QueueEventItem.cs | 2 +- .../QueueEventsBufferEngine.cs | 39 +++------ .../BatchQueueSender/QueueEventsWriter.cs | 10 +-- .../Configuration/AppConstants.cs | 2 +- .../ConfigFormats/JsonV1Binder.cs | 11 +-- .../ConfigFormats/JsonV2Binder.cs | 14 ++-- .../ConfigModels/QueueConfig.cs | 7 +- .../ConfigModels/SubscriptionConfig.cs | 6 +- .../ApplicationBuilderExtentions.cs | 7 +- .../Extensions/BuilderExtensions.cs | 9 +- .../Extensions/ServiceCollectionExtensions.cs | 7 +- .../DependencyInjection/IQueueConsumer.cs | 2 - .../IRabbitMQCoreClientBuilder.cs | 3 +- .../IRabbitMQCoreClientConsumerBuilder.cs | 2 - .../Options/ErrorHandlingOptions.cs | 1 - .../Options/ExchangeOptions.cs | 2 - .../Options/RabbitMQCoreClientOptions.cs | 2 - .../RabbitMQCoreClientBuilder.cs | 7 +- .../RabbitMQCoreClientConsumerBuilder.cs | 6 +- .../Exceptions/BadMessageException.cs | 2 - .../ClientConfigurationException.cs | 2 - .../Exceptions/NotConnectedException.cs | 2 - .../Exceptions/QueueBindException.cs | 2 - .../ReconnectAttemptsExceededException.cs | 2 - .../SystemTextJsonBuilderExtentions.cs | 1 - .../SystemTextJsonQueueServiceExtentions.cs | 3 - src/RabbitMQCoreClient/IMessageHandler.cs | 2 - src/RabbitMQCoreClient/IQueueService.cs | 3 - src/RabbitMQCoreClient/MessageHandlerJson.cs | 7 +- src/RabbitMQCoreClient/Models/Exchange.cs | 4 +- src/RabbitMQCoreClient/Models/Queue.cs | 10 +-- src/RabbitMQCoreClient/Models/QueueBase.cs | 14 +--- .../Models/RabbitMessageEventArgs.cs | 2 - src/RabbitMQCoreClient/Models/Subscription.cs | 21 ++--- src/RabbitMQCoreClient/QueueService.cs | 82 ++++++++++++------- .../RabbitMQCoreClientConsumer.cs | 19 ++--- .../Serializers/IMessageSerializer.cs | 2 - .../SystemTextJsonMessageSerializer.cs | 1 - 43 files changed, 129 insertions(+), 216 deletions(-) diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs index c9cacdb..4780918 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -7,7 +7,6 @@ using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.ConsoleClient; -using RabbitMQCoreClient.Serializers; using System; using System.Linq; using System.Text; diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index ab31581..860ca2a 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using System; namespace RabbitMQCoreClient.BatchQueueSender.DependencyInjection; @@ -46,7 +45,8 @@ public static IServiceCollection AddBatchQueueSender( public static IServiceCollection AddBatchQueueSender(this IServiceCollection services, Action? setupAction) { - services.Configure(setupAction); + if (setupAction != null) + services.Configure(setupAction); return services.AddBatchQueueSenderCore(); } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs index 042c22a..e14d194 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.BatchQueueSender.Exceptions; public class PersistingException : Exception diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs index a4e0d24..b1b776d 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - namespace RabbitMQCoreClient.BatchQueueSender; /// @@ -17,16 +13,6 @@ public interface IQueueEventsBufferEngine /// showing the completion of the operation. Task AddEvent(T @event, string routingKey); - /// - /// Add events to send to the data bus. - /// - /// The type of list item of the property. - /// The list of objects to send to the data bus. - /// The name of the route key with which you want to send events to the data bus. - /// - [Obsolete("Use AddEvents instead.")] - Task AddEvent(IEnumerable events, string routingKey); - /// /// Add events to send to the data bus. /// diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs index ee4264b..0511c87 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - namespace RabbitMQCoreClient.BatchQueueSender; /// diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs index 22dd70c..3c00d08 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs @@ -1,6 +1,6 @@ namespace RabbitMQCoreClient.BatchQueueSender; -public struct QueueEventItem +public readonly struct QueueEventItem { /// /// The event to be written to the database. diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs index 9d705ae..544172b 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs @@ -1,11 +1,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RabbitMQCoreClient.BatchQueueSender.Exceptions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace RabbitMQCoreClient.BatchQueueSender; @@ -15,13 +10,13 @@ namespace RabbitMQCoreClient.BatchQueueSender; public sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable { readonly Timer _flushTimer; - readonly List _events = new List(); + readonly List _events = []; readonly IQueueEventsWriter _eventsWriter; readonly QueueBatchSenderOptions _engineOptions; readonly ILogger _logger; - static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private bool _disposedValue; + static readonly SemaphoreSlim _semaphore = new(1, 1); + bool _disposedValue; /// /// Event storage buffer implementation constructor. @@ -39,7 +34,7 @@ public QueueEventsBufferEngine( _eventsWriter = eventsWriter; _logger = logger; - _flushTimer = new Timer(async obj => await FlushTimerDelegate(obj), null, + _flushTimer = new Timer(async obj => await FlushTimerDelegate(), null, _engineOptions.EventsFlushPeriodSec * 1000, _engineOptions.EventsFlushPeriodSec * 1000); } @@ -66,10 +61,6 @@ public async Task AddEvent(T @event, string routingKey) } } - /// - public Task AddEvent(IEnumerable events, string routingKey) - => AddEvents(events, routingKey); - /// public async Task AddEvents(IEnumerable events, string routingKey) { @@ -77,11 +68,10 @@ public async Task AddEvents(IEnumerable events, string routingKey) try { - foreach (var @event in events) - { - if (@event is not null) - _events.Add(new QueueEventItem(@event, routingKey)); - } + _events.AddRange(events + .Where(@event => @event is not null) + .Select(@event => new QueueEventItem(@event!, routingKey)) + ); if (_events.Count < _engineOptions.EventsFlushCount) return; @@ -94,7 +84,7 @@ public async Task AddEvents(IEnumerable events, string routingKey) } } - async Task FlushTimerDelegate(object? _) + async Task FlushTimerDelegate() { await _semaphore.WaitAsync(); try @@ -143,14 +133,11 @@ async Task HandleEvents(IEnumerable streamDataEvents) .ToList(); foreach (var aggregateException in exceptions) { - var persistException = aggregateException?.InnerExceptions?.First() as PersistingException; - if (persistException != null) + if (aggregateException?.InnerExceptions?[0] is PersistingException persistException) { - var extendedError = string.Join(Environment.NewLine, new[] - { - $"Routing key: {persistException.RoutingKey}. Source: ", - System.Text.Json.JsonSerializer.Serialize(persistException.Items) - }); + var extendedError = $"Routing key: {persistException.RoutingKey}. Source: " + + string.Join(Environment.NewLine, + System.Text.Json.JsonSerializer.Serialize(persistException.Items)); _logger.LogDebug(extendedError); } // Unrecorded events are not sent anywhere. For the current implementation, this is not fatal. diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs index 39ccca2..7ac9ea9 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.Logging; using RabbitMQCoreClient.BatchQueueSender.Exceptions; -using System; using System.Diagnostics; -using System.Threading.Tasks; namespace RabbitMQCoreClient.BatchQueueSender; @@ -31,7 +29,8 @@ public async Task Write(object[] items, string routingKey) if (items.Length == 0) return; - _logger.LogInformation("Start writing {RowsCount} data rows from the buffer.", items.Length); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Start writing {RowsCount} data rows from the buffer.", items.Length); var sw = new Stopwatch(); sw.Start(); @@ -47,8 +46,9 @@ public async Task Write(object[] items, string routingKey) sw.Stop(); - _logger.LogInformation("Buffer has sent {RowsCount} rows to the queue bus at {ElapsedMilliseconds} ms.", - items.Length, sw.ElapsedMilliseconds); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Buffer has sent {RowsCount} rows to the queue bus at {ElapsedMilliseconds} ms.", + items.Length, sw.ElapsedMilliseconds); _writtenCount += items.Length; diff --git a/src/RabbitMQCoreClient/Configuration/AppConstants.cs b/src/RabbitMQCoreClient/Configuration/AppConstants.cs index 6cf7f1e..2347845 100644 --- a/src/RabbitMQCoreClient/Configuration/AppConstants.cs +++ b/src/RabbitMQCoreClient/Configuration/AppConstants.cs @@ -1,7 +1,7 @@ namespace RabbitMQCoreClient.Configuration; #pragma warning disable CS1591 -internal static class AppConstants +static class AppConstants { public static class RabbitMQHeaders { diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs index f440267..0398716 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs @@ -4,8 +4,6 @@ using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; -using System; -using System.Linq; namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; @@ -15,7 +13,6 @@ public static class JsonV1Binder const string QueueName = "Queue:QueueName"; const string ExchangeName = "Exchange:Name"; const string SubscriptionSection = "Subscription"; - const string UseQuorumQueuesName = "UseQuorumQueues"; public static IRabbitMQCoreClientBuilder RegisterV1Configuration(this IRabbitMQCoreClientBuilder builder, IConfiguration? configuration) @@ -44,13 +41,9 @@ public static IRabbitMQCoreClientConsumerBuilder RegisterV1Configuration(this IR return builder; // Old queue format detected. - var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == oldExchangeName); - if (exchange is null) - throw new ClientConfigurationException($"The exchange {oldExchangeName} is " + + var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == oldExchangeName) + ?? throw new ClientConfigurationException($"The exchange {oldExchangeName} is " + "not found in \"Exchange\" section."); - - var useQuorumQueues = configuration.GetValue(UseQuorumQueuesName); - if (configuration.GetSection(QueueSection).Exists()) { // Register a queue and bind it to exchange points. diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs index 5d4063b..0c6dfde 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs @@ -3,8 +3,6 @@ using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; -using System; -using System.Linq; namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; @@ -78,20 +76,18 @@ static void RegisterQueues(IRabbitMQCoreClientConsumerBuilder b static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, TQueue queue) where TQueue : QueueBase { - if (!queue.Exchanges.Any()) + if (queue.Exchanges.Count == 0) { - var defaultExchange = builder.Builder.DefaultExchange; - if (defaultExchange is null) - throw new QueueBindException($"Queue {queue.Name} has no configured exchanges and the Default Exchange not found."); + var defaultExchange = builder.Builder.DefaultExchange + ?? throw new QueueBindException($"Queue {queue.Name} has no configured exchanges and the Default Exchange not found."); queue.Exchanges.Add(defaultExchange.Name); } else { - // Checking echange points declared in queue in configured "Exchanges". + // Checking exchange points declared in queue in configured "Exchanges". foreach (var exchangeName in queue.Exchanges) { - var exchange = builder.Builder.Exchanges.FirstOrDefault(x => x.Name == exchangeName); - if (exchange is null) + if (!builder.Builder.Exchanges.Any(x => x.Name == exchangeName)) throw new ClientConfigurationException($"The exchange {exchangeName} configured in queue {queue.Name} " + $"not found in Exchanges section."); } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs index 8c00890..d9f405e 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace RabbitMQCoreClient.DependencyInjection.ConfigModels; /// @@ -58,10 +55,10 @@ public string Name /// /// List of routing keys for the queue. /// - public HashSet RoutingKeys { get; set; } = new HashSet(); + public HashSet RoutingKeys { get; set; } = []; /// /// The list of exchange points to which the queue is bound. /// - public HashSet Exchanges { get; set; } = new HashSet(); + public HashSet Exchanges { get; set; } = []; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs index 910ec29..c10bbeb 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace RabbitMQCoreClient.DependencyInjection.ConfigModels; /// @@ -27,10 +25,10 @@ public class SubscriptionConfig /// /// List of routing keys for the queue. /// - public HashSet RoutingKeys { get; set; } = new HashSet(); + public HashSet RoutingKeys { get; set; } = []; /// /// The list of exchange points to which the queue is bound. /// - public HashSet Exchanges { get; set; } = new HashSet(); + public HashSet Exchanges { get; set; } = []; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs index ef2d473..6a1ebbe 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs @@ -11,12 +11,9 @@ public static class ApplicationBuilderExtentions /// The lifetime. public static IApplicationBuilder StartRabbitMqCore(this IApplicationBuilder app, IHostApplicationLifetime lifetime) { - var consumer = app.ApplicationServices.GetService(); - - if (consumer is null) - throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + + var consumer = app.ApplicationServices.GetService() + ?? throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + "Add services.AddRabbitMQCoreClient(...).AddConsumer(); to the DI."); - lifetime.ApplicationStarted.Register(() => consumer.Start()); lifetime.ApplicationStopping.Register(() => consumer.Shutdown()); diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index 81d7228..e15a326 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -6,9 +6,6 @@ using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; -using System; -using System.Collections.Generic; -using System.Linq; namespace Microsoft.Extensions.DependencyInjection; @@ -34,7 +31,7 @@ public static IRabbitMQCoreClientBuilder AddRequiredPlatformServices(this IRabbi /// /// Use default serializer as NewtonsoftJson. /// - public static IRabbitMQCoreClientBuilder AddDefaultSerializer(this IRabbitMQCoreClientBuilder builder) => + public static IRabbitMQCoreClientBuilder AddDefaultSerializer(this IRabbitMQCoreClientBuilder builder) => builder.AddSystemTextJson(); /// @@ -244,9 +241,9 @@ static void AddHandlerToBuilder(IRabbitMQCoreClientConsumerBuilder builder, { foreach (var routingKey in routingKeys) { - if (builder.RoutingHandlerTypes.ContainsKey(routingKey)) + if (builder.RoutingHandlerTypes.TryGetValue(routingKey, out var result)) throw new ClientConfigurationException("The routing key is already being processed by a handler like " + - $"{builder.RoutingHandlerTypes[routingKey].Type.FullName}."); + $"{result.Type.FullName}."); builder.RoutingHandlerTypes.Add(routingKey, (handlerType, options)); } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs index cde0ade..1f234cc 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigFormats; -using System; namespace Microsoft.Extensions.DependencyInjection; @@ -13,7 +12,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services) + static RabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services) { var builder = new RabbitMQCoreClientBuilder(services); @@ -55,7 +54,9 @@ public static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient( public static IRabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services, Action? setupAction) { - services.Configure(setupAction); + if (setupAction != null) + services.Configure(setupAction); + return services.AddRabbitMQCoreClient(); } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs index da2f6d6..4c84449 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs @@ -1,7 +1,5 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; -using System; -using System.Threading.Tasks; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs index a4b6130..dc724d3 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs @@ -1,6 +1,5 @@ using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Serializers; -using System.Collections.Generic; namespace Microsoft.Extensions.DependencyInjection; @@ -24,5 +23,5 @@ public interface IRabbitMQCoreClientBuilder /// /// The default JSON serializer. /// - IMessageSerializer Serializer { get; set; } + IMessageSerializer? Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs index ba49832..c191822 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs @@ -1,6 +1,4 @@ using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; -using System.Collections.Generic; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs index 5a43330..9161caa 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs @@ -1,5 +1,4 @@ using RabbitMQ.Client.Events; -using System; namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs index 927551b..01c241f 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; /// diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs index f3f20bd..c2efb9f 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs @@ -1,12 +1,10 @@ using RabbitMQ.Client.Events; -using System; using System.Net.Security; using System.Security.Authentication; using System.Text.Json.Serialization; namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; - public class RabbitMQCoreClientOptions { /// diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs index 4e109a4..a2c4df1 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs @@ -1,8 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Serializers; -using System; -using System.Collections.Generic; -using System.Linq; namespace RabbitMQCoreClient.Configuration.DependencyInjection; @@ -20,11 +17,11 @@ public RabbitMQCoreClientBuilder(IServiceCollection services) => public IServiceCollection Services { get; } /// - public IList Exchanges { get; } = new List(); + public IList Exchanges { get; } = []; /// public Exchange? DefaultExchange => Exchanges.FirstOrDefault(x => x.Options.IsDefault); /// - public IMessageSerializer Serializer { get; set; } + public IMessageSerializer? Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs index 2d886d5..73916b3 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; -using System.Collections.Generic; namespace RabbitMQCoreClient.Configuration.DependencyInjection; @@ -25,9 +23,9 @@ public RabbitMQCoreClientConsumerBuilder(IRabbitMQCoreClientBuilder builder) public IServiceCollection Services { get; } /// - public IList Queues { get; } = new List(); + public IList Queues { get; } = []; /// public Dictionary RoutingHandlerTypes { get; } = - new Dictionary(); + []; } diff --git a/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs b/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs index 4795ff6..a71b7d1 100644 --- a/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs +++ b/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Exceptions; public class BadMessageException : Exception diff --git a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs index 34673f9..33700e6 100644 --- a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Exceptions; public class ClientConfigurationException : Exception diff --git a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs index fff38db..da3bcd5 100644 --- a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs +++ b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Exceptions; public class NotConnectedException : Exception diff --git a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs index d1f53cf..17c27ed 100644 --- a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs +++ b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Exceptions; public class QueueBindException : Exception diff --git a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs index 7e75729..5b62a51 100644 --- a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Exceptions; public class ReconnectAttemptsExceededException : Exception diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs index 490b2bb..be96763 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs @@ -1,5 +1,4 @@ using RabbitMQCoreClient.Serializers; -using System; using System.Text.Json; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs index 138a098..daf1cd9 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs @@ -1,9 +1,6 @@ using RabbitMQCoreClient.Serializers; -using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization.Metadata; -using System.Threading.Tasks; namespace RabbitMQCoreClient; diff --git a/src/RabbitMQCoreClient/IMessageHandler.cs b/src/RabbitMQCoreClient/IMessageHandler.cs index 1502970..bd02c6e 100644 --- a/src/RabbitMQCoreClient/IMessageHandler.cs +++ b/src/RabbitMQCoreClient/IMessageHandler.cs @@ -1,8 +1,6 @@ using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; -using System; -using System.Threading.Tasks; namespace RabbitMQCoreClient; diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index 113471f..e5d927d 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -2,9 +2,6 @@ using RabbitMQ.Client.Events; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Events; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; namespace RabbitMQCoreClient; diff --git a/src/RabbitMQCoreClient/MessageHandlerJson.cs b/src/RabbitMQCoreClient/MessageHandlerJson.cs index 88442a9..332437e 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -1,9 +1,7 @@ using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; -using System; using System.Text; -using System.Threading.Tasks; namespace RabbitMQCoreClient; @@ -58,9 +56,8 @@ public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEvent TModel messageModel; try { - var obj = Serializer.Deserialize(message); - if (obj is null) - throw new InvalidOperationException("The json parser returns null."); + var obj = Serializer.Deserialize(message) + ?? throw new InvalidOperationException("The json parser returns null."); messageModel = obj; } catch (Exception e) diff --git a/src/RabbitMQCoreClient/Models/Exchange.cs b/src/RabbitMQCoreClient/Models/Exchange.cs index f902670..49051ff 100644 --- a/src/RabbitMQCoreClient/Models/Exchange.cs +++ b/src/RabbitMQCoreClient/Models/Exchange.cs @@ -1,7 +1,5 @@ using RabbitMQ.Client; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; -using System.Threading.Tasks; namespace RabbitMQCoreClient.Configuration.DependencyInjection; @@ -33,7 +31,7 @@ public Exchange(ExchangeOptions options) Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); if (string.IsNullOrEmpty(options.Name)) - throw new ArgumentException($"{nameof(options.Name)} is null or empty.", nameof(options.Name)); + throw new ArgumentException($"Name in {nameof(options)} is null or empty.", nameof(options)); } /// diff --git a/src/RabbitMQCoreClient/Models/Queue.cs b/src/RabbitMQCoreClient/Models/Queue.cs index 58d2db8..a0b4729 100644 --- a/src/RabbitMQCoreClient/Models/Queue.cs +++ b/src/RabbitMQCoreClient/Models/Queue.cs @@ -1,6 +1,4 @@ using RabbitMQCoreClient.DependencyInjection.ConfigModels; -using System; -using System.Collections.Generic; namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; @@ -21,8 +19,8 @@ public Queue(string name, bool durable = true, bool exclusive = false, bool auto /// /// Queue model from IConfiguration. /// - public static Queue Create(QueueConfig queueConfig) => - new Queue(name: queueConfig.Name, + public static Queue Create(QueueConfig queueConfig) => + new(name: queueConfig.Name, durable: queueConfig.Durable, exclusive: queueConfig.Exclusive, autoDelete: queueConfig.AutoDelete, @@ -31,7 +29,7 @@ public static Queue Create(QueueConfig queueConfig) => Arguments = queueConfig.Arguments ?? new Dictionary(), DeadLetterExchange = queueConfig.DeadLetterExchange, UseQuorum = queueConfig.UseQuorum, - Exchanges = queueConfig.Exchanges ?? new HashSet(), - RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() + Exchanges = queueConfig.Exchanges ?? [], + RoutingKeys = queueConfig.RoutingKeys ?? [] }; } diff --git a/src/RabbitMQCoreClient/Models/QueueBase.cs b/src/RabbitMQCoreClient/Models/QueueBase.cs index 76c8e5b..bb4515f 100644 --- a/src/RabbitMQCoreClient/Models/QueueBase.cs +++ b/src/RabbitMQCoreClient/Models/QueueBase.cs @@ -1,9 +1,6 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQCoreClient.Exceptions; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; @@ -59,12 +56,12 @@ protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, /// /// ist of routing keys for the queue. /// - public virtual HashSet RoutingKeys { get; set; } = new HashSet(); + public virtual HashSet RoutingKeys { get; set; } = []; /// /// The list of exchange points to which the queue is bound. /// - public virtual HashSet Exchanges { get; set; } = new HashSet(); + public virtual HashSet Exchanges { get; set; } = []; /// /// Declare the queue on and start consuming messages. @@ -87,11 +84,8 @@ public virtual async Task StartQueue(IChannel channel, AsyncEventingBasicConsume durable: UseQuorum || Durable, exclusive: !UseQuorum && Exclusive, autoDelete: !UseQuorum && AutoDelete, - arguments: Arguments); - - if (declaredQueue is null) - throw new QueueBindException("Queue is not properly bind."); - + arguments: Arguments) + ?? throw new QueueBindException("Queue is not properly bind."); if (RoutingKeys.Count > 0) foreach (var exchangeName in Exchanges) { diff --git a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs index ca37de5..240ca26 100644 --- a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs +++ b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Models; public class RabbitMessageEventArgs : EventArgs diff --git a/src/RabbitMQCoreClient/Models/Subscription.cs b/src/RabbitMQCoreClient/Models/Subscription.cs index cb697b8..974ee9c 100644 --- a/src/RabbitMQCoreClient/Models/Subscription.cs +++ b/src/RabbitMQCoreClient/Models/Subscription.cs @@ -1,6 +1,4 @@ using RabbitMQCoreClient.DependencyInjection.ConfigModels; -using System; -using System.Collections.Generic; namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; @@ -11,18 +9,15 @@ namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; public sealed class Subscription : QueueBase { public Subscription(bool useQuorum = false) - : base($"sub_{Guid.NewGuid().ToString()}", false, true, true, useQuorum) + : base($"sub_{Guid.NewGuid()}", false, true, true, useQuorum) { } - public static Subscription Create(SubscriptionConfig queueConfig) + public static Subscription Create(SubscriptionConfig queueConfig) => new() { - return new Subscription - { - Arguments = queueConfig.Arguments ?? new Dictionary(), - DeadLetterExchange = queueConfig.DeadLetterExchange, - UseQuorum = queueConfig.UseQuorum, - Exchanges = queueConfig.Exchanges ?? new HashSet(), - RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() - }; - } + Arguments = queueConfig.Arguments ?? new Dictionary(), + DeadLetterExchange = queueConfig.DeadLetterExchange, + UseQuorum = queueConfig.UseQuorum, + Exchanges = queueConfig.Exchanges ?? [], + RoutingKeys = queueConfig.RoutingKeys ?? [] + }; } diff --git a/src/RabbitMQCoreClient/QueueService.cs b/src/RabbitMQCoreClient/QueueService.cs index f8ad522..d8da3f7 100644 --- a/src/RabbitMQCoreClient/QueueService.cs +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -7,13 +7,8 @@ using RabbitMQCoreClient.Events; using RabbitMQCoreClient.Exceptions; using RabbitMQCoreClient.Serializers; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; using static RabbitMQCoreClient.Configuration.AppConstants.RabbitMQHeaders; namespace RabbitMQCoreClient; @@ -62,7 +57,7 @@ public QueueService(RabbitMQCoreClientOptions options, ILoggerFactory loggerFact Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); _log = loggerFactory.CreateLogger(); _exchanges = builder.Exchanges; - _serializer = builder.Serializer; + _serializer = builder.Serializer ?? new SystemTextJsonMessageSerializer(); } /// @@ -277,20 +272,17 @@ public ValueTask SendAsync( } /// - public async ValueTask SendAsync( + public ValueTask SendAsync( byte[] obj, BasicProperties props, string routingKey, string? exchange = default, - bool decreaseTtl = true) - { - await SendAsync(new ReadOnlyMemory(obj), + bool decreaseTtl = true) => SendAsync(new ReadOnlyMemory(obj), props: props, exchange: exchange, routingKey: routingKey, decreaseTtl: decreaseTtl ); - } /// public ValueTask SendBatchAsync( @@ -384,7 +376,7 @@ public async ValueTask SendAsync( if (obj.Length > Options.MaxBodySize) { - string decodedString = DecodeMessageAsString(obj); + var decodedString = DecodeMessageAsString(obj); throw new BadMessageException($"The message size \"{obj.Length}\" exceeds max body limit of \"{Options.MaxBodySize}\" " + $"on routing key \"{routingKey}\" (exchange: \"{exchange}\"). Decoded message part: {decodedString}"); } @@ -419,7 +411,7 @@ public async ValueTask SendBatchAsync( const ushort MAX_OUTSTANDING_CONFIRMS = 256; - int batchSize = Math.Max(1, MAX_OUTSTANDING_CONFIRMS / 2); + var batchSize = Math.Max(1, MAX_OUTSTANDING_CONFIRMS / 2); var publishTasks = new List(); @@ -427,7 +419,7 @@ public async ValueTask SendBatchAsync( { if (body.Length > Options.MaxBodySize) { - string decodedString = DecodeMessageAsString(body); + var decodedString = DecodeMessageAsString(body); _log.LogError("Skipped message due to message size '{MessageSize}' exceeds max body limit of '{MaxBodySize}' " + "on routing key '{RoutingKey}' (exchange: '{Exchange}'. Decoded message part: {DecodedString})", @@ -464,7 +456,7 @@ async Task MaybeAwaitPublishes(List publishTasks, int batchSize) { if (publishTasks.Count >= batchSize) { - foreach (ValueTask pt in publishTasks) + foreach (var pt in publishTasks) { try { @@ -506,7 +498,7 @@ async Task Connection_ConnectionShutdown(object? sender, ShutdownEventArgs args) _log.LogError("Connection broke! Reason: {BrokeReason}", args.ReplyText); if (ConnectionShutdownAsync != null) - await ConnectionShutdownAsync.Invoke(sender ?? this, args); + await ConnectionShutdownAsync.Invoke(sender ?? this, args!); await Reconnect(); } @@ -532,15 +524,49 @@ static BasicProperties CreateBasicJsonProperties() void AddTtl(BasicProperties props, bool decreaseTtl) { - if (decreaseTtl) + // Не делаем ничего, если признак “уменьшать TTL” выключен + if (!decreaseTtl) return; + // Убедимся, что словарь заголовков существует + props.Headers ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + // Попробуем получить текущий TTL + var currentTtl = Options.DefaultTtl; // <‑- значение по умолчанию + if (props.Headers.TryGetValue(TtlHeader, out var ttlObj) + && !TryParseInt(ttlObj, out currentTtl)) // Попытка безопасно преобразовать к int { - if (props.Headers == null) - props.Headers = new Dictionary(); + // В случае неправильного типа – оставляем default. + _log.LogWarning("Header '{TtlHeader}' contains invalid value: {TtlObj}", + TtlHeader, ttlObj); + currentTtl = Options.DefaultTtl; + } + // Снижаем TTL, но не допускаем отрицательных значений + currentTtl = Math.Max(currentTtl - 1, 0); + // Обновляем заголовок + props.Headers[TtlHeader] = currentTtl; + } - if (props.Headers.ContainsKey(TtlHeader)) - props.Headers[TtlHeader] = (int)props.Headers[TtlHeader] - 1; - else - props.Headers.Add(TtlHeader, Options.DefaultTtl); + static bool TryParseInt(object? value, out int result) + { + result = 0; + switch (value) + { + case int i: + result = i; + return true; + case long l when l is >= int.MinValue and <= int.MaxValue: + result = (int)l; + return true; + case short s: + result = s; + return true; + case byte b: + result = b; + return true; + case string str when int.TryParse(str, out var parsed): + result = parsed; + return true; + case null: + default: + return false; } } @@ -562,11 +588,11 @@ void CheckSendChannelOpened() static string DecodeMessageAsString(ReadOnlyMemory obj) { int bufferSize = 1024; // We need ~1 KB of text to log. - int decodedStringLength = Math.Min(obj.Length, bufferSize); - ReadOnlySpan slice = obj.Span.Slice(0, decodedStringLength); + var decodedStringLength = Math.Min(obj.Length, bufferSize); + var slice = obj.Span.Slice(0, decodedStringLength); // Find the index of the last complete character - int lastValidIndex = slice.Length - 1; + var lastValidIndex = slice.Length - 1; while (lastValidIndex >= 0 && (slice[lastValidIndex] & 0b11000000) == 0b10000000) { // If a byte is a "continuation" of a UTF-8 character (starts with 10xxxxxx), @@ -579,8 +605,8 @@ static string DecodeMessageAsString(ReadOnlyMemory obj) // Checking the string is UTF8 var decoder = Encoding.UTF8.GetDecoder(); - char[] buffer = new char[decodedStringLength]; - decoder.Convert(slice, buffer, flush: true, out _, out int charsUsed, out bool completed); + var buffer = new char[decodedStringLength]; + decoder.Convert(slice, buffer, flush: true, out _, out var charsUsed, out var completed); if (completed) return new string(buffer, 0, charsUsed); else diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index ce48ece..9525137 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -6,10 +6,8 @@ using RabbitMQCoreClient.Events; using RabbitMQCoreClient.Exceptions; using RabbitMQCoreClient.Models; -using System; -using System.Linq; +using RabbitMQCoreClient.Serializers; using System.Net; -using System.Threading.Tasks; namespace RabbitMQCoreClient; @@ -133,13 +131,13 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) return; } - if (!_builder.RoutingHandlerTypes.ContainsKey(@event.RoutingKey)) + if (!_builder.RoutingHandlerTypes.TryGetValue(@event.RoutingKey, out var result)) { await RejectDueToNoHandler(@event); return; } - var handlerType = _builder.RoutingHandlerTypes[@event.RoutingKey].Type; - var handlerOptions = _builder.RoutingHandlerTypes[@event.RoutingKey].Options; + var handlerType = result.Type; + var handlerOptions = result.Options; // Get the message handler service. using var scope = _scopeFactory.CreateScope(); @@ -152,7 +150,8 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) handler.Options = handlerOptions ?? new(); // If user overrides the default serializer then the custom serializer will be used for the handler. - handler.Serializer = handler.Options.CustomSerializer ?? _builder.Builder.Serializer; + handler.Serializer = handler.Options.CustomSerializer ?? _builder.Builder.Serializer + ?? new SystemTextJsonMessageSerializer(); _log.LogDebug("Created scope for handler type {TypeName}. Start processing message.", handler.GetType().Name); @@ -199,10 +198,8 @@ async Task QueueService_OnReconnected(object? sender, ReconnectEventArgs args) await Start(); } - async Task QueueService_OnConnectionShutdown(object? sender, ShutdownEventArgs args) - { - await StopAndClearConsumer(); - } + Task QueueService_OnConnectionShutdown(object? sender, ShutdownEventArgs args) => + StopAndClearConsumer(); async Task ConfigureDeadLetterExchange() { diff --git a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs index 0e6fd4a..58a7aba 100644 --- a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs @@ -1,5 +1,3 @@ -using System; - namespace RabbitMQCoreClient.Serializers; /// diff --git a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs index f34ae2d..62049be 100644 --- a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs @@ -1,4 +1,3 @@ -using System; using System.Text.Json.Serialization; namespace RabbitMQCoreClient.Serializers; From 21d5cbe4d58f55fac0c9c5ff91ea820df6103997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Mon, 8 Dec 2025 17:34:32 +0300 Subject: [PATCH 04/13] Add new rabbitmq start logic. --- .../Program.cs | 8 +-- .../RabbitMQCoreClient.ConsoleClient.csproj | 4 -- .../Controllers/WeatherForecastController.cs | 37 ------------- samples/RabbitMQCoreClient.WebApp/Program.cs | 45 ++++++++++------ .../Properties/launchSettings.json | 28 +++------- .../RabbitMQCoreClient.WebApp.csproj | 4 +- samples/RabbitMQCoreClient.WebApp/Startup.cs | 52 ------------------- .../WeatherForecast.cs | 14 ----- .../ApplicationBuilderExtentions.cs | 36 ++++++++++--- .../DependencyInjection/RunModes.cs | 22 ++++++++ src/RabbitMQCoreClient/IQueueService.cs | 12 +++-- src/RabbitMQCoreClient/QueueService.cs | 38 ++++++++------ .../RabbitMQCoreClientConsumer.cs | 12 ++++- 13 files changed, 136 insertions(+), 176 deletions(-) delete mode 100644 samples/RabbitMQCoreClient.WebApp/Controllers/WeatherForecastController.cs delete mode 100644 samples/RabbitMQCoreClient.WebApp/Startup.cs delete mode 100644 samples/RabbitMQCoreClient.WebApp/WeatherForecast.cs create mode 100644 src/RabbitMQCoreClient/DependencyInjection/RunModes.cs diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs index 4780918..86835dd 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -67,7 +67,7 @@ var queueService = serviceProvider.GetRequiredService(); var consumer = serviceProvider.GetRequiredService(); var batchSender = serviceProvider.GetRequiredService(); -consumer.Start(); +await consumer.Start(); //var body = new SimpleObj { Name = "test sending" }; //await queueService.SendAsync(body, "test_routing_key"); @@ -89,7 +89,7 @@ // return Task.CompletedTask; // } // })); -CancellationTokenSource source = new CancellationTokenSource(); +using CancellationTokenSource source = new CancellationTokenSource(); //await CreateSender(queueService, source.Token); await CreateBatchSender(batchSender, source.Token); @@ -105,7 +105,7 @@ static async Task CreateSender(IQueueService queueService, CancellationToken tok try { await Task.Delay(1000, token); - var bodyList = Enumerable.Range(1, 1).Select(x => new SimpleObj { Name = $"test sending {x}" }); + var bodyList = Enumerable.Range(1, 2).Select(x => new SimpleObj { Name = $"test sending {x}" }); await queueService.SendBatchAsync(bodyList, "test_routing_key", SimpleObjContext.Default.SimpleObj); } catch (Exception e) @@ -122,7 +122,7 @@ static async Task CreateBatchSender(IQueueEventsBufferEngine batchSender, Cancel try { await Task.Delay(500, token); - var bodyList = Enumerable.Range(1, 1).Select(x => new SimpleObj { Name = $"test sending {x}" }); + var bodyList = Enumerable.Range(1, 2).Select(x => new SimpleObj { Name = $"test sending {x}" }); await batchSender.AddEvents(bodyList, "test_routing_key"); } catch (Exception e) diff --git a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj b/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj index cba8f14..13156fb 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj +++ b/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj @@ -5,10 +5,6 @@ net10.0 - - - - PreserveNewest diff --git a/samples/RabbitMQCoreClient.WebApp/Controllers/WeatherForecastController.cs b/samples/RabbitMQCoreClient.WebApp/Controllers/WeatherForecastController.cs deleted file mode 100644 index f83ef2d..0000000 --- a/samples/RabbitMQCoreClient.WebApp/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace RabbitMQCoreClient.WebApp.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/samples/RabbitMQCoreClient.WebApp/Program.cs b/samples/RabbitMQCoreClient.WebApp/Program.cs index 000b5c6..907c408 100644 --- a/samples/RabbitMQCoreClient.WebApp/Program.cs +++ b/samples/RabbitMQCoreClient.WebApp/Program.cs @@ -1,19 +1,34 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; +using RabbitMQCoreClient; +using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.WebApp; -namespace RabbitMQCoreClient.WebApp; +var builder = WebApplication.CreateBuilder(args); -public class Program -{ - public static void Main(string[] args) +// Just for sending messages. +builder.Services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")); + +// For sending and consuming messages config with subscriptions. +builder.Services + .AddRabbitMQCoreClientConsumer(builder.Configuration.GetSection("RabbitMQ")) + .AddHandler(["test_routing_key"], new ConsumerHandlerOptions { - CreateHostBuilder(args).Build().Run(); - } + RetryKey = "test_routing_key_retry" + }) + .AddHandler(["test_routing_key_subscription"], new ConsumerHandlerOptions + { + RetryKey = "test_routing_key_retry" + }); +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.MapPost("/send", async (IQueueService queueService) => +{ + await queueService.SendAsync(new SimpleObj { Name = "test" }, "test_routing_key"); + return Results.Ok(); +}); + +app.StartRabbitMqCore(app.Lifetime); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); -} +app.Run(); diff --git a/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json b/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json index 4b61b5c..222a322 100644 --- a/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json +++ b/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json @@ -1,27 +1,11 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:2904", - "sslPort": 44346 - } - }, +{ + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "RabbitMQCoreClient.WebApp": { + "http": { "commandName": "Project", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5107", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj b/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj index e054fb4..65fef84 100644 --- a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj +++ b/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj @@ -1,7 +1,9 @@ - + net10.0 + enable + enable diff --git a/samples/RabbitMQCoreClient.WebApp/Startup.cs b/samples/RabbitMQCoreClient.WebApp/Startup.cs deleted file mode 100644 index 0b5872f..0000000 --- a/samples/RabbitMQCoreClient.WebApp/Startup.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace RabbitMQCoreClient.WebApp; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - services - .AddRabbitMQCoreClient(x => x.HostName = "localhost") - .AddExchange("default") - .AddConsumer() - .AddHandler(new[] { "test_routing_key" }) - .AddQueue("test"); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) - { - app.StartRabbitMqCore(lifetime); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} diff --git a/samples/RabbitMQCoreClient.WebApp/WeatherForecast.cs b/samples/RabbitMQCoreClient.WebApp/WeatherForecast.cs deleted file mode 100644 index 07b01f9..0000000 --- a/samples/RabbitMQCoreClient.WebApp/WeatherForecast.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace RabbitMQCoreClient.WebApp; - -public class WeatherForecast -{ - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } -} diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs index 6a1ebbe..7e8b461 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; +using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Exceptions; namespace Microsoft.Extensions.DependencyInjection; @@ -9,13 +11,35 @@ public static class ApplicationBuilderExtentions /// Starts the rabbit mq core client consuming queues. /// The application. /// The lifetime. - public static IApplicationBuilder StartRabbitMqCore(this IApplicationBuilder app, IHostApplicationLifetime lifetime) + /// Describes the mode in which the service should start. + public static IApplicationBuilder StartRabbitMqCore(this IApplicationBuilder app, + IHostApplicationLifetime lifetime, + RunModes runMode = RunModes.PublisherAndConsumer) { - var consumer = app.ApplicationServices.GetService() - ?? throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + - "Add services.AddRabbitMQCoreClient(...).AddConsumer(); to the DI."); - lifetime.ApplicationStarted.Register(() => consumer.Start()); - lifetime.ApplicationStopping.Register(() => consumer.Shutdown()); + if (runMode is RunModes.PublisherAndConsumer) + { + var publisher = app.ApplicationServices.GetService() + ?? throw new ClientConfigurationException("Rabbit MQ Core Client Service is not configured. " + + "Add services.AddRabbitMQCoreClient(...); to the DI."); + var consumer = app.ApplicationServices.GetService() + ?? throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + + "Add services.AddRabbitMQCoreClient(...).AddConsumer(); to the DI."); + lifetime.ApplicationStarted.Register(async () => await consumer.Start()); + lifetime.ApplicationStopping.Register(async () => + { + await consumer.Shutdown(); + await publisher.Shutdown(); + }); + } + else + { + var publisher = app.ApplicationServices.GetService() + ?? throw new ClientConfigurationException("Rabbit MQ Core Client Service is not configured. " + + "Add services.AddRabbitMQCoreClient(...); to the DI."); + lifetime.ApplicationStarted.Register(() => publisher.Connect()); + lifetime.ApplicationStopping.Register(() => publisher.Shutdown()); + } + return app; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/RunModes.cs b/src/RabbitMQCoreClient/DependencyInjection/RunModes.cs new file mode 100644 index 0000000..92bc938 --- /dev/null +++ b/src/RabbitMQCoreClient/DependencyInjection/RunModes.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// Describes the mode in which the service should start. +/// +public enum RunModes +{ + /// + /// Run the service as both a publisher and a consumer. + /// + PublisherAndConsumer = 0, + /// + /// Run the service only as a publisher (no consuming logic) and no pushed channel open. + /// + PublisherOnly = 1 +} diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index e5d927d..c018d5a 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -11,14 +11,14 @@ namespace RabbitMQCoreClient; public interface IQueueService : IAsyncDisposable { /// - /// RabbitMQ connection interface. + /// RabbitMQ connection interface. Null until first connected. /// - IConnection Connection { get; } + IConnection? Connection { get; } /// - /// A channel for sending RabbitMQ data. + /// A channel for sending RabbitMQ data. Null until first connected. /// - IChannel SendChannel { get; } + IChannel? PublishChannel { get; } /// /// MQ service settings. @@ -197,4 +197,8 @@ ValueTask SendJsonBatchAsync( string routingKey, string? exchange = default, bool decreaseTtl = true); + + Task Connect(); + + Task Shutdown(); } diff --git a/src/RabbitMQCoreClient/QueueService.cs b/src/RabbitMQCoreClient/QueueService.cs index d8da3f7..a7ff858 100644 --- a/src/RabbitMQCoreClient/QueueService.cs +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -30,10 +30,10 @@ public sealed class QueueService : IQueueService public RabbitMQCoreClientOptions Options { get; } /// - public IConnection Connection => _connection!; // So far, that's it. The property is completely initialized in the constructor. + public IConnection? Connection => _connection; /// - public IChannel SendChannel => _publishChannel!; // So far, that's it. The property is completely initialized in the constructor. + public IChannel? PublishChannel => _publishChannel; /// public event AsyncEventHandler ReconnectedAsync = default!; @@ -63,7 +63,7 @@ public QueueService(RabbitMQCoreClientOptions options, ILoggerFactory loggerFact /// /// Connects this instance to RabbitMQ. /// - public async Task Connect() + async Task ConnectInternal() { // Protection against repeated calls to the `Connect()` method. if (_connection?.IsOpen == true) @@ -86,6 +86,7 @@ public async Task Connect() Port = Options.Port, VirtualHost = Options.VirtualHost }; + if (Options.SslEnabled) { var ssl = new SslOption @@ -109,7 +110,7 @@ public async Task Connect() _connection.CallbackExceptionAsync += Options.ConnectionCallbackExceptionHandler; _log.LogDebug("Connection opened."); - _publishChannel = await Connection.CreateChannelAsync(); + _publishChannel = await _connection.CreateChannelAsync(); _publishChannel.CallbackExceptionAsync += Channel_CallbackException; await _publishChannel.BasicQosAsync(0, Options.PrefetchCount, false); // Per consumer limit @@ -126,9 +127,9 @@ public async Task Connect() /// /// Close all connections and Cleans up. /// - public async Task Cleanup() + public async Task Shutdown() { - _log.LogInformation("Closing and cleaning up old connection and channels."); + _log.LogInformation("Closing and cleaning up publisher connection and channels."); try { if (_connection != null) @@ -162,10 +163,15 @@ public async Task Cleanup() /// /// Reconnects this instance to RabbitMQ. /// - async Task Reconnect() + public async Task Connect() { - _log.LogInformation("Reconnect requested"); - await Cleanup(); + if (_connection is null) + _log.LogInformation("Start RabbitMQ connection"); + else + { + _log.LogInformation("RabbitMQ reconnect requested"); + await Shutdown(); + } // TODO: возможно нужно заменить эту конструкцию из-за Task. var mres = new ManualResetEventSlim(false); // state is initially false @@ -178,7 +184,7 @@ async Task Reconnect() try { _log.LogInformation("Trying to connect with reconnect attempt {ReconnectAttempt}", _reconnectAttemptsCount); - await Connect(); + await ConnectInternal(); _reconnectAttemptsCount = 0; if (ReconnectedAsync != null) @@ -201,7 +207,7 @@ async Task Reconnect() } /// - public async ValueTask AsyncDispose() => await Cleanup(); + public async ValueTask AsyncDispose() => await Shutdown(); /// public ValueTask SendJsonAsync( @@ -310,7 +316,7 @@ public ValueTask SendJsonBatchAsync( string? exchange = default, bool decreaseTtl = true) { - var messages = new List<(byte[] Body, IBasicProperties Props)>(); + var messages = new List<(ReadOnlyMemory Body, BasicProperties Props)>(); foreach (var json in serializedJsonList) { var props = CreateBasicJsonProperties(); @@ -338,7 +344,7 @@ public ValueTask SendJsonBatchAsync( string? exchange = default, bool decreaseTtl = true) { - var messages = new List<(ReadOnlyMemory Body, IBasicProperties Props)>(); + var messages = new List<(ReadOnlyMemory Body, BasicProperties Props)>(); foreach (var json in serializedJsonList) { var props = CreateBasicJsonProperties(); @@ -385,7 +391,7 @@ public async ValueTask SendAsync( AddTtl(props, decreaseTtl); - await SendChannel.BasicPublishAsync(exchange: exchange, + await _publishChannel.BasicPublishAsync(exchange: exchange, routingKey: routingKey, mandatory: false, // Just not reacting when no queue is subscribed for key. basicProperties: props, @@ -500,7 +506,7 @@ async Task Connection_ConnectionShutdown(object? sender, ShutdownEventArgs args) if (ConnectionShutdownAsync != null) await ConnectionShutdownAsync.Invoke(sender ?? this, args!); - await Reconnect(); + await Connect(); } async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventArgs e) @@ -508,7 +514,7 @@ async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventAr if (e != null) _log.LogError("Connection blocked! Reason: {Reason}", e.Reason); _connectionBlocked = true; - await Reconnect(); + await Connect(); } #endregion diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index 9525137..d070c19 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -66,10 +66,13 @@ public async Task Start() if (_consumer != null && _consumer.IsRunning) return; + if (_queueService.Connection is null || !_queueService.Connection.IsOpen) + await _queueService.Connect(); + if (_queueService.Connection is null || !_queueService.Connection.IsOpen) throw new NotConnectedException("Connection is not opened."); - if (_queueService.SendChannel is null || _queueService.SendChannel.IsClosed) + if (_queueService.PublishChannel is null || _queueService.PublishChannel.IsClosed) throw new NotConnectedException("Send channel is not opened."); if (!_wasSubscribed) @@ -258,8 +261,15 @@ async Task RejectDueToNoHandler(BasicDeliverEventArgs ea) async Task StopAndClearConsumer() { + _log.LogInformation("Closing and cleaning up consumer connection and channels."); try { + if (_queueService != null) + { + _queueService.ReconnectedAsync -= QueueService_OnReconnected; + _queueService.ConnectionShutdownAsync -= QueueService_OnConnectionShutdown; + } + if (_consumer != null) { _consumer.ReceivedAsync -= Consumer_Received; From cdc88236e2a64e62819d0b58df9793be619c4661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Tue, 13 Jan 2026 17:11:08 +0300 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=92=A5=20Replace=20start=20logic=20?= =?UTF-8?q?for=20using=20IHostedService.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 💥 Rename some methods with Async suffix. - ✨ Implement CancellationToken on connect methods. - 🐞 Fix reconnection logic. --- samples/RabbitMQCoreClient.WebApp/Program.cs | 2 - .../ApplicationBuilderExtentions.cs | 46 ----------------- .../Extensions/BuilderExtensions.cs | 3 ++ .../DependencyInjection/IQueueConsumer.cs | 3 +- .../RabbitMQHostedService.cs | 33 ++++++++++++ .../DependencyInjection/RunModes.cs | 22 -------- src/RabbitMQCoreClient/IQueueService.cs | 13 ++++- src/RabbitMQCoreClient/Models/Exchange.cs | 7 ++- src/RabbitMQCoreClient/Models/QueueBase.cs | 15 +++--- src/RabbitMQCoreClient/QueueService.cs | 51 ++++++++++--------- .../RabbitMQCoreClientConsumer.cs | 38 +++++++++----- 11 files changed, 114 insertions(+), 119 deletions(-) delete mode 100644 src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs create mode 100644 src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs delete mode 100644 src/RabbitMQCoreClient/DependencyInjection/RunModes.cs diff --git a/samples/RabbitMQCoreClient.WebApp/Program.cs b/samples/RabbitMQCoreClient.WebApp/Program.cs index 907c408..7ed6ea5 100644 --- a/samples/RabbitMQCoreClient.WebApp/Program.cs +++ b/samples/RabbitMQCoreClient.WebApp/Program.cs @@ -29,6 +29,4 @@ return Results.Ok(); }); -app.StartRabbitMqCore(app.Lifetime); - app.Run(); diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs deleted file mode 100644 index 7e8b461..0000000 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using RabbitMQCoreClient; -using RabbitMQCoreClient.DependencyInjection; -using RabbitMQCoreClient.Exceptions; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class ApplicationBuilderExtentions -{ - /// Starts the rabbit mq core client consuming queues. - /// The application. - /// The lifetime. - /// Describes the mode in which the service should start. - public static IApplicationBuilder StartRabbitMqCore(this IApplicationBuilder app, - IHostApplicationLifetime lifetime, - RunModes runMode = RunModes.PublisherAndConsumer) - { - if (runMode is RunModes.PublisherAndConsumer) - { - var publisher = app.ApplicationServices.GetService() - ?? throw new ClientConfigurationException("Rabbit MQ Core Client Service is not configured. " + - "Add services.AddRabbitMQCoreClient(...); to the DI."); - var consumer = app.ApplicationServices.GetService() - ?? throw new ClientConfigurationException("Rabbit MQ Core Client Consumer is not configured. " + - "Add services.AddRabbitMQCoreClient(...).AddConsumer(); to the DI."); - lifetime.ApplicationStarted.Register(async () => await consumer.Start()); - lifetime.ApplicationStopping.Register(async () => - { - await consumer.Shutdown(); - await publisher.Shutdown(); - }); - } - else - { - var publisher = app.ApplicationServices.GetService() - ?? throw new ClientConfigurationException("Rabbit MQ Core Client Service is not configured. " + - "Add services.AddRabbitMQCoreClient(...); to the DI."); - lifetime.ApplicationStarted.Register(() => publisher.Connect()); - lifetime.ApplicationStopping.Register(() => publisher.Shutdown()); - } - - - return app; - } -} diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index e15a326..ca94840 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -4,6 +4,7 @@ using RabbitMQCoreClient; using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; @@ -19,6 +20,8 @@ public static IRabbitMQCoreClientBuilder AddRequiredPlatformServices(this IRabbi { builder.Services.TryAddSingleton(); + builder.Services.AddHostedService(); + builder.Services.AddOptions(); builder.Services.AddLogging(); diff --git a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs index 4c84449..b1d3bf2 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs @@ -11,8 +11,9 @@ public interface IQueueConsumer : IAsyncDisposable /// /// Connect to all queues and start receiving messages. /// + /// Cancellation token. /// - Task Start(); + Task Start(CancellationToken cancellationToken = default); /// /// Stops listening Queues. diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs new file mode 100644 index 0000000..e38994e --- /dev/null +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace RabbitMQCoreClient.DependencyInjection; + +sealed class RabbitMQHostedService : Microsoft.Extensions.Hosting.IHostedService +{ + readonly IQueueService _publisher; + readonly IQueueConsumer? _consumer; + + public RabbitMQHostedService(IServiceProvider serviceProvider) + { + _publisher = serviceProvider.GetRequiredService(); + _consumer = serviceProvider.GetService(); + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + // If the AddConsumer() method was not called, + // then we do not start the event listening channel. + if (_consumer != null) + await _consumer.Start(cancellationToken); // Consumer starts publisher first. + else + await _publisher.ConnectAsync(cancellationToken); + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_publisher != null) + await _publisher.ShutdownAsync(); // Publisher shuts down consumer too as it is Connection God. + } +} diff --git a/src/RabbitMQCoreClient/DependencyInjection/RunModes.cs b/src/RabbitMQCoreClient/DependencyInjection/RunModes.cs deleted file mode 100644 index 92bc938..0000000 --- a/src/RabbitMQCoreClient/DependencyInjection/RunModes.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace RabbitMQCoreClient.DependencyInjection; - -/// -/// Describes the mode in which the service should start. -/// -public enum RunModes -{ - /// - /// Run the service as both a publisher and a consumer. - /// - PublisherAndConsumer = 0, - /// - /// Run the service only as a publisher (no consuming logic) and no pushed channel open. - /// - PublisherOnly = 1 -} diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index c018d5a..0652bcf 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -198,7 +198,16 @@ ValueTask SendJsonBatchAsync( string? exchange = default, bool decreaseTtl = true); - Task Connect(); + /// + /// Connect to RabbitMQ server and run publisher channel. + /// + /// Cancellation token. + /// + Task ConnectAsync(CancellationToken cancellationToken = default); - Task Shutdown(); + /// + /// Shutdown RabbitMQ connection. + /// + /// + Task ShutdownAsync(); } diff --git a/src/RabbitMQCoreClient/Models/Exchange.cs b/src/RabbitMQCoreClient/Models/Exchange.cs index 49051ff..387611e 100644 --- a/src/RabbitMQCoreClient/Models/Exchange.cs +++ b/src/RabbitMQCoreClient/Models/Exchange.cs @@ -38,11 +38,14 @@ public Exchange(ExchangeOptions options) /// Starts the exchange. /// /// The channel. - public Task StartExchange(IChannel _channel) => _channel.ExchangeDeclareAsync( + /// Cancellation token. + public Task StartExchangeAsync(IChannel _channel, CancellationToken cancellationToken = default) => + _channel.ExchangeDeclareAsync( exchange: Name, type: Options.Type, durable: Options.Durable, autoDelete: Options.AutoDelete, - arguments: Options.Arguments + arguments: Options.Arguments, + cancellationToken: cancellationToken ); } diff --git a/src/RabbitMQCoreClient/Models/QueueBase.cs b/src/RabbitMQCoreClient/Models/QueueBase.cs index bb4515f..9284afd 100644 --- a/src/RabbitMQCoreClient/Models/QueueBase.cs +++ b/src/RabbitMQCoreClient/Models/QueueBase.cs @@ -68,7 +68,7 @@ protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, /// /// /// - public virtual async Task StartQueue(IChannel channel, AsyncEventingBasicConsumer consumer) + public virtual async Task StartQueueAsync(IChannel channel, AsyncEventingBasicConsumer consumer, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(DeadLetterExchange) && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.DeadLetterExchangeHeader)) @@ -84,28 +84,31 @@ public virtual async Task StartQueue(IChannel channel, AsyncEventingBasicConsume durable: UseQuorum || Durable, exclusive: !UseQuorum && Exclusive, autoDelete: !UseQuorum && AutoDelete, - arguments: Arguments) + arguments: Arguments, + cancellationToken: cancellationToken) ?? throw new QueueBindException("Queue is not properly bind."); if (RoutingKeys.Count > 0) foreach (var exchangeName in Exchanges) { - await BindToExchange(channel, declaredQueue, exchangeName); + await BindToExchangeAsync(channel, declaredQueue, exchangeName, cancellationToken); } await channel.BasicConsumeAsync(queue: declaredQueue.QueueName, autoAck: false, consumer: consumer, - consumerTag: $"amq.{declaredQueue.QueueName}.{Guid.NewGuid()}" + consumerTag: $"amq.{declaredQueue.QueueName}.{Guid.NewGuid()}", + cancellationToken: cancellationToken ); } - async Task BindToExchange(IChannel channel, QueueDeclareOk declaredQueue, string exchangeName) + async Task BindToExchangeAsync(IChannel channel, QueueDeclareOk declaredQueue, string exchangeName, CancellationToken cancellationToken = default) { foreach (var route in RoutingKeys) await channel.QueueBindAsync( queue: declaredQueue.QueueName, exchange: exchangeName, - routingKey: route + routingKey: route, + cancellationToken: cancellationToken ); } } diff --git a/src/RabbitMQCoreClient/QueueService.cs b/src/RabbitMQCoreClient/QueueService.cs index a7ff858..076c1ab 100644 --- a/src/RabbitMQCoreClient/QueueService.cs +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -14,7 +14,7 @@ namespace RabbitMQCoreClient; /// -/// Implementations of the . +/// Implementation of the . /// /// public sealed class QueueService : IQueueService @@ -26,6 +26,8 @@ public sealed class QueueService : IQueueService IConnection? _connection; IChannel? _publishChannel; + CancellationToken _serviceLifetimeToken = default; + /// public RabbitMQCoreClientOptions Options { get; } @@ -101,7 +103,7 @@ async Task ConnectInternal() }; factory.Ssl = ssl; } - _connection = await factory.CreateConnectionAsync(); + _connection = await factory.CreateConnectionAsync(_serviceLifetimeToken); _connection.ConnectionShutdownAsync += Connection_ConnectionShutdown; _connection.ConnectionBlockedAsync += Connection_ConnectionBlocked; @@ -110,13 +112,13 @@ async Task ConnectInternal() _connection.CallbackExceptionAsync += Options.ConnectionCallbackExceptionHandler; _log.LogDebug("Connection opened."); - _publishChannel = await _connection.CreateChannelAsync(); + _publishChannel = await _connection.CreateChannelAsync(cancellationToken: _serviceLifetimeToken); _publishChannel.CallbackExceptionAsync += Channel_CallbackException; - await _publishChannel.BasicQosAsync(0, Options.PrefetchCount, false); // Per consumer limit + await _publishChannel.BasicQosAsync(0, Options.PrefetchCount, false, _serviceLifetimeToken); // Per consumer limit foreach (var exchange in _exchanges) { - await exchange.StartExchange(_publishChannel); + await exchange.StartExchangeAsync(_publishChannel, _serviceLifetimeToken); } _connectionBlocked = false; @@ -127,7 +129,7 @@ async Task ConnectInternal() /// /// Close all connections and Cleans up. /// - public async Task Shutdown() + public async Task ShutdownAsync() { _log.LogInformation("Closing and cleaning up publisher connection and channels."); try @@ -160,25 +162,26 @@ public async Task Shutdown() } } - /// - /// Reconnects this instance to RabbitMQ. - /// - public async Task Connect() + /// + public async Task ConnectAsync(CancellationToken cancellationToken = default) { + if (cancellationToken != default) + _serviceLifetimeToken = cancellationToken; + if (_connection is null) _log.LogInformation("Start RabbitMQ connection"); else { _log.LogInformation("RabbitMQ reconnect requested"); - await Shutdown(); + await ShutdownAsync(); } - // TODO: возможно нужно заменить эту конструкцию из-за Task. - var mres = new ManualResetEventSlim(false); // state is initially false - - while (!mres.Wait(Options.ReconnectionTimeout)) // loop until state is true, checking every Options.ReconnectionTimeout + while (true) // loop until state is true, checking every Options.ReconnectionTimeout { - if (Options.ReconnectionAttemptsCount is not null && _reconnectAttemptsCount > Options.ReconnectionAttemptsCount) + cancellationToken.ThrowIfCancellationRequested(); + + if (Options.ReconnectionAttemptsCount is not null + && _reconnectAttemptsCount > Options.ReconnectionAttemptsCount) throw new ReconnectAttemptsExceededException($"Max reconnect attempts {Options.ReconnectionAttemptsCount} reached."); try @@ -190,24 +193,23 @@ public async Task Connect() if (ReconnectedAsync != null) await ReconnectedAsync.Invoke(this, new ReconnectEventArgs()); - break; - //mres.Set(); // state set to true - breaks out of loop + break; // state set to true - breaks out of loop } catch (Exception e) { _reconnectAttemptsCount++; - await Task.Delay(Options.ReconnectionTimeout); + await Task.Delay(Options.ReconnectionTimeout, cancellationToken); string? innerExceptionMessage = null; if (e.InnerException != null) innerExceptionMessage = e.InnerException.Message; - _log.LogCritical(e, "Connection failed. Details: {ErrorMessage}. Reconnect attempts: {ReconnectAttempt}", - e.Message + innerExceptionMessage, _reconnectAttemptsCount); + _log.LogCritical(e, "Connection failed. Details: {ErrorMessage} Reconnect attempts: {ReconnectAttempt}", + e.Message + " " + innerExceptionMessage, _reconnectAttemptsCount); } } } /// - public async ValueTask AsyncDispose() => await Shutdown(); + public async ValueTask AsyncDispose() => await ShutdownAsync(); /// public ValueTask SendJsonAsync( @@ -506,7 +508,7 @@ async Task Connection_ConnectionShutdown(object? sender, ShutdownEventArgs args) if (ConnectionShutdownAsync != null) await ConnectionShutdownAsync.Invoke(sender ?? this, args!); - await Connect(); + await ConnectAsync(); } async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventArgs e) @@ -514,7 +516,7 @@ async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventAr if (e != null) _log.LogError("Connection blocked! Reason: {Reason}", e.Reason); _connectionBlocked = true; - await Connect(); + await ConnectAsync(); } #endregion @@ -622,6 +624,7 @@ static string DecodeMessageAsString(ReadOnlyMemory obj) } } + /// public async ValueTask DisposeAsync() { if (_publishChannel != null) diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index d070c19..552a09d 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -11,12 +11,16 @@ namespace RabbitMQCoreClient; +/// +/// Default RabbitMQ consumer. +/// public sealed class RabbitMQCoreClientConsumer : IQueueConsumer { readonly IQueueService _queueService; readonly IServiceScopeFactory _scopeFactory; readonly IRabbitMQCoreClientConsumerBuilder _builder; readonly ILogger _log; + CancellationToken _serviceLifetimeToken = default; /// /// The RabbitMQ consume messages channel. @@ -61,13 +65,16 @@ public RabbitMQCoreClientConsumer( } /// inheritdoc /> - public async Task Start() + public async Task Start(CancellationToken cancellationToken = default) { + if (cancellationToken != default) + _serviceLifetimeToken = cancellationToken; + if (_consumer != null && _consumer.IsRunning) return; if (_queueService.Connection is null || !_queueService.Connection.IsOpen) - await _queueService.Connect(); + await _queueService.ConnectAsync(cancellationToken); if (_queueService.Connection is null || !_queueService.Connection.IsOpen) throw new NotConnectedException("Connection is not opened."); @@ -82,9 +89,9 @@ public async Task Start() _wasSubscribed = true; } - ConsumeChannel = await _queueService.Connection.CreateChannelAsync(); + ConsumeChannel = await _queueService.Connection.CreateChannelAsync(cancellationToken: cancellationToken); ConsumeChannel.CallbackExceptionAsync += Channel_CallbackException; - await ConsumeChannel.BasicQosAsync(0, _queueService.Options.PrefetchCount, false); // Per consumer limit + await ConsumeChannel.BasicQosAsync(0, _queueService.Options.PrefetchCount, false, cancellationToken); // Per consumer limit await ConnectToAllQueues(); } @@ -110,7 +117,7 @@ async Task ConnectToAllQueues() // Set queue parameters from main configuration. if (_queueService.Options.UseQuorumQueues) queue.UseQuorum = true; - await queue.StartQueue(ConsumeChannel, _consumer); + await queue.StartQueueAsync(ConsumeChannel, _consumer, _serviceLifetimeToken); } _log.LogInformation("Consumer connected to {QueuesCount} queues.", _builder.Queues.Count); } @@ -129,7 +136,7 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) && ttl is int ttlInt && ttlInt <= 0) { - await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false); + await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false, _serviceLifetimeToken); _log.LogDebug("Message was rejected due to low ttl."); return; } @@ -161,7 +168,7 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) try { await handler.HandleMessage(@event.Body, rabbitArgs); - await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false); + await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false, _serviceLifetimeToken); _log.LogDebug("Message successfully processed by handler type {TypeName} " + "with deliveryTag={DeliveryTag}.", handler?.GetType().Name, @event.DeliveryTag); } @@ -171,13 +178,13 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) switch (handler.ErrorMessageRouter.Route) { case Routes.DeadLetter: - await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false); + await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false, _serviceLifetimeToken); _log.LogError(e, "Error message with deliveryTag={DeliveryTag}. " + "Sent to dead letter exchange.", @event.DeliveryTag); break; case Routes.SourceQueue: var decreaseTtl = handler.ErrorMessageRouter.TtlAction == TtlActions.Decrease; - await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false); + await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false, _serviceLifetimeToken); // Forward the message back to the queue, while the TTL of the message is reduced by 1, // depending on the settings of handler.ErrorMessageRouter.TtlAction. @@ -215,7 +222,7 @@ async Task ConfigureDeadLetterExchange() .Select(x => x.DeadLetterExchange!) .Distinct(); - // TODO: Redo the configuration of the dead message queue in the future. So far, hardcode. + // TODO: Rewrite the configuration of the dead message queue in the future. So far, hardcode. const string deadLetterQueueName = "dead_letter"; // We register the queue where the "rejected" messages will be stored. @@ -223,7 +230,8 @@ await ConsumeChannel.QueueDeclareAsync(queue: deadLetterQueueName, durable: true, exclusive: false, autoDelete: false, - arguments: null); + arguments: null, + cancellationToken: _serviceLifetimeToken); var allRoutingKeys = _builder .Queues .SelectMany(x => x.RoutingKeys) @@ -237,7 +245,8 @@ await ConsumeChannel.ExchangeDeclareAsync( type: "direct", durable: true, autoDelete: false, - arguments: null + arguments: null, + cancellationToken: _serviceLifetimeToken ); foreach (var route in allRoutingKeys) @@ -245,7 +254,8 @@ await ConsumeChannel.QueueBindAsync( queue: deadLetterQueueName, exchange: deadLetterEx, routingKey: route, - arguments: null + arguments: null, + cancellationToken: _serviceLifetimeToken ); } } @@ -256,7 +266,7 @@ async Task RejectDueToNoHandler(BasicDeliverEventArgs ea) ea.RoutingKey); if (ConsumeChannel != null) - await ConsumeChannel.BasicNackAsync(ea.DeliveryTag, false, false); + await ConsumeChannel.BasicNackAsync(ea.DeliveryTag, false, false, _serviceLifetimeToken); } async Task StopAndClearConsumer() From ea67e4ca212f1a99098e3584fc36c0065a8f4bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Thu, 15 Jan 2026 17:03:52 +0300 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=92=A5=20Rewrite=20SendAsync=20and?= =?UTF-8?q?=20SendBatchAsync=20methods.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✨ Add cancellation token to SendAsync and SendBatchAsync methods. - 💥 Remove SendJsonAsync methods. - ⚡️ Only core SendAsync and SendBatchAsync methods stay at IQueueService interface, other methods moved to extensions. - 💥 Removed Json Basic Properties method. Now using default BasicProperties with Persistent = true. - 💥 `AddBatchQueueSender()` method moves to IRabbitMqCoreClientBuilder. - 💡 Add a bunch of public method comments. - 📚 Add sample publish project examples. --- CHANGELOG.md | 24 -- README.md | 105 +++---- RabbitMQCoreClient.sln | 32 ++- .../Program.cs | 6 +- .../publish/HostConsole/HostConsole.csproj | 28 ++ samples/publish/HostConsole/Program.cs | 36 +++ .../HostConsole/PublisherBackgroundService.cs | 19 ++ samples/publish/HostConsole/appsettings.json | 20 ++ samples/publish/SimpleConsole/Program.cs | 32 +++ .../SimpleConsole/SimpleConsole.csproj | 28 ++ .../publish/SimpleConsole/appsettings.json | 20 ++ samples/publish/WebApp/Program.cs | 20 ++ .../WebApp/Properties/launchSettings.json | 14 + samples/publish/WebApp/WebApp.csproj | 13 + samples/publish/WebApp/WebApp.http | 6 + samples/publish/WebApp/appsettings.json | 21 ++ .../ServiceCollectionExtensions.cs | 38 +-- .../QueueEventsBufferEngine.cs | 3 +- .../Extensions/BuilderExtensions.cs | 3 + .../IRabbitMQCoreClientBuilder.cs | 5 +- .../IRabbitMQCoreClientConsumerBuilder.cs | 3 + .../Exceptions/BadMessageException.cs | 5 +- .../ClientConfigurationException.cs | 5 +- .../Exceptions/NotConnectedException.cs | 5 +- .../Exceptions/QueueBindException.cs | 3 + .../ReconnectAttemptsExceededException.cs | 3 + .../Extentions/QueueServiceExtensions.cs | 236 +++++++++++++++ ....cs => SystemTextJsonBuilderExtensions.cs} | 5 +- ...> SystemTextJsonQueueServiceExtensions.cs} | 109 +++---- src/RabbitMQCoreClient/IQueueService.cs | 142 ++------- src/RabbitMQCoreClient/Models/QueueBase.cs | 16 +- src/RabbitMQCoreClient/Models/Routes.cs | 3 + src/RabbitMQCoreClient/Models/Subscription.cs | 10 + src/RabbitMQCoreClient/QueueService.cs | 270 ++++++------------ .../RabbitMQCoreClient.csproj | 2 +- .../SystemTextJsonMessageSerializer.cs | 36 ++- 36 files changed, 852 insertions(+), 474 deletions(-) delete mode 100644 CHANGELOG.md create mode 100644 samples/publish/HostConsole/HostConsole.csproj create mode 100644 samples/publish/HostConsole/Program.cs create mode 100644 samples/publish/HostConsole/PublisherBackgroundService.cs create mode 100644 samples/publish/HostConsole/appsettings.json create mode 100644 samples/publish/SimpleConsole/Program.cs create mode 100644 samples/publish/SimpleConsole/SimpleConsole.csproj create mode 100644 samples/publish/SimpleConsole/appsettings.json create mode 100644 samples/publish/WebApp/Program.cs create mode 100644 samples/publish/WebApp/Properties/launchSettings.json create mode 100644 samples/publish/WebApp/WebApp.csproj create mode 100644 samples/publish/WebApp/WebApp.http create mode 100644 samples/publish/WebApp/appsettings.json create mode 100644 src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs rename src/RabbitMQCoreClient/Extentions/{SystemTextJsonBuilderExtentions.cs => SystemTextJsonBuilderExtensions.cs} (84%) rename src/RabbitMQCoreClient/Extentions/{SystemTextJsonQueueServiceExtentions.cs => SystemTextJsonQueueServiceExtensions.cs} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7b426ce..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -### v4.1.0 -- Добавлена возможность изменения настроек сериализатора JSON как для всей конфигурации в целом, -так и для отдельных обработчиков и методов отправки. -- Добавлена возможность задавать ключ маршрутизации, который будет использован при обработке исключений. -Раньше при выбросе исключения сообщение отправлялось обратно в очередь с тем же ключом маршрутизации, с каким оно и пришло. -Теперь ключ можно изменять. - -### v4.0.0 -- Изменен способ конфигурации. Конфигурация для отправки данных и для приема выделены в отдельные методы. -- Добавлен механизм переподключения при сбое подключения. - -### v1.1.1 -- Добавлен метод в интерфейс `RabbitMQCoreClient.IQueueService` для отсылки строковых сообщений (сериализованных в json). -- Добавлены дополнительные поля конфигурации сообщений, которые не могут быть обработаны `ResendFailedMessageEnabled` e `ResendFailedMessageCount`. -- Обновлены зависимости, в частности, базовая библиотека до v 5.0; произведены соответствующие изменения для совместимости; -- Добавлены выбрасываемые исключения при ошибках вызовов и восстановления соединения; -- Удалены избыточности; -- Добавлена возможность определять собственные обработчики ошибок; -- Добавлено логирование ошибок соединения; -- Исправлены некорректные именования и другие неточности в коде; - -### v1.1.0 -- Добавлен метод в интерфейс `RabbitMQCoreClient.IQueueService` для отсылки строковых сообщений (сериализованных в json). -- Добавлены дополнительные поля конфигурации сообщений, которые не могут быть обработаны `ResendFailedMessageEnabled` e `ResendFailedMessageCount`. diff --git a/README.md b/README.md index 5b9cf02..98eb4f9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # RabbitMQ Client library for .net core applications with Dependency Injection support -Library Version: v6 - The library allows you to quickly connect and get started with the RabbitMQ message broker. -The library serializes and deserializes messages to JSON using _System.Text.Json_ as default or _Newtonsoft.Json_. +The library serializes and deserializes messages to JSON using _System.Text.Json_ as default or can use custom serializers. The library allows you to work with multiple queues, connected to various exchanges. It allows you to work with subscriptions. The library implements a custom errored messages mechanism, using the TTL and the dead message queue. @@ -39,64 +37,73 @@ The library allows you to configure parameters both from the configuration file } ``` -*Program.cs - console application* -``` -class Program +*Program.cs - simple console application* + +```csharp +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQCoreClient; + +Console.WriteLine("Simple console message publishing only example"); + +var config = new ConfigurationBuilder() + .AddJsonFile($"appsettings.json", optional: false) + .AddJsonFile($"appsettings.Development.json", optional: true) + .Build(); + +var services = new ServiceCollection(); +services.AddLogging(); +services.AddSingleton(LoggerFactory.Create(x => { - static async Task Main(string[] args) - { - var config = new ConfigurationBuilder() - .AddJsonFile($"appsettings.json", optional: false) - .Build(); + x.SetMinimumLevel(LogLevel.Trace); + x.AddConsole(); +})); - var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(LoggerFactory.Create(x => - { - x.SetMinimumLevel(LogLevel.Trace); - x.AddConsole(); - })); +// Just for sending messages. +services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")); + +var provider = services.BuildServiceProvider(); + +var publisher = provider.GetRequiredService(); + +await publisher.ConnectAsync(); + +await publisher.SendAsync("""{ "foo": "bar" }""", "test_key"); - // Just for sending messages. - services - .AddRabbitMQCoreClient(config); - } -} ``` -*Startup.cs - ASP.NET Core application* +*Program.cs - ASP.NET Core application* ``` -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); +using RabbitMQCoreClient; - ... +var builder = WebApplication.CreateBuilder(args); - // Just for sending messages. - services - .AddRabbitMQCoreClient(config); - } -} +// Add services to the container. +// Just for sending messages. +builder.Services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.MapPost("/send", async (IQueueService publisher) => +{ + await publisher.SendAsync("""{ "foo": "bar" }""", "test_key"); + return Results.Ok(); +}); + +app.Run(); ``` +More examples can be found at `samples` folder in this repository. + The `RabbitMQCoreClient.IQueueService` interface is responsible for sending messages. In order to send a message, it is enough to get the interface `RabbitMQCoreClient.IQueueService` from DI -and use one of the following methods - -```csharp -ValueTask SendAsync(T obj, string routingKey, string exchange = default, bool decreaseTtl = true); -ValueTask SendJsonAsync(string json, string routingKey, string exchange = default, bool decreaseTtl = true); -ValueTask SendAsync(byte[] obj, IBasicProperties props, string routingKey, string exchange, bool decreaseTtl = true); - -// Batch sending -ValueTask SendBatchAsync(IEnumerable objs, string routingKey, string exchange = default, bool decreaseTtl = true); -ValueTask SendJsonBatchAsync(IEnumerable serializedJsonList, string routingKey, string exchange = default, bool decreaseTtl = true); -ValueTask SendBatchAsync(IEnumerable<(byte[] Body, IBasicProperties Props)> objs, string routingKey, string exchange, bool decreaseTtl = true); -``` +and use one of the methods of `SendAsync` or `SendBatchAsync`. In this case, if you do not specify `exchange`, then the default exchange will be used (from the configuration), if configured, otherwise you need to explicitly specify the `exchange` parameter. @@ -108,7 +115,7 @@ Each time a message is re-sending to the queue, for example, due to an exception The message will be sent to the dead message queue if the TTL drops to 0. If you set the parameter `decreaseTtl = false` in the `SendAsync` methods, then the TTL will not be reduced accordingly, -which can lead to an endless message processing cycle. +which can lead to an endless message processing cycle. `decreaseTtl` only can be used in methods with `BasicProperties` argument. The default TTL setting can be defined in the configuration (see the Configuration section). @@ -128,6 +135,7 @@ await queueService.SendBatchAsync(bodyList, "test_routing_key"); ``` #### Buffer messages in memory and send them at separate thread + From the version v5.1.0 there was introduced a new mechanic of the sending messages using separate thread. You can use this feature when you have to send many parallel small messages to the queue (for example from the ASP.NET requests). The feature allows you to buffer that messages at the in-memory list and flush them at once using the `SendBatchAsync` method. @@ -532,6 +540,7 @@ class Program ``` #### Quorum queues at cluster environment + Started from v5.1.0 you can set option `"UseQuorumQueues": true` at root configuration level and `"UseQuorum": true` at queue configuration level. This option adds argument `"x-queue-type": "quorum"` on queue declaration and can be used at the configured cluster environment. diff --git a/RabbitMQCoreClient.sln b/RabbitMQCoreClient.sln index 07e6499..ae19c97 100644 --- a/RabbitMQCoreClient.sln +++ b/RabbitMQCoreClient.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.3.32929.385 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11312.210 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F06C16D8-3CB0-45C7-BC44-EDB4EE4EDBA7}" EndProject @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE = LICENSE .github\workflows\netcorelibrary.yml = .github\workflows\netcorelibrary.yml README.md = README.md + v7-MIGRATION.md = v7-MIGRATION.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient", "src\RabbitMQCoreClient\RabbitMQCoreClient.csproj", "{CE80F1BF-A85A-4282-B461-174A086985F5}" @@ -23,6 +24,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{1393 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.WebApp", "samples\RabbitMQCoreClient.WebApp\RabbitMQCoreClient.WebApp.csproj", "{E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "publish", "publish", "{8C256A46-AE61-4D61-A221-F94741C36310}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "consume", "consume", "{A778C338-849C-4472-AD4A-174298FA492A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleConsole", "samples\publish\SimpleConsole\SimpleConsole.csproj", "{656DAD79-76E8-4E5D-8C29-3BC6CAF75731}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostConsole", "samples\publish\HostConsole\HostConsole.csproj", "{7BBECA17-DCDD-469C-ACE6-E227360EA257}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp", "samples\publish\WebApp\WebApp.csproj", "{E9E078E3-7167-4C11-807E-00324D513C22}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +56,18 @@ Global {E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}.Release|Any CPU.Build.0 = Release|Any CPU + {656DAD79-76E8-4E5D-8C29-3BC6CAF75731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {656DAD79-76E8-4E5D-8C29-3BC6CAF75731}.Debug|Any CPU.Build.0 = Debug|Any CPU + {656DAD79-76E8-4E5D-8C29-3BC6CAF75731}.Release|Any CPU.ActiveCfg = Release|Any CPU + {656DAD79-76E8-4E5D-8C29-3BC6CAF75731}.Release|Any CPU.Build.0 = Release|Any CPU + {7BBECA17-DCDD-469C-ACE6-E227360EA257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BBECA17-DCDD-469C-ACE6-E227360EA257}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BBECA17-DCDD-469C-ACE6-E227360EA257}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BBECA17-DCDD-469C-ACE6-E227360EA257}.Release|Any CPU.Build.0 = Release|Any CPU + {E9E078E3-7167-4C11-807E-00324D513C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9E078E3-7167-4C11-807E-00324D513C22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9E078E3-7167-4C11-807E-00324D513C22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9E078E3-7167-4C11-807E-00324D513C22}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -54,6 +77,11 @@ Global {B35BE626-60CC-4EAC-90FB-422885DC3506} = {F06C16D8-3CB0-45C7-BC44-EDB4EE4EDBA7} {5925B853-0BC5-48D8-86DE-43943E5943D3} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} {E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} + {8C256A46-AE61-4D61-A221-F94741C36310} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} + {A778C338-849C-4472-AD4A-174298FA492A} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} + {656DAD79-76E8-4E5D-8C29-3BC6CAF75731} = {8C256A46-AE61-4D61-A221-F94741C36310} + {7BBECA17-DCDD-469C-ACE6-E227360EA257} = {8C256A46-AE61-4D61-A221-F94741C36310} + {E9E078E3-7167-4C11-807E-00324D513C22} = {8C256A46-AE61-4D61-A221-F94741C36310} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4CC19D97-DE19-4541-802F-F841284C47C7} diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs index 86835dd..b393f63 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -34,6 +34,7 @@ // Just for sending messages. services .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")) + .AddBatchQueueSender() .AddSystemTextJson(x => x.PropertyNamingPolicy = JsonNamingPolicy.CamelCase); // For sending and consuming messages config with subscriptions. @@ -48,8 +49,6 @@ RetryKey = "test_routing_key_retry" }); - services.AddBatchQueueSender(); - // For sending and consuming messages full configuration. //services // .AddRabbitMQCoreClient(config) @@ -91,13 +90,14 @@ // })); using CancellationTokenSource source = new CancellationTokenSource(); //await CreateSender(queueService, source.Token); -await CreateBatchSender(batchSender, source.Token); //var bodyList = Enumerable.Range(1, 1).Select(x => new SimpleObj { Name = $"test sending {x}" }); //await queueService.SendBatchAsync(bodyList, "test_routing_key", jsonSerializerSettings: new Newtonsoft.Json.JsonSerializerSettings()).AsTask(); +await CreateBatchSender(batchSender, source.Token); await host.RunAsync(); + static async Task CreateSender(IQueueService queueService, CancellationToken token) { while (!token.IsCancellationRequested) diff --git a/samples/publish/HostConsole/HostConsole.csproj b/samples/publish/HostConsole/HostConsole.csproj new file mode 100644 index 0000000..1334343 --- /dev/null +++ b/samples/publish/HostConsole/HostConsole.csproj @@ -0,0 +1,28 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + PreserveNewest + true + PreserveNewest + + + + + + PreserveNewest + + + + diff --git a/samples/publish/HostConsole/Program.cs b/samples/publish/HostConsole/Program.cs new file mode 100644 index 0000000..0140747 --- /dev/null +++ b/samples/publish/HostConsole/Program.cs @@ -0,0 +1,36 @@ +using HostConsole; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +Console.WriteLine("Host console message publishing only example"); + +using IHost host = new HostBuilder() + .ConfigureHostConfiguration(configHost => + { + configHost.AddCommandLine(args); + + configHost + .AddJsonFile($"appsettings.json", optional: false) + .AddJsonFile($"appsettings.Development.json", optional: false); + }) + .ConfigureServices((builder, services) => + { + services.AddLogging(); + services.AddSingleton(LoggerFactory.Create(x => + { + x.SetMinimumLevel(LogLevel.Trace); + x.AddConsole(); + })); + + // Just for sending messages. + services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")); + + services.AddHostedService(); + }) + .UseConsoleLifetime() + .Build(); + +await host.RunAsync(); diff --git a/samples/publish/HostConsole/PublisherBackgroundService.cs b/samples/publish/HostConsole/PublisherBackgroundService.cs new file mode 100644 index 0000000..467a564 --- /dev/null +++ b/samples/publish/HostConsole/PublisherBackgroundService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Hosting; +using RabbitMQCoreClient; + +namespace HostConsole; + +class PublisherBackgroundService : BackgroundService +{ + readonly IQueueService _publisher; + + public PublisherBackgroundService(IQueueService publisher) + { + _publisher = publisher; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _publisher.SendAsync("""{ "foo": "bar" }""", "test_key", cancellationToken: stoppingToken); + } +} diff --git a/samples/publish/HostConsole/appsettings.json b/samples/publish/HostConsole/appsettings.json new file mode 100644 index 0000000..5d3363a --- /dev/null +++ b/samples/publish/HostConsole/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "RabbitMQ": { + "HostName": "rabbit-1", + "UserName": "user", + "Password": "password", + "Exchanges": [ + { + "Name": "direct_exchange", + "IsDefault": true + } + ] + } +} diff --git a/samples/publish/SimpleConsole/Program.cs b/samples/publish/SimpleConsole/Program.cs new file mode 100644 index 0000000..40cf7ac --- /dev/null +++ b/samples/publish/SimpleConsole/Program.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQCoreClient; +using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; + +Console.WriteLine("Simple console message publishing only example"); + +var config = new ConfigurationBuilder() + .AddJsonFile($"appsettings.json", optional: false) + .AddJsonFile($"appsettings.Development.json", optional: true) + .Build(); + +var services = new ServiceCollection(); +services.AddLogging(); +services.AddSingleton(LoggerFactory.Create(x => +{ + x.SetMinimumLevel(LogLevel.Trace); + x.AddConsole(); +})); + +// Just for sending messages. +services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(); + +var provider = services.BuildServiceProvider(); + +var publisher = provider.GetRequiredService(); + +await publisher.ConnectAsync(); + +await publisher.SendAsync("""{ "foo": "bar" }""", "test_key"); diff --git a/samples/publish/SimpleConsole/SimpleConsole.csproj b/samples/publish/SimpleConsole/SimpleConsole.csproj new file mode 100644 index 0000000..839fb08 --- /dev/null +++ b/samples/publish/SimpleConsole/SimpleConsole.csproj @@ -0,0 +1,28 @@ + + + + Exe + net10.0 + enable + enable + + + + + PreserveNewest + true + PreserveNewest + + + + + + + + + + PreserveNewest + + + + diff --git a/samples/publish/SimpleConsole/appsettings.json b/samples/publish/SimpleConsole/appsettings.json new file mode 100644 index 0000000..5d3363a --- /dev/null +++ b/samples/publish/SimpleConsole/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "RabbitMQ": { + "HostName": "rabbit-1", + "UserName": "user", + "Password": "password", + "Exchanges": [ + { + "Name": "direct_exchange", + "IsDefault": true + } + ] + } +} diff --git a/samples/publish/WebApp/Program.cs b/samples/publish/WebApp/Program.cs new file mode 100644 index 0000000..a59289d --- /dev/null +++ b/samples/publish/WebApp/Program.cs @@ -0,0 +1,20 @@ +using RabbitMQCoreClient; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Just for sending messages. +builder.Services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.MapPost("/send", async (IQueueService publisher) => +{ + await publisher.SendAsync("""{ "foo": "bar" }""", "test_key"); + return Results.Ok(); +}); + +app.Run(); diff --git a/samples/publish/WebApp/Properties/launchSettings.json b/samples/publish/WebApp/Properties/launchSettings.json new file mode 100644 index 0000000..35b4d46 --- /dev/null +++ b/samples/publish/WebApp/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5197", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/publish/WebApp/WebApp.csproj b/samples/publish/WebApp/WebApp.csproj new file mode 100644 index 0000000..860aadf --- /dev/null +++ b/samples/publish/WebApp/WebApp.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/samples/publish/WebApp/WebApp.http b/samples/publish/WebApp/WebApp.http new file mode 100644 index 0000000..3ed34d5 --- /dev/null +++ b/samples/publish/WebApp/WebApp.http @@ -0,0 +1,6 @@ +@WebApp_HostAddress = http://localhost:5197 + +GET {{WebApp_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/publish/WebApp/appsettings.json b/samples/publish/WebApp/appsettings.json new file mode 100644 index 0000000..ac60e29 --- /dev/null +++ b/samples/publish/WebApp/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "RabbitMQ": { + "HostName": "rabbit-1", + "UserName": "user", + "Password": "password", + "Exchanges": [ + { + "Name": "direct_exchange", + "IsDefault": true + } + ] + } +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index 860ca2a..65365d4 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace RabbitMQCoreClient.BatchQueueSender.DependencyInjection; @@ -11,8 +12,8 @@ public static class ServiceCollectionExtensions { static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) { - services.AddTransient(); - services.AddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(); return services; } @@ -21,17 +22,18 @@ static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection servic /// Injected service can be used to send messages /// to the inmemory queue and process them at the separate thread. /// - /// List of services registered in DI. + /// The RabbitMQ Client builder. /// Configuration section containing fields for configuring the message processing service. /// Use this method if you need to override the configuration. - public static IServiceCollection AddBatchQueueSender( - this IServiceCollection services, + public static IRabbitMQCoreClientBuilder AddBatchQueueSender( + this IRabbitMQCoreClientBuilder builder, IConfiguration configuration, Action? setupAction = null) { - RegisterOptions(services, configuration, setupAction); + RegisterOptions(builder.Services, configuration, setupAction); - return services.AddBatchQueueSenderCore(); + builder.Services.AddBatchQueueSenderCore(); + return builder; } /// @@ -39,15 +41,17 @@ public static IServiceCollection AddBatchQueueSender( /// Injected service can be used to send messages /// to the inmemory queue and process them at the separate thread. /// - /// List of services registered in DI. + /// The RabbitMQ Client builder. /// Use this method if you need to override the configuration. /// - public static IServiceCollection AddBatchQueueSender(this IServiceCollection services, + public static IRabbitMQCoreClientBuilder AddBatchQueueSender(this IRabbitMQCoreClientBuilder builder, Action? setupAction) { if (setupAction != null) - services.Configure(setupAction); - return services.AddBatchQueueSenderCore(); + builder.Services.Configure(setupAction); + builder.Services.AddBatchQueueSenderCore(); + + return builder; } /// @@ -55,12 +59,14 @@ public static IServiceCollection AddBatchQueueSender(this IServiceCollection ser /// Injected service can be used to send messages /// to the inmemory queue and process them at the separate thread. /// - /// List of services registered in DI. + /// List of services registered in DI. /// - public static IServiceCollection AddBatchQueueSender(this IServiceCollection services) + public static IRabbitMQCoreClientBuilder AddBatchQueueSender(this IRabbitMQCoreClientBuilder builder) { - services.AddSingleton((x) => Options.Create(new QueueBatchSenderOptions())); - return services.AddBatchQueueSenderCore(); + builder.Services.TryAddSingleton((x) => Options.Create(new QueueBatchSenderOptions())); + builder.Services.AddBatchQueueSenderCore(); + + return builder; } static void RegisterOptions(IServiceCollection services, @@ -71,6 +77,6 @@ static void RegisterOptions(IServiceCollection services, setupAction?.Invoke(instance); var options = Options.Create(instance); - services.AddSingleton((x) => options); + services.TryAddSingleton((x) => options); } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs index 544172b..e448b6d 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs @@ -157,7 +157,8 @@ void Dispose(bool disposing) _disposedValue = true; } } - + + /// public void Dispose() { // Do not change this code. Place the cleanup code in the "Dispose(bool disposing)" method. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index ca94840..6d2cf60 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -10,6 +10,9 @@ namespace Microsoft.Extensions.DependencyInjection; +/// +/// extension methods. +/// public static class BuilderExtensions { /// diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs index dc724d3..af5fd1a 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs @@ -3,6 +3,9 @@ namespace Microsoft.Extensions.DependencyInjection; +/// +/// RabbitMQCoreClient builder. +/// public interface IRabbitMQCoreClientBuilder { /// @@ -21,7 +24,7 @@ public interface IRabbitMQCoreClientBuilder Exchange? DefaultExchange { get; } /// - /// The default JSON serializer. + /// The default message serializer. /// IMessageSerializer? Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs index c191822..2638a3b 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs @@ -2,6 +2,9 @@ namespace Microsoft.Extensions.DependencyInjection; +/// +/// Consumer builder interface. +/// public interface IRabbitMQCoreClientConsumerBuilder { /// diff --git a/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs b/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs index a71b7d1..9be86ba 100644 --- a/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs +++ b/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs @@ -1,6 +1,9 @@ namespace RabbitMQCoreClient.Exceptions; -public class BadMessageException : Exception +/// +/// Create new object of . +/// +public sealed class BadMessageException : Exception { /// Initializes a new instance of the class. public BadMessageException() diff --git a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs index 33700e6..656a15a 100644 --- a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs @@ -1,6 +1,9 @@ namespace RabbitMQCoreClient.Exceptions; -public class ClientConfigurationException : Exception +/// +/// Create new object of . +/// +public sealed class ClientConfigurationException : Exception { /// Initializes a new instance of the class with a specified error message. /// The message that describes the error. diff --git a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs index da3bcd5..b051daf 100644 --- a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs +++ b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs @@ -1,6 +1,9 @@ namespace RabbitMQCoreClient.Exceptions; -public class NotConnectedException : Exception +/// +/// Create new object of . +/// +public sealed class NotConnectedException : Exception { /// Initializes a new instance of the class with a specified error message. /// The message that describes the error. diff --git a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs index 17c27ed..f62bd1a 100644 --- a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs +++ b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs @@ -1,5 +1,8 @@ namespace RabbitMQCoreClient.Exceptions; +/// +/// Create new object of . +/// public class QueueBindException : Exception { /// Initializes a new instance of the class with a specified error message. diff --git a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs index 5b62a51..4c438ed 100644 --- a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs @@ -1,5 +1,8 @@ namespace RabbitMQCoreClient.Exceptions; +/// +/// Create new object of . +/// public class ReconnectAttemptsExceededException : Exception { /// Initializes a new instance of the class with a specified error message. diff --git a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs new file mode 100644 index 0000000..5cf9f78 --- /dev/null +++ b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs @@ -0,0 +1,236 @@ +using System.Text; + +namespace RabbitMQCoreClient; + +/// +/// Extended publish methods. +/// +public static class QueueServiceExtensions +{ + /// + /// Send the message to the queue. will be serialized with default serializer. + /// + /// The class type of the message. + /// The object. + /// An instance of the class that will be serialized with default serializer and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + public static ValueTask SendAsync( + this IQueueService service, + T obj, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default + ) + where T : class + { + if (obj is null) + throw new ArgumentNullException(nameof(obj)); + + var serializedObj = service.Serializer.Serialize(obj); + return service.SendAsync( + serializedObj, + props: QueueService.CreateDefaultProperties(), + exchange: exchange, + routingKey: routingKey, + cancellationToken: cancellationToken + ); + } + + /// + /// Send a raw message to the queue with the default properties. + /// + /// The object. + /// An array of bytes to be sent to the queue as the body of the message. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + /// obj - obj is null + public static ValueTask SendAsync( + this IQueueService service, + ReadOnlyMemory obj, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) => + service.SendAsync(obj, + props: QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + decreaseTtl: false, + cancellationToken: cancellationToken + ); + + /// + /// Send a bytes array message to the queue with the default properties. + /// + /// The object. + /// An array of bytes to be sent to the queue as the body of the message. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + /// obj - obj is null + public static ValueTask SendAsync( + this IQueueService service, + byte[] obj, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) => + service.SendAsync(new ReadOnlyMemory(obj), + props: QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + decreaseTtl: false, + cancellationToken: cancellationToken + ); + + /// + /// Send a string message to the queue with the default properties. + /// + /// The object. + /// A string to be sent to the queue as the body of the message. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + /// obj - obj is null + public static ValueTask SendAsync( + this IQueueService service, + string obj, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(obj)) + throw new ArgumentException($"{nameof(obj)} is null or empty.", nameof(obj)); + + var body = Encoding.UTF8.GetBytes(obj).AsMemory(); + + //_log.LogDebug("Sending json message '{Message}' to exchange '{Exchange}' " + + // "with routing key '{RoutingKey}'.", json, exchange, routingKey); + + return service.SendAsync(body, + props: QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + decreaseTtl: false, + cancellationToken: cancellationToken + ); + } + + /// + /// Send messages pack to the queue. will be serialized with default serializer. + /// + /// The class type of the message. + /// The object. + /// A list of objects that are instances of the class + /// that will be serialized with default serializer and sent to the queue. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + /// obj + public static ValueTask SendBatchAsync( + this IQueueService service, + IEnumerable objs, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default + ) where T : class => + service.SendBatchAsync( + objs: objs.Select(x => service.Serializer.Serialize(x)), + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + + /// + /// Send a batch of bytes array messages to the queue with default properties. + /// + /// The object. + /// List of objects to be sent to the queue as the body of the message. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + public static ValueTask SendBatchAsync( + this IQueueService service, + IEnumerable objs, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) => + service.SendBatchAsync( + objs: objs.Select(x => new ReadOnlyMemory(x)), + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + + /// + /// Send a batch of string messages to the queue with default properties. + /// + /// The object. + /// List of objects to be sent to the queue as the body of the message. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + public static ValueTask SendBatchAsync( + this IQueueService service, + IEnumerable objs, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) + { + //_log.LogDebug("Sending json messages batch to exchange '{Exchange}' " + + // "with routing key '{RoutingKey}'.", exchange, routingKey); + + return service.SendBatchAsync( + objs: objs.Select(x => new ReadOnlyMemory(Encoding.UTF8.GetBytes(x))), + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + } + + /// + /// Send a batch of string messages to the queue with default properties. + /// + /// The object. + /// List of objects to be sent to the queue as the body of the message. + /// The routing key with which the message will be sent. + /// The name of the exchange point to which the message is to be sent. + /// Cancellation token. + /// + public static ValueTask SendBatchAsync( + this IQueueService service, + IEnumerable> objs, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) + { + //_log.LogDebug("Sending json messages batch to exchange '{Exchange}' " + + // "with routing key '{RoutingKey}'.", exchange, routingKey); + + return service.SendBatchAsync( + objs: objs, + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + } +} diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtensions.cs similarity index 84% rename from src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs rename to src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtensions.cs index be96763..8b3a0c0 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtensions.cs @@ -3,7 +3,10 @@ namespace Microsoft.Extensions.DependencyInjection; -public static class SystemTextJsonBuilderExtentions +/// +/// RabbitMQClient builder extensions for System.Text.Json serializer support. +/// +public static class SystemTextJsonBuilderExtensions { /// /// Use System.Text.Json serializer as default serializer for the RabbitMQ messages. diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs similarity index 58% rename from src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs rename to src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs index daf1cd9..05fe03f 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs @@ -4,156 +4,141 @@ namespace RabbitMQCoreClient; -public static class SystemTextJsonQueueServiceExtentions +/// +/// Extended System.Text.Json publish methods. +/// +public static class SystemTextJsonQueueServiceExtensions { /// - /// Send the message to the queue (thread safe method). will be serialized to Json. + /// Send the message to the queue with serialization to Json. /// /// The class type of the message. - /// The service object. + /// The service object. /// An instance of the class that will be serialized to JSON and sent to the queue. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. /// The json serializer settings. + /// Cancellation token. /// - /// jsonString - jsonString - /// or - /// exchange - exchange /// obj public static ValueTask SendAsync( - this IQueueService queueService, + this IQueueService service, T obj, string routingKey, JsonSerializerOptions? jsonSerializerSettings, string? exchange = default, - bool decreaseTtl = true - ) + CancellationToken cancellationToken = default + ) where T: class { - // Checking for Null without boxing. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) + if (obj is null) throw new ArgumentNullException(nameof(obj)); var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions); - return queueService.SendJsonAsync( + return QueueServiceExtensions.SendAsync(service, serializedObj, - exchange: exchange, routingKey: routingKey, - decreaseTtl: decreaseTtl + exchange: exchange, + cancellationToken: cancellationToken ); } /// - /// Send the message to the queue (thread safe method). will be serialized to Json with source generator. + /// Send the message to the queue with serialization to Json by source generator. /// /// The class type of the message. - /// The service object. + /// The service object. /// An instance of the class that will be serialized to JSON and sent to the queue. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. /// Metadata about the type to convert. + /// Cancellation token. /// - /// jsonString - jsonString - /// or - /// exchange - exchange /// obj public static ValueTask SendAsync( - this IQueueService queueService, + this IQueueService service, T obj, string routingKey, JsonTypeInfo jsonTypeInfo, string? exchange = default, - bool decreaseTtl = true - ) + CancellationToken cancellationToken = default + ) where T : class { - // Checking for Null without boxing. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) + if (obj is null) throw new ArgumentNullException(nameof(obj)); var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo); - return queueService.SendJsonAsync( + return QueueServiceExtensions.SendAsync(service, serializedObj, - exchange: exchange, routingKey: routingKey, - decreaseTtl: decreaseTtl + exchange: exchange, + cancellationToken: cancellationToken ); } /// - /// Send pack of messages to the queue (thread safe method). will be serialized to Json. + /// Send pack of messages to the queue with serialization to Json. /// /// The class type of the message. - /// The service object. + /// The service object. /// A list of objects that are instances of the class /// that will be serialized to JSON and sent to the queue. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. /// The json serializer settings. + /// Cancellation token. /// - /// jsonString - jsonString - /// or - /// exchange - exchange /// obj public static ValueTask SendBatchAsync( - this IQueueService queueService, + this IQueueService service, IEnumerable objs, string routingKey, JsonSerializerOptions? jsonSerializerSettings, string? exchange = default, - bool decreaseTtl = true) + CancellationToken cancellationToken = default + ) where T : class { - var messages = new List>(); var serializeSettings = jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions; - foreach (var obj in objs) - { - messages.Add(JsonSerializer.SerializeToUtf8Bytes(obj, serializeSettings)); - } + var messages = objs.Select(x => new ReadOnlyMemory( + JsonSerializer.SerializeToUtf8Bytes(x, serializeSettings))); - return queueService.SendJsonBatchAsync( - serializedJsonList: messages, - exchange: exchange, + return service.SendBatchAsync( + objs: messages, routingKey: routingKey, - decreaseTtl: decreaseTtl + exchange: exchange, + cancellationToken: cancellationToken ); } /// - /// Send pack of messages to the queue (thread safe method). will be serialized to Json with source generator. + /// Send pack of messages to the queue with serialization to Json by source generator. /// /// The class type of the message. - /// The service object. + /// The service object. /// A list of objects that are instances of the class /// that will be serialized to JSON and sent to the queue. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. /// Metadata about the type to convert. + /// Cancellation token. /// - /// jsonString - jsonString - /// or - /// exchange - exchange /// obj public static ValueTask SendBatchAsync( - this IQueueService queueService, + this IQueueService service, IEnumerable objs, string routingKey, JsonTypeInfo jsonTypeInfo, string? exchange = default, - bool decreaseTtl = true) + CancellationToken cancellationToken = default + ) where T : class { - var messages = new List>(); - foreach (var obj in objs) - { - messages.Add(JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo)); - } + var messages = objs.Select(x => new ReadOnlyMemory( + JsonSerializer.SerializeToUtf8Bytes(x, jsonTypeInfo))); - return queueService.SendJsonBatchAsync( - serializedJsonList: messages, + return service.SendBatchAsync( + objs: messages, exchange: exchange, routingKey: routingKey, - decreaseTtl: decreaseTtl + cancellationToken: cancellationToken ); } } diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index 0652bcf..a7f1210 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -2,6 +2,7 @@ using RabbitMQ.Client.Events; using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Events; +using RabbitMQCoreClient.Serializers; namespace RabbitMQCoreClient; @@ -25,6 +26,11 @@ public interface IQueueService : IAsyncDisposable /// RabbitMQCoreClientOptions Options { get; } + /// + /// Message serializer to be used to serialize objects to sent to queue. + /// + IMessageSerializer Serializer { get; } + /// /// Occurs when connection restored after reconnect. /// @@ -36,86 +42,14 @@ public interface IQueueService : IAsyncDisposable event AsyncEventHandler ConnectionShutdownAsync; /// - /// Send a message to the queue (thread safe method). - /// - /// The json. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonAsync( - string json, - string routingKey, - string? exchange = default, - bool decreaseTtl = true); - - /// - /// Send a message to the queue (thread safe method). - /// - /// The json converted to UTF-8 bytes array. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonAsync( - ReadOnlyMemory jsonBytes, - string routingKey, - string? exchange = default, - bool decreaseTtl = true); - - /// - /// Send the message to the queue (thread safe method). will be serialized to Json. - /// - /// The class type of the message. - /// An instance of the class that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If set to true [decrease TTL]. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - ValueTask SendAsync( - T obj, - string routingKey, - string? exchange = default, - bool decreaseTtl = true - ); - - /// - /// Send a raw message to the queue with the specified properties (thread safe). - /// - /// An array of bytes to be sent to the queue as the body of the message. - /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendAsync( - byte[] obj, - BasicProperties props, - string routingKey, - string? exchange = default, - bool decreaseTtl = true); - - /// - /// Send a raw message to the queue with the specified properties (thread safe). + /// Send a raw message to the queue with the specified properties . /// /// An array of bytes to be sent to the queue as the body of the message. /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. /// If true then decrease TTL. + /// Cancellation token. /// /// jsonString - jsonString /// or @@ -125,35 +59,17 @@ ValueTask SendAsync( BasicProperties props, string routingKey, string? exchange = default, - bool decreaseTtl = true); - - /// - /// Send messages pack to the queue (thread safe method). will be serialized to Json. - /// - /// The class type of the message. - /// A list of objects that are instances of the class - /// that will be serialized to JSON and sent to the queue. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// if set to true [decrease TTL]. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - /// obj - ValueTask SendBatchAsync( - IEnumerable objs, - string routingKey, - string? exchange = default, - bool decreaseTtl = true); + bool decreaseTtl = true, + CancellationToken cancellationToken = default); /// - /// Send a batch raw message to the queue with the specified properties (thread safe). + /// Send a batch raw message to the queue with the specified properties. /// /// List of objects and settings that will be sent to the queue. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. /// If true then decrease TTL. + /// Cancellation token. /// /// jsonString - jsonString /// or @@ -162,41 +78,27 @@ ValueTask SendBatchAsync( IEnumerable<(ReadOnlyMemory Body, BasicProperties Props)> objs, string routingKey, string? exchange = default, - bool decreaseTtl = true); + bool decreaseTtl = true, + CancellationToken cancellationToken = default); /// - /// Send batch messages to the queue (thread safe method). + /// Send a batch raw message to the queue with single properties for all messages. /// - /// A list of serialized json to be sent to the queue in batch. - /// The routing key with which the message will be sent. - /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. - /// - /// jsonString - jsonString - /// or - /// exchange - exchange - ValueTask SendJsonBatchAsync( - IEnumerable serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true); - - /// - /// Send batch messages to the queue (thread safe method). - /// - /// A list of serialized json to be sent to the queue in batch. + /// List of objects and settings that will be sent to the queue. + /// Message properties such as add. headers. Can be created via `Channel.CreateBasicProperties()`. /// The routing key with which the message will be sent. /// The name of the exchange point to which the message is to be sent. - /// If true then decrease TTL. + /// Cancellation token. /// /// jsonString - jsonString /// or /// exchange - exchange - ValueTask SendJsonBatchAsync( - IEnumerable> serializedJsonList, + ValueTask SendBatchAsync( + IEnumerable> objs, + BasicProperties props, string routingKey, string? exchange = default, - bool decreaseTtl = true); + CancellationToken cancellationToken = default); /// /// Connect to RabbitMQ server and run publisher channel. diff --git a/src/RabbitMQCoreClient/Models/QueueBase.cs b/src/RabbitMQCoreClient/Models/QueueBase.cs index 9284afd..19d2aac 100644 --- a/src/RabbitMQCoreClient/Models/QueueBase.cs +++ b/src/RabbitMQCoreClient/Models/QueueBase.cs @@ -9,6 +9,15 @@ namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; /// public abstract class QueueBase { + /// + /// Create new object of . + /// + /// The queue Name. If null, then the name will be automatically chosen. + /// If true, the queue will be saved on disc. + /// If true, then the queue will be used by single service and will be deleted after client will disconnect. + /// Except is true. Then the queue will be created with be created with header + /// If true, the queue will be automatically deleted on client disconnect. + /// While creating the queue use parameter "x-queue-type": "quorum". protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, bool useQuorum) { Name = name; @@ -30,6 +39,7 @@ protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, /// /// If true, then the queue will be used by single service and will be deleted after client will disconnect. + /// Except is true. Then the queue will be created with be created with header /// public virtual bool Exclusive { get; protected set; } @@ -66,9 +76,9 @@ protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, /// /// Declare the queue on and start consuming messages. /// - /// - /// - public virtual async Task StartQueueAsync(IChannel channel, AsyncEventingBasicConsumer consumer, CancellationToken cancellationToken = default) + public virtual async Task StartQueueAsync(IChannel channel, + AsyncEventingBasicConsumer consumer, + CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(DeadLetterExchange) && !Arguments.ContainsKey(AppConstants.RabbitMQHeaders.DeadLetterExchangeHeader)) diff --git a/src/RabbitMQCoreClient/Models/Routes.cs b/src/RabbitMQCoreClient/Models/Routes.cs index a15dc8c..55bc891 100644 --- a/src/RabbitMQCoreClient/Models/Routes.cs +++ b/src/RabbitMQCoreClient/Models/Routes.cs @@ -1,5 +1,8 @@ namespace RabbitMQCoreClient.Models; +/// +/// Errored messages processing types. +/// public enum Routes { /// diff --git a/src/RabbitMQCoreClient/Models/Subscription.cs b/src/RabbitMQCoreClient/Models/Subscription.cs index 974ee9c..a802bcc 100644 --- a/src/RabbitMQCoreClient/Models/Subscription.cs +++ b/src/RabbitMQCoreClient/Models/Subscription.cs @@ -8,10 +8,20 @@ namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; /// public sealed class Subscription : QueueBase { + /// + /// Create new object of . + /// + /// If true - than the queue with header + /// will be created otherwise the autodelete queue will be created. public Subscription(bool useQuorum = false) : base($"sub_{Guid.NewGuid()}", false, true, true, useQuorum) { } + /// + /// Create new subscription from configuration. + /// + /// + /// public static Subscription Create(SubscriptionConfig queueConfig) => new() { Arguments = queueConfig.Arguments ?? new Dictionary(), diff --git a/src/RabbitMQCoreClient/QueueService.cs b/src/RabbitMQCoreClient/QueueService.cs index 076c1ab..e7f08df 100644 --- a/src/RabbitMQCoreClient/QueueService.cs +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -21,7 +21,6 @@ public sealed class QueueService : IQueueService { readonly ILogger _log; readonly IList _exchanges; - readonly IMessageSerializer _serializer; bool _connectionBlocked = false; IConnection? _connection; IChannel? _publishChannel; @@ -31,6 +30,9 @@ public sealed class QueueService : IQueueService /// public RabbitMQCoreClientOptions Options { get; } + /// + public IMessageSerializer Serializer { get; } + /// public IConnection? Connection => _connection; @@ -59,7 +61,7 @@ public QueueService(RabbitMQCoreClientOptions options, ILoggerFactory loggerFact Options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(options)} is null."); _log = loggerFactory.CreateLogger(); _exchanges = builder.Exchanges; - _serializer = builder.Serializer ?? new SystemTextJsonMessageSerializer(); + Serializer = builder.Serializer ?? new SystemTextJsonMessageSerializer(); } /// @@ -74,7 +76,7 @@ async Task ConnectInternal() return; } - _log.LogInformation("Connecting to RabbitMQ endpoint {HostName}.", Options.HostName); + _log.LogInformation("Connecting to RabbitMQ endpoint '{HostName}'.", Options.HostName); var factory = new ConnectionFactory { @@ -123,7 +125,7 @@ async Task ConnectInternal() _connectionBlocked = false; CheckSendChannelOpened(); - _log.LogInformation("Connected to RabbitMQ endpoint {HostName}", Options.HostName); + _log.LogInformation("Connected to RabbitMQ endpoint '{HostName}'", Options.HostName); } /// @@ -180,13 +182,13 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - if (Options.ReconnectionAttemptsCount is not null + if (Options.ReconnectionAttemptsCount is not null && _reconnectAttemptsCount > Options.ReconnectionAttemptsCount) - throw new ReconnectAttemptsExceededException($"Max reconnect attempts {Options.ReconnectionAttemptsCount} reached."); + throw new ReconnectAttemptsExceededException($"Max reconnect attempts '{Options.ReconnectionAttemptsCount}' reached."); try { - _log.LogInformation("Trying to connect with reconnect attempt {ReconnectAttempt}", _reconnectAttemptsCount); + _log.LogInformation("Trying to connect with reconnect attempt '{ReconnectAttempt}'", _reconnectAttemptsCount); await ConnectInternal(); _reconnectAttemptsCount = 0; @@ -202,7 +204,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) string? innerExceptionMessage = null; if (e.InnerException != null) innerExceptionMessage = e.InnerException.Message; - _log.LogCritical(e, "Connection failed. Details: {ErrorMessage} Reconnect attempts: {ReconnectAttempt}", + _log.LogCritical(e, "Connection failed. Details: '{ErrorMessage}' Reconnect attempts: '{ReconnectAttempt}'", e.Message + " " + innerExceptionMessage, _reconnectAttemptsCount); } } @@ -211,162 +213,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) /// public async ValueTask AsyncDispose() => await ShutdownAsync(); - /// - public ValueTask SendJsonAsync( - string json, - string routingKey, - string? exchange = default, - bool decreaseTtl = true) - { - if (string.IsNullOrEmpty(json)) - throw new ArgumentException($"{nameof(json)} is null or empty.", nameof(json)); - - var body = Encoding.UTF8.GetBytes(json); - var properties = CreateBasicJsonProperties(); - - _log.LogDebug("Sending json message {Message} to exchange {Exchange} " + - "with routing key {RoutingKey}.", json, exchange, routingKey); - - return SendAsync(body, - props: properties, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - } - - /// - public ValueTask SendJsonAsync( - ReadOnlyMemory jsonBytes, - string routingKey, - string? exchange = default, - bool decreaseTtl = true) - { - if (jsonBytes.Length == 0) - throw new ArgumentException($"{nameof(jsonBytes)} is null or empty.", nameof(jsonBytes)); - - var properties = CreateBasicJsonProperties(); - - _log.LogDebug("Sending json message to exchange {Exchange} " + - "with routing key {RoutingKey}.", exchange, routingKey); - - return SendAsync(jsonBytes, - props: properties, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - } - - /// - public ValueTask SendAsync( - T obj, - string routingKey, - string? exchange = default, - bool decreaseTtl = true - ) - { - // Проверка на Null без боксинга. // https://stackoverflow.com/a/864860 - if (EqualityComparer.Default.Equals(obj, default)) - throw new ArgumentNullException(nameof(obj)); - - var serializedObj = _serializer.Serialize(obj); - return SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - } - - /// - public ValueTask SendAsync( - byte[] obj, - BasicProperties props, - string routingKey, - string? exchange = default, - bool decreaseTtl = true) => SendAsync(new ReadOnlyMemory(obj), - props: props, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - - /// - public ValueTask SendBatchAsync( - IEnumerable objs, - string routingKey, - string? exchange = default, - bool decreaseTtl = true) - { - var messages = new List>(); - foreach (var obj in objs) - messages.Add(_serializer.Serialize(obj)); - - return SendJsonBatchAsync( - serializedJsonList: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - } - - /// - public ValueTask SendJsonBatchAsync( - IEnumerable serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true) - { - var messages = new List<(ReadOnlyMemory Body, BasicProperties Props)>(); - foreach (var json in serializedJsonList) - { - var props = CreateBasicJsonProperties(); - AddTtl(props, decreaseTtl); - - var body = Encoding.UTF8.GetBytes(json); - messages.Add((body, props)); - } - - _log.LogDebug("Sending json messages batch to exchange {Exchange} " + - "with routing key {RoutingKey}.", exchange, routingKey); - - return SendBatchAsync( - objs: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - } - - /// - public ValueTask SendJsonBatchAsync( - IEnumerable> serializedJsonList, - string routingKey, - string? exchange = default, - bool decreaseTtl = true) - { - var messages = new List<(ReadOnlyMemory Body, BasicProperties Props)>(); - foreach (var json in serializedJsonList) - { - var props = CreateBasicJsonProperties(); - AddTtl(props, decreaseTtl); - - messages.Add((json, props)); - } - - _log.LogDebug("Sending json messages batch to exchange {Exchange} " + - "with routing key {RoutingKey}.", exchange, routingKey); - - return SendBatchAsync( - objs: messages, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl - ); - } - - #region Base Publish methods + #region Publish Single methods /// public async ValueTask SendAsync( @@ -374,8 +221,12 @@ public async ValueTask SendAsync( BasicProperties props, string routingKey, string? exchange = default, - bool decreaseTtl = true) + bool decreaseTtl = true, + CancellationToken cancellationToken = default) { + if (obj.Length == 0) + throw new ArgumentException($"{nameof(obj)} is null or empty.", nameof(obj)); + if (string.IsNullOrEmpty(exchange)) exchange = GetDefaultExchange(); @@ -385,8 +236,8 @@ public async ValueTask SendAsync( if (obj.Length > Options.MaxBodySize) { var decodedString = DecodeMessageAsString(obj); - throw new BadMessageException($"The message size \"{obj.Length}\" exceeds max body limit of \"{Options.MaxBodySize}\" " + - $"on routing key \"{routingKey}\" (exchange: \"{exchange}\"). Decoded message part: {decodedString}"); + throw new BadMessageException($"The message size '{obj.Length}' exceeds max body limit of '{Options.MaxBodySize}' " + + $"on routing key '{routingKey}' (exchange: '{exchange}'). Decoded message part: {decodedString}"); } CheckSendChannelOpened(); @@ -398,16 +249,21 @@ await _publishChannel.BasicPublishAsync(exchange: exchange, mandatory: false, // Just not reacting when no queue is subscribed for key. basicProperties: props, body: obj); - _log.LogDebug("Sent raw message to exchange {Exchange} with routing key {RoutingKey}.", + _log.LogDebug("Sent raw message to exchange '{Exchange}' with routing key '{RoutingKey}'.", exchange, routingKey); } + #endregion + + #region Publish Batch methods + /// public async ValueTask SendBatchAsync( IEnumerable<(ReadOnlyMemory Body, BasicProperties Props)> objs, string routingKey, string? exchange = default, - bool decreaseTtl = true) + bool decreaseTtl = true, + CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(exchange)) exchange = GetDefaultExchange(); @@ -446,7 +302,68 @@ public async ValueTask SendBatchAsync( routingKey: routingKey, body: body, mandatory: false, // Just not reacting when no queue is subscribed for key. - basicProperties: props); + basicProperties: props, + cancellationToken: cancellationToken); + publishTasks.Add(publishTask); + + await MaybeAwaitPublishes(publishTasks, batchSize); + } + + // Await any remaining tasks in case message count was not + // evenly divisible by batch size. + await MaybeAwaitPublishes(publishTasks, 0); + + _log.LogDebug("Sent raw messages batch to exchange '{Exchange}' " + + "with routing key '{RoutingKey}'.", exchange, routingKey); + } + + /// + public async ValueTask SendBatchAsync( + IEnumerable> objs, + BasicProperties props, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(exchange)) + exchange = GetDefaultExchange(); + + if (string.IsNullOrEmpty(exchange)) + throw new ArgumentException($"{nameof(exchange)} is null or empty.", nameof(exchange)); + + CheckSendChannelOpened(); + + const ushort MAX_OUTSTANDING_CONFIRMS = 256; + + var batchSize = Math.Max(1, MAX_OUTSTANDING_CONFIRMS / 2); + + var publishTasks = new List(); + + AddTtl(props, false); + + foreach (var body in objs) + { + if (body.Length > Options.MaxBodySize) + { + var decodedString = DecodeMessageAsString(body); + + _log.LogError("Skipped message due to message size '{MessageSize}' exceeds max body limit of '{MaxBodySize}' " + + "on routing key '{RoutingKey}' (exchange: '{Exchange}'. Decoded message part: {DecodedString})", + body.Length, + Options.MaxBodySize, + routingKey, + exchange, + decodedString); + continue; + } + + var publishTask = _publishChannel.BasicPublishAsync( + exchange: exchange, + routingKey: routingKey, + body: body, + mandatory: false, // Just not reacting when no queue is subscribed for key. + basicProperties: props, + cancellationToken: cancellationToken); publishTasks.Add(publishTask); await MaybeAwaitPublishes(publishTasks, batchSize); @@ -456,8 +373,8 @@ public async ValueTask SendBatchAsync( // evenly divisible by batch size. await MaybeAwaitPublishes(publishTasks, 0); - _log.LogDebug("Sent raw messages batch to exchange {Exchange} " + - "with routing key {RoutingKey}.", exchange, routingKey); + _log.LogDebug("Sent raw messages batch to exchange '{Exchange}' " + + "with routing key '{RoutingKey}'.", exchange, routingKey); } async Task MaybeAwaitPublishes(List publishTasks, int batchSize) @@ -520,12 +437,15 @@ async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventAr } #endregion - static BasicProperties CreateBasicJsonProperties() + /// + /// Creates a object with Persistent = true. + /// + /// + public static BasicProperties CreateDefaultProperties() { var properties = new BasicProperties { - Persistent = true, - ContentType = "application/json" + Persistent = true }; return properties; } diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj index 40675ff..6164d8d 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj +++ b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj @@ -1,7 +1,7 @@  - 6.1.2 + 7.0.0 $(VersionSuffix) $(Version)-$(VersionSuffix) true diff --git a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs index 62049be..4396cb7 100644 --- a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs @@ -2,28 +2,36 @@ namespace RabbitMQCoreClient.Serializers; +/// +/// System.Text.Json message serializer. +/// public class SystemTextJsonMessageSerializer : IMessageSerializer { - static readonly System.Text.Json.JsonSerializerOptions _defaultOptions = new System.Text.Json.JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase - }; - - public static System.Text.Json.JsonSerializerOptions DefaultOptions => _defaultOptions; - - static SystemTextJsonMessageSerializer() - { - _defaultOptions.Converters.Add(new JsonStringEnumConverter()); - } - + /// + /// Default serialization options. + /// + public static System.Text.Json.JsonSerializerOptions DefaultOptions { get; } = + new System.Text.Json.JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }; + + /// + /// Current serialization options. + /// public System.Text.Json.JsonSerializerOptions Options { get; } + /// + /// Creates new object of . + /// + /// Setup parameters. public SystemTextJsonMessageSerializer(Action? setupAction = null) { if (setupAction is null) { - Options = _defaultOptions; + Options = DefaultOptions; } else { From f6de5f742cffab06e2ad368e128570820355594b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Fri, 16 Jan 2026 16:05:09 +0300 Subject: [PATCH 07/13] Comletely rewritten BatchQueueSender - Changed namespaces for DependencyInjection. --- README.md | 6 +- .../Handler.cs | 2 +- .../Program.cs | 5 +- samples/RabbitMQCoreClient.WebApp/Handler.cs | 4 +- samples/RabbitMQCoreClient.WebApp/Program.cs | 2 +- samples/publish/HostConsole/Program.cs | 1 + samples/publish/SimpleConsole/Program.cs | 2 +- samples/publish/WebApp/Program.cs | 1 + .../ExchangeDeclareTests.cs | 12 +- .../BatchQueueSender/BatchQueueExtensions.cs | 101 ++++++ .../QueueBatchSenderOptions.cs | 2 +- .../ServiceCollectionExtensions.cs | 9 +- .../BatchQueueSender/EventItem.cs | 53 +++ .../BatchQueueSender/EventsWriter.cs | 27 ++ .../Exceptions/PersistingException.cs | 9 +- .../BatchQueueSender/IEventsHandler.cs | 21 ++ .../BatchQueueSender/IEventsWriter.cs | 16 + .../IQueueEventsBufferEngine.cs | 21 +- .../BatchQueueSender/IQueueEventsWriter.cs | 15 - .../BatchQueueSender/QueueEventItem.cs | 20 -- .../QueueEventsBufferEngine.cs | 326 +++++++++++++----- .../BatchQueueSender/QueueEventsWriter.cs | 64 ---- .../ConfigFormats/JsonV1Binder.cs | 4 +- .../ConfigFormats/JsonV2Binder.cs | 7 +- .../ConfigModels/QueueConfig.cs | 2 +- .../ConfigModels/SubscriptionConfig.cs | 2 +- .../Extensions/BuilderExtensions.cs | 8 +- .../Extensions/ServiceCollectionExtensions.cs | 9 +- .../SystemTextJsonBuilderExtensions.cs | 3 +- .../IRabbitMQCoreClientBuilder.cs | 2 +- .../IRabbitMQCoreClientConsumerBuilder.cs | 3 +- .../Options/ConsumerHandlerOptions.cs | 2 +- .../Options/ErrorHandlingOptions.cs | 2 +- .../Options/ExchangeOptions.cs | 2 +- .../Options/RabbitMQCoreClientOptions.cs | 2 +- .../RabbitMQCoreClientBuilder.cs | 1 + .../RabbitMQCoreClientConsumerBuilder.cs | 3 +- .../Extentions/QueueServiceExtensions.cs | 2 +- .../SystemTextJsonQueueServiceExtensions.cs | 2 +- src/RabbitMQCoreClient/IMessageHandler.cs | 2 +- src/RabbitMQCoreClient/IQueueService.cs | 2 +- src/RabbitMQCoreClient/MessageHandlerJson.cs | 2 +- src/RabbitMQCoreClient/Models/Exchange.cs | 6 +- src/RabbitMQCoreClient/Models/Queue.cs | 4 +- src/RabbitMQCoreClient/Models/QueueBase.cs | 9 +- src/RabbitMQCoreClient/Models/Subscription.cs | 5 +- src/RabbitMQCoreClient/QueueService.cs | 4 +- 47 files changed, 534 insertions(+), 275 deletions(-) create mode 100644 src/RabbitMQCoreClient/BatchQueueSender/BatchQueueExtensions.cs rename src/RabbitMQCoreClient/BatchQueueSender/{ => DependencyInjection}/QueueBatchSenderOptions.cs (90%) create mode 100644 src/RabbitMQCoreClient/BatchQueueSender/EventItem.cs create mode 100644 src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs create mode 100644 src/RabbitMQCoreClient/BatchQueueSender/IEventsHandler.cs create mode 100644 src/RabbitMQCoreClient/BatchQueueSender/IEventsWriter.cs delete mode 100644 src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs delete mode 100644 src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs delete mode 100644 src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs rename src/RabbitMQCoreClient/{Extentions => DependencyInjection/Extensions}/SystemTextJsonBuilderExtensions.cs (91%) diff --git a/README.md b/README.md index 98eb4f9..6fc5ad8 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; Console.WriteLine("Simple console message publishing only example"); @@ -77,6 +78,7 @@ await publisher.SendAsync("""{ "foo": "bar" }""", "test_key"); ``` using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; var builder = WebApplication.CreateBuilder(args); @@ -143,7 +145,7 @@ The feature allows you to buffer that messages at the in-memory list and flush t To use this feature register it at DI: ```csharp -using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; +using RabbitMQCoreClient.DependencyInjection; ... services.AddBatchQueueSender(); @@ -167,7 +169,7 @@ You can configure the flush options by Action or IConfiguration. Example of the ``` ```csharp -using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; +using RabbitMQCoreClient.DependencyInjection; ... services.AddBatchQueueSender(configuration.GetSection("QueueFlushSettings")); diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs b/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs index 5199757..276d775 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs +++ b/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs @@ -1,4 +1,4 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; using System; diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs index b393f63..894eb83 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -4,9 +4,8 @@ using Microsoft.Extensions.Logging; using RabbitMQCoreClient; using RabbitMQCoreClient.BatchQueueSender; -using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.ConsoleClient; +using RabbitMQCoreClient.DependencyInjection; using System; using System.Linq; using System.Text; @@ -123,7 +122,7 @@ static async Task CreateBatchSender(IQueueEventsBufferEngine batchSender, Cancel { await Task.Delay(500, token); var bodyList = Enumerable.Range(1, 2).Select(x => new SimpleObj { Name = $"test sending {x}" }); - await batchSender.AddEvents(bodyList, "test_routing_key"); + batchSender.AddEvents(bodyList, "test_routing_key"); } catch (Exception e) { diff --git a/samples/RabbitMQCoreClient.WebApp/Handler.cs b/samples/RabbitMQCoreClient.WebApp/Handler.cs index 5f488ee..04a5583 100644 --- a/samples/RabbitMQCoreClient.WebApp/Handler.cs +++ b/samples/RabbitMQCoreClient.WebApp/Handler.cs @@ -1,9 +1,7 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; -using System; using System.Text; -using System.Threading.Tasks; namespace RabbitMQCoreClient.WebApp; diff --git a/samples/RabbitMQCoreClient.WebApp/Program.cs b/samples/RabbitMQCoreClient.WebApp/Program.cs index 7ed6ea5..8bd15f9 100644 --- a/samples/RabbitMQCoreClient.WebApp/Program.cs +++ b/samples/RabbitMQCoreClient.WebApp/Program.cs @@ -1,5 +1,5 @@ using RabbitMQCoreClient; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.WebApp; var builder = WebApplication.CreateBuilder(args); diff --git a/samples/publish/HostConsole/Program.cs b/samples/publish/HostConsole/Program.cs index 0140747..9ec6b97 100644 --- a/samples/publish/HostConsole/Program.cs +++ b/samples/publish/HostConsole/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using RabbitMQCoreClient.DependencyInjection; Console.WriteLine("Host console message publishing only example"); diff --git a/samples/publish/SimpleConsole/Program.cs b/samples/publish/SimpleConsole/Program.cs index 40cf7ac..c41f1b8 100644 --- a/samples/publish/SimpleConsole/Program.cs +++ b/samples/publish/SimpleConsole/Program.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using RabbitMQCoreClient; -using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; +using RabbitMQCoreClient.DependencyInjection; Console.WriteLine("Simple console message publishing only example"); diff --git a/samples/publish/WebApp/Program.cs b/samples/publish/WebApp/Program.cs index a59289d..5b3c672 100644 --- a/samples/publish/WebApp/Program.cs +++ b/samples/publish/WebApp/Program.cs @@ -1,4 +1,5 @@ using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; var builder = WebApplication.CreateBuilder(args); diff --git a/src/RabbitMQCoreClient.Tests/ExchangeDeclareTests.cs b/src/RabbitMQCoreClient.Tests/ExchangeDeclareTests.cs index 89158af..cc369c9 100644 --- a/src/RabbitMQCoreClient.Tests/ExchangeDeclareTests.cs +++ b/src/RabbitMQCoreClient.Tests/ExchangeDeclareTests.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Configuration.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using System.Collections.Generic; using System.Linq; using Xunit; @@ -16,11 +16,11 @@ public void ShouldProperlyBindQueueByOptionsV1() var services = new ServiceCollection(); var builder = new RabbitMQCoreClientBuilder(services); var consumerBuilder = new RabbitMQCoreClientConsumerBuilder(builder); - var exchange = new Exchange(new ExchangeOptions { Name = "test" }); + var exchange = new Models.Exchange(new ExchangeOptions { Name = "test" }); const string queueName = "queue1"; const string deadLetterExchange = "testdeadletter"; - var options = new Queue(queueName, exclusive: true, durable: false) + var options = new Models.Queue(queueName, exclusive: true, durable: false) { RoutingKeys = { "r1", "r2" }, DeadLetterExchange = deadLetterExchange, @@ -125,10 +125,10 @@ public void ShouldProperlyBindSubscriptionByOptionsV1() var services = new ServiceCollection(); var builder = new RabbitMQCoreClientBuilder(services); var consumerBuilder = new RabbitMQCoreClientConsumerBuilder(builder); - var exchange = new Exchange(new ExchangeOptions { Name = "test" }); + var exchange = new Models.Exchange(new ExchangeOptions { Name = "test" }); const string deadLetterExchange = "testdeadletter"; - var options = new Subscription() + var options = new Models.Subscription() { RoutingKeys = { "r1", "r2" }, DeadLetterExchange = deadLetterExchange, diff --git a/src/RabbitMQCoreClient/BatchQueueSender/BatchQueueExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/BatchQueueExtensions.cs new file mode 100644 index 0000000..1bbee64 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/BatchQueueExtensions.cs @@ -0,0 +1,101 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// BatchQueue extension methods. +/// +public static class BatchQueueExtensions +{ + /// + /// Add an object to be send as event to the data bus. + /// + /// The object. + /// The object to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + public static void AddEvent(this IQueueEventsBufferEngine service, [NotNull] T obj, string routingKey) + where T : class => + service.Add(new EventItem(service.Serializer.Serialize(obj), routingKey)); + + /// + /// Add a byte array object to be send as event to the data bus. + /// + /// The object. + /// The byte array object to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + public static void AddEvent(this IQueueEventsBufferEngine service, ReadOnlyMemory obj, string routingKey) => + service.Add(new EventItem(obj, routingKey)); + + /// + /// Add a byte array object to send as event to the data bus. + /// + /// The object. + /// The byte array object to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + public static void AddEvent(this IQueueEventsBufferEngine service, byte[] obj, string routingKey) => + service.Add(new EventItem(obj, routingKey)); + + /// + /// Add a string object to send as event to the data bus. + /// + /// The object. + /// The string object to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + public static void AddEvent(this IQueueEventsBufferEngine service, string obj, string routingKey) => + service.Add(new EventItem(Encoding.UTF8.GetBytes(obj).AsMemory(), routingKey)); + + /// + /// Add objects collection to send as events to the data bus. + /// + /// The type of list item of the property. + /// The object. + /// The list of objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// + public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) + where T : class + { + foreach (var obj in objs) + service.Add(new EventItem(service.Serializer.Serialize(obj), routingKey)); + } + + /// + /// Add a byte array objects collection to send as event to the data bus. + /// + /// The object. + /// The list of byte array objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// + public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable> objs, string routingKey) + { + foreach (var obj in objs) + service.Add(new EventItem(obj, routingKey)); + } + + /// + /// Add a byte array objects collection to send as event to the data bus. + /// + /// The object. + /// The list of byte array objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// + public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) + { + foreach (var obj in objs) + service.Add(new EventItem(obj, routingKey)); + } + + /// + /// Add a byte array objects collection to send as event to the data bus. + /// + /// The object. + /// The list of byte array objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// + public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) + { + foreach (var obj in objs) + service.Add(new EventItem(Encoding.UTF8.GetBytes(obj).AsMemory(), routingKey)); + } +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs similarity index 90% rename from src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs rename to src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs index 37bdd5f..41df7b2 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs @@ -1,4 +1,4 @@ -namespace RabbitMQCoreClient.BatchQueueSender; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Queue bus buffer options. diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index 65365d4..c0bd347 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -2,17 +2,18 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using RabbitMQCoreClient.BatchQueueSender; -namespace RabbitMQCoreClient.BatchQueueSender.DependencyInjection; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Class containing extension methods for registering the BatchQueueSender services at DI. /// -public static class ServiceCollectionExtensions +public static partial class ServiceCollectionExtensions { static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) { - services.TryAddTransient(); + services.TryAddTransient(); services.TryAddSingleton(); return services; } @@ -33,7 +34,7 @@ public static IRabbitMQCoreClientBuilder AddBatchQueueSender( RegisterOptions(builder.Services, configuration, setupAction); builder.Services.AddBatchQueueSenderCore(); - return builder; + return builder; } /// diff --git a/src/RabbitMQCoreClient/BatchQueueSender/EventItem.cs b/src/RabbitMQCoreClient/BatchQueueSender/EventItem.cs new file mode 100644 index 0000000..2c31f20 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/EventItem.cs @@ -0,0 +1,53 @@ +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Basic buffer queue message event. +/// It contains only routingKey and message in bytes for sending to queue. +/// +public class EventItem +{ + /// + /// Basic buffer event constructor. + /// + /// The message in bytes to be send to queue. + /// The routing key the message to be send to queue. + public EventItem(ReadOnlyMemory message, string routingKey) + { + Message = message; + RoutingKey = routingKey; + } + + /// + /// The message in bytes to be send to queue. + /// + public ReadOnlyMemory Message { get; } + + /// + /// The routing key the message to be send to queue. + /// + public string RoutingKey { get; } +} + +/// +/// Basic buffer queue message event with the source object. +/// Use this class for custom logic. +/// +public class EventItemWithSourceObject : EventItem +{ + /// + /// The source object of the event that you want to sent to the queue. + /// + public object Source { get; } + + /// + /// Buffer event constructor. + /// + /// Source object. + /// The message in bytes to be send to queue. + /// The routing key the message to be send to queue. + public EventItemWithSourceObject(object @event, ReadOnlyMemory message, string routingKey) + : base(message, routingKey) + { + Source = @event; + } +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs new file mode 100644 index 0000000..e39f96e --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs @@ -0,0 +1,27 @@ +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Implementation of the service for sending events to the data bus. +/// +public sealed class EventsWriter : IEventsWriter +{ + readonly IQueueService _queueService; + + /// + /// Create new object of . + /// + /// Queue publisher. + public EventsWriter(IQueueService queueService) + { + _queueService = queueService; + } + + /// + public async Task WriteBatch(IEnumerable events, string routingKey) + { + if (!events.Any()) + return; + + await _queueService.SendBatchAsync(events.Select(x => x.Message), routingKey); + } +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs index e14d194..7291086 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs @@ -1,11 +1,14 @@ namespace RabbitMQCoreClient.BatchQueueSender.Exceptions; -public class PersistingException : Exception +/// +/// Create new object of . +/// +public sealed class PersistingException : Exception { /// /// The data items. /// - public object[] Items { get; } + public IEnumerable Items { get; } /// /// The routing key of the queue bus. @@ -23,7 +26,7 @@ public class PersistingException : Exception /// The routing key of the queue bus. /// public PersistingException(string message, - object[] items, + IEnumerable items, string routingKey, Exception innerException) : base(message, innerException) { diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IEventsHandler.cs b/src/RabbitMQCoreClient/BatchQueueSender/IEventsHandler.cs new file mode 100644 index 0000000..9daf603 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/IEventsHandler.cs @@ -0,0 +1,21 @@ +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Interface of the additional message processing service. +/// +public interface IEventsHandler +{ + /// + /// Executes after events batch was sent to the . + /// + /// List of sent events. + /// + Task OnAfterWriteEvents(IEnumerable events); + + /// + /// Executes on batch write process throws error. + /// + /// List of errored events. + /// + Task OnWriteErrors(IEnumerable events); +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/IEventsWriter.cs new file mode 100644 index 0000000..e1e499e --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/IEventsWriter.cs @@ -0,0 +1,16 @@ +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// The service interface that represents methods to work with queue bus. +/// +public interface IEventsWriter +{ + /// + /// Send events batch to queue. + /// + /// List of events to send to queue. + /// The routing key. + /// when the operation completes. + /// + Task WriteBatch(IEnumerable events, string routingKey); +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs index b1b776d..c2c3a52 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs @@ -1,24 +1,21 @@ +using RabbitMQCoreClient.Serializers; +using System.Diagnostics.CodeAnalysis; + namespace RabbitMQCoreClient.BatchQueueSender; /// -/// Event buffer interface. +/// Batch Queue Events buffer interface. /// public interface IQueueEventsBufferEngine { /// - /// Add an event to send to the data bus. + /// Message serializer to be used to serialize objects to sent to queue. /// - /// The object to send to the data bus. - /// The name of the route key with which you want to send events to the data bus. - /// showing the completion of the operation. - Task AddEvent(T @event, string routingKey); + IMessageSerializer Serializer { get; } /// - /// Add events to send to the data bus. + /// Add the event to the buffer to be sent to the data bus. /// - /// The type of list item of the property. - /// The list of objects to send to the data bus. - /// The name of the route key with which you want to send events to the data bus. - /// - Task AddEvents(IEnumerable events, string routingKey); + /// The item with user-provided values array. + void Add([NotNull] EventItem item); } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs deleted file mode 100644 index 0511c87..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace RabbitMQCoreClient.BatchQueueSender; - -/// -/// The service interface that represents methods to work with queue bus. -/// -public interface IQueueEventsWriter -{ - /// - /// Send events to the queue. - /// - /// The item objects to send to the queue. - /// The routing key to send. - /// when the operation completes. - Task Write(object[] items, string routingKey); -} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs deleted file mode 100644 index 3c00d08..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace RabbitMQCoreClient.BatchQueueSender; - -public readonly struct QueueEventItem -{ - /// - /// The event to be written to the database. - /// - public object Event { get; } - - /// - /// The routing key to which you want to send the event. - /// - public string RoutingKey { get; } - - public QueueEventItem(object @event, string tableName) - { - Event = @event; - RoutingKey = tableName; - } -} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs index e448b6d..aae0ec2 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs @@ -1,6 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using RabbitMQCoreClient.BatchQueueSender.Exceptions; +using Microsoft.Extensions.ObjectPool; +using RabbitMQCoreClient.Serializers; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace RabbitMQCoreClient.BatchQueueSender; @@ -9,160 +14,291 @@ namespace RabbitMQCoreClient.BatchQueueSender; /// public sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable { - readonly Timer _flushTimer; - readonly List _events = []; - readonly IQueueEventsWriter _eventsWriter; - readonly QueueBatchSenderOptions _engineOptions; - readonly ILogger _logger; + readonly Queue _buffer = new Queue(); + readonly object _syncRoot = new object(); + readonly Timer _timer; + readonly int _sizeLimit; + readonly TimeSpan _timeLimit; + readonly IEventsWriter _writer; + readonly IEventsHandler? _eventsHandler; + readonly ILogger? _log; + int _count; + Task _currentFlushTask = Task.CompletedTask; - static readonly SemaphoreSlim _semaphore = new(1, 1); - bool _disposedValue; + /// + public IMessageSerializer Serializer { get; } + + const string ErrorWhileWritingEvents = "There was an error while writing events. Details: {ErrorMessage}"; + const string ErrorOnAfterWriteEvents = "There was an error execution OnAfterWriteEvents method. Details: {ErrorMessage}"; + const string ErrorOnWriteErrors = "There was an error execution OnWriteErrors method. Details: {ErrorMessage}"; /// - /// Event storage buffer implementation constructor. - /// Creates a new instance of the class. + /// The implementation constructor of the event storage buffer. + /// Creates a new instance of the class . /// - public QueueEventsBufferEngine( - IOptions engineOptions, - IQueueEventsWriter eventsWriter, - ILogger logger) + public QueueEventsBufferEngine(IEventsWriter writer, + int sizeLimit, + TimeSpan timeLimit, + IEventsHandler? eventsHandler, + IRabbitMQCoreClientBuilder builder, + ILogger? log) + { + _log = log; + _writer = writer ?? throw new ArgumentNullException(nameof(writer)); + _eventsHandler = eventsHandler; + _sizeLimit = sizeLimit; + _timeLimit = timeLimit; + _timer = new Timer(_ => _ = FlushByTimerAsync(), null, Timeout.Infinite, Timeout.Infinite); + Serializer = builder.Serializer ?? new SystemTextJsonMessageSerializer(); + } + + /// + public void Add([NotNull] EventItem item) { - if (engineOptions?.Value == null) - throw new ArgumentNullException(nameof(engineOptions), $"{nameof(engineOptions)} is null."); + lock (_syncRoot) + { + _buffer.Enqueue(item); + _count++; - _engineOptions = engineOptions.Value; - _eventsWriter = eventsWriter; - _logger = logger; + if (_count == 1) + _timer.Change(_timeLimit, Timeout.InfiniteTimeSpan); - _flushTimer = new Timer(async obj => await FlushTimerDelegate(), null, - _engineOptions.EventsFlushPeriodSec * 1000, - _engineOptions.EventsFlushPeriodSec * 1000); + if (_count >= _sizeLimit) + _currentFlushTask = FlushAsync(); + } } /// - public async Task AddEvent(T @event, string routingKey) + public void AddEvent([NotNull] T @event, string routingKey) + where T : class { - if (@event is null) - return; + Add(new EventItem(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(@event), routingKey)); + } - await _semaphore.WaitAsync(); + async Task FlushByTimerAsync() + { + EventItem[]? array = null; + int count = 0; - try + lock (_syncRoot) { - _events.Add(new QueueEventItem(@event, routingKey)); - if (_events.Count < _engineOptions.EventsFlushCount) + if (_count == 0) return; - - await Flush(); + (array, count) = ExtractItems(); } - finally + + await ProcessItemsAsync(array, count).ConfigureAwait(false); + } + + async Task FlushAsync() + { + EventItem[]? array = null; + int count = 0; + + lock (_syncRoot) { - _semaphore.Release(); + (array, count) = ExtractItems(); } + + await ProcessItemsAsync(array, count).ConfigureAwait(false); } - /// - public async Task AddEvents(IEnumerable events, string routingKey) + (EventItem[] Array, int Count) ExtractItems() { - await _semaphore.WaitAsync(); + _timer.Change(Timeout.Infinite, Timeout.Infinite); + int count = _buffer.Count; + var array = ArrayPool.Shared.Rent(count); + + _buffer.CopyTo(array, 0); + _buffer.Clear(); + _count = 0; + + return (array, count); + } + + async Task ProcessItemsAsync(EventItem[] array, int count) + { + List? errorEvents = null; + List? completedEvents = null; + var sw = new Stopwatch(); + sw.Start(); try { - _events.AddRange(events - .Where(@event => @event is not null) - .Select(@event => new QueueEventItem(@event!, routingKey)) - ); + var batch = new ArraySegment(array, 0, count); + var routeKeyGroups = GroupByKey(batch); + var tasksWithData = new List<(Task Task, IEnumerable Events)>(); + var tasks = new List(); + foreach (var routeKeyGroup in routeKeyGroups) + { + var task = _writer.WriteBatch(routeKeyGroup.Items, routeKeyGroup.Key); + tasks.Add(task); + tasksWithData.Add((Task: task, Events: routeKeyGroup.Items.AsEnumerable())); + } - if (_events.Count < _engineOptions.EventsFlushCount) - return; + try + { + if (tasks.Count > 0) + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch + { + // Ignore the single exception, as we'll handle everything below. + } + + foreach (var tuple in tasksWithData) + { + var task = tuple.Task; + var eventsGroup = tuple.Events; - await Flush(); + if (task.IsFaulted && task.Exception != null) + { + errorEvents ??= new List(); + foreach (var innerEx in task.Exception.InnerExceptions) + { + _log?.LogError(innerEx, ErrorWhileWritingEvents, innerEx.Message); + // Log exception + errorEvents.AddRange(eventsGroup); + } + } + else if (!task.IsFaulted && _eventsHandler != null) + { + completedEvents ??= new List(); + completedEvents.AddRange(eventsGroup); + } + } } finally { - _semaphore.Release(); + ArrayPool.Shared.Return(array); + + _log?.LogInformation("Buffer has written '{RecordsCount}' records to the database at '{ElapsedMilliseconds}' ms.", + count, sw.ElapsedMilliseconds); + } + + try + { + if (_eventsHandler != null) + await _eventsHandler.OnAfterWriteEvents(completedEvents ?? Enumerable.Empty()).ConfigureAwait(false); + } + catch (Exception e) + { + _log?.LogError(e, ErrorOnAfterWriteEvents, e.Message); + } + + try + { + if (_eventsHandler != null && errorEvents != null && errorEvents.Count > 0) + await _eventsHandler.OnWriteErrors(errorEvents).ConfigureAwait(false); + } + catch (Exception e) + { + _log?.LogError(e, ErrorOnWriteErrors, e.Message); } } - async Task FlushTimerDelegate() + static IEnumerable GroupByKey(ArraySegment batch) { - await _semaphore.WaitAsync(); + if (batch.Array is null) + yield break; + + var groups = DictionaryPool>.Rent(); try { - await Flush(); + for (int i = 0; i < batch.Count; i++) + { + var item = batch.Array[batch.Offset + i]; + if (!groups.TryGetValue(item.RoutingKey, out var list)) + { + list = ListPool.Rent(); + groups[item.RoutingKey] = list; + } + list.Add(item); + } + + foreach (var pair in groups) + { + yield return new GroupData(pair.Key, pair.Value.ToArray()); + } } finally { - _semaphore.Release(); + foreach (var list in groups.Values) + { + ListPool.Return(list); + } + DictionaryPool>.Return(groups); } } - Task Flush() + /// + public async Task CompleteAsync() { - if (_events.Count == 0) - return Task.CompletedTask; - - var eventsCache = _events.ToArray(); - _events.Clear(); + Task flushTask; + lock (_syncRoot) + { + flushTask = _currentFlushTask; + } - return HandleEvents(eventsCache); + await flushTask.ConfigureAwait(false); } - async Task HandleEvents(IEnumerable streamDataEvents) + /// + public void Dispose() { - var routingGroups = streamDataEvents.GroupBy(x => x.RoutingKey); + _timer?.Dispose(); + GC.SuppressFinalize(this); + } - var tasks = new List(); + readonly struct GroupData + { + public readonly string Key; + public readonly EventItem[] Items; - foreach (var routingGroup in routingGroups) - { - var itemsToSend = routingGroup.Select(val => val.Event).ToArray(); - tasks.Add(_eventsWriter.Write(itemsToSend, routingGroup.Key)); - } - try + public GroupData(string key, EventItem[] items) { - await Task.WhenAll(tasks); + Key = key; + Items = items; } - catch (Exception e) - { - _logger.LogError(e, "Error while trying to write batch of data to Storage."); + } + + static class ListPool + { + static readonly ObjectPool> _pool = + new DefaultObjectPool>(new ListPolicy()); + + public static List Rent() => _pool.Get(); + public static void Return(List list) => _pool.Return(list); - var exceptions = tasks - .Where(t => t.Exception != null) - .Select(t => t.Exception) - .ToList(); - foreach (var aggregateException in exceptions) + class ListPolicy : PooledObjectPolicy> + { + public override List Create() => new List(); + public override bool Return(List list) { - if (aggregateException?.InnerExceptions?[0] is PersistingException persistException) - { - var extendedError = $"Routing key: {persistException.RoutingKey}. Source: " + - string.Join(Environment.NewLine, - System.Text.Json.JsonSerializer.Serialize(persistException.Items)); - _logger.LogDebug(extendedError); - } - // Unrecorded events are not sent anywhere. For the current implementation, this is not fatal. + list.Clear(); + return true; } } } - void Dispose(bool disposing) + static class DictionaryPool + where TKey : notnull { - if (!_disposedValue) + static readonly ConcurrentBag> _pool = new(); + + public static Dictionary Rent() { - if (disposing) + if (_pool.TryTake(out var dict)) { - _flushTimer?.Dispose(); + return dict; } + return []; + } - _disposedValue = true; + public static void Return(Dictionary dict) + { + dict.Clear(); + _pool.Add(dict); } } - - /// - public void Dispose() - { - // Do not change this code. Place the cleanup code in the "Dispose(bool disposing)" method. - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs deleted file mode 100644 index 7ac9ea9..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.Extensions.Logging; -using RabbitMQCoreClient.BatchQueueSender.Exceptions; -using System.Diagnostics; - -namespace RabbitMQCoreClient.BatchQueueSender; - -/// -/// Implementation of the service for sending events to the data bus. -/// -public class QueueEventsWriter : IQueueEventsWriter -{ - readonly ILogger _logger; - readonly IQueueService _queueService; - - long _writtenCount; - long _avgWriteTimeMs; - - public QueueEventsWriter( - IQueueService queueService, - ILogger logger) - { - _queueService = queueService; - _logger = logger; - } - - /// - public async Task Write(object[] items, string routingKey) - { - if (items.Length == 0) - return; - - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Start writing {RowsCount} data rows from the buffer.", items.Length); - - var sw = new Stopwatch(); - sw.Start(); - - try - { - await _queueService.SendBatchAsync(items, routingKey); - } - catch (Exception e) - { - throw new PersistingException("Error while persisting data", items, routingKey, e); - } - - sw.Stop(); - - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Buffer has sent {RowsCount} rows to the queue bus at {ElapsedMilliseconds} ms.", - items.Length, sw.ElapsedMilliseconds); - - _writtenCount += items.Length; - - if (_avgWriteTimeMs == 0) - _avgWriteTimeMs = sw.ElapsedMilliseconds; - else - _avgWriteTimeMs = (sw.ElapsedMilliseconds + _avgWriteTimeMs) / 2; - - _logger.LogInformation("From start of the service {RowsCount} total rows has been sent to the queue bus. " + - "The average sending time per row is {AvgWriteTime} ms.", - _writtenCount, _avgWriteTimeMs); - } -} diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs index 0398716..4e1b5e5 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs @@ -1,9 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; +using RabbitMQCoreClient.Models; namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs index 0c6dfde..cfb9356 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs @@ -1,14 +1,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; +using RabbitMQCoreClient.Models; namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; public static class JsonV2Binder { - const string ExhangesSection = "Exchanges"; + const string ExchangesSection = "Exchanges"; const string QueuesSection = "Queues"; const string SubscriptionsSection = "Subscriptions"; @@ -26,7 +25,7 @@ public static IRabbitMQCoreClientBuilder RegisterV2Configuration(this IRabbitMQC static void RegisterExchanges(IRabbitMQCoreClientBuilder builder, IConfiguration configuration) { - var exchanges = configuration.GetSection(ExhangesSection); + var exchanges = configuration.GetSection(ExchangesSection); foreach (var exchangeConfig in exchanges.GetChildren()) { var options = new ExchangeOptions(); diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs index d9f405e..fdb592c 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs @@ -1,4 +1,4 @@ -namespace RabbitMQCoreClient.DependencyInjection.ConfigModels; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Simple custom message queue. diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs index c10bbeb..1e7096f 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs @@ -1,4 +1,4 @@ -namespace RabbitMQCoreClient.DependencyInjection.ConfigModels; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Message queue for subscribing to events. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index 6d2cf60..9775068 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -1,14 +1,12 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using RabbitMQCoreClient; using RabbitMQCoreClient.Configuration.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using RabbitMQCoreClient.DependencyInjection; -using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; +using RabbitMQCoreClient.Models; -namespace Microsoft.Extensions.DependencyInjection; +namespace RabbitMQCoreClient.DependencyInjection; /// /// extension methods. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs index 1f234cc..9e0d83e 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs @@ -1,16 +1,17 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using RabbitMQCoreClient.Configuration.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.DependencyInjection.ConfigFormats; -namespace Microsoft.Extensions.DependencyInjection; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Class containing extension methods for creating a RabbitMQ message handler configuration interface. /// . /// -public static class ServiceCollectionExtensions +public static partial class ServiceCollectionExtensions { static RabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services) { @@ -85,7 +86,7 @@ static void RegisterOptions(IServiceCollection services, IConfiguration configur { var instance = configuration.Get() ?? new RabbitMQCoreClientOptions(); setupAction?.Invoke(instance); - var options = Options.Options.Create(instance); + var options = Options.Create(instance); services.AddSingleton((x) => options); } diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/SystemTextJsonBuilderExtensions.cs similarity index 91% rename from src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtensions.cs rename to src/RabbitMQCoreClient/DependencyInjection/Extensions/SystemTextJsonBuilderExtensions.cs index 8b3a0c0..927bbab 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/SystemTextJsonBuilderExtensions.cs @@ -1,7 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using RabbitMQCoreClient.Serializers; using System.Text.Json; -namespace Microsoft.Extensions.DependencyInjection; +namespace RabbitMQCoreClient.DependencyInjection; /// /// RabbitMQClient builder extensions for System.Text.Json serializer support. diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs index af5fd1a..a26cb6f 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs @@ -1,4 +1,4 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection; +using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs index 2638a3b..75b432d 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs @@ -1,4 +1,5 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.Models; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs index 8c61e8c..e0a33b8 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs @@ -1,6 +1,6 @@ using RabbitMQCoreClient.Serializers; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Consumer Handler Options. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs index 9161caa..3775fcf 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs @@ -1,6 +1,6 @@ using RabbitMQ.Client.Events; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Parameters that allow you to organize your own client exception handling mechanisms. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs index 01c241f..5788dbe 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs @@ -1,4 +1,4 @@ -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.DependencyInjection; /// /// Exchange point settings. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs index c2efb9f..d9b114a 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs @@ -3,7 +3,7 @@ using System.Security.Authentication; using System.Text.Json.Serialization; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.DependencyInjection; public class RabbitMQCoreClientOptions { diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs index a2c4df1..2a15bfd 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; namespace RabbitMQCoreClient.Configuration.DependencyInjection; diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs index 73916b3..5450cdf 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.Models; namespace RabbitMQCoreClient.Configuration.DependencyInjection; diff --git a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs index 5cf9f78..90635d6 100644 --- a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs +++ b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs @@ -82,7 +82,7 @@ public static ValueTask SendAsync( byte[] obj, string routingKey, string? exchange = default, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default) => service.SendAsync(new ReadOnlyMemory(obj), props: QueueService.CreateDefaultProperties(), routingKey: routingKey, diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs index 05fe03f..d076cb2 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs @@ -28,7 +28,7 @@ public static ValueTask SendAsync( JsonSerializerOptions? jsonSerializerSettings, string? exchange = default, CancellationToken cancellationToken = default - ) where T: class + ) where T : class { if (obj is null) throw new ArgumentNullException(nameof(obj)); diff --git a/src/RabbitMQCoreClient/IMessageHandler.cs b/src/RabbitMQCoreClient/IMessageHandler.cs index bd02c6e..fbf15b6 100644 --- a/src/RabbitMQCoreClient/IMessageHandler.cs +++ b/src/RabbitMQCoreClient/IMessageHandler.cs @@ -1,4 +1,4 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index a7f1210..8eec334 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -1,6 +1,6 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Events; using RabbitMQCoreClient.Serializers; diff --git a/src/RabbitMQCoreClient/MessageHandlerJson.cs b/src/RabbitMQCoreClient/MessageHandlerJson.cs index 332437e..0f42af9 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -1,4 +1,4 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; using System.Text; diff --git a/src/RabbitMQCoreClient/Models/Exchange.cs b/src/RabbitMQCoreClient/Models/Exchange.cs index 387611e..14a4bd6 100644 --- a/src/RabbitMQCoreClient/Models/Exchange.cs +++ b/src/RabbitMQCoreClient/Models/Exchange.cs @@ -1,7 +1,7 @@ using RabbitMQ.Client; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; -namespace RabbitMQCoreClient.Configuration.DependencyInjection; +namespace RabbitMQCoreClient.Models; /// /// The RabbitMQ Exchange @@ -39,7 +39,7 @@ public Exchange(ExchangeOptions options) /// /// The channel. /// Cancellation token. - public Task StartExchangeAsync(IChannel _channel, CancellationToken cancellationToken = default) => + public Task StartExchangeAsync(IChannel _channel, CancellationToken cancellationToken = default) => _channel.ExchangeDeclareAsync( exchange: Name, type: Options.Type, diff --git a/src/RabbitMQCoreClient/Models/Queue.cs b/src/RabbitMQCoreClient/Models/Queue.cs index a0b4729..6688847 100644 --- a/src/RabbitMQCoreClient/Models/Queue.cs +++ b/src/RabbitMQCoreClient/Models/Queue.cs @@ -1,6 +1,6 @@ -using RabbitMQCoreClient.DependencyInjection.ConfigModels; +using RabbitMQCoreClient.DependencyInjection; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.Models; /// /// Simple custom message queue. diff --git a/src/RabbitMQCoreClient/Models/QueueBase.cs b/src/RabbitMQCoreClient/Models/QueueBase.cs index 19d2aac..2c42696 100644 --- a/src/RabbitMQCoreClient/Models/QueueBase.cs +++ b/src/RabbitMQCoreClient/Models/QueueBase.cs @@ -1,8 +1,9 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; +using RabbitMQCoreClient.Configuration; using RabbitMQCoreClient.Exceptions; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.Models; /// /// Options to be applied to the message queue. @@ -76,8 +77,8 @@ protected QueueBase(string? name, bool durable, bool exclusive, bool autoDelete, /// /// Declare the queue on and start consuming messages. /// - public virtual async Task StartQueueAsync(IChannel channel, - AsyncEventingBasicConsumer consumer, + public virtual async Task StartQueueAsync(IChannel channel, + AsyncEventingBasicConsumer consumer, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(DeadLetterExchange) @@ -94,7 +95,7 @@ public virtual async Task StartQueueAsync(IChannel channel, durable: UseQuorum || Durable, exclusive: !UseQuorum && Exclusive, autoDelete: !UseQuorum && AutoDelete, - arguments: Arguments, + arguments: Arguments, cancellationToken: cancellationToken) ?? throw new QueueBindException("Queue is not properly bind."); if (RoutingKeys.Count > 0) diff --git a/src/RabbitMQCoreClient/Models/Subscription.cs b/src/RabbitMQCoreClient/Models/Subscription.cs index a802bcc..bb10f68 100644 --- a/src/RabbitMQCoreClient/Models/Subscription.cs +++ b/src/RabbitMQCoreClient/Models/Subscription.cs @@ -1,6 +1,7 @@ -using RabbitMQCoreClient.DependencyInjection.ConfigModels; +using RabbitMQCoreClient.Configuration; +using RabbitMQCoreClient.DependencyInjection; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options; +namespace RabbitMQCoreClient.Models; /// /// Message queue for subscribing to events. diff --git a/src/RabbitMQCoreClient/QueueService.cs b/src/RabbitMQCoreClient/QueueService.cs index e7f08df..407986f 100644 --- a/src/RabbitMQCoreClient/QueueService.cs +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; -using RabbitMQCoreClient.Configuration.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Events; using RabbitMQCoreClient.Exceptions; +using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; using System.Diagnostics.CodeAnalysis; using System.Text; From dc939182066aaa4c72b61393e879f1d65110ad73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Fri, 16 Jan 2026 16:29:56 +0300 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=93=9A=20Add=20documentaion=20for?= =?UTF-8?q?=20BatchQueueSender.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🐞 Fix AddBatchQueueSender() dependency problem. - 🔒 Make QueueEventsBuffer classes internal. --- README.md | 85 +++++++++++++++++-- .../ServiceCollectionExtensions.cs | 13 +++ .../BatchQueueSender/EventsWriter.cs | 2 +- .../{ => Extensions}/BatchQueueExtensions.cs | 0 .../QueueEventsBufferEngine.cs | 2 +- 5 files changed, 91 insertions(+), 11 deletions(-) rename src/RabbitMQCoreClient/BatchQueueSender/{ => Extensions}/BatchQueueExtensions.cs (100%) diff --git a/README.md b/README.md index 6fc5ad8..f40a189 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,8 @@ var bodyList = Enumerable.Range(1, 10).Select(x => new SimpleObj { Name = $"test await queueService.SendBatchAsync(bodyList, "test_routing_key"); ``` -#### Buffer messages in memory and send them at separate thread +#### Buffer messages in memory and send them batch by timer or count -From the version v5.1.0 there was introduced a new mechanic of the sending messages using separate thread. You can use this feature when you have to send many parallel small messages to the queue (for example from the ASP.NET requests). The feature allows you to buffer that messages at the in-memory list and flush them at once using the `SendBatchAsync` method. @@ -148,15 +147,12 @@ To use this feature register it at DI: using RabbitMQCoreClient.DependencyInjection; ... -services.AddBatchQueueSender(); +services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(); ``` Instead of injecting the interface `RabbitMQCoreClient.IQueueService` inject `RabbitMQCoreClient.BatchQueueSender.IQueueEventsBufferEngine`. -Then use methods to queue your messages. The methods are thread safe. -```csharp -Task AddEvent(T @event, string routingKey); -Task AddEvent(IEnumerable events, string routingKey); -``` +Then use methods `void AddEvent()` to queue your messages. The methods are thread safe. You can configure the flush options by Action or IConfiguration. Example of the configuration JSON: ```json @@ -172,7 +168,78 @@ You can configure the flush options by Action or IConfiguration. Example of the using RabbitMQCoreClient.DependencyInjection; ... -services.AddBatchQueueSender(configuration.GetSection("QueueFlushSettings")); +services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(configuration.GetSection("QueueFlushSettings")); +``` + +#### Extended buffer configuration + +If you need you own implementation of sending events from buffer then implement the interface `IEventsWriter` and add it to DI after `.AddBatchQueueSender()`: + +```csharp +builder.Services.AddTransient(); +``` + +Implementation example: + +```csharp +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Implementation of the service for sending events to the data bus. +/// +internal sealed class EventsWriter : IEventsWriter +{ + readonly IQueueService _queueService; + + /// + /// Create new object of . + /// + /// Queue publisher. + public EventsWriter(IQueueService queueService) + { + _queueService = queueService; + } + + /// + public async Task WriteBatch(IEnumerable events, string routingKey) + { + if (!events.Any()) + return; + + await _queueService.SendBatchAsync(events.Select(x => x.Message), routingKey); + } +} +``` + +You also can implement `IEventsHandler` to run you methods `OnAfterWriteEvents` or `OnWriteErrors`. +You must add you custom implementation to DI: + +```csharp +builder.Services.AddTransient(); +``` + +##### Custom EventItem model + +If you need to add custom properties to EventItem model and handle these properties in `IEventsWriter.WriteBatch` +you can inherit from `EventItem` and use `IEventsBufferEngine.Add`. There is a ready to use class `EventItemWithSourceObject`. +This class contains the source object on the basis of which an array of bytes will be calculated to be sent. +At `IEventsWriter.WriteBatch` cast to the class `EventItemWithSourceObject`. +Keep in mind that such a mechanism adds additional pressure to the GC. + +```csharp +public async Task WriteBatch(IEnumerable events, string routingKey) +{ + ... + foreach (EventItem item in events) + { + if (item is EventItemWithSourceObject eventWithSource) + { + // Working с item.Source + } + } + ... +} ``` ### Receiving and processing messages diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index c0bd347..93e5131 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RabbitMQCoreClient.BatchQueueSender; @@ -14,6 +15,18 @@ public static partial class ServiceCollectionExtensions static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) { services.TryAddTransient(); + + services.AddSingleton(sp => + { + var options = sp.GetRequiredService>(); + + return new QueueEventsBufferEngine(sp.GetRequiredService(), + options?.Value?.EventsFlushCount ?? 10000, + TimeSpan.FromSeconds(options?.Value?.EventsFlushPeriodSec ?? 2), + sp.GetService(), + sp.GetRequiredService(), + sp.GetService>()); + }); services.TryAddSingleton(); return services; } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs index e39f96e..8a51a07 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/EventsWriter.cs @@ -3,7 +3,7 @@ namespace RabbitMQCoreClient.BatchQueueSender; /// /// Implementation of the service for sending events to the data bus. /// -public sealed class EventsWriter : IEventsWriter +internal sealed class EventsWriter : IEventsWriter { readonly IQueueService _queueService; diff --git a/src/RabbitMQCoreClient/BatchQueueSender/BatchQueueExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs similarity index 100% rename from src/RabbitMQCoreClient/BatchQueueSender/BatchQueueExtensions.cs rename to src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs index aae0ec2..4ce0028 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs @@ -12,7 +12,7 @@ namespace RabbitMQCoreClient.BatchQueueSender; /// /// Implementation of the stream data event store buffer. /// -public sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable +internal sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable { readonly Queue _buffer = new Queue(); readonly object _syncRoot = new object(); From 2c8ea6ffd4ba038eceb9ba626657fa594bcf41ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Mon, 19 Jan 2026 10:49:27 +0300 Subject: [PATCH 09/13] Add trimming support for serializers. - Add EnableConfigurationBindingGenerator configuration parameter. --- .../DependencyInjection/QueueBatchSenderOptions.cs | 2 +- .../BatchQueueSender/QueueEventsBufferEngine.cs | 7 ------- .../DependencyInjection/Extensions/BuilderExtensions.cs | 9 +++++++-- .../Extentions/SystemTextJsonQueueServiceExtensions.cs | 3 +++ src/RabbitMQCoreClient/RabbitMQCoreClient.csproj | 6 ++++-- src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs | 4 ++++ .../Serializers/SystemTextJsonMessageSerializer.cs | 3 +++ 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs index 41df7b2..90a1d92 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs @@ -3,7 +3,7 @@ namespace RabbitMQCoreClient.DependencyInjection; /// /// Queue bus buffer options. /// -public sealed class QueueBatchSenderOptions +public sealed partial class QueueBatchSenderOptions { /// /// The period for resetting (writing) events in RabbitMQ. diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs index 4ce0028..e5dd5d1 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs @@ -68,13 +68,6 @@ public void Add([NotNull] EventItem item) } } - /// - public void AddEvent([NotNull] T @event, string routingKey) - where T : class - { - Add(new EventItem(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(@event), routingKey)); - } - async Task FlushByTimerAsync() { EventItem[]? array = null; diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index 9775068..b890c69 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -5,6 +5,7 @@ using RabbitMQCoreClient.Configuration.DependencyInjection; using RabbitMQCoreClient.Exceptions; using RabbitMQCoreClient.Models; +using System.Diagnostics.CodeAnalysis; namespace RabbitMQCoreClient.DependencyInjection; @@ -208,7 +209,9 @@ public static IRabbitMQCoreClientConsumerBuilder AddSubscription( /// IRabbitMQCoreClientConsumerBuilder instance. /// Routing keys bound to the queue. /// The handler needs to set at least one routing key. - public static IRabbitMQCoreClientConsumerBuilder AddHandler(this IRabbitMQCoreClientConsumerBuilder builder, + public static IRabbitMQCoreClientConsumerBuilder AddHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMessageHandler>( + this IRabbitMQCoreClientConsumerBuilder builder, params string[] routingKeys) where TMessageHandler : class, IMessageHandler { @@ -226,7 +229,9 @@ public static IRabbitMQCoreClientConsumerBuilder AddHandler(thi /// IRabbitMQCoreClientConsumerBuilder instance. /// The options that can change consumer handler default behavior. /// Routing keys bound to the queue. - public static IRabbitMQCoreClientConsumerBuilder AddHandler(this IRabbitMQCoreClientConsumerBuilder builder, + public static IRabbitMQCoreClientConsumerBuilder AddHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMessageHandler>( + this IRabbitMQCoreClientConsumerBuilder builder, IList routingKeys, ConsumerHandlerOptions? options = default) where TMessageHandler : class, IMessageHandler diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs index d076cb2..a6c8cd6 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs @@ -1,4 +1,5 @@ using RabbitMQCoreClient.Serializers; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -21,6 +22,7 @@ public static class SystemTextJsonQueueServiceExtensions /// Cancellation token. /// /// obj + [RequiresUnreferencedCode("Method uses System.Text.Json.JsonSerializer.SerializeToUtf8Bytes witch is incompatible with trimming.")] public static ValueTask SendAsync( this IQueueService service, T obj, @@ -88,6 +90,7 @@ public static ValueTask SendAsync( /// Cancellation token. /// /// obj + [RequiresUnreferencedCode("Method uses System.Text.Json.JsonSerializer.SerializeToUtf8Bytes witch is incompatible with trimming.")] public static ValueTask SendBatchAsync( this IQueueService service, IEnumerable objs, diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj index 6164d8d..65b2c2d 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj +++ b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj @@ -5,6 +5,7 @@ $(VersionSuffix) $(Version)-$(VersionSuffix) true + true net8.0;net9.0;net10.0 Sergey Pismennyi MONQ Digital lab @@ -15,12 +16,13 @@ rabbitmq library queue dependenci-injection di netcore https://github.com/MONQDL/RabbitMQCoreClient https://github.com/MONQDL/RabbitMQCoreClient - The RabbitMQ Client library introduces easy-to-configure methods to consume and send RabbitMQ Messages. + The RabbitMQ Client library introduces easy-to-configure methods to consume and send RabbitMQ Messages with batch sending. true true snupkg enable enable + true @@ -29,7 +31,7 @@ - + diff --git a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs index 58a7aba..68f0bbc 100644 --- a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace RabbitMQCoreClient.Serializers; /// @@ -11,6 +13,7 @@ public interface IMessageSerializer /// The value type. /// The object to serialize. /// Serialized string. + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] ReadOnlyMemory Serialize(TValue value); /// @@ -19,5 +22,6 @@ public interface IMessageSerializer /// The result type. /// The byte array of the message from the provider as ReadOnlyMemory <byte>. /// The object of type or null. + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] TResult? Deserialize(ReadOnlyMemory value); } diff --git a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs index 4396cb7..510a7b9 100644 --- a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace RabbitMQCoreClient.Serializers; @@ -41,10 +42,12 @@ public SystemTextJsonMessageSerializer(Action + [RequiresUnreferencedCode("Method uses System.Text.Json.JsonSerializer.SerializeToUtf8Bytes witch is incompatible with trimming.")] public ReadOnlyMemory Serialize(TValue value) => System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, Options); /// + [RequiresUnreferencedCode("Method uses System.Text.Json.JsonSerializer.SerializeToUtf8Bytes witch is incompatible with trimming.")] public TResult? Deserialize(ReadOnlyMemory value) => System.Text.Json.JsonSerializer.Deserialize(value.Span, Options); } From 9ce5ca91c74e7548128b79a5f51c3d948078d080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Mon, 19 Jan 2026 16:55:00 +0300 Subject: [PATCH 10/13] Add trimming support to Configuration binders. - Add additional [RequiresUnreferencedCode] attributes. --- .../Program.cs | 1 - .../Extensions/BatchQueueExtensions.cs | 2 ++ .../ConfigFormats/JsonV1Binder.cs | 29 +++++++++++++++---- .../ConfigFormats/JsonV2Binder.cs | 29 +++++++++++++++---- .../Extentions/QueueServiceExtensions.cs | 9 ++---- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs index 894eb83..ba7a86d 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -96,7 +96,6 @@ await host.RunAsync(); - static async Task CreateSender(IQueueService queueService, CancellationToken token) { while (!token.IsCancellationRequested) diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs index 1bbee64..5943e03 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs @@ -14,6 +14,7 @@ public static class BatchQueueExtensions /// The object. /// The object to send to the data bus. /// The name of the route key with which you want to send events to the data bus. + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] public static void AddEvent(this IQueueEventsBufferEngine service, [NotNull] T obj, string routingKey) where T : class => service.Add(new EventItem(service.Serializer.Serialize(obj), routingKey)); @@ -53,6 +54,7 @@ public static void AddEvent(this IQueueEventsBufferEngine service, string obj, s /// The list of objects to send to the data bus. /// The name of the route key with which you want to send events to the data bus. /// + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) where T : class { diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs index 4e1b5e5..4f304b1 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs @@ -75,7 +75,18 @@ static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder bu if (queueConfig is null) return; - var q = BindConfig(queueConfig); + TConfig q; + // Support of source generators. + if (typeof(TConfig) == typeof(QueueConfig)) + { + q = (TConfig)(object)BindQueueConfig(queueConfig); + } + else if (typeof(TConfig) == typeof(SubscriptionConfig)) + { + q = (TConfig)(object)BindSubscriptionConfig(queueConfig); + } + else + throw new ClientConfigurationException("Configuration supports only QueueConfig or SubscriptionConfig classes"); var queue = createQueue(q); queue.Exchanges.Add(exchange.Name); @@ -83,12 +94,18 @@ static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder bu AddQueue(builder, queue); } - static TConfig BindConfig(IConfigurationSection queueConfig) - where TConfig : new() + static QueueConfig BindQueueConfig(IConfigurationSection queueConfig) + { + var config = new QueueConfig(); + queueConfig.Bind(config); + return config; + } + + static SubscriptionConfig BindSubscriptionConfig(IConfigurationSection queueConfig) { - var q = new TConfig(); - queueConfig.Bind(q); - return q; + var config = new SubscriptionConfig(); + queueConfig.Bind(config); + return config; } static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs index cfb9356..36ad120 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs @@ -64,7 +64,18 @@ static void RegisterQueues(IRabbitMQCoreClientConsumerBuilder b foreach (var queueConfig in queuesConfiguration.GetChildren()) { - var q = BindConfig(queueConfig); + TConfig q; + // Support of source generators. + if (typeof(TConfig) == typeof(QueueConfig)) + { + q = (TConfig)(object)BindQueueConfig(queueConfig); + } + else if (typeof(TConfig) == typeof(SubscriptionConfig)) + { + q = (TConfig)(object)BindSubscriptionConfig(queueConfig); + } + else + throw new ClientConfigurationException("Configuration supports only QueueConfig or SubscriptionConfig classes"); var queue = createQueue(q); @@ -95,12 +106,18 @@ static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, TQ AddQueue(builder, queue); } - static TConfig BindConfig(IConfigurationSection queueConfig) - where TConfig : new() + static QueueConfig BindQueueConfig(IConfigurationSection queueConfig) + { + var config = new QueueConfig(); + queueConfig.Bind(config); + return config; + } + + static SubscriptionConfig BindSubscriptionConfig(IConfigurationSection queueConfig) { - var q = new TConfig(); - queueConfig.Bind(q); - return q; + var config = new SubscriptionConfig(); + queueConfig.Bind(config); + return config; } static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) diff --git a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs index 90635d6..a18d798 100644 --- a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs +++ b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; namespace RabbitMQCoreClient; @@ -17,10 +18,8 @@ public static class QueueServiceExtensions /// The name of the exchange point to which the message is to be sent. /// Cancellation token. /// - /// jsonString - jsonString - /// or - /// exchange - exchange /// obj + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] public static ValueTask SendAsync( this IQueueService service, T obj, @@ -136,10 +135,8 @@ public static ValueTask SendAsync( /// The name of the exchange point to which the message is to be sent. /// Cancellation token. /// - /// jsonString - jsonString - /// or - /// exchange - exchange /// obj + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] public static ValueTask SendBatchAsync( this IQueueService service, IEnumerable objs, From 8f24dbe371e5e66b3829dd0e3bcefa904929b9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Tue, 20 Jan 2026 15:19:45 +0300 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=92=A5=20Remove=20Serializer=20for?= =?UTF-8?q?=20message=20consumers.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📚 Add publishing examples. - 📚 Update README.md documentation. - 📚 Add v7-MIGRATION.md guide. - 💥 Change MessageHandlerJson class for using source generated json contexts. - 💥 Rename consumer Start and Shutdown methods to use Async naming pattern. - 🐞 Fix multiple BatchQueueSender DI registration problem. - ✨ Add BatchQueueSender DI methods to register with IRabbitMQCoreClientConsumerBuilder. - 💥 Rename `IQueueEventsBufferEngine` to `IQueueBufferService` to match with `IQueueService` name. - 📚 Add some documentation for public visible classes and methods. - ✨ Add extension methods for BatchQueueSender to use JsonSerializerContext (json source generators), --- README.md | 275 ++++++------------ RabbitMQCoreClient.sln | 25 +- .../RabbitMQCoreClient.WebApp/SimpleObj.cs | 6 - .../consume/HostConsole/HostConsole.csproj | 28 ++ samples/consume/HostConsole/Program.cs | 38 +++ .../HostConsole/PublisherBackgroundService.cs | 19 ++ samples/consume/HostConsole/SimpleObj.cs | 13 + .../HostConsole/SimpleObjectHandler.cs | 26 ++ samples/consume/HostConsole/appsettings.json | 20 ++ samples/consume/SimpleConsole/Program.cs | 62 ++++ .../SimpleConsole/SimpleConsole.csproj | 28 ++ samples/consume/SimpleConsole/SimpleObj.cs | 13 + .../SimpleConsole/SimpleObjectHandler.cs | 32 ++ .../consume/SimpleConsole/appsettings.json | 31 ++ .../Handler.cs | 5 +- .../Program.cs | 6 +- .../RabbitMQCoreClient.ConsoleClient.csproj | 0 .../RandomStringGenerator.cs | 0 .../SimpleObj.cs | 4 +- .../appsettings.json | 10 - .../RabbitMQCoreClient.WebApp/Handler.cs | 9 +- .../RabbitMQCoreClient.WebApp/Program.cs | 0 .../Properties/launchSettings.json | 0 .../RabbitMQCoreClient.WebApp.csproj | 0 .../RabbitMQCoreClient.WebApp/SimpleObj.cs | 13 + .../appsettings.json | 0 .../ServiceCollectionExtensions.cs | 73 ++++- .../Extensions/BatchQueueExtensions.cs | 32 +- ...BufferEngine.cs => IQueueBufferService.cs} | 2 +- ...sBufferEngine.cs => QueueBufferService.cs} | 10 +- .../ConfigFormats/JsonV1Binder.cs | 15 + .../ConfigFormats/JsonV2Binder.cs | 37 ++- .../DependencyInjection/IQueueConsumer.cs | 6 +- .../IRabbitMQCoreClientBuilder.cs | 2 +- .../Options/ConsumerHandlerOptions.cs | 5 - .../Options/RabbitMQCoreClientOptions.cs | 6 + .../RabbitMQCoreClientBuilder.cs | 5 +- .../RabbitMQCoreClientConsumerBuilder.cs | 3 + .../RabbitMQHostedService.cs | 2 +- .../Events/ReconnectEventArgs.cs | 3 + .../Extentions/QueueServiceExtensions.cs | 45 +-- .../SystemTextJsonQueueServiceExtensions.cs | 34 +++ src/RabbitMQCoreClient/IMessageHandler.cs | 6 - src/RabbitMQCoreClient/MessageHandlerJson.cs | 11 +- src/RabbitMQCoreClient/Models/Queue.cs | 11 + .../Models/RabbitMessageEventArgs.cs | 3 + .../RabbitMQCoreClientConsumer.cs | 26 +- .../Serializers/IMessageSerializer.cs | 9 - .../SystemTextJsonMessageSerializer.cs | 6 - v7-MIGRATION.md | 69 +++++ 50 files changed, 744 insertions(+), 340 deletions(-) delete mode 100644 samples/RabbitMQCoreClient.WebApp/SimpleObj.cs create mode 100644 samples/consume/HostConsole/HostConsole.csproj create mode 100644 samples/consume/HostConsole/Program.cs create mode 100644 samples/consume/HostConsole/PublisherBackgroundService.cs create mode 100644 samples/consume/HostConsole/SimpleObj.cs create mode 100644 samples/consume/HostConsole/SimpleObjectHandler.cs create mode 100644 samples/consume/HostConsole/appsettings.json create mode 100644 samples/consume/SimpleConsole/Program.cs create mode 100644 samples/consume/SimpleConsole/SimpleConsole.csproj create mode 100644 samples/consume/SimpleConsole/SimpleObj.cs create mode 100644 samples/consume/SimpleConsole/SimpleObjectHandler.cs create mode 100644 samples/consume/SimpleConsole/appsettings.json rename samples/{ => test-playground}/RabbitMQCoreClient.ConsoleClient/Handler.cs (91%) rename samples/{ => test-playground}/RabbitMQCoreClient.ConsoleClient/Program.cs (95%) rename samples/{ => test-playground}/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj (100%) rename samples/{ => test-playground}/RabbitMQCoreClient.ConsoleClient/RandomStringGenerator.cs (100%) rename samples/{ => test-playground}/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs (83%) rename samples/{ => test-playground}/RabbitMQCoreClient.ConsoleClient/appsettings.json (78%) rename samples/{ => test-playground}/RabbitMQCoreClient.WebApp/Handler.cs (77%) rename samples/{ => test-playground}/RabbitMQCoreClient.WebApp/Program.cs (100%) rename samples/{ => test-playground}/RabbitMQCoreClient.WebApp/Properties/launchSettings.json (100%) rename samples/{ => test-playground}/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj (100%) create mode 100644 samples/test-playground/RabbitMQCoreClient.WebApp/SimpleObj.cs rename samples/{ => test-playground}/RabbitMQCoreClient.WebApp/appsettings.json (100%) rename src/RabbitMQCoreClient/BatchQueueSender/{IQueueEventsBufferEngine.cs => IQueueBufferService.cs} (93%) rename src/RabbitMQCoreClient/BatchQueueSender/{QueueEventsBufferEngine.cs => QueueBufferService.cs} (96%) create mode 100644 v7-MIGRATION.md diff --git a/README.md b/README.md index f40a189..a5cae92 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ The library implements a custom errored messages mechanism, using the TTL and th Install-Package RabbitMQCoreClient ``` +## MIGRATION from v6 to v7 + +See [v7 migration guide](v7-MIGRATION.md) + ## Using the library The library allows you to both send and receive messages. It makes possible to subscribe to named queues, @@ -151,8 +155,8 @@ services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) .AddBatchQueueSender(); ``` -Instead of injecting the interface `RabbitMQCoreClient.IQueueService` inject `RabbitMQCoreClient.BatchQueueSender.IQueueEventsBufferEngine`. -Then use methods `void AddEvent()` to queue your messages. The methods are thread safe. +Instead of injecting the interface `RabbitMQCoreClient.IQueueService` inject `RabbitMQCoreClient.BatchQueueSender.IQueueBufferService`. +Then use methods `void AddEvent*()` to queue your messages. The methods are thread safe. You can configure the flush options by Action or IConfiguration. Example of the configuration JSON: ```json @@ -244,90 +248,41 @@ public async Task WriteBatch(IEnumerable events, string routingKey) ### Receiving and processing messages -##### Console application - -``` -class Program -{ - static readonly AutoResetEvent _closing = new AutoResetEvent(false); - - static async Task Main(string[] args) - { - Console.OutputEncoding = Encoding.UTF8; +By default `builder.Services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ"))` adds support of publisher. +If you need to consume messages from queues, you must configure consumer. - var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(LoggerFactory.Create(x => - { - x.SetMinimumLevel(LogLevel.Trace); - x.AddConsole(); - })); - - // For sending and consuming messages full. - services - .AddRabbitMQCoreClient(opt => opt.Host = "localhost") - .AddExchange("default") - .AddConsumer() - .AddHandler("test_routing_key") - .AddQueue("my-test-queue") - .AddSubscription(); - - var serviceProvider = services.BuildServiceProvider(); - var consumer = serviceProvider.GetRequiredService(); - consumer.Start(); - - var body = new SimpleObj { Name = "test sending" }; - await queueService.SendAsync(body, "test_routing_key"); - - _closing.WaitOne(); - Environment.Exit(0); - } -} -``` +#### Configuring consumer -`_closing.WaitOne ();` is used to prevent the program from terminating immediately after starting. +You can configure consummer by 2 methods - fluent and from configuration. -The `.Start ();` method does not block the main thread. +Fluent example: -##### ASP.NET Core 3.1+ ``` -public class Startup -{ - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } +builder.Services + .AddRabbitMQCoreClient(opt => opt.Host = "localhost") + .AddExchange("default") + .AddConsumer() + .AddHandler("test_routing_key") + .AddQueue("my-test-queue") + .AddSubscription(); +``` - public IConfiguration Configuration { get; } +IConfiguration example: - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) +``` +builder.Services + .AddRabbitMQCoreClientConsumer(builder.Configuration.GetSection("RabbitMQ")) + .AddHandler(["test_routing_key"], new ConsumerHandlerOptions { - services.AddControllers(); - - services - .AddRabbitMQCoreClient(opt => opt.Host = "localhost") - .AddExchange("default") - .AddConsumer() - .AddHandler("test_routing_key") - .AddQueue("my-test-queue"); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) + RetryKey = "test_routing_key_retry" + }) + .AddHandler(["test_routing_key_subscription"], new ConsumerHandlerOptions { - app.StartRabbitMqCore(lifetime); - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} + RetryKey = "test_routing_key_retry" + }); ``` -#### `IMessageHandler` +#### Processing messages with `IMessageHandler` In the basic version, messages are received using the implementation of the interface `RabbitMQCoreClient.IMessageHandler`. The interface requires the implementation of a message handling method `Task HandleMessage(string message, RabbitMessageEventArgs args)`, @@ -341,70 +296,75 @@ Example: public class RawHandler : IMessageHandler { public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); - public ConsumerHandlerOptions Options { get; set; } - public IMessageSerializer Serializer { get; set; } + public ConsumerHandlerOptions? Options { get; set; } - public Task HandleMessage(string message, RabbitMessageEventArgs args) + public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) { Console.WriteLine(message); return Task.CompletedTask; } } +``` -class Program -{ - static async Task Main(string[] args) - { - var services = new ServiceCollection(); - services - .AddRabbitMQCoreClientConsumer(config) - .AddHandler("test_routing_key"); - - var serviceProvider = services.BuildServiceProvider(); - var consumer = serviceProvider.GetRequiredService(); - consumer.Start(); - } -} +Program.cs partial example + +``` +services + .AddRabbitMQCoreClientConsumer(config.GetSection("RabbitMQ")) + .AddBatchQueueSender() // If you want to send messages with inmemory buffering. + .AddHandler("test_key"); ``` **Note**: A message can only be processed by one handler. Although one handler can handle many messages with different routing keys. This limitation is due to the routing of erroneous messages in the handler. +More examples can be found at `samples` folder in this repository. + #### `MessageHandlerJson` -Since messages in this client are serialized in Json, the interface implementation has been added as an abstract class `RabbitMQCoreClient.MessageHandlerJson`, -which itself deserializes the Json into the model of the desired type. Usage example: +If your messages is JSON serialized, you can use an abstract class `RabbitMQCoreClient.MessageHandlerJson`, +which itself deserializes the Json into the class model of the desired type. + +**Note**: If you want to use this class, you must provide source generated JsonSerializerContext for you class. +If you want to use your own deserializer, then use your custom `IMessageHandler` implementation. + +Usage example: ```csharp -public class Handler : MessageHandlerJson +internal sealed class SimpleObjectHandler : MessageHandlerJson { + readonly ILogger _logger; + + public SimpleObjectHandler(ILogger logger) + { + _logger = logger; + } + + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) { - Console.WriteLine(JsonConvert.SerializeObject(message)); + _logger.LogInformation("Incoming simple object name: {Name}", message.Name); + return Task.CompletedTask; } protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) { - Console.WriteLine(e.Message); + _logger.LogError(e, "Incoming message can't be deserialized. Error: {ErrorMessage}", e.Message); return base.OnParseError(json, e, args); } } -class Program +public class SimpleObj +{ + public required string Name { get; set; } +} + +[JsonSerializable(typeof(SimpleObj))] +public partial class SimpleObjContext : JsonSerializerContext { - static async Task Main(string[] args) - { - var services = new ServiceCollection(); - services - .AddRabbitMQCoreClientConsumer(config) - .AddHandler("test_routing_key"); - - var serviceProvider = services.BuildServiceProvider(); - var consumer = serviceProvider.GetRequiredService(); - consumer.Start(); - } } ``` @@ -428,8 +388,10 @@ If the method succeeds normally, the message will be considered delivered. Usage example: ```csharp -public class Handler : MessageHandlerJson +internal class Handler : MessageHandlerJson { + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) { try @@ -462,60 +424,10 @@ public class Handler : MessageHandlerJson ### Json Serializers -You can choose what serializer to use. The library supports `System.Text.Json` or `Newtonsoft.Json` serializers. -To configure the serializer for the sender and consumer you can call `AddNewtonsoftJson()` or `AddSystemTextJson()` method at the configuration stage. - -Example -*Program.cs - console application* -``` -class Program -{ - static async Task Main(string[] args) - { - var config = new ConfigurationBuilder() - .AddJsonFile($"appsettings.json", optional: false) - .Build(); - - var services = new ServiceCollection(); - - services - .AddRabbitMQCoreClient(config) - .AddSystemTextJson(); - } -} -``` +You can choose what serializer to use to *publish* messages with serialization. +The library by default supports `System.Text.Json` serializer. -The default serializer is set to `System.Text.Json` due to improved performance compared to `Newtonsoft.Json`. -Due to legacy models that contains `JObject` and `JArray` properties the System.Text.Json default serializer has converters of the NewtonsoftJson objects to serialize and deserialize. - -If you want to use different serializers for different message handlers that you can set CustomSerializer at the Handler configuration stage. - -Example - -Example -*Program.cs - console application* -``` -class Program -{ - static async Task Main(string[] args) - { - var services = new ServiceCollection(); - services - .AddRabbitMQCoreClientConsumer(config) - .AddSystemTextJson() - .AddHandler("test_routing_key", new ConsumerHandlerOptions - { - CustomSerializer = new NewtonsoftJsonMessageSerializer() - })); - - var serviceProvider = services.BuildServiceProvider(); - var consumer = serviceProvider.GetRequiredService(); - consumer.Start(); - } -} -``` - -#### Custom serializer +#### Custom publish serializer You can make make your own custom serializer. To do that you must implement the `RabbitMQCoreClient.Serializers.IMessageSerializer` interface. Example: @@ -591,39 +503,28 @@ Use the extension method at the configuration stage. *Program.cs - console application* ``` -class Program -{ - static async Task Main(string[] args) - { - var config = new ConfigurationBuilder() - .AddJsonFile($"appsettings.json", optional: false) - .Build(); - - var services = new ServiceCollection(); - - services - .AddRabbitMQCoreClient(config) - .AddCustomSerializer(); - } -} +services + .AddRabbitMQCoreClient(config) + .AddCustomSerializer(); ``` #### Quorum queues at cluster environment -Started from v5.1.0 you can set option `"UseQuorumQueues": true` at root configuration level +You can set option `"UseQuorumQueues": true` at root configuration level and `"UseQuorum": true` at queue configuration level. This option adds argument `"x-queue-type": "quorum"` on queue declaration and can be used at the configured cluster environment. ### Configuration with file -Configuration can be done either through options or through configuration from appsettings.json. +Configuration can be done either through options or through configuration from `appsettings.json`. -In version 4.0 of the library, the old (<= v3) queue auto-registration format is still supported. But with limitations: +The old legacy queue auto-registration format is still supported. But with limitations: - Only one queue can be automatically registered. The queue is registered at the exchange point "Exchange". #### SSL support -Started from v5.2.0 you can set options to configure SSL secured connection to the server. + +You can set options to configure SSL secured connection to the server. To enable the SSL connection you must set `"SslEnabled": true` option at root configuration level. You can use ssl options to setup the SSL connection: @@ -638,9 +539,10 @@ Acceptable values: - RemoteCertificateChainErrors - System.Security.Cryptography.X509Certificates.X509Chain.ChainStatus has returned a non empty array. - __SslVersion__ [optional] - the TLS protocol version. The client will let the OS pick a suitable version by using value `"None"`. -If this option is unavailable on somne environments or effectively disabled, -e.g.see via app context, the client will attempt to fall backto TLSv1.2. The default is `"None"`. +If this option is unavailable on some environments or effectively disabled, +e.g.see via app context, the client will attempt to fall back to TLSv1.2. The default is `"None"`. You can supply multiple arguments separated by comma. For example: `"SslVersion": "Ssl3,Tls13"`. + Acceptable values: - None - allows the operating system to choose the best protocol to use, and to block protocols that are not secure. Unless your app has a specific reason not to, @@ -667,7 +569,7 @@ Set to true to check peer certificate for revocation. ##### Configuration format -###### Full configuration: +###### Full configuration example ```json { @@ -742,9 +644,10 @@ Set to true to check peer certificate for revocation. } ``` -###### Reduced configuration that is used on a daily basis +###### Reduced configuration example that is used on a daily basis If `Exchanges` is not specified in the `Queues` section, then the queue will use the default exchange. +You can skip Queues or Subscriptions or both, if you do not have consumers of this types. ```json { diff --git a/RabbitMQCoreClient.sln b/RabbitMQCoreClient.sln index ae19c97..c595d30 100644 --- a/RabbitMQCoreClient.sln +++ b/RabbitMQCoreClient.sln @@ -18,11 +18,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient", "src\R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.Tests", "src\RabbitMQCoreClient.Tests\RabbitMQCoreClient.Tests.csproj", "{B35BE626-60CC-4EAC-90FB-422885DC3506}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.ConsoleClient", "samples\RabbitMQCoreClient.ConsoleClient\RabbitMQCoreClient.ConsoleClient.csproj", "{5925B853-0BC5-48D8-86DE-43943E5943D3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.ConsoleClient", "samples\test-playground\RabbitMQCoreClient.ConsoleClient\RabbitMQCoreClient.ConsoleClient.csproj", "{5925B853-0BC5-48D8-86DE-43943E5943D3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{1393D7D2-3B09-49BF-A616-B25D1365E58B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.WebApp", "samples\RabbitMQCoreClient.WebApp\RabbitMQCoreClient.WebApp.csproj", "{E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.WebApp", "sample\test-playground\RabbitMQCoreClient.WebApp\RabbitMQCoreClient.WebApp.csproj", "{E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "publish", "publish", "{8C256A46-AE61-4D61-A221-F94741C36310}" EndProject @@ -34,6 +34,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostConsole", "samples\publ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp", "samples\publish\WebApp\WebApp.csproj", "{E9E078E3-7167-4C11-807E-00324D513C22}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostConsole", "samples\consume\HostConsole\HostConsole.csproj", "{5131378B-3910-3D47-6B73-2BDF78930B8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleConsole", "samples\consume\SimpleConsole\SimpleConsole.csproj", "{38F911F3-712C-DC44-0FF2-854306CFA892}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test-playground", "test-playground", "{DB0FF024-8A5E-446D-8D0C-74EA3886D46C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +74,14 @@ Global {E9E078E3-7167-4C11-807E-00324D513C22}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9E078E3-7167-4C11-807E-00324D513C22}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9E078E3-7167-4C11-807E-00324D513C22}.Release|Any CPU.Build.0 = Release|Any CPU + {5131378B-3910-3D47-6B73-2BDF78930B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5131378B-3910-3D47-6B73-2BDF78930B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5131378B-3910-3D47-6B73-2BDF78930B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5131378B-3910-3D47-6B73-2BDF78930B8E}.Release|Any CPU.Build.0 = Release|Any CPU + {38F911F3-712C-DC44-0FF2-854306CFA892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38F911F3-712C-DC44-0FF2-854306CFA892}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38F911F3-712C-DC44-0FF2-854306CFA892}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38F911F3-712C-DC44-0FF2-854306CFA892}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -75,13 +89,16 @@ Global GlobalSection(NestedProjects) = preSolution {CE80F1BF-A85A-4282-B461-174A086985F5} = {F06C16D8-3CB0-45C7-BC44-EDB4EE4EDBA7} {B35BE626-60CC-4EAC-90FB-422885DC3506} = {F06C16D8-3CB0-45C7-BC44-EDB4EE4EDBA7} - {5925B853-0BC5-48D8-86DE-43943E5943D3} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} - {E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} + {5925B853-0BC5-48D8-86DE-43943E5943D3} = {DB0FF024-8A5E-446D-8D0C-74EA3886D46C} + {E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B} = {DB0FF024-8A5E-446D-8D0C-74EA3886D46C} {8C256A46-AE61-4D61-A221-F94741C36310} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} {A778C338-849C-4472-AD4A-174298FA492A} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} {656DAD79-76E8-4E5D-8C29-3BC6CAF75731} = {8C256A46-AE61-4D61-A221-F94741C36310} {7BBECA17-DCDD-469C-ACE6-E227360EA257} = {8C256A46-AE61-4D61-A221-F94741C36310} {E9E078E3-7167-4C11-807E-00324D513C22} = {8C256A46-AE61-4D61-A221-F94741C36310} + {5131378B-3910-3D47-6B73-2BDF78930B8E} = {A778C338-849C-4472-AD4A-174298FA492A} + {38F911F3-712C-DC44-0FF2-854306CFA892} = {A778C338-849C-4472-AD4A-174298FA492A} + {DB0FF024-8A5E-446D-8D0C-74EA3886D46C} = {1393D7D2-3B09-49BF-A616-B25D1365E58B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4CC19D97-DE19-4541-802F-F841284C47C7} diff --git a/samples/RabbitMQCoreClient.WebApp/SimpleObj.cs b/samples/RabbitMQCoreClient.WebApp/SimpleObj.cs deleted file mode 100644 index 111403e..0000000 --- a/samples/RabbitMQCoreClient.WebApp/SimpleObj.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace RabbitMQCoreClient.WebApp; - -public class SimpleObj -{ - public string Name { get; set; } -} diff --git a/samples/consume/HostConsole/HostConsole.csproj b/samples/consume/HostConsole/HostConsole.csproj new file mode 100644 index 0000000..1334343 --- /dev/null +++ b/samples/consume/HostConsole/HostConsole.csproj @@ -0,0 +1,28 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + PreserveNewest + true + PreserveNewest + + + + + + PreserveNewest + + + + diff --git a/samples/consume/HostConsole/Program.cs b/samples/consume/HostConsole/Program.cs new file mode 100644 index 0000000..620e9f2 --- /dev/null +++ b/samples/consume/HostConsole/Program.cs @@ -0,0 +1,38 @@ +using HostConsole; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RabbitMQCoreClient.DependencyInjection; + +Console.WriteLine("Host console message publishing and consuming example"); + +using IHost host = new HostBuilder() + .ConfigureHostConfiguration(configHost => + { + configHost.AddCommandLine(args); + + configHost + .AddJsonFile($"appsettings.json", optional: false) + .AddJsonFile($"appsettings.Development.json", optional: false); + }) + .ConfigureServices((builder, services) => + { + services.AddLogging(); + services.AddSingleton(LoggerFactory.Create(x => + { + x.SetMinimumLevel(LogLevel.Trace); + x.AddConsole(); + })); + + // You can just call AddRabbitMQCoreClientConsumer(). It configures AddRabbitMQCoreClient() automatically. + services + .AddRabbitMQCoreClientConsumer(builder.Configuration.GetSection("RabbitMQ")) + .AddHandler("test_key"); + + services.AddHostedService(); + }) + .UseConsoleLifetime() + .Build(); + +await host.RunAsync(); diff --git a/samples/consume/HostConsole/PublisherBackgroundService.cs b/samples/consume/HostConsole/PublisherBackgroundService.cs new file mode 100644 index 0000000..467a564 --- /dev/null +++ b/samples/consume/HostConsole/PublisherBackgroundService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Hosting; +using RabbitMQCoreClient; + +namespace HostConsole; + +class PublisherBackgroundService : BackgroundService +{ + readonly IQueueService _publisher; + + public PublisherBackgroundService(IQueueService publisher) + { + _publisher = publisher; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _publisher.SendAsync("""{ "foo": "bar" }""", "test_key", cancellationToken: stoppingToken); + } +} diff --git a/samples/consume/HostConsole/SimpleObj.cs b/samples/consume/HostConsole/SimpleObj.cs new file mode 100644 index 0000000..c60feb5 --- /dev/null +++ b/samples/consume/HostConsole/SimpleObj.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace HostConsole; + +public class SimpleObj +{ + public required string Name { get; set; } +} + +[JsonSerializable(typeof(SimpleObj))] +public partial class SimpleObjContext : JsonSerializerContext +{ +} diff --git a/samples/consume/HostConsole/SimpleObjectHandler.cs b/samples/consume/HostConsole/SimpleObjectHandler.cs new file mode 100644 index 0000000..ba93836 --- /dev/null +++ b/samples/consume/HostConsole/SimpleObjectHandler.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging; +using RabbitMQCoreClient; +using RabbitMQCoreClient.Models; +using System.Text.Json.Serialization.Metadata; + +namespace HostConsole; + +internal sealed class SimpleObjectHandler : MessageHandlerJson +{ + readonly ILogger _logger; + + public SimpleObjectHandler(ILogger logger) + { + _logger = logger; + } + + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + { + _logger.LogInformation("Incoming simple object name: {Name}", message.Name); + + return Task.CompletedTask; + } +} + diff --git a/samples/consume/HostConsole/appsettings.json b/samples/consume/HostConsole/appsettings.json new file mode 100644 index 0000000..5d3363a --- /dev/null +++ b/samples/consume/HostConsole/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "RabbitMQ": { + "HostName": "rabbit-1", + "UserName": "user", + "Password": "password", + "Exchanges": [ + { + "Name": "direct_exchange", + "IsDefault": true + } + ] + } +} diff --git a/samples/consume/SimpleConsole/Program.cs b/samples/consume/SimpleConsole/Program.cs new file mode 100644 index 0000000..e620b27 --- /dev/null +++ b/samples/consume/SimpleConsole/Program.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; +using SimpleConsole; + +Console.WriteLine("Simple console message publishing and consuming messages example"); + +using var cts = new CancellationTokenSource(); + +Console.CancelKeyPress += (sender, eventArgs) => +{ + Console.WriteLine("\nCtrl+C pressed. Exiting..."); + cts.Cancel(); + eventArgs.Cancel = true; // Предотвращаем немедленное завершение процесса +}; + +var config = new ConfigurationBuilder() + .AddJsonFile($"appsettings.json", optional: false) + .AddJsonFile($"appsettings.Development.json", optional: true) + .Build(); + +var services = new ServiceCollection(); +services.AddLogging(); +services.AddSingleton(LoggerFactory.Create(x => +{ + x.SetMinimumLevel(LogLevel.Trace); + x.AddConsole(); +})); + +// You can just call AddRabbitMQCoreClientConsumer(). It configures AddRabbitMQCoreClient() automatically. +services + .AddRabbitMQCoreClientConsumer(config.GetSection("RabbitMQ")) + .AddBatchQueueSender() // If you want to send messages with inmemory buffering. + .AddHandler("test_key"); + +var provider = services.BuildServiceProvider(); + +var publisher = provider.GetRequiredService(); + +await publisher.ConnectAsync(cts.Token); + +var consumer = provider.GetRequiredService(); + +await consumer.StartAsync(cts.Token); + +await publisher.SendAsync("""{ "Name": "bar" }""", "test_key", cancellationToken: cts.Token); + +await WaitForCancellationAsync(cts.Token); + +static async Task WaitForCancellationAsync(CancellationToken cancellationToken) +{ + // Creating a TaskCompletionSource that will terminate when the token is canceled. + var tcs = new TaskCompletionSource(); + + // Registering a callback for token cancellation. + using (cancellationToken.Register(() => tcs.SetResult(true))) + { + await tcs.Task; + } +} diff --git a/samples/consume/SimpleConsole/SimpleConsole.csproj b/samples/consume/SimpleConsole/SimpleConsole.csproj new file mode 100644 index 0000000..839fb08 --- /dev/null +++ b/samples/consume/SimpleConsole/SimpleConsole.csproj @@ -0,0 +1,28 @@ + + + + Exe + net10.0 + enable + enable + + + + + PreserveNewest + true + PreserveNewest + + + + + + + + + + PreserveNewest + + + + diff --git a/samples/consume/SimpleConsole/SimpleObj.cs b/samples/consume/SimpleConsole/SimpleObj.cs new file mode 100644 index 0000000..9a0692d --- /dev/null +++ b/samples/consume/SimpleConsole/SimpleObj.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SimpleConsole; + +public class SimpleObj +{ + public required string Name { get; set; } +} + +[JsonSerializable(typeof(SimpleObj))] +public partial class SimpleObjContext : JsonSerializerContext +{ +} diff --git a/samples/consume/SimpleConsole/SimpleObjectHandler.cs b/samples/consume/SimpleConsole/SimpleObjectHandler.cs new file mode 100644 index 0000000..e569f13 --- /dev/null +++ b/samples/consume/SimpleConsole/SimpleObjectHandler.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; +using RabbitMQCoreClient; +using RabbitMQCoreClient.Models; +using System.Text.Json.Serialization.Metadata; + +namespace SimpleConsole; + +internal sealed class SimpleObjectHandler : MessageHandlerJson +{ + readonly ILogger _logger; + + public SimpleObjectHandler(ILogger logger) + { + _logger = logger; + } + + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + { + _logger.LogInformation("Incoming simple object name: {Name}", message.Name); + + return Task.CompletedTask; + } + + protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) + { + _logger.LogError(e, "Incoming message can't be deserialized. Error: {ErrorMessage}", e.Message); + return base.OnParseError(json, e, args); + } +} + diff --git a/samples/consume/SimpleConsole/appsettings.json b/samples/consume/SimpleConsole/appsettings.json new file mode 100644 index 0000000..86162a9 --- /dev/null +++ b/samples/consume/SimpleConsole/appsettings.json @@ -0,0 +1,31 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "RabbitMQ": { + "HostName": "rabbit-1", + "UserName": "user", + "Password": "password", + "Queues": [ + { + "Name": "test_queue_dev", + "RoutingKeys": [ + "test_key" + ], + "Exchanges": [ + "direct_exchange" + ] + } + ], + "Exchanges": [ + { + "Name": "direct_exchange", + "IsDefault": true + } + ] + } +} diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs similarity index 91% rename from samples/RabbitMQCoreClient.ConsoleClient/Handler.cs rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs index 276d775..3aa6464 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs @@ -3,12 +3,16 @@ using RabbitMQCoreClient.Serializers; using System; using System.Text; +using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; namespace RabbitMQCoreClient.ConsoleClient; public class Handler : MessageHandlerJson { + protected override JsonTypeInfo GetSerializerContext() => + SimpleObjContext.Default.SimpleObj; + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) { Console.WriteLine($"message from {args.RoutingKey}"); @@ -35,7 +39,6 @@ public class RawHandler : IMessageHandler { public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); public ConsumerHandlerOptions Options { get; set; } - public IMessageSerializer Serializer { get; set; } public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) { diff --git a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Program.cs similarity index 95% rename from samples/RabbitMQCoreClient.ConsoleClient/Program.cs rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/Program.cs index ba7a86d..13cd485 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -64,8 +64,8 @@ var queueService = serviceProvider.GetRequiredService(); var consumer = serviceProvider.GetRequiredService(); -var batchSender = serviceProvider.GetRequiredService(); -await consumer.Start(); +var batchSender = serviceProvider.GetRequiredService(); +await consumer.StartAsync(); //var body = new SimpleObj { Name = "test sending" }; //await queueService.SendAsync(body, "test_routing_key"); @@ -113,7 +113,7 @@ static async Task CreateSender(IQueueService queueService, CancellationToken tok } } -static async Task CreateBatchSender(IQueueEventsBufferEngine batchSender, CancellationToken token) +static async Task CreateBatchSender(IQueueBufferService batchSender, CancellationToken token) { while (!token.IsCancellationRequested) { diff --git a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj similarity index 100% rename from samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj diff --git a/samples/RabbitMQCoreClient.ConsoleClient/RandomStringGenerator.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RandomStringGenerator.cs similarity index 100% rename from samples/RabbitMQCoreClient.ConsoleClient/RandomStringGenerator.cs rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/RandomStringGenerator.cs diff --git a/samples/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs similarity index 83% rename from samples/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs index a832ee3..522e0f2 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/SimpleObj.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace RabbitMQCoreClient.ConsoleClient; @@ -11,4 +11,4 @@ public class SimpleObj [JsonSerializable(typeof(SimpleObj))] public partial class SimpleObjContext : JsonSerializerContext { -} \ No newline at end of file +} diff --git a/samples/RabbitMQCoreClient.ConsoleClient/appsettings.json b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/appsettings.json similarity index 78% rename from samples/RabbitMQCoreClient.ConsoleClient/appsettings.json rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/appsettings.json index cca6e04..d268455 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/appsettings.json +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/appsettings.json @@ -25,16 +25,6 @@ ] } ], - "Subscriptions": [ - { - "RoutingKeys": [ - "test_routing_key_subscription" - ], - "Exchanges": [ - "direct_exchange" - ] - } - ], "Exchanges": [ { "Name": "direct_exchange", diff --git a/samples/RabbitMQCoreClient.WebApp/Handler.cs b/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs similarity index 77% rename from samples/RabbitMQCoreClient.WebApp/Handler.cs rename to samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs index 04a5583..dbb4407 100644 --- a/samples/RabbitMQCoreClient.WebApp/Handler.cs +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs @@ -1,12 +1,14 @@ using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; using System.Text; +using System.Text.Json.Serialization.Metadata; namespace RabbitMQCoreClient.WebApp; public class Handler : MessageHandlerJson { + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) { ProcessMessage(message); @@ -14,8 +16,6 @@ protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs return Task.CompletedTask; } - protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) => base.OnParseError(json, e, args); - void ProcessMessage(SimpleObj obj) => //if (obj.Name != "my test name") //{ @@ -29,8 +29,7 @@ void ProcessMessage(SimpleObj obj) => public class RawHandler : IMessageHandler { public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); - public ConsumerHandlerOptions Options { get; set; } - public IMessageSerializer Serializer { get; set; } + public ConsumerHandlerOptions? Options { get; set; } public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) { diff --git a/samples/RabbitMQCoreClient.WebApp/Program.cs b/samples/test-playground/RabbitMQCoreClient.WebApp/Program.cs similarity index 100% rename from samples/RabbitMQCoreClient.WebApp/Program.cs rename to samples/test-playground/RabbitMQCoreClient.WebApp/Program.cs diff --git a/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json b/samples/test-playground/RabbitMQCoreClient.WebApp/Properties/launchSettings.json similarity index 100% rename from samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json rename to samples/test-playground/RabbitMQCoreClient.WebApp/Properties/launchSettings.json diff --git a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj b/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj similarity index 100% rename from samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj rename to samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj diff --git a/samples/test-playground/RabbitMQCoreClient.WebApp/SimpleObj.cs b/samples/test-playground/RabbitMQCoreClient.WebApp/SimpleObj.cs new file mode 100644 index 0000000..00fefc6 --- /dev/null +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/SimpleObj.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace RabbitMQCoreClient.WebApp; + +public class SimpleObj +{ + public required string Name { get; set; } +} + +[JsonSerializable(typeof(SimpleObj))] +public partial class SimpleObjContext : JsonSerializerContext +{ +} diff --git a/samples/RabbitMQCoreClient.WebApp/appsettings.json b/samples/test-playground/RabbitMQCoreClient.WebApp/appsettings.json similarity index 100% rename from samples/RabbitMQCoreClient.WebApp/appsettings.json rename to samples/test-playground/RabbitMQCoreClient.WebApp/appsettings.json diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index 93e5131..314359f 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -16,24 +16,23 @@ static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection servic { services.TryAddTransient(); - services.AddSingleton(sp => + services.TryAddSingleton((IServiceProvider sp) => { var options = sp.GetRequiredService>(); - return new QueueEventsBufferEngine(sp.GetRequiredService(), + return new QueueBufferService(sp.GetRequiredService(), options?.Value?.EventsFlushCount ?? 10000, TimeSpan.FromSeconds(options?.Value?.EventsFlushPeriodSec ?? 2), sp.GetService(), sp.GetRequiredService(), - sp.GetService>()); + sp.GetService>()); }); - services.TryAddSingleton(); return services; } /// - /// Registers an instance of the interface in the DI. - /// Injected service can be used to send messages + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages /// to the inmemory queue and process them at the separate thread. /// /// The RabbitMQ Client builder. @@ -51,8 +50,8 @@ public static IRabbitMQCoreClientBuilder AddBatchQueueSender( } /// - /// Registers an instance of the interface in the DI. - /// Injected service can be used to send messages + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages /// to the inmemory queue and process them at the separate thread. /// /// The RabbitMQ Client builder. @@ -69,8 +68,8 @@ public static IRabbitMQCoreClientBuilder AddBatchQueueSender(this IRabbitMQCoreC } /// - /// Registers an instance of the interface in the DI. - /// Injected service can be used to send messages + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages /// to the inmemory queue and process them at the separate thread. /// /// List of services registered in DI. @@ -83,6 +82,60 @@ public static IRabbitMQCoreClientBuilder AddBatchQueueSender(this IRabbitMQCoreC return builder; } + /// + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages + /// to the inmemory queue and process them at the separate thread. + /// + /// The RabbitMQ Client builder. + /// Configuration section containing fields for configuring the message processing service. + /// Use this method if you need to override the configuration. + public static IRabbitMQCoreClientConsumerBuilder AddBatchQueueSender( + this IRabbitMQCoreClientConsumerBuilder builder, + IConfiguration configuration, + Action? setupAction = null) + { + RegisterOptions(builder.Services, configuration, setupAction); + + builder.Services.AddBatchQueueSenderCore(); + return builder; + } + + /// + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages + /// to the inmemory queue and process them at the separate thread. + /// + /// The RabbitMQ Client builder. + /// Use this method if you need to override the configuration. + /// + public static IRabbitMQCoreClientConsumerBuilder AddBatchQueueSender( + this IRabbitMQCoreClientConsumerBuilder builder, + Action? setupAction) + { + if (setupAction != null) + builder.Services.Configure(setupAction); + builder.Services.AddBatchQueueSenderCore(); + + return builder; + } + + /// + /// Registers an instance of the interface in the DI. + /// Injected service can be used to send messages + /// to the inmemory queue and process them at the separate thread. + /// + /// List of services registered in DI. + /// + public static IRabbitMQCoreClientConsumerBuilder AddBatchQueueSender( + this IRabbitMQCoreClientConsumerBuilder builder) + { + builder.Services.TryAddSingleton((x) => Options.Create(new QueueBatchSenderOptions())); + builder.Services.AddBatchQueueSenderCore(); + + return builder; + } + static void RegisterOptions(IServiceCollection services, IConfiguration configuration, Action? setupAction) diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs index 5943e03..40909c4 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs @@ -11,51 +11,51 @@ public static class BatchQueueExtensions /// /// Add an object to be send as event to the data bus. /// - /// The object. + /// The object. /// The object to send to the data bus. /// The name of the route key with which you want to send events to the data bus. [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] - public static void AddEvent(this IQueueEventsBufferEngine service, [NotNull] T obj, string routingKey) + public static void AddEvent(this IQueueBufferService service, [NotNull] T obj, string routingKey) where T : class => service.Add(new EventItem(service.Serializer.Serialize(obj), routingKey)); /// /// Add a byte array object to be send as event to the data bus. /// - /// The object. + /// The object. /// The byte array object to send to the data bus. /// The name of the route key with which you want to send events to the data bus. - public static void AddEvent(this IQueueEventsBufferEngine service, ReadOnlyMemory obj, string routingKey) => + public static void AddEvent(this IQueueBufferService service, ReadOnlyMemory obj, string routingKey) => service.Add(new EventItem(obj, routingKey)); /// /// Add a byte array object to send as event to the data bus. /// - /// The object. + /// The object. /// The byte array object to send to the data bus. /// The name of the route key with which you want to send events to the data bus. - public static void AddEvent(this IQueueEventsBufferEngine service, byte[] obj, string routingKey) => + public static void AddEvent(this IQueueBufferService service, byte[] obj, string routingKey) => service.Add(new EventItem(obj, routingKey)); /// /// Add a string object to send as event to the data bus. /// - /// The object. + /// The object. /// The string object to send to the data bus. /// The name of the route key with which you want to send events to the data bus. - public static void AddEvent(this IQueueEventsBufferEngine service, string obj, string routingKey) => + public static void AddEvent(this IQueueBufferService service, string obj, string routingKey) => service.Add(new EventItem(Encoding.UTF8.GetBytes(obj).AsMemory(), routingKey)); /// /// Add objects collection to send as events to the data bus. /// /// The type of list item of the property. - /// The object. + /// The object. /// The list of objects to send to the data bus. /// The name of the route key with which you want to send events to the data bus. /// [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] - public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) + public static void AddEvents(this IQueueBufferService service, IEnumerable objs, string routingKey) where T : class { foreach (var obj in objs) @@ -65,11 +65,11 @@ public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerab /// /// Add a byte array objects collection to send as event to the data bus. /// - /// The object. + /// The object. /// The list of byte array objects to send to the data bus. /// The name of the route key with which you want to send events to the data bus. /// - public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable> objs, string routingKey) + public static void AddEvents(this IQueueBufferService service, IEnumerable> objs, string routingKey) { foreach (var obj in objs) service.Add(new EventItem(obj, routingKey)); @@ -78,11 +78,11 @@ public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable< /// /// Add a byte array objects collection to send as event to the data bus. /// - /// The object. + /// The object. /// The list of byte array objects to send to the data bus. /// The name of the route key with which you want to send events to the data bus. /// - public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) + public static void AddEvents(this IQueueBufferService service, IEnumerable objs, string routingKey) { foreach (var obj in objs) service.Add(new EventItem(obj, routingKey)); @@ -91,11 +91,11 @@ public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable< /// /// Add a byte array objects collection to send as event to the data bus. /// - /// The object. + /// The object. /// The list of byte array objects to send to the data bus. /// The name of the route key with which you want to send events to the data bus. /// - public static void AddEvents(this IQueueEventsBufferEngine service, IEnumerable objs, string routingKey) + public static void AddEvents(this IQueueBufferService service, IEnumerable objs, string routingKey) { foreach (var obj in objs) service.Add(new EventItem(Encoding.UTF8.GetBytes(obj).AsMemory(), routingKey)); diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueBufferService.cs similarity index 93% rename from src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs rename to src/RabbitMQCoreClient/BatchQueueSender/IQueueBufferService.cs index c2c3a52..8beedc2 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueBufferService.cs @@ -6,7 +6,7 @@ namespace RabbitMQCoreClient.BatchQueueSender; /// /// Batch Queue Events buffer interface. /// -public interface IQueueEventsBufferEngine +public interface IQueueBufferService { /// /// Message serializer to be used to serialize objects to sent to queue. diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueBufferService.cs similarity index 96% rename from src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs rename to src/RabbitMQCoreClient/BatchQueueSender/QueueBufferService.cs index e5dd5d1..bc3da01 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueBufferService.cs @@ -12,7 +12,7 @@ namespace RabbitMQCoreClient.BatchQueueSender; /// /// Implementation of the stream data event store buffer. /// -internal sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDisposable +internal sealed class QueueBufferService : IQueueBufferService, IDisposable { readonly Queue _buffer = new Queue(); readonly object _syncRoot = new object(); @@ -21,7 +21,7 @@ internal sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDispo readonly TimeSpan _timeLimit; readonly IEventsWriter _writer; readonly IEventsHandler? _eventsHandler; - readonly ILogger? _log; + readonly ILogger? _log; int _count; Task _currentFlushTask = Task.CompletedTask; @@ -34,14 +34,14 @@ internal sealed class QueueEventsBufferEngine : IQueueEventsBufferEngine, IDispo /// /// The implementation constructor of the event storage buffer. - /// Creates a new instance of the class . + /// Creates a new instance of the class . /// - public QueueEventsBufferEngine(IEventsWriter writer, + public QueueBufferService(IEventsWriter writer, int sizeLimit, TimeSpan timeLimit, IEventsHandler? eventsHandler, IRabbitMQCoreClientBuilder builder, - ILogger? log) + ILogger? log) { _log = log; _writer = writer ?? throw new ArgumentNullException(nameof(writer)); diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs index 4f304b1..eaa27e2 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs @@ -5,6 +5,9 @@ namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; +/// +/// Configure RabbitMQCore client builder from IConfiguration. +/// public static class JsonV1Binder { const string QueueSection = "Queue"; @@ -12,6 +15,12 @@ public static class JsonV1Binder const string ExchangeName = "Exchange:Name"; const string SubscriptionSection = "Subscription"; + /// + /// Configure RabbitMQCoreClient with v1 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// public static IRabbitMQCoreClientBuilder RegisterV1Configuration(this IRabbitMQCoreClientBuilder builder, IConfiguration? configuration) { @@ -26,6 +35,12 @@ public static IRabbitMQCoreClientBuilder RegisterV1Configuration(this IRabbitMQC return builder; } + /// + /// Configure RabbitMQCoreClient with v1 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// public static IRabbitMQCoreClientConsumerBuilder RegisterV1Configuration(this IRabbitMQCoreClientConsumerBuilder builder, IConfiguration? configuration) { diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs index 36ad120..e94341a 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs @@ -5,12 +5,21 @@ namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; +/// +/// Configure RabbitMQCore client builder from IConfiguration. +/// public static class JsonV2Binder { const string ExchangesSection = "Exchanges"; const string QueuesSection = "Queues"; const string SubscriptionsSection = "Subscriptions"; + /// + /// Configure RabbitMQCoreClient with v2 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// public static IRabbitMQCoreClientBuilder RegisterV2Configuration(this IRabbitMQCoreClientBuilder builder, IConfiguration configuration) { @@ -23,17 +32,12 @@ public static IRabbitMQCoreClientBuilder RegisterV2Configuration(this IRabbitMQC return builder; } - static void RegisterExchanges(IRabbitMQCoreClientBuilder builder, IConfiguration configuration) - { - var exchanges = configuration.GetSection(ExchangesSection); - foreach (var exchangeConfig in exchanges.GetChildren()) - { - var options = new ExchangeOptions(); - exchangeConfig.Bind(options); - builder.AddExchange(options.Name, options: options); - } - } - + /// + /// Configure RabbitMQCoreClient with v2 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// public static IRabbitMQCoreClientConsumerBuilder RegisterV2Configuration(this IRabbitMQCoreClientConsumerBuilder builder, IConfiguration configuration) { @@ -53,6 +57,17 @@ public static IRabbitMQCoreClientConsumerBuilder RegisterV2Configuration(this IR return builder; } + static void RegisterExchanges(IRabbitMQCoreClientBuilder builder, IConfiguration configuration) + { + var exchanges = configuration.GetSection(ExchangesSection); + foreach (var exchangeConfig in exchanges.GetChildren()) + { + var options = new ExchangeOptions(); + exchangeConfig.Bind(options); + builder.AddExchange(options.Name, options: options); + } + } + static void RegisterQueues(IRabbitMQCoreClientConsumerBuilder builder, IConfigurationSection? queuesConfiguration, Func createQueue) diff --git a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs index b1d3bf2..0522173 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs @@ -13,16 +13,16 @@ public interface IQueueConsumer : IAsyncDisposable /// /// Cancellation token. /// - Task Start(CancellationToken cancellationToken = default); + Task StartAsync(CancellationToken cancellationToken = default); /// /// Stops listening Queues. /// - Task Shutdown(); + Task ShutdownAsync(); /// /// The channel that consume messages from the RabbitMQ Instance. - /// Can be Null, if method was not called. + /// Can be Null, if method was not called. /// IChannel? ConsumeChannel { get; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs index a26cb6f..81931ac 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs @@ -16,7 +16,7 @@ public interface IRabbitMQCoreClientBuilder /// /// List of configured exchange points. /// - IList Exchanges { get; } + List Exchanges { get; } /// /// Gets the default exchange. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs index e0a33b8..440cf17 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs @@ -7,11 +7,6 @@ namespace RabbitMQCoreClient.DependencyInjection; /// public class ConsumerHandlerOptions { - /// - /// Gets or sets the json serializer that overrides default serializer for the following massage handler. - /// - public IMessageSerializer? CustomSerializer { get; set; } = default; - /// /// The routing key that will mark the message on the exception handling stage. /// If configured, the message will return to the exchange with this key instead of original Key. diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs index d9b114a..4890f38 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs @@ -5,6 +5,9 @@ namespace RabbitMQCoreClient.DependencyInjection; +/// +/// RabbitMQCoreClient options. +/// public class RabbitMQCoreClientOptions { /// @@ -28,6 +31,9 @@ public class RabbitMQCoreClientOptions /// public int RequestedConnectionTimeout { get; set; } = 30000; + /// + /// Heartbeat timeout to use when negotiating with the server. + /// public ushort RequestedHeartbeat { get; set; } = 60; /// diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs index 2a15bfd..b29befc 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs @@ -4,6 +4,9 @@ namespace RabbitMQCoreClient.Configuration.DependencyInjection; +/// +/// RabbitMQCore client builder. +/// public sealed class RabbitMQCoreClientBuilder : IRabbitMQCoreClientBuilder { /// @@ -18,7 +21,7 @@ public RabbitMQCoreClientBuilder(IServiceCollection services) => public IServiceCollection Services { get; } /// - public IList Exchanges { get; } = []; + public List Exchanges { get; } = []; /// public Exchange? DefaultExchange => Exchanges.FirstOrDefault(x => x.Options.IsDefault); diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs index 5450cdf..a8a50ab 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs @@ -4,6 +4,9 @@ namespace RabbitMQCoreClient.Configuration.DependencyInjection; +/// +/// RabbitMQCoreClient consumer builder. +/// public sealed class RabbitMQCoreClientConsumerBuilder : IRabbitMQCoreClientConsumerBuilder { /// diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs index e38994e..31aa23e 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs @@ -19,7 +19,7 @@ public async Task StartAsync(CancellationToken cancellationToken) // If the AddConsumer() method was not called, // then we do not start the event listening channel. if (_consumer != null) - await _consumer.Start(cancellationToken); // Consumer starts publisher first. + await _consumer.StartAsync(cancellationToken); // Consumer starts publisher first. else await _publisher.ConnectAsync(cancellationToken); } diff --git a/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs b/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs index f0ce632..24001e3 100644 --- a/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs +++ b/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs @@ -2,6 +2,9 @@ namespace RabbitMQCoreClient.Events; +/// +/// ReconnectEventArgs. +/// public class ReconnectEventArgs : AsyncEventArgs { } diff --git a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs index a18d798..15ee03a 100644 --- a/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs +++ b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs @@ -112,9 +112,6 @@ public static ValueTask SendAsync( var body = Encoding.UTF8.GetBytes(obj).AsMemory(); - //_log.LogDebug("Sending json message '{Message}' to exchange '{Exchange}' " + - // "with routing key '{RoutingKey}'.", json, exchange, routingKey); - return service.SendAsync(body, props: QueueService.CreateDefaultProperties(), routingKey: routingKey, @@ -189,19 +186,14 @@ public static ValueTask SendBatchAsync( IEnumerable objs, string routingKey, string? exchange = default, - CancellationToken cancellationToken = default) - { - //_log.LogDebug("Sending json messages batch to exchange '{Exchange}' " + - // "with routing key '{RoutingKey}'.", exchange, routingKey); - - return service.SendBatchAsync( - objs: objs.Select(x => new ReadOnlyMemory(Encoding.UTF8.GetBytes(x))), - QueueService.CreateDefaultProperties(), - routingKey: routingKey, - exchange: exchange, - cancellationToken: cancellationToken - ); - } + CancellationToken cancellationToken = default) => + service.SendBatchAsync( + objs: objs.Select(x => new ReadOnlyMemory(Encoding.UTF8.GetBytes(x))), + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); /// /// Send a batch of string messages to the queue with default properties. @@ -217,17 +209,12 @@ public static ValueTask SendBatchAsync( IEnumerable> objs, string routingKey, string? exchange = default, - CancellationToken cancellationToken = default) - { - //_log.LogDebug("Sending json messages batch to exchange '{Exchange}' " + - // "with routing key '{RoutingKey}'.", exchange, routingKey); - - return service.SendBatchAsync( - objs: objs, - QueueService.CreateDefaultProperties(), - routingKey: routingKey, - exchange: exchange, - cancellationToken: cancellationToken - ); - } + CancellationToken cancellationToken = default) => + service.SendBatchAsync( + objs: objs, + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); } diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs index a6c8cd6..93df876 100644 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs @@ -1,3 +1,4 @@ +using RabbitMQCoreClient.BatchQueueSender; using RabbitMQCoreClient.Serializers; using System.Diagnostics.CodeAnalysis; using System.Text.Json; @@ -144,4 +145,37 @@ public static ValueTask SendBatchAsync( cancellationToken: cancellationToken ); } + + /// + /// Add an object to be send as event to the data bus. + /// + /// The object. + /// The object to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// Metadata about the type to convert. + public static void AddEvent(this IQueueBufferService service, + [NotNull] T obj, + string routingKey, + JsonTypeInfo jsonTypeInfo) + where T : class => + service.Add(new EventItem(JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo), routingKey)); + + /// + /// Add objects collection to send as events to the data bus. + /// + /// The type of list item of the property. + /// The object. + /// The list of objects to send to the data bus. + /// The name of the route key with which you want to send events to the data bus. + /// Metadata about the type to convert. + /// + public static void AddEvents(this IQueueBufferService service, + IEnumerable objs, + string routingKey, + JsonTypeInfo jsonTypeInfo) + where T : class + { + foreach (var obj in objs) + service.Add(new EventItem(JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo), routingKey)); + } } diff --git a/src/RabbitMQCoreClient/IMessageHandler.cs b/src/RabbitMQCoreClient/IMessageHandler.cs index fbf15b6..54328e2 100644 --- a/src/RabbitMQCoreClient/IMessageHandler.cs +++ b/src/RabbitMQCoreClient/IMessageHandler.cs @@ -1,6 +1,5 @@ using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; namespace RabbitMQCoreClient; @@ -25,9 +24,4 @@ public interface IMessageHandler /// Consumer handler options, that was used during configuration. /// ConsumerHandlerOptions? Options { get; set; } - - /// - /// The default json serializer. - /// - IMessageSerializer Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/MessageHandlerJson.cs b/src/RabbitMQCoreClient/MessageHandlerJson.cs index 0f42af9..d450a6b 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -1,7 +1,7 @@ using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; using System.Text; +using System.Text.Json.Serialization.Metadata; namespace RabbitMQCoreClient; @@ -11,6 +11,7 @@ namespace RabbitMQCoreClient; /// The type of model that will be deserialized into. /// public abstract class MessageHandlerJson : IMessageHandler + where TModel : class { /// /// Incoming message routing methods. @@ -45,9 +46,10 @@ public abstract class MessageHandlerJson : IMessageHandler public ConsumerHandlerOptions? Options { get; set; } /// - /// The default json serializer. + /// You must provide TModel json serialization context. /// - public IMessageSerializer Serializer { get; set; } = new SystemTextJsonMessageSerializer(); + /// + protected abstract JsonTypeInfo GetSerializerContext(); /// public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) @@ -56,7 +58,8 @@ public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEvent TModel messageModel; try { - var obj = Serializer.Deserialize(message) + var context = GetSerializerContext(); + var obj = System.Text.Json.JsonSerializer.Deserialize(RawJson, context) ?? throw new InvalidOperationException("The json parser returns null."); messageModel = obj; } diff --git a/src/RabbitMQCoreClient/Models/Queue.cs b/src/RabbitMQCoreClient/Models/Queue.cs index 6688847..d44ef79 100644 --- a/src/RabbitMQCoreClient/Models/Queue.cs +++ b/src/RabbitMQCoreClient/Models/Queue.cs @@ -1,3 +1,4 @@ +using RabbitMQCoreClient.Configuration; using RabbitMQCoreClient.DependencyInjection; namespace RabbitMQCoreClient.Models; @@ -7,6 +8,16 @@ namespace RabbitMQCoreClient.Models; /// public sealed class Queue : QueueBase { + /// + /// Create new object of . + /// + /// The queue Name. + /// If true, the queue will be saved on disc. + /// If true, then the queue will be used by single service and will be deleted after client will disconnect. + /// Except is true. Then the queue will be created with be created with header + /// If true, the queue will be automatically deleted on client disconnect. + /// While creating the queue use parameter "x-queue-type": "quorum". + /// name - name public Queue(string name, bool durable = true, bool exclusive = false, bool autoDelete = false, bool useQuorum = false) : base(name, durable, exclusive, autoDelete, useQuorum) { diff --git a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs index 240ca26..258b409 100644 --- a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs +++ b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs @@ -1,5 +1,8 @@ namespace RabbitMQCoreClient.Models; +/// +/// Arguments that used in message consumer methods. +/// public class RabbitMessageEventArgs : EventArgs { /// diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index 552a09d..bcaafa7 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -6,7 +6,6 @@ using RabbitMQCoreClient.Events; using RabbitMQCoreClient.Exceptions; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; using System.Net; namespace RabbitMQCoreClient; @@ -65,7 +64,7 @@ public RabbitMQCoreClientConsumer( } /// inheritdoc /> - public async Task Start(CancellationToken cancellationToken = default) + public async Task StartAsync(CancellationToken cancellationToken = default) { if (cancellationToken != default) _serviceLifetimeToken = cancellationToken; @@ -97,7 +96,7 @@ public async Task Start(CancellationToken cancellationToken = default) } /// inheritdoc /> - public async Task Shutdown() => await StopAndClearConsumer(); + public async Task ShutdownAsync() => await StopAndClearConsumer(); async Task ConnectToAllQueues() { @@ -119,7 +118,7 @@ async Task ConnectToAllQueues() queue.UseQuorum = true; await queue.StartQueueAsync(ConsumeChannel, _consumer, _serviceLifetimeToken); } - _log.LogInformation("Consumer connected to {QueuesCount} queues.", _builder.Queues.Count); + _log.LogInformation("Consumer connected to '{QueuesCount}' queues.", _builder.Queues.Count); } async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) @@ -129,7 +128,7 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) var rabbitArgs = new RabbitMessageEventArgs(@event.RoutingKey, @event.ConsumerTag); - _log.LogDebug("New message received with deliveryTag={DeliveryTag}.", @event.DeliveryTag); + _log.LogDebug("New message received with deliveryTag='{DeliveryTag}'.", @event.DeliveryTag); // Send a message to the death queue if ttl is over. if (@event.BasicProperties.Headers?.TryGetValue(AppConstants.RabbitMQHeaders.TtlHeader, out var ttl) == true @@ -159,18 +158,15 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) } handler.Options = handlerOptions ?? new(); - // If user overrides the default serializer then the custom serializer will be used for the handler. - handler.Serializer = handler.Options.CustomSerializer ?? _builder.Builder.Serializer - ?? new SystemTextJsonMessageSerializer(); - _log.LogDebug("Created scope for handler type {TypeName}. Start processing message.", + _log.LogDebug("Created scope for handler type '{TypeName}'. Start processing message.", handler.GetType().Name); try { await handler.HandleMessage(@event.Body, rabbitArgs); await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false, _serviceLifetimeToken); - _log.LogDebug("Message successfully processed by handler type {TypeName} " + - "with deliveryTag={DeliveryTag}.", handler?.GetType().Name, @event.DeliveryTag); + _log.LogDebug("Message successfully processed by handler type '{TypeName}' " + + "with deliveryTag='{DeliveryTag}'.", handler?.GetType().Name, @event.DeliveryTag); } catch (Exception e) { @@ -179,7 +175,7 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) { case Routes.DeadLetter: await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false, _serviceLifetimeToken); - _log.LogError(e, "Error message with deliveryTag={DeliveryTag}. " + + _log.LogError(e, "Error message with deliveryTag='{DeliveryTag}'. " + "Sent to dead letter exchange.", @event.DeliveryTag); break; case Routes.SourceQueue: @@ -196,7 +192,7 @@ await _queueService.SendAsync( exchange: @event.Exchange, routingKey: !string.IsNullOrEmpty(handlerOptions?.RetryKey) ? handlerOptions.RetryKey : @event.RoutingKey, decreaseTtl: decreaseTtl); - _log.LogError(e, "Error message with deliveryTag={DeliveryTag}. Requeue.", @event.DeliveryTag); + _log.LogError(e, "Error message with deliveryTag='{DeliveryTag}'. Requeue.", @event.DeliveryTag); break; } } @@ -205,7 +201,7 @@ await _queueService.SendAsync( async Task QueueService_OnReconnected(object? sender, ReconnectEventArgs args) { await StopAndClearConsumer(); - await Start(); + await StartAsync(); } Task QueueService_OnConnectionShutdown(object? sender, ShutdownEventArgs args) => @@ -262,7 +258,7 @@ await ConsumeChannel.QueueBindAsync( async Task RejectDueToNoHandler(BasicDeliverEventArgs ea) { - _log.LogDebug("Message was rejected due to no handler configured for the routing key {RoutingKey}.", + _log.LogDebug("Message was rejected due to no handler configured for the routing key '{RoutingKey}'.", ea.RoutingKey); if (ConsumeChannel != null) diff --git a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs index 68f0bbc..75926ae 100644 --- a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs @@ -15,13 +15,4 @@ public interface IMessageSerializer /// Serialized string. [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] ReadOnlyMemory Serialize(TValue value); - - /// - /// Deserialize the value from string to type. - /// - /// The result type. - /// The byte array of the message from the provider as ReadOnlyMemory <byte>. - /// The object of type or null. - [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] - TResult? Deserialize(ReadOnlyMemory value); } diff --git a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs index 510a7b9..9c4357c 100644 --- a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs @@ -14,7 +14,6 @@ public class SystemTextJsonMessageSerializer : IMessageSerializer public static System.Text.Json.JsonSerializerOptions DefaultOptions { get; } = new System.Text.Json.JsonSerializerOptions { - PropertyNameCaseInsensitive = true, DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter() } }; @@ -45,9 +44,4 @@ public SystemTextJsonMessageSerializer(Action Serialize(TValue value) => System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, Options); - - /// - [RequiresUnreferencedCode("Method uses System.Text.Json.JsonSerializer.SerializeToUtf8Bytes witch is incompatible with trimming.")] - public TResult? Deserialize(ReadOnlyMemory value) => - System.Text.Json.JsonSerializer.Deserialize(value.Span, Options); } diff --git a/v7-MIGRATION.md b/v7-MIGRATION.md new file mode 100644 index 0000000..b95e9d8 --- /dev/null +++ b/v7-MIGRATION.md @@ -0,0 +1,69 @@ +# Migrating to RabbitMQCoreClient 7.x + +This document makes note of major changes in the API of this library for +version 7. + +## Namespaces + +Изменилась структура пространств имен. Теперь вся логика регистрации сервиса переехала в `using RabbitMQCoreClient.DependencyInjection;`. + +## Start methods + +Имплементирована новая модель запуска publisher и consumer. +Теперь сервисы регистрируются и запускаются как `Microsoft.Extensions.Hosting.IHostedService`. +Нужно удалить вызовы `app.StartRabbitMqCore(lifetime)`. + +## `async` / `await` + +Методы, которые выполняют конфигурацию очередей, exchange, соединение с сервером переименованы для использования Async паттерна в именовании. + +## `Send` methods + +Переработана API для методов `Send*`. +Из некоторых `Send*` методов удалены необязательные параметры `correlationId` и `decreaseTtl`. Добавлены `CancellationToken` атрибуты. +- `correlationId` в данном контексте неправильно использовался, предполагалось, что это должен быть некоторого рода trace идентификатор, но это поле используется при передаче сообщений в протоколе. +- `decreaseTtl` без передачи `BasicProperties` не имеет смысла, т.к. чтобы выполнить понижение ttl, этот ttl нужно получить из заголовков. Для новых сообщений ttl всегда максимальный. + +Удалены методы `SendJson*`, взамен добавлены перегрузки для приема строк. Используйте их. + +## `AddBatchQueueSender` + +Изменился метод подключения `BatchQueueSender`. Теперь это выполняется с помощью билдера RabbitMQCoreClient так: + +```csharp +services + .AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(); +``` + +Раньше `AddBatchQueueSender` можно было выполнить непосредственно на IServiceCollection и забыть при этом зарегистрировать AddRabbitMQCoreClient. + +`IQueueEventsBufferEngine` переименован в `IQueueBufferService`. + +## IMessageSerializer + +Удалена возможность определения десериализации в интерфейсе. Теперь для того, чтобы использовать кастомный десериализатор используйте собственное переопределение интерфейса IMessageHandler. + +Изменился MessageHandlerJson. Теперь он работает с контекстом сериализации JSON и требует получения контекста. Пример: + +```csharp +public class SimpleObj +{ + public required string Name { get; set; } +} + +[JsonSerializable(typeof(SimpleObj))] +public partial class SimpleObjContext : JsonSerializerContext +{ +} + +public class Handler : MessageHandlerJson +{ + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + { + return Task.CompletedTask; + } +} +``` \ No newline at end of file From 29446e04b59f6e2dd30519b44edbb91034c52f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9F=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= Date: Wed, 21 Jan 2026 11:25:32 +0300 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=92=A5=20Changed=20IMessageHandler?= =?UTF-8?q?=20-=20properties=20moved=20to=20context=20class,=20add=20conte?= =?UTF-8?q?xt=20argument=20to=20HandleMessage=20method.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📚 Update README.md - 📚 Add v7 migration guide. - 💥 Updated MessageHandlerJson to support new IMessageHandler format. - Library now supports full trimming. --- README.md | 92 ++++++++++++------- RabbitMQCoreClient.sln | 2 +- .../HostConsole/SimpleObjectHandler.cs | 2 +- .../SimpleConsole/SimpleObjectHandler.cs | 6 +- samples/publish/WebApp/Program.cs | 4 +- .../Handler.cs | 17 +--- .../RabbitMQCoreClient.ConsoleClient.csproj | 4 +- .../RabbitMQCoreClient.WebApp/Handler.cs | 4 +- .../RabbitMQCoreClient.WebApp.csproj | 3 +- src/RabbitMQCoreClient/IMessageHandler.cs | 16 +--- .../MessageHandlerContext.cs | 31 +++++++ src/RabbitMQCoreClient/MessageHandlerJson.cs | 27 +++--- .../Models/RabbitMessageEventArgs.cs | 4 +- .../RabbitMQCoreClientConsumer.cs | 8 +- v7-MIGRATION.md | 69 ++++++++++---- 15 files changed, 183 insertions(+), 106 deletions(-) create mode 100644 src/RabbitMQCoreClient/MessageHandlerContext.cs diff --git a/README.md b/README.md index a5cae92..0b92ae9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ as well as creating short-lived queues to implement the Publish/Subscribe patter ### Sending messages -The library allows you to configure parameters both from the configuration file or through the Fluent interface. +The library allows you to configure parameters both from the configuration file or through the fluent interface. ##### An example of using a configuration file @@ -140,7 +140,7 @@ var bodyList = Enumerable.Range(1, 10).Select(x => new SimpleObj { Name = $"test await queueService.SendBatchAsync(bodyList, "test_routing_key"); ``` -#### Buffer messages in memory and send them batch by timer or count +### Buffer messages in memory and send them batch by timer or count You can use this feature when you have to send many parallel small messages to the queue (for example from the ASP.NET requests). The feature allows you to buffer that messages at the in-memory list and flush them at once using the `SendBatchAsync` method. @@ -152,7 +152,7 @@ using RabbitMQCoreClient.DependencyInjection; ... services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) - .AddBatchQueueSender(); + .AddBatchQueueSender(); // <- Add buffered batch sending. ``` Instead of injecting the interface `RabbitMQCoreClient.IQueueService` inject `RabbitMQCoreClient.BatchQueueSender.IQueueBufferService`. @@ -216,7 +216,7 @@ internal sealed class EventsWriter : IEventsWriter } ``` -You also can implement `IEventsHandler` to run you methods `OnAfterWriteEvents` or `OnWriteErrors`. +You also can implement `IEventsHandler` if you want to hook events `OnAfterWriteEvents` or `OnWriteErrors`. You must add you custom implementation to DI: ```csharp @@ -251,9 +251,15 @@ public async Task WriteBatch(IEnumerable events, string routingKey) By default `builder.Services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ"))` adds support of publisher. If you need to consume messages from queues, you must configure consumer. +You can consume messages from queue or from subscription. Queue ca be consumed by multiple consumers (k8s pods) and live long, +while subscription is exclusive to one consumer and destroys when consumer disconnects from this type of queue. + +- Use queue for event processing patterns that must implement parallelization. +- Use subscription for process commands like replace cached entity with new on every pod. + #### Configuring consumer -You can configure consummer by 2 methods - fluent and from configuration. +You can configure consumer by 2 methods - fluent and from configuration. Fluent example: @@ -285,8 +291,7 @@ builder.Services #### Processing messages with `IMessageHandler` In the basic version, messages are received using the implementation of the interface `RabbitMQCoreClient.IMessageHandler`. -The interface requires the implementation of a message handling method `Task HandleMessage(string message, RabbitMessageEventArgs args)`, -and also the error message router: `ErrorMessageRouting`. +The interface requires the implementation of a message handling method `Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context)`. In order to specify which routing keys need to be processed by the handler, you need to configure the handler in RabbitMQCoreClient. @@ -295,10 +300,7 @@ Example: ```csharp public class RawHandler : IMessageHandler { - public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); - public ConsumerHandlerOptions? Options { get; set; } - - public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context) { Console.WriteLine(message); @@ -312,8 +314,7 @@ Program.cs partial example ``` services .AddRabbitMQCoreClientConsumer(config.GetSection("RabbitMQ")) - .AddBatchQueueSender() // If you want to send messages with inmemory buffering. - .AddHandler("test_key"); + .AddHandler("test_key"); // <-- configure handler with routing keys. ``` **Note**: A message can only be processed by one handler. Although one handler can handle many messages with different routing keys. @@ -343,17 +344,17 @@ internal sealed class SimpleObjectHandler : MessageHandlerJson protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { _logger.LogInformation("Incoming simple object name: {Name}", message.Name); return Task.CompletedTask; } - protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) + protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args, MessageHandlerContext context) { _logger.LogError(e, "Incoming message can't be deserialized. Error: {ErrorMessage}", e.Message); - return base.OnParseError(json, e, args); + return base.OnParseError(json, e, args, context); } } @@ -368,14 +369,14 @@ public partial class SimpleObjContext : JsonSerializerContext } ``` -The `RabbitMQCoreClient.MessageHandlerJson ` class allows you to define behavior on serialization error -by overriding the `ValueTask OnParseError (string json, JsonException e, RabbitMessageEventArgs args)` method. +The `RabbitMQCoreClient.MessageHandlerJson` class allows you to define behavior on serialization error +by overriding the `ValueTask OnParseError (string json, JsonException e, RabbitMessageEventArgs args, MessageHandlerContext context)` method. #### Routing messages By default, if the handler throws any exception, then the message will be sent back to the queue with reduced TTL. When processing messages, you often need to specify different behavior for different exceptions. -The `ErrorMessageRouting` message router is used to determine the client's behavior when throwing an exception. +The `context.ErrorMessageRouter` message router is used to determine the client's behavior when throwing an exception. There are 2 options for behavior: @@ -392,7 +393,7 @@ internal class Handler : MessageHandlerJson { protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { try { @@ -400,12 +401,12 @@ internal class Handler : MessageHandlerJson } catch (ArgumentException e) when (e.Message == "parser failed") { - ErrorMessageRouter.MoveToDeadLetter(); + context.ErrorMessageRouter.MoveToDeadLetter(); throw; } catch (Exception) { - ErrorMessageRouter.MoveBackToQueue(); + context.ErrorMessageRouter.MoveBackToQueue(); throw; } @@ -424,15 +425,48 @@ internal class Handler : MessageHandlerJson ### Json Serializers -You can choose what serializer to use to *publish* messages with serialization. +You can choose what serializer to use to *publish* class objects as messages with serialization. The library by default supports `System.Text.Json` serializer. +Every time you use generic `SendAsync()` methods, serializer is used. For example +```csharp +public static ValueTask SendAsync( + this IQueueService service, + T obj, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = default + ) + where T : class +``` + +Default reflection based `System.Text.Json` serializer configured with options: + +``` +public static System.Text.Json.JsonSerializerOptions DefaultOptions { get; } = + new System.Text.Json.JsonSerializerOptions + { + DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }; +``` + +If you want to change this options you can use extension method with options configured + +``` +builder.Services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")) + .AddSystemTextJson(opt => opt.PropertyNamingPolicy = JsonNamingPolicy.CamelCase); +``` + #### Custom publish serializer + You can make make your own custom serializer. To do that you must implement the `RabbitMQCoreClient.Serializers.IMessageSerializer` interface. Example: *CustomSerializer.cs* + ```csharp public class CustomMessageSerializer : IMessageSerializer { @@ -465,12 +499,6 @@ public class CustomMessageSerializer : IMessageSerializer { return Newtonsoft.Json.JsonConvert.SerializeObject(value, Options); } - - /// - public TResult? Deserialize(string value) - { - return Newtonsoft.Json.JsonConvert.DeserializeObject(value, Options); - } } ``` @@ -518,6 +546,8 @@ and can be used at the configured cluster environment. Configuration can be done either through options or through configuration from `appsettings.json`. +There are two formats of configuration: old and new. The old format is less expressive. + The old legacy queue auto-registration format is still supported. But with limitations: - Only one queue can be automatically registered. The queue is registered at the exchange point "Exchange". @@ -569,7 +599,7 @@ Set to true to check peer certificate for revocation. ##### Configuration format -###### Full configuration example +###### Full configuration example (new fprmat) ```json { @@ -644,7 +674,7 @@ Set to true to check peer certificate for revocation. } ``` -###### Reduced configuration example that is used on a daily basis +###### Reduced configuration example that is used on a daily basis (new format) If `Exchanges` is not specified in the `Queues` section, then the queue will use the default exchange. You can skip Queues or Subscriptions or both, if you do not have consumers of this types. diff --git a/RabbitMQCoreClient.sln b/RabbitMQCoreClient.sln index c595d30..24b1330 100644 --- a/RabbitMQCoreClient.sln +++ b/RabbitMQCoreClient.sln @@ -22,7 +22,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.ConsoleC EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{1393D7D2-3B09-49BF-A616-B25D1365E58B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.WebApp", "sample\test-playground\RabbitMQCoreClient.WebApp\RabbitMQCoreClient.WebApp.csproj", "{E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQCoreClient.WebApp", "samples\test-playground\RabbitMQCoreClient.WebApp\RabbitMQCoreClient.WebApp.csproj", "{E2A1AB4C-6E1E-455B-A713-4EE5D60C1A6B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "publish", "publish", "{8C256A46-AE61-4D61-A221-F94741C36310}" EndProject diff --git a/samples/consume/HostConsole/SimpleObjectHandler.cs b/samples/consume/HostConsole/SimpleObjectHandler.cs index ba93836..ca472d6 100644 --- a/samples/consume/HostConsole/SimpleObjectHandler.cs +++ b/samples/consume/HostConsole/SimpleObjectHandler.cs @@ -16,7 +16,7 @@ public SimpleObjectHandler(ILogger logger) protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { _logger.LogInformation("Incoming simple object name: {Name}", message.Name); diff --git a/samples/consume/SimpleConsole/SimpleObjectHandler.cs b/samples/consume/SimpleConsole/SimpleObjectHandler.cs index e569f13..e90838e 100644 --- a/samples/consume/SimpleConsole/SimpleObjectHandler.cs +++ b/samples/consume/SimpleConsole/SimpleObjectHandler.cs @@ -16,17 +16,17 @@ public SimpleObjectHandler(ILogger logger) protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { _logger.LogInformation("Incoming simple object name: {Name}", message.Name); return Task.CompletedTask; } - protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) + protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args, MessageHandlerContext context) { _logger.LogError(e, "Incoming message can't be deserialized. Error: {ErrorMessage}", e.Message); - return base.OnParseError(json, e, args); + return base.OnParseError(json, e, args, context); } } diff --git a/samples/publish/WebApp/Program.cs b/samples/publish/WebApp/Program.cs index 5b3c672..fd8f5ae 100644 --- a/samples/publish/WebApp/Program.cs +++ b/samples/publish/WebApp/Program.cs @@ -1,12 +1,14 @@ using RabbitMQCoreClient; using RabbitMQCoreClient.DependencyInjection; +using System.Text.Json; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Just for sending messages. builder.Services - .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")); + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")) + .AddSystemTextJson(opt => opt.PropertyNamingPolicy = JsonNamingPolicy.CamelCase); var app = builder.Build(); diff --git a/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs index 3aa6464..78529d1 100644 --- a/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs @@ -1,6 +1,4 @@ -using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; using System; using System.Text; using System.Text.Json.Serialization.Metadata; @@ -13,7 +11,7 @@ public class Handler : MessageHandlerJson protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { Console.WriteLine($"message from {args.RoutingKey}"); ProcessMessage(message); @@ -21,8 +19,6 @@ protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs return Task.CompletedTask; } - protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) => base.OnParseError(json, e, args); - void ProcessMessage(SimpleObj obj) { //if (obj.Name != "my test name") @@ -37,10 +33,7 @@ void ProcessMessage(SimpleObj obj) public class RawHandler : IMessageHandler { - public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); - public ConsumerHandlerOptions Options { get; set; } - - public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context) { Console.WriteLine(Encoding.UTF8.GetString(message.ToArray())); return Task.CompletedTask; @@ -49,11 +42,7 @@ public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs a public class RawErrorHandler : IMessageHandler { - public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); - public ConsumerHandlerOptions Options { get; set; } - public IMessageSerializer Serializer { get; set; } - - public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context) { Console.WriteLine(Encoding.UTF8.GetString(message.ToArray())); throw new Exception(); diff --git a/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj index 13156fb..2064cd4 100644 --- a/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj @@ -14,9 +14,9 @@ - + - + PreserveNewest diff --git a/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs b/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs index dbb4407..98139db 100644 --- a/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs @@ -9,7 +9,7 @@ public class Handler : MessageHandlerJson { protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { ProcessMessage(message); @@ -31,7 +31,7 @@ public class RawHandler : IMessageHandler public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); public ConsumerHandlerOptions? Options { get; set; } - public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context) { Console.WriteLine(Encoding.UTF8.GetString(message.ToArray())); throw new Exception(); diff --git a/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj b/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj index 65fef84..860aadf 100644 --- a/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj @@ -7,8 +7,7 @@ - + - diff --git a/src/RabbitMQCoreClient/IMessageHandler.cs b/src/RabbitMQCoreClient/IMessageHandler.cs index 54328e2..67e471f 100644 --- a/src/RabbitMQCoreClient/IMessageHandler.cs +++ b/src/RabbitMQCoreClient/IMessageHandler.cs @@ -1,4 +1,3 @@ -using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; namespace RabbitMQCoreClient; @@ -13,15 +12,8 @@ public interface IMessageHandler /// /// Input byte array from consumed queue. /// The instance containing the message data. - Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args); - - /// - /// Instructions to the router in case of an exception while processing a message. - /// - ErrorMessageRouting ErrorMessageRouter { get; } - - /// - /// Consumer handler options, that was used during configuration. - /// - ConsumerHandlerOptions? Options { get; set; } + /// Handler configuration context. + Task HandleMessage(ReadOnlyMemory message, + RabbitMessageEventArgs args, + MessageHandlerContext context); } diff --git a/src/RabbitMQCoreClient/MessageHandlerContext.cs b/src/RabbitMQCoreClient/MessageHandlerContext.cs new file mode 100644 index 0000000..138b0b9 --- /dev/null +++ b/src/RabbitMQCoreClient/MessageHandlerContext.cs @@ -0,0 +1,31 @@ +using RabbitMQCoreClient.DependencyInjection; + +namespace RabbitMQCoreClient; + +/// +/// Represents the context for processing a RabbitMQ message, including error routing +/// instructions and handler options. +/// +public class MessageHandlerContext +{ + /// + /// Instructions to the router in case of an exception while processing a message. + /// + public ErrorMessageRouting ErrorMessageRouter { get; } + + /// + /// Consumer handler options, that was used during configuration. + /// + public ConsumerHandlerOptions? Options { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The error message routing instructions. + /// The consumer handler options. + public MessageHandlerContext(ErrorMessageRouting errorMessageRouter, ConsumerHandlerOptions? options) + { + ErrorMessageRouter = errorMessageRouter; + Options = options; + } +} diff --git a/src/RabbitMQCoreClient/MessageHandlerJson.cs b/src/RabbitMQCoreClient/MessageHandlerJson.cs index d450a6b..b391f97 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -1,4 +1,3 @@ -using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; using System.Text; using System.Text.Json.Serialization.Metadata; @@ -24,27 +23,29 @@ public abstract class MessageHandlerJson : IMessageHandler /// The json. /// The exception. /// The instance containing the event data. + /// Handler configuration context. /// - protected virtual ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args) => default; + protected virtual ValueTask OnParseError(string json, + Exception e, + RabbitMessageEventArgs args, + MessageHandlerContext context) => default; /// /// Process json message. /// /// The message deserialized into an object. /// The instance containing the event data. + /// Handler configuration context. /// - protected abstract Task HandleMessage(TModel message, RabbitMessageEventArgs args); + protected abstract Task HandleMessage(TModel message, + RabbitMessageEventArgs args, + MessageHandlerContext context); /// /// Raw Json formatted message. /// protected string? RawJson { get; set; } - /// - /// Gets the options. - /// - public ConsumerHandlerOptions? Options { get; set; } - /// /// You must provide TModel json serialization context. /// @@ -52,24 +53,24 @@ public abstract class MessageHandlerJson : IMessageHandler protected abstract JsonTypeInfo GetSerializerContext(); /// - public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args) + public async Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context) { RawJson = Encoding.UTF8.GetString(message.ToArray()); TModel messageModel; try { - var context = GetSerializerContext(); - var obj = System.Text.Json.JsonSerializer.Deserialize(RawJson, context) + var jsonContext = GetSerializerContext(); + var obj = System.Text.Json.JsonSerializer.Deserialize(RawJson, jsonContext) ?? throw new InvalidOperationException("The json parser returns null."); messageModel = obj; } catch (Exception e) { - await OnParseError(RawJson, e, args); + await OnParseError(RawJson, e, args, context); // Fall to the top-level exception handler. throw; } - await HandleMessage(messageModel, args); + await HandleMessage(messageModel, args, context); } } diff --git a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs index 258b409..f606616 100644 --- a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs +++ b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs @@ -1,9 +1,11 @@ +using RabbitMQ.Client.Events; + namespace RabbitMQCoreClient.Models; /// /// Arguments that used in message consumer methods. /// -public class RabbitMessageEventArgs : EventArgs +public class RabbitMessageEventArgs { /// /// The routing key used when the message was originally published. diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index bcaafa7..2bb52c8 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -157,13 +157,13 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) return; } - handler.Options = handlerOptions ?? new(); + var handlerContext = new MessageHandlerContext(new(), handlerOptions); _log.LogDebug("Created scope for handler type '{TypeName}'. Start processing message.", handler.GetType().Name); try { - await handler.HandleMessage(@event.Body, rabbitArgs); + await handler.HandleMessage(@event.Body, rabbitArgs, handlerContext); await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false, _serviceLifetimeToken); _log.LogDebug("Message successfully processed by handler type '{TypeName}' " + "with deliveryTag='{DeliveryTag}'.", handler?.GetType().Name, @event.DeliveryTag); @@ -171,7 +171,7 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) catch (Exception e) { // Process the message depending on the given route. - switch (handler.ErrorMessageRouter.Route) + switch (handlerContext.ErrorMessageRouter.Route) { case Routes.DeadLetter: await ConsumeChannel.BasicNackAsync(@event.DeliveryTag, false, false, _serviceLifetimeToken); @@ -179,7 +179,7 @@ async Task Consumer_Received(object? sender, BasicDeliverEventArgs @event) "Sent to dead letter exchange.", @event.DeliveryTag); break; case Routes.SourceQueue: - var decreaseTtl = handler.ErrorMessageRouter.TtlAction == TtlActions.Decrease; + var decreaseTtl = handlerContext.ErrorMessageRouter.TtlAction == TtlActions.Decrease; await ConsumeChannel.BasicAckAsync(@event.DeliveryTag, false, _serviceLifetimeToken); // Forward the message back to the queue, while the TTL of the message is reduced by 1, diff --git a/v7-MIGRATION.md b/v7-MIGRATION.md index b95e9d8..30f7178 100644 --- a/v7-MIGRATION.md +++ b/v7-MIGRATION.md @@ -1,34 +1,65 @@ # Migrating to RabbitMQCoreClient 7.x -This document makes note of major changes in the API of this library for -version 7. +This document outlines the major API changes in version 7 of this library. ## Namespaces -Изменилась структура пространств имен. Теперь вся логика регистрации сервиса переехала в `using RabbitMQCoreClient.DependencyInjection;`. +The namespace structure has changed. All service registration logic has been moved to `using RabbitMQCoreClient.DependencyInjection;`. -## Start methods +## Start Methods -Имплементирована новая модель запуска publisher и consumer. -Теперь сервисы регистрируются и запускаются как `Microsoft.Extensions.Hosting.IHostedService`. -Нужно удалить вызовы `app.StartRabbitMqCore(lifetime)`. +A new startup model for publishers and consumers has been implemented. +Services are now registered and started as `Microsoft.Extensions.Hosting.IHostedService`. +You should remove the calls to `app.StartRabbitMqCore(lifetime)` in _Program.cs_. ## `async` / `await` -Методы, которые выполняют конфигурацию очередей, exchange, соединение с сервером переименованы для использования Async паттерна в именовании. +Methods that perform queue, exchange configuration, and server connection have been renamed to follow the Async naming pattern. -## `Send` methods +## `Send` Methods -Переработана API для методов `Send*`. -Из некоторых `Send*` методов удалены необязательные параметры `correlationId` и `decreaseTtl`. Добавлены `CancellationToken` атрибуты. -- `correlationId` в данном контексте неправильно использовался, предполагалось, что это должен быть некоторого рода trace идентификатор, но это поле используется при передаче сообщений в протоколе. -- `decreaseTtl` без передачи `BasicProperties` не имеет смысла, т.к. чтобы выполнить понижение ttl, этот ttl нужно получить из заголовков. Для новых сообщений ttl всегда максимальный. +The API for `Send*` methods has been reworked. +Optional parameters `correlationId` and `decreaseTtl` have been removed from some `Send*` methods. `CancellationToken` arguments have been added. +- `correlationId` was used incorrectly in this context. It was intended to be some kind of trace identifier, but this field is used for message correlation in the AMQP protocol. +- `decreaseTtl` is meaningless without passing `BasicProperties`, because to decrement the TTL, it must first be retrieved from the headers in BasicProperties. For new messages, the TTL is always the maximum. -Удалены методы `SendJson*`, взамен добавлены перегрузки для приема строк. Используйте их. +The `SendJson*` methods have been removed. Overloads accepting strings have been added in their place. Please use those. + +## `IMessageHandler` + +The properties `ErrorMessageRouting ErrorMessageRouter { get; }` and `ConsumerHandlerOptions? Options { get; set; }` +have been moved to the new `MessageHandlerContext` class and are now passed in the `context` field of the `IMessageHandler.HandleMessage` method. + +The signature of the `IMessageHandler.HandleMessage` method has changed: + +``` + Task HandleMessage(ReadOnlyMemory message, + RabbitMessageEventArgs args, + MessageHandlerContext context); +``` + +**Migration Guide:** + +- Add a new argument to the `HandleMessage` method: `MessageHandlerContext context`. +- Remove the following properties from your `IMessageHandler` implementation: +``` + public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); + public ConsumerHandlerOptions? Options { get; set; } +``` +- Use `context.ErrorMessageRouter` instead of `this.ErrorMessageRouter`. + +## `MessageHandlerJson` + +Due to the changes in `IMessageHandler`, the signatures of the `OnParseError` and `HandleMessage` methods have also changed. + +**Migration Guide:** + +- Add a new argument to both `OnParseError` and `HandleMessage` methods: `MessageHandlerContext context`. +- Use `context.ErrorMessageRouter` instead of `this.ErrorMessageRouter`. ## `AddBatchQueueSender` -Изменился метод подключения `BatchQueueSender`. Теперь это выполняется с помощью билдера RabbitMQCoreClient так: +The method for setting up `BatchQueueSender` has changed. It is now configured via the RabbitMQCoreClient builder like this: ```csharp services @@ -36,15 +67,15 @@ services .AddBatchQueueSender(); ``` -Раньше `AddBatchQueueSender` можно было выполнить непосредственно на IServiceCollection и забыть при этом зарегистрировать AddRabbitMQCoreClient. +Previously, `AddBatchQueueSender` could be called directly on `IServiceCollection`, potentially forgetting to register `AddRabbitMQCoreClient`. -`IQueueEventsBufferEngine` переименован в `IQueueBufferService`. +`IQueueEventsBufferEngine` has been renamed to `IQueueBufferService`. ## IMessageSerializer -Удалена возможность определения десериализации в интерфейсе. Теперь для того, чтобы использовать кастомный десериализатор используйте собственное переопределение интерфейса IMessageHandler. +The ability to define deserialization logic within the interface has been removed. To use a custom deserializer, implement your own override of the `IMessageHandler` interface. -Изменился MessageHandlerJson. Теперь он работает с контекстом сериализации JSON и требует получения контекста. Пример: +`MessageHandlerJson` has changed. It now works with JSON serialization context and requires a context to be provided. Example: ```csharp public class SimpleObj From c07785f9e6781ddf33460f226006e383ce2c7520 Mon Sep 17 00:00:00 2001 From: Sergey Pismennyi <8alig8@gmail.com> Date: Wed, 21 Jan 2026 16:52:37 +0300 Subject: [PATCH 13/13] Remove ErrorMessageRouting ErrorMessageRouter { get; } from MessageHandlerJson --- src/RabbitMQCoreClient/MessageHandlerJson.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/RabbitMQCoreClient/MessageHandlerJson.cs b/src/RabbitMQCoreClient/MessageHandlerJson.cs index b391f97..5fde531 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -12,11 +12,6 @@ namespace RabbitMQCoreClient; public abstract class MessageHandlerJson : IMessageHandler where TModel : class { - /// - /// Incoming message routing methods. - /// - public ErrorMessageRouting ErrorMessageRouter { get; } = new ErrorMessageRouting(); - /// /// The method will be called when there is an error parsing Json into the model. ///