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/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 80a7771..0b92ae9 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. @@ -13,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, @@ -20,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 @@ -39,65 +41,76 @@ 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; +using RabbitMQCoreClient.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 => { - 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")); - // Just for sending messages. - services - .AddRabbitMQCoreClient(config); - } -} -``` +var provider = services.BuildServiceProvider(); -*Startup.cs - ASP.NET Core application* +var publisher = provider.GetRequiredService(); + +await publisher.ConnectAsync(); + +await publisher.SendAsync("""{ "foo": "bar" }""", "test_key"); ``` -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - ... +*Program.cs - ASP.NET Core application* - // Just for sending messages. - services - .AddRabbitMQCoreClient(config); - } -} ``` +using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; -The `RabbitMQCoreClient.IQueueService` interface is responsible for sending messages. +var builder = WebApplication.CreateBuilder(args); -In order to send a message, it is enough to get the interface `RabbitMQCoreClient.IQueueService` from DI -and use one of the following methods +// Add services to the container. +// Just for sending messages. +builder.Services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")); -```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); +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(); +}); -// 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); +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 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 +121,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). @@ -127,26 +140,23 @@ 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 -From the version v5.1.0 there was introduced a new mechanic of the sending messages using separate thread. +### 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. To use this feature register it at DI: ```csharp -using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; +using RabbitMQCoreClient.DependencyInjection; ... -services.AddBatchQueueSender(); +services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(); // <- Add buffered batch sending. ``` -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); -``` +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 @@ -159,102 +169,129 @@ 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")); +services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(configuration.GetSection("QueueFlushSettings")); ``` -### Receiving and processing messages +#### Extended buffer configuration -##### Console application +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(); ``` -class Program + +Implementation example: + +```csharp +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Implementation of the service for sending events to the data bus. +/// +internal sealed class EventsWriter : IEventsWriter { - static readonly AutoResetEvent _closing = new AutoResetEvent(false); + readonly IQueueService _queueService; + + /// + /// Create new object of . + /// + /// Queue publisher. + public EventsWriter(IQueueService queueService) + { + _queueService = queueService; + } - static async Task Main(string[] args) + /// + public async Task WriteBatch(IEnumerable events, string routingKey) { - Console.OutputEncoding = Encoding.UTF8; + if (!events.Any()) + return; - 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); + await _queueService.SendBatchAsync(events.Select(x => x.Message), routingKey); } } ``` -`_closing.WaitOne ();` is used to prevent the program from terminating immediately after starting. - -The `.Start ();` method does not block the main thread. +You also can implement `IEventsHandler` if you want to hook events `OnAfterWriteEvents` or `OnWriteErrors`. +You must add you custom implementation to DI: -##### ASP.NET Core 3.1+ +```csharp +builder.Services.AddTransient(); ``` -public class Startup + +##### 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) { - public Startup(IConfiguration configuration) + ... + foreach (EventItem item in events) { - Configuration = configuration; + if (item is EventItemWithSourceObject eventWithSource) + { + // Working с item.Source + } } + ... +} +``` + +### Receiving and processing messages - public IConfiguration Configuration { get; } +By default `builder.Services.AddRabbitMQCoreClient(config.GetSection("RabbitMQ"))` adds support of publisher. +If you need to consume messages from queues, you must configure consumer. - // 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(opt => opt.Host = "localhost") - .AddExchange("default") - .AddConsumer() - .AddHandler("test_routing_key") - .AddQueue("my-test-queue"); - } +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. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) - { - app.StartRabbitMqCore(lifetime); +- Use queue for event processing patterns that must implement parallelization. +- Use subscription for process commands like replace cached entity with new on every pod. - app.UseRouting(); +#### Configuring consumer - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} +You can configure consumer by 2 methods - fluent and from configuration. + +Fluent example: + +``` +builder.Services + .AddRabbitMQCoreClient(opt => opt.Host = "localhost") + .AddExchange("default") + .AddConsumer() + .AddHandler("test_routing_key") + .AddQueue("my-test-queue") + .AddSubscription(); +``` + +IConfiguration example: + +``` +builder.Services + .AddRabbitMQCoreClientConsumer(builder.Configuration.GetSection("RabbitMQ")) + .AddHandler(["test_routing_key"], new ConsumerHandlerOptions + { + RetryKey = "test_routing_key_retry" + }) + .AddHandler(["test_routing_key_subscription"], new ConsumerHandlerOptions + { + 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)`, -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. @@ -263,82 +300,83 @@ Example: ```csharp public class RawHandler : IMessageHandler { - public ErrorMessageRouting ErrorMessageRouter => new ErrorMessageRouting(); - public ConsumerHandlerOptions Options { get; set; } - public IMessageSerializer Serializer { get; set; } - - public Task HandleMessage(string message, RabbitMessageEventArgs args) + public Task HandleMessage(ReadOnlyMemory message, RabbitMessageEventArgs args, MessageHandlerContext context) { 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")) + .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. 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 { - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + readonly ILogger _logger; + + public SimpleObjectHandler(ILogger logger) + { + _logger = logger; + } + + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { - 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) + protected override ValueTask OnParseError(string json, Exception e, RabbitMessageEventArgs args, MessageHandlerContext context) { - Console.WriteLine(e.Message); - return base.OnParseError(json, e, args); + _logger.LogError(e, "Incoming message can't be deserialized. Error: {ErrorMessage}", e.Message); + return base.OnParseError(json, e, args, context); } } -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(); - } } ``` -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: @@ -351,9 +389,11 @@ If the method succeeds normally, the message will be considered delivered. Usage example: ```csharp -public class Handler : MessageHandlerJson +internal class Handler : MessageHandlerJson { - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { try { @@ -361,12 +401,12 @@ public 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; } @@ -385,65 +425,48 @@ 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. +You can choose what serializer to use to *publish* class objects as messages with serialization. +The library by default supports `System.Text.Json` serializer. -Example -*Program.cs - console application* +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 ``` -class Program -{ - static async Task Main(string[] args) - { - var config = new ConfigurationBuilder() - .AddJsonFile($"appsettings.json", optional: false) - .Build(); - var services = new ServiceCollection(); +Default reflection based `System.Text.Json` serializer configured with options: - services - .AddRabbitMQCoreClient(config) - .AddSystemTextJson(); - } -} +``` +public static System.Text.Json.JsonSerializerOptions DefaultOptions { get; } = + new System.Text.Json.JsonSerializerOptions + { + DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }; ``` -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 +If you want to change this options you can use extension method with options configured -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(); - } -} +builder.Services + .AddRabbitMQCoreClient(builder.Configuration.GetSection("RabbitMQ")) + .AddSystemTextJson(opt => opt.PropertyNamingPolicy = JsonNamingPolicy.CamelCase); ``` -#### 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: *CustomSerializer.cs* + ```csharp public class CustomMessageSerializer : IMessageSerializer { @@ -476,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); - } } ``` @@ -514,38 +531,30 @@ 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`. + +There are two formats of configuration: old and new. The old format is less expressive. -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: @@ -560,9 +569,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, @@ -589,7 +599,7 @@ Set to true to check peer certificate for revocation. ##### Configuration format -###### Full configuration: +###### Full configuration example (new fprmat) ```json { @@ -664,9 +674,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 (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. ```json { diff --git a/RabbitMQCoreClient.sln b/RabbitMQCoreClient.sln index 07e6499..24b1330 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,17 +11,34 @@ 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}" 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", "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 +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 +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 @@ -45,6 +62,26 @@ 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 + {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 @@ -52,8 +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/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 deleted file mode 100644 index 000b5c6..0000000 --- a/samples/RabbitMQCoreClient.WebApp/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace RabbitMQCoreClient.WebApp; - -public class Program -{ - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); -} diff --git a/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json b/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json deleted file mode 100644 index 4b61b5c..0000000 --- a/samples/RabbitMQCoreClient.WebApp/Properties/launchSettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:2904", - "sslPort": 44346 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "RabbitMQCoreClient.WebApp": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj b/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj deleted file mode 100644 index b5f0cc0..0000000 --- a/samples/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net8.0 - - - - - - - - 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/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/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..ca472d6 --- /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, MessageHandlerContext context) + { + _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..e90838e --- /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, MessageHandlerContext context) + { + _logger.LogInformation("Incoming simple object name: {Name}", message.Name); + + return Task.CompletedTask; + } + + 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, context); + } +} + 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/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..9ec6b97 --- /dev/null +++ b/samples/publish/HostConsole/Program.cs @@ -0,0 +1,37 @@ +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 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..c41f1b8 --- /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.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..fd8f5ae --- /dev/null +++ b/samples/publish/WebApp/Program.cs @@ -0,0 +1,23 @@ +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")) + .AddSystemTextJson(opt => opt.PropertyNamingPolicy = JsonNamingPolicy.CamelCase); + +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/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs similarity index 62% rename from samples/RabbitMQCoreClient.ConsoleClient/Handler.cs rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs index 5199757..78529d1 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Handler.cs +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Handler.cs @@ -1,15 +1,17 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Models; -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 Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override JsonTypeInfo GetSerializerContext() => + SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { Console.WriteLine($"message from {args.RoutingKey}"); ProcessMessage(message); @@ -17,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") @@ -33,11 +33,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 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; @@ -46,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/RabbitMQCoreClient.ConsoleClient/Program.cs b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Program.cs similarity index 85% rename from samples/RabbitMQCoreClient.ConsoleClient/Program.cs rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/Program.cs index c9cacdb..13cd485 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/Program.cs +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/Program.cs @@ -1,13 +1,11 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQCoreClient; using RabbitMQCoreClient.BatchQueueSender; -using RabbitMQCoreClient.BatchQueueSender.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.ConsoleClient; -using RabbitMQCoreClient.Serializers; +using RabbitMQCoreClient.DependencyInjection; using System; using System.Linq; using System.Text; @@ -35,6 +33,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. @@ -49,8 +48,6 @@ RetryKey = "test_routing_key_retry" }); - services.AddBatchQueueSender(); - // For sending and consuming messages full configuration. //services // .AddRabbitMQCoreClient(config) @@ -67,8 +64,8 @@ var queueService = serviceProvider.GetRequiredService(); var consumer = serviceProvider.GetRequiredService(); -var batchSender = serviceProvider.GetRequiredService(); -consumer.Start(); +var batchSender = serviceProvider.GetRequiredService(); +await consumer.StartAsync(); //var body = new SimpleObj { Name = "test sending" }; //await queueService.SendAsync(body, "test_routing_key"); @@ -90,12 +87,12 @@ // return Task.CompletedTask; // } // })); -CancellationTokenSource source = new CancellationTokenSource(); +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(); @@ -106,7 +103,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) @@ -116,15 +113,15 @@ 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) { try { await Task.Delay(500, token); - var bodyList = Enumerable.Range(1, 1).Select(x => new SimpleObj { Name = $"test sending {x}" }); - await batchSender.AddEvents(bodyList, "test_routing_key"); + var bodyList = Enumerable.Range(1, 2).Select(x => new SimpleObj { Name = $"test sending {x}" }); + batchSender.AddEvents(bodyList, "test_routing_key"); } catch (Exception e) { diff --git a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj similarity index 74% rename from samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj rename to samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj index 9eef8e2..2064cd4 100644 --- a/samples/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj +++ b/samples/test-playground/RabbitMQCoreClient.ConsoleClient/RabbitMQCoreClient.ConsoleClient.csproj @@ -2,13 +2,9 @@ Exe - net8.0 + net10.0 - - - - PreserveNewest @@ -18,9 +14,9 @@ - + - + PreserveNewest 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 66% rename from samples/RabbitMQCoreClient.WebApp/Handler.cs rename to samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs index 5f488ee..98139db 100644 --- a/samples/RabbitMQCoreClient.WebApp/Handler.cs +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/Handler.cs @@ -1,23 +1,21 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; +using RabbitMQCoreClient.DependencyInjection; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; -using System; using System.Text; -using System.Threading.Tasks; +using System.Text.Json.Serialization.Metadata; namespace RabbitMQCoreClient.WebApp; public class Handler : MessageHandlerJson { - protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args) + protected override JsonTypeInfo GetSerializerContext() => SimpleObjContext.Default.SimpleObj; + + protected override Task HandleMessage(SimpleObj message, RabbitMessageEventArgs args, MessageHandlerContext context) { ProcessMessage(message); 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") //{ @@ -31,10 +29,9 @@ 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) + 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/Program.cs b/samples/test-playground/RabbitMQCoreClient.WebApp/Program.cs new file mode 100644 index 0000000..8bd15f9 --- /dev/null +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/Program.cs @@ -0,0 +1,32 @@ +using RabbitMQCoreClient; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.WebApp; + +var builder = WebApplication.CreateBuilder(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 + { + 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.Run(); diff --git a/samples/test-playground/RabbitMQCoreClient.WebApp/Properties/launchSettings.json b/samples/test-playground/RabbitMQCoreClient.WebApp/Properties/launchSettings.json new file mode 100644 index 0000000..222a322 --- /dev/null +++ b/samples/test-playground/RabbitMQCoreClient.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:5107", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj b/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj new file mode 100644 index 0000000..860aadf --- /dev/null +++ b/samples/test-playground/RabbitMQCoreClient.WebApp/RabbitMQCoreClient.WebApp.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + 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.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.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/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs new file mode 100644 index 0000000..90a1d92 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/QueueBatchSenderOptions.cs @@ -0,0 +1,19 @@ +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// Queue bus buffer options. +/// +public sealed partial class QueueBatchSenderOptions +{ + /// + /// The period for resetting (writing) events in RabbitMQ. + /// Default: 2 sec. + /// + 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; +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs index fa43f62..314359f 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,77 +1,149 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System; +using RabbitMQCoreClient.BatchQueueSender; -namespace RabbitMQCoreClient.BatchQueueSender.DependencyInjection +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// Class containing extension methods for registering the BatchQueueSender services at DI. +/// +public static partial class ServiceCollectionExtensions { + static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) + { + services.TryAddTransient(); + + services.TryAddSingleton((IServiceProvider sp) => + { + var options = sp.GetRequiredService>(); + + return new QueueBufferService(sp.GetRequiredService(), + options?.Value?.EventsFlushCount ?? 10000, + TimeSpan.FromSeconds(options?.Value?.EventsFlushPeriodSec ?? 2), + sp.GetService(), + sp.GetRequiredService(), + sp.GetService>()); + }); + 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 + /// 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 IRabbitMQCoreClientBuilder AddBatchQueueSender( + this IRabbitMQCoreClientBuilder builder, + IConfiguration configuration, + Action? setupAction = null) { - static IServiceCollection AddBatchQueueSenderCore(this IServiceCollection services) - { - services.AddTransient(); - services.AddSingleton(); - return services; - } - - /// - /// 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(); - } - - /// - /// 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(); - } + RegisterOptions(builder.Services, configuration, setupAction); - static void RegisterOptions(IServiceCollection services, - IConfiguration configuration, - Action? setupAction) - { - var instance = configuration.Get(); - setupAction?.Invoke(instance); - var options = Options.Create(instance); + 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 IRabbitMQCoreClientBuilder AddBatchQueueSender(this IRabbitMQCoreClientBuilder 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 IRabbitMQCoreClientBuilder AddBatchQueueSender(this IRabbitMQCoreClientBuilder builder) + { + builder.Services.TryAddSingleton((x) => Options.Create(new QueueBatchSenderOptions())); + 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. + /// 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) + { + var instance = configuration.Get() ?? new QueueBatchSenderOptions(); + setupAction?.Invoke(instance); + var options = Options.Create(instance); - services.AddSingleton((x) => options); - } + services.TryAddSingleton((x) => options); } } 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..8a51a07 --- /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. +/// +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); + } +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs index 6f64bb6..7291086 100644 --- a/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs +++ b/src/RabbitMQCoreClient/BatchQueueSender/Exceptions/PersistingException.cs @@ -1,36 +1,36 @@ -using System; +namespace RabbitMQCoreClient.BatchQueueSender.Exceptions; -namespace RabbitMQCoreClient.BatchQueueSender.Exceptions +/// +/// Create new object of . +/// +public sealed class PersistingException : Exception { - public class PersistingException : Exception - { - /// - /// The data items. - /// - public object[] Items { get; } + /// + /// The data items. + /// + public IEnumerable 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, + IEnumerable items, + string routingKey, + Exception innerException) : base(message, innerException) + { + Items = items; + RoutingKey = routingKey; } } diff --git a/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs new file mode 100644 index 0000000..40909c4 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/Extensions/BatchQueueExtensions.cs @@ -0,0 +1,103 @@ +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. + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] + 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 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 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 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 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 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 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 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 IQueueBufferService 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 IQueueBufferService 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 IQueueBufferService 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 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/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/IQueueBufferService.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueBufferService.cs new file mode 100644 index 0000000..8beedc2 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/IQueueBufferService.cs @@ -0,0 +1,21 @@ +using RabbitMQCoreClient.Serializers; +using System.Diagnostics.CodeAnalysis; + +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Batch Queue Events buffer interface. +/// +public interface IQueueBufferService +{ + /// + /// Message serializer to be used to serialize objects to sent to queue. + /// + IMessageSerializer Serializer { get; } + + /// + /// Add the event to the buffer to be sent to the data bus. + /// + /// The item with user-provided values array. + void Add([NotNull] EventItem item); +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs deleted file mode 100644 index 0b2ee28..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsBufferEngine.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace RabbitMQCoreClient.BatchQueueSender -{ - /// - /// Event buffer interface. - /// - 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); - - /// - /// 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); - } -} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs b/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs deleted file mode 100644 index 805eb3a..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/IQueueEventsWriter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -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/QueueBatchSenderOptions.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs deleted file mode 100644 index fb2f8f5..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueBatchSenderOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace RabbitMQCoreClient.BatchQueueSender -{ - /// - /// Queue bus buffer options. - /// - public sealed class QueueBatchSenderOptions - { - /// - /// The period for resetting (writing) events in RabbitMQ. - /// Default: 2 sec. - /// - 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; - } -} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueBufferService.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueBufferService.cs new file mode 100644 index 0000000..bc3da01 --- /dev/null +++ b/src/RabbitMQCoreClient/BatchQueueSender/QueueBufferService.cs @@ -0,0 +1,297 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using RabbitMQCoreClient.Serializers; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace RabbitMQCoreClient.BatchQueueSender; + +/// +/// Implementation of the stream data event store buffer. +/// +internal sealed class QueueBufferService : IQueueBufferService, IDisposable +{ + 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; + + /// + 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}"; + + /// + /// The implementation constructor of the event storage buffer. + /// Creates a new instance of the class . + /// + public QueueBufferService(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) + { + lock (_syncRoot) + { + _buffer.Enqueue(item); + _count++; + + if (_count == 1) + _timer.Change(_timeLimit, Timeout.InfiniteTimeSpan); + + if (_count >= _sizeLimit) + _currentFlushTask = FlushAsync(); + } + } + + async Task FlushByTimerAsync() + { + EventItem[]? array = null; + int count = 0; + + lock (_syncRoot) + { + if (_count == 0) + return; + (array, count) = ExtractItems(); + } + + await ProcessItemsAsync(array, count).ConfigureAwait(false); + } + + async Task FlushAsync() + { + EventItem[]? array = null; + int count = 0; + + lock (_syncRoot) + { + (array, count) = ExtractItems(); + } + + await ProcessItemsAsync(array, count).ConfigureAwait(false); + } + + (EventItem[] Array, int Count) ExtractItems() + { + _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 + { + 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())); + } + + 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; + + 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 + { + 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); + } + } + + static IEnumerable GroupByKey(ArraySegment batch) + { + if (batch.Array is null) + yield break; + + var groups = DictionaryPool>.Rent(); + try + { + 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 + { + foreach (var list in groups.Values) + { + ListPool.Return(list); + } + DictionaryPool>.Return(groups); + } + } + + /// + public async Task CompleteAsync() + { + Task flushTask; + lock (_syncRoot) + { + flushTask = _currentFlushTask; + } + + await flushTask.ConfigureAwait(false); + } + + /// + public void Dispose() + { + _timer?.Dispose(); + GC.SuppressFinalize(this); + } + + readonly struct GroupData + { + public readonly string Key; + public readonly EventItem[] Items; + + public GroupData(string key, EventItem[] items) + { + Key = key; + Items = items; + } + } + + 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); + + class ListPolicy : PooledObjectPolicy> + { + public override List Create() => new List(); + public override bool Return(List list) + { + list.Clear(); + return true; + } + } + } + + static class DictionaryPool + where TKey : notnull + { + static readonly ConcurrentBag> _pool = new(); + + public static Dictionary Rent() + { + if (_pool.TryTake(out var dict)) + { + return dict; + } + return []; + } + + public static void Return(Dictionary dict) + { + dict.Clear(); + _pool.Add(dict); + } + } +} diff --git a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs b/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs deleted file mode 100644 index bd68fea..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventItem.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace RabbitMQCoreClient.BatchQueueSender -{ - public 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 deleted file mode 100644 index 2387463..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsBufferEngine.cs +++ /dev/null @@ -1,181 +0,0 @@ -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 -{ - /// - /// 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; - - /// - /// 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."); - - _engineOptions = engineOptions.Value; - _eventsWriter = eventsWriter; - _logger = logger; - - _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; - - await _semaphore.WaitAsync(); - - try - { - _events.Add(new QueueEventItem(@event, routingKey)); - if (_events.Count < _engineOptions.EventsFlushCount) - return; - - await Flush(); - } - finally - { - _semaphore.Release(); - } - } - - /// - public Task AddEvent(IEnumerable events, string routingKey) - => AddEvents(events, routingKey); - - /// - public async Task AddEvents(IEnumerable events, string routingKey) - { - await _semaphore.WaitAsync(); - - try - { - foreach (var @event in events) - { - if (@event is not null) - _events.Add(new QueueEventItem(@event, routingKey)); - } - - if (_events.Count < _engineOptions.EventsFlushCount) - return; - - await Flush(); - } - finally - { - _semaphore.Release(); - } - } - - async Task FlushTimerDelegate(object? _) - { - await _semaphore.WaitAsync(); - try - { - await Flush(); - } - finally - { - _semaphore.Release(); - } - } - - Task Flush() - { - if (_events.Count == 0) - return Task.CompletedTask; - - var eventsCache = _events.ToArray(); - _events.Clear(); - - return HandleEvents(eventsCache); - } - - async Task HandleEvents(IEnumerable streamDataEvents) - { - var routingGroups = streamDataEvents.GroupBy(x => x.RoutingKey); - - var tasks = new List(); - - 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 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. - } - } - } - - void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _flushTimer?.Dispose(); - } - - _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 deleted file mode 100644 index aa335c5..0000000 --- a/src/RabbitMQCoreClient/BatchQueueSender/QueueEventsWriter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Microsoft.Extensions.Logging; -using RabbitMQCoreClient.BatchQueueSender.Exceptions; -using System; -using System.Diagnostics; -using System.Threading.Tasks; - -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; - - _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(); - - _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/Configuration/AppConstants.cs b/src/RabbitMQCoreClient/Configuration/AppConstants.cs index 8802154..2347845 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 +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..eaa27e2 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV1Binder.cs @@ -1,114 +1,136 @@ -using Microsoft.Extensions.Configuration; +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 System; -using System.Linq; +using RabbitMQCoreClient.Models; -namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats +namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; + +/// +/// Configure RabbitMQCore client builder from IConfiguration. +/// +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"; + + /// + /// Configure RabbitMQCoreClient with v1 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// + 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; + } + + /// + /// Configure RabbitMQCoreClient with v1 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// + 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) + ?? throw new ClientConfigurationException($"The exchange {oldExchangeName} is " + + "not found in \"Exchange\" section."); + 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); + TConfig q; + // Support of source generators. + if (typeof(TConfig) == typeof(QueueConfig)) + { + q = (TConfig)(object)BindQueueConfig(queueConfig); } - - static TConfig BindConfig(IConfigurationSection queueConfig) - where TConfig : new() + else if (typeof(TConfig) == typeof(SubscriptionConfig)) { - var q = new TConfig(); - queueConfig.Bind(q); - return q; + 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); - static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) - where T : QueueBase + AddQueue(builder, queue); + } + + static QueueConfig BindQueueConfig(IConfigurationSection queueConfig) + { + var config = new QueueConfig(); + queueConfig.Bind(config); + return config; + } + + static SubscriptionConfig BindSubscriptionConfig(IConfigurationSection queueConfig) + { + var config = new SubscriptionConfig(); + queueConfig.Bind(config); + return config; + } + + 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..e94341a 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigFormats/JsonV2Binder.cs @@ -1,122 +1,148 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using RabbitMQCoreClient.DependencyInjection.ConfigModels; using RabbitMQCoreClient.Exceptions; -using System; -using System.Linq; +using RabbitMQCoreClient.Models; -namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats +namespace RabbitMQCoreClient.DependencyInjection.ConfigFormats; + +/// +/// Configure RabbitMQCore client builder from IConfiguration. +/// +public static class JsonV2Binder { - 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) { - 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; - - // Register exchange points. - RegisterExchanges(builder, configuration); - + if (configuration is null) return builder; - } - - static void RegisterExchanges(IRabbitMQCoreClientBuilder builder, IConfiguration configuration) - { - var exchanges = configuration.GetSection(ExhangesSection); - foreach (var exchangeConfig in exchanges.GetChildren()) - { - 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; - - // Register queues and link them to exchange points. - RegisterQueues(builder, - configuration.GetSection(QueuesSection), - (qConfig) => Queue.Create(qConfig)); + // Register exchange points. + RegisterExchanges(builder, configuration); - // Register subscriptions and link them to exchange points. - RegisterQueues(builder, - configuration.GetSection(SubscriptionsSection), - (qConfig) => Subscription.Create(qConfig)); + return builder; + } + /// + /// Configure RabbitMQCoreClient with v2 configuration. + /// + /// RabbitMQCoreClient consumer builder. + /// configuration + /// + public static IRabbitMQCoreClientConsumerBuilder RegisterV2Configuration(this IRabbitMQCoreClientConsumerBuilder builder, + IConfiguration configuration) + { + if (configuration is null) return builder; - } - static void RegisterQueues(IRabbitMQCoreClientConsumerBuilder builder, - IConfigurationSection? queuesConfiguration, - Func createQueue) - where TConfig : new() - where TQueue : QueueBase - { - if (queuesConfiguration is null) - return; + // Register queues and link them to exchange points. + RegisterQueues(builder, + configuration.GetSection(QueuesSection), + (qConfig) => Queue.Create(qConfig)); - foreach (var queueConfig in queuesConfiguration.GetChildren()) - { - var q = BindConfig(queueConfig); + // Register subscriptions and link them to exchange points. + RegisterQueues(builder, + configuration.GetSection(SubscriptionsSection), + (qConfig) => Subscription.Create(qConfig)); - var queue = createQueue(q); + return builder; + } - RegisterQueue(builder, queue); - } + 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) + where TConfig : new() + where TQueue : QueueBase + { + if (queuesConfiguration is null) + return; - static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, TQueue queue) - where TQueue : QueueBase + foreach (var queueConfig in queuesConfiguration.GetChildren()) { - if (!queue.Exchanges.Any()) + TConfig q; + // Support of source generators. + if (typeof(TConfig) == typeof(QueueConfig)) { - 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); + q = (TConfig)(object)BindQueueConfig(queueConfig); } - else + else if (typeof(TConfig) == typeof(SubscriptionConfig)) { - // 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."); - } + q = (TConfig)(object)BindSubscriptionConfig(queueConfig); } + else + throw new ClientConfigurationException("Configuration supports only QueueConfig or SubscriptionConfig classes"); + + var queue = createQueue(q); - AddQueue(builder, queue); + RegisterQueue(builder, queue); } + } - static TConfig BindConfig(IConfigurationSection queueConfig) - where TConfig : new() + static void RegisterQueue(IRabbitMQCoreClientConsumerBuilder builder, TQueue queue) + where TQueue : QueueBase + { + if (queue.Exchanges.Count == 0) { - var q = new TConfig(); - queueConfig.Bind(q); - return q; + 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); } - - static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) - where T : QueueBase + else { - // So-so solution, but without dubbing. - switch (queue) + // Checking exchange points declared in queue in configured "Exchanges". + foreach (var exchangeName in queue.Exchanges) { - case Queue q: builder.AddQueue(q); break; - case Subscription q: builder.AddSubscription(q); break; + 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."); } } + + AddQueue(builder, queue); + } + + static QueueConfig BindQueueConfig(IConfigurationSection queueConfig) + { + var config = new QueueConfig(); + queueConfig.Bind(config); + return config; + } + + static SubscriptionConfig BindSubscriptionConfig(IConfigurationSection queueConfig) + { + var config = new SubscriptionConfig(); + queueConfig.Bind(config); + return config; + } + + static void AddQueue(IRabbitMQCoreClientConsumerBuilder builder, T queue) + where T : QueueBase + { + // So-so solution, but without dubbing. + switch (queue) + { + 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..fdb592c 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/QueueConfig.cs @@ -1,68 +1,64 @@ -using System; -using System.Collections.Generic; +namespace RabbitMQCoreClient.DependencyInjection; -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; } = []; - /// - /// 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; } = []; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs index 7a88a0c..1e7096f 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/ConfigModels/SubscriptionConfig.cs @@ -1,37 +1,34 @@ -using System.Collections.Generic; +namespace RabbitMQCoreClient.DependencyInjection; -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; } = []; - /// - /// 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; } = []; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs deleted file mode 100644 index b6205ea..0000000 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ApplicationBuilderExtentions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using RabbitMQCoreClient.Exceptions; - -namespace Microsoft.Extensions.DependencyInjection -{ - 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) - { - 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."); - - lifetime.ApplicationStarted.Register(() => consumer.Start()); - lifetime.ApplicationStopping.Register(() => consumer.Shutdown()); - - return app; - } - } -} diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs index 9870f41..b890c69 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/BuilderExtensions.cs @@ -1,261 +1,266 @@ -using Microsoft.Extensions.Configuration; +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.ConfigModels; using RabbitMQCoreClient.Exceptions; -using System; -using System.Collections.Generic; -using System.Linq; +using RabbitMQCoreClient.Models; +using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Extensions.DependencyInjection +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// extension methods. +/// +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.AddHostedService(); - builder.Services.TryAddSingleton( - resolver => resolver.GetRequiredService>().Value); + builder.Services.AddOptions(); + builder.Services.AddLogging(); - return builder; - } + builder.Services.TryAddSingleton( + resolver => resolver.GetRequiredService>().Value); - /// - /// 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."); + return builder; + } - var exchange = new Exchange(options ?? new ExchangeOptions { Name = exchangeName }); - builder.Exchanges.Add(exchange); + /// + /// 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."); - return builder; - } + var exchange = new Exchange(options ?? new ExchangeOptions { Name = exchangeName }); + builder.Exchanges.Add(exchange); - /// - /// Adds the consumer builder. - /// - /// The rabbit MQ builder. - public static IRabbitMQCoreClientConsumerBuilder AddConsumer( - this IRabbitMQCoreClientBuilder builder) - { - var consumerBuilder = new RabbitMQCoreClientConsumerBuilder(builder); + return builder; + } - builder.Services.TryAddSingleton(consumerBuilder); - builder.Services.TryAddSingleton(); + /// + /// Adds the consumer builder. + /// + /// The rabbit MQ builder. + public static IRabbitMQCoreClientConsumerBuilder AddConsumer( + this IRabbitMQCoreClientBuilder builder) + { + var consumerBuilder = new RabbitMQCoreClientConsumerBuilder(builder); - return consumerBuilder; - } + builder.Services.TryAddSingleton(consumerBuilder); + builder.Services.TryAddSingleton(); - /// - /// 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); + return consumerBuilder; + } - builder.AddQueue(queue); + /// + /// 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); - return builder; - } + builder.AddQueue(queue); - /// - /// 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."); + return builder; + } - var q = new QueueConfig(); - queueConfig.Bind(q); + /// + /// 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 queue = Queue.Create(q); + var q = new QueueConfig(); + queueConfig.Bind(q); - builder.AddQueue(queue); + var queue = Queue.Create(q); - return builder; - } + builder.AddQueue(queue); - /// - /// 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."); + return builder; + } - if (builder.Queues.Any(x => (x is Queue) && x.Name == queue.Name)) - throw new ArgumentException("The queue with same name was added earlier."); + /// + /// 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."); - builder.Queues.Add(queue); + if (builder.Queues.Any(x => (x is Queue) && x.Name == queue.Name)) + throw new ArgumentException("The queue with same name was added earlier."); - return builder; - } + builder.Queues.Add(queue); - /// - /// 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."); + return builder; + } - var s = new SubscriptionConfig(); - subscriptionConfig.Bind(s); + /// + /// 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 subscription = Subscription.Create(s); + var s = new SubscriptionConfig(); + subscriptionConfig.Bind(s); - builder.AddSubscription(subscription); + var subscription = Subscription.Create(s); - return builder; - } + builder.AddSubscription(subscription); - /// - /// 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); + return builder; + } - builder.AddSubscription(subscription); + /// + /// 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); - return builder; - } + builder.AddSubscription(subscription); - /// - /// 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."); + return builder; + } - builder.Queues.Add(subscription); + /// + /// 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."); - return builder; - } + builder.Queues.Add(subscription); - /// - /// 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); + 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. + /// Routing keys bound to the queue. + /// The handler needs to set at least one routing key. + public static IRabbitMQCoreClientConsumerBuilder AddHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMessageHandler>( + 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. - /// 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); + AddHandlerToBuilder(builder, typeof(TMessageHandler), default!, routingKeys); + builder.Services.TryAddTransient(); + return builder; + } - AddHandlerToBuilder(builder, typeof(TMessageHandler), options ?? new ConsumerHandlerOptions(), 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 bound to the queue. + public static IRabbitMQCoreClientConsumerBuilder AddHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMessageHandler>( + this IRabbitMQCoreClientConsumerBuilder builder, + IList routingKeys, + ConsumerHandlerOptions? options = default) + where TMessageHandler : class, IMessageHandler + { + CheckRoutingKeysParam(routingKeys); - static void AddHandlerToBuilder(IRabbitMQCoreClientConsumerBuilder builder, - Type handlerType, - ConsumerHandlerOptions options, - IList 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)); - } - } + AddHandlerToBuilder(builder, typeof(TMessageHandler), options ?? new ConsumerHandlerOptions(), routingKeys); + builder.Services.TryAddTransient(); + return builder; + } - static void CheckRoutingKeysParam(IEnumerable routingKeys) + static void AddHandlerToBuilder(IRabbitMQCoreClientConsumerBuilder builder, + Type handlerType, + ConsumerHandlerOptions options, + IList routingKeys) + { + foreach (var routingKey in routingKeys) { - if (routingKeys is null || !routingKeys.Any()) - throw new ClientConfigurationException("The handler needs to set at least one routing key."); + if (builder.RoutingHandlerTypes.TryGetValue(routingKey, out var result)) + throw new ClientConfigurationException("The routing key is already being processed by a handler like " + + $"{result.Type.FullName}."); + + 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..9e0d83e 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/ServiceCollectionExtensions.cs @@ -1,92 +1,93 @@ -using Microsoft.Extensions.Configuration; +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; -using System; -namespace Microsoft.Extensions.DependencyInjection +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// Class containing extension methods for creating a RabbitMQ message handler configuration interface. +/// . +/// +public static partial class ServiceCollectionExtensions { + static RabbitMQCoreClientBuilder AddRabbitMQCoreClient(this IServiceCollection services) + { + var builder = new RabbitMQCoreClientBuilder(services); + + builder.AddRequiredPlatformServices(); + + builder.AddDefaultSerializer(); + + services.TryAddSingleton(builder); + + return builder; + } + /// - /// 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 IRabbitMQCoreClientBuilder AddRabbitMQCoreClient( + 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) - { + 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) + { + if (setupAction != null) 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); - } + + 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() ?? new RabbitMQCoreClientOptions(); + setupAction?.Invoke(instance); + var options = Options.Create(instance); + + services.AddSingleton((x) => options); } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Extensions/SystemTextJsonBuilderExtensions.cs b/src/RabbitMQCoreClient/DependencyInjection/Extensions/SystemTextJsonBuilderExtensions.cs new file mode 100644 index 0000000..927bbab --- /dev/null +++ b/src/RabbitMQCoreClient/DependencyInjection/Extensions/SystemTextJsonBuilderExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using RabbitMQCoreClient.Serializers; +using System.Text.Json; + +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// 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. + /// + public static IRabbitMQCoreClientBuilder AddSystemTextJson(this IRabbitMQCoreClientBuilder builder, Action? setupAction = null) + { + 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; + } +} diff --git a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs index b646b7e..0522173 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IQueueConsumer.cs @@ -1,34 +1,33 @@ -using RabbitMQ.Client; +using RabbitMQ.Client; using RabbitMQ.Client.Events; -using System; -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(); + /// Cancellation token. + /// + Task StartAsync(CancellationToken cancellationToken = default); - /// - /// Stops listening Queues. - /// - void Shutdown(); + /// + /// Stops listening Queues. + /// + Task ShutdownAsync(); - /// - /// 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..81931ac 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientBuilder.cs @@ -1,29 +1,30 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection; +using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; -using System.Collections.Generic; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// RabbitMQCoreClient builder. +/// +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. + /// + List 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 message serializer. + /// + IMessageSerializer? Serializer { get; set; } } diff --git a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs index 307ce89..75b432d 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/IRabbitMQCoreClientConsumerBuilder.cs @@ -1,29 +1,30 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; -using System.Collections.Generic; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.Models; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Consumer builder interface. +/// +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..440cf17 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ConsumerHandlerOptions.cs @@ -1,21 +1,15 @@ -using RabbitMQCoreClient.Serializers; +using RabbitMQCoreClient.Serializers; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// Consumer Handler Options. +/// +public class ConsumerHandlerOptions { /// - /// Consumer Handler Options. + /// 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 class ConsumerHandlerOptions - { - /// - /// Gets or sets the json serializer that overides 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 . - /// - public string? RetryKey { get; set; } = default; - } + public string? RetryKey { get; set; } = default; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs index de73f43..3775fcf 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ErrorHandlingOptions.cs @@ -1,21 +1,19 @@ -using RabbitMQ.Client.Events; -using System; +using RabbitMQ.Client.Events; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.DependencyInjection; + +/// +/// 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..5788dbe 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/ExchangeOptions.cs @@ -1,49 +1,46 @@ -using System.Collections.Generic; +namespace RabbitMQCoreClient.DependencyInjection; -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..4890f38 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/Options/RabbitMQCoreClientOptions.cs @@ -1,136 +1,139 @@ using RabbitMQ.Client.Events; -using System; using System.Net.Security; using System.Security.Authentication; using System.Text.Json.Serialization; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options -{ +namespace RabbitMQCoreClient.DependencyInjection; - 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 МБи - } +/// +/// RabbitMQCoreClient 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; + + /// + /// Heartbeat timeout to use when negotiating with the server. + /// + 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..b29befc 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientBuilder.cs @@ -1,31 +1,31 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using RabbitMQCoreClient.Models; using RabbitMQCoreClient.Serializers; -using System; -using System.Collections.Generic; -using System.Linq; -namespace RabbitMQCoreClient.Configuration.DependencyInjection +namespace RabbitMQCoreClient.Configuration.DependencyInjection; + +/// +/// RabbitMQCore client builder. +/// +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 List Exchanges { get; } = []; - /// - 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..a8a50ab 100644 --- a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs +++ b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQCoreClientConsumerBuilder.cs @@ -1,34 +1,35 @@ -using Microsoft.Extensions.DependencyInjection; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; -using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.Models; -namespace RabbitMQCoreClient.Configuration.DependencyInjection +namespace RabbitMQCoreClient.Configuration.DependencyInjection; + +/// +/// RabbitMQCoreClient consumer builder. +/// +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; } = []; - /// - public Dictionary RoutingHandlerTypes { get; } = - new Dictionary(); - } + /// + public Dictionary RoutingHandlerTypes { get; } = + []; } diff --git a/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs b/src/RabbitMQCoreClient/DependencyInjection/RabbitMQHostedService.cs new file mode 100644 index 0000000..31aa23e --- /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.StartAsync(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/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..24001e3 --- /dev/null +++ b/src/RabbitMQCoreClient/Events/ReconnectEventArgs.cs @@ -0,0 +1,10 @@ +using RabbitMQ.Client.Events; + +namespace RabbitMQCoreClient.Events; + +/// +/// ReconnectEventArgs. +/// +public class ReconnectEventArgs : AsyncEventArgs +{ +} diff --git a/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs b/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs index 4795ff6..9be86ba 100644 --- a/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs +++ b/src/RabbitMQCoreClient/Exceptions/BadMessageException.cs @@ -1,8 +1,9 @@ -using System; - 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 85644b2..656a15a 100644 --- a/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ClientConfigurationException.cs @@ -1,22 +1,22 @@ -using System; +namespace RabbitMQCoreClient.Exceptions; -namespace RabbitMQCoreClient.Exceptions +/// +/// Create new object of . +/// +public sealed 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..b051daf 100644 --- a/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs +++ b/src/RabbitMQCoreClient/Exceptions/NotConnectedException.cs @@ -1,23 +1,22 @@ -using System; +namespace RabbitMQCoreClient.Exceptions; -namespace RabbitMQCoreClient.Exceptions +/// +/// Create new object of . +/// +public sealed 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..f62bd1a 100644 --- a/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs +++ b/src/RabbitMQCoreClient/Exceptions/QueueBindException.cs @@ -1,22 +1,22 @@ -using System; +namespace RabbitMQCoreClient.Exceptions; -namespace RabbitMQCoreClient.Exceptions +/// +/// Create new object of . +/// +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..4c438ed 100644 --- a/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs +++ b/src/RabbitMQCoreClient/Exceptions/ReconnectAttemptsExceededException.cs @@ -1,23 +1,22 @@ -using System; +namespace RabbitMQCoreClient.Exceptions; -namespace RabbitMQCoreClient.Exceptions +/// +/// Create new object of . +/// +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/QueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs new file mode 100644 index 0000000..15ee03a --- /dev/null +++ b/src/RabbitMQCoreClient/Extentions/QueueServiceExtensions.cs @@ -0,0 +1,220 @@ +using System.Diagnostics.CodeAnalysis; +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. + /// + /// obj + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] + 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(); + + 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. + /// + /// obj + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] + 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) => + 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) => + service.SendBatchAsync( + objs: objs, + QueueService.CreateDefaultProperties(), + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); +} diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs deleted file mode 100644 index 90142d5..0000000 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonBuilderExtentions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using RabbitMQCoreClient.Serializers; -using System; -using System.Text.Json; - -namespace Microsoft.Extensions.DependencyInjection -{ - 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) - { - 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; - } - } -} diff --git a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs new file mode 100644 index 0000000..93df876 --- /dev/null +++ b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtensions.cs @@ -0,0 +1,181 @@ +using RabbitMQCoreClient.BatchQueueSender; +using RabbitMQCoreClient.Serializers; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace RabbitMQCoreClient; + +/// +/// Extended System.Text.Json publish methods. +/// +public static class SystemTextJsonQueueServiceExtensions +{ + /// + /// Send the message to the queue with serialization 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. + /// The json serializer settings. + /// 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, + string routingKey, + JsonSerializerOptions? jsonSerializerSettings, + string? exchange = default, + CancellationToken cancellationToken = default + ) where T : class + { + if (obj is null) + throw new ArgumentNullException(nameof(obj)); + + var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions); + return QueueServiceExtensions.SendAsync(service, + serializedObj, + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + } + + /// + /// Send the message to the queue with serialization to Json by 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. + /// Metadata about the type to convert. + /// Cancellation token. + /// + /// obj + public static ValueTask SendAsync( + this IQueueService service, + T obj, + string routingKey, + JsonTypeInfo jsonTypeInfo, + string? exchange = default, + CancellationToken cancellationToken = default + ) where T : class + { + if (obj is null) + throw new ArgumentNullException(nameof(obj)); + + var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo); + return QueueServiceExtensions.SendAsync(service, + serializedObj, + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + } + + /// + /// Send pack of messages to the queue with serialization 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. + /// The json serializer settings. + /// 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, + string routingKey, + JsonSerializerOptions? jsonSerializerSettings, + string? exchange = default, + CancellationToken cancellationToken = default + ) where T : class + { + var serializeSettings = jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions; + var messages = objs.Select(x => new ReadOnlyMemory( + JsonSerializer.SerializeToUtf8Bytes(x, serializeSettings))); + + return service.SendBatchAsync( + objs: messages, + routingKey: routingKey, + exchange: exchange, + cancellationToken: cancellationToken + ); + } + + /// + /// Send pack of messages to the queue with serialization to Json by 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. + /// Metadata about the type to convert. + /// Cancellation token. + /// + /// obj + public static ValueTask SendBatchAsync( + this IQueueService service, + IEnumerable objs, + string routingKey, + JsonTypeInfo jsonTypeInfo, + string? exchange = default, + CancellationToken cancellationToken = default + ) where T : class + { + var messages = objs.Select(x => new ReadOnlyMemory( + JsonSerializer.SerializeToUtf8Bytes(x, jsonTypeInfo))); + + return service.SendBatchAsync( + objs: messages, + exchange: exchange, + routingKey: routingKey, + 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/Extentions/SystemTextJsonQueueServiceExtentions.cs b/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs deleted file mode 100644 index c44951c..0000000 --- a/src/RabbitMQCoreClient/Extentions/SystemTextJsonQueueServiceExtentions.cs +++ /dev/null @@ -1,182 +0,0 @@ -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 -{ - 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]. - /// 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)); - - var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonSerializerSettings ?? SystemTextJsonMessageSerializer.DefaultOptions); - return queueService.SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } - -#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)); - - var serializedObj = JsonSerializer.SerializeToUtf8Bytes(obj, jsonTypeInfo); - return queueService.SendJsonAsync( - serializedObj, - exchange: exchange, - routingKey: routingKey, - decreaseTtl: decreaseTtl, - correlationId: correlationId - ); - } -#endif - - /// - /// 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) - { - 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 - ); - } - -#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, - correlationId: correlationId - ); - } -#endif - } -} diff --git a/src/RabbitMQCoreClient/IMessageHandler.cs b/src/RabbitMQCoreClient/IMessageHandler.cs index 5c4a6cb..67e471f 100644 --- a/src/RabbitMQCoreClient/IMessageHandler.cs +++ b/src/RabbitMQCoreClient/IMessageHandler.cs @@ -1,36 +1,19 @@ -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); - - /// - /// 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; } - - /// - /// The default json serializer. - /// - IMessageSerializer Serializer { get; set; } - } -} \ No newline at end of file + /// Input byte array from consumed queue. + /// The instance containing the message data. + /// Handler configuration context. + Task HandleMessage(ReadOnlyMemory message, + RabbitMessageEventArgs args, + MessageHandlerContext context); +} diff --git a/src/RabbitMQCoreClient/IQueueService.cs b/src/RabbitMQCoreClient/IQueueService.cs index 8f70c92..8eec334 100644 --- a/src/RabbitMQCoreClient/IQueueService.cs +++ b/src/RabbitMQCoreClient/IQueueService.cs @@ -1,220 +1,115 @@ -using RabbitMQ.Client; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.Events; +using RabbitMQCoreClient.Serializers; -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. Null until first connected. /// - public interface IQueueService : IDisposable - { - /// - /// RabbitMQ connection interface. - /// - IConnection Connection { get; } - - /// - /// A channel for sending RabbitMQ data. - /// - IModel SendChannel { get; } - - /// - /// MQ service settings. - /// - RabbitMQCoreClientOptions Options { get; } + IConnection? Connection { get; } - /// - /// Occurs when connection restored after reconnect. - /// - event Action OnReconnected; - - /// - /// Occurs when connection is shuted down on any reason. - /// - event Action OnConnectionShutdown; + /// + /// A channel for sending RabbitMQ data. Null until first connected. + /// + IChannel? PublishChannel { get; } - /// - /// 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); + /// + /// MQ service settings. + /// + RabbitMQCoreClientOptions Options { get; } - /// - /// 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); + /// + /// Message serializer to be used to serialize objects to sent to queue. + /// + IMessageSerializer Serializer { get; } - /// - /// 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 - ); + /// + /// Occurs when connection restored after reconnect. + /// + event AsyncEventHandler ReconnectedAsync; - /// - /// 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); + /// + /// Occurs when connection is interrupted for some reason. + /// + event AsyncEventHandler ConnectionShutdownAsync; - /// - /// 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 . + /// + /// 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 + /// exchange - exchange + ValueTask SendAsync( + ReadOnlyMemory obj, + BasicProperties props, + string routingKey, + string? exchange = default, + bool decreaseTtl = true, + CancellationToken cancellationToken = default); - /// - /// 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 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 + /// exchange - exchange + ValueTask SendBatchAsync( + IEnumerable<(ReadOnlyMemory Body, BasicProperties Props)> objs, + string routingKey, + string? exchange = default, + bool decreaseTtl = true, + CancellationToken cancellationToken = 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. - /// 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 single properties for all messages. + /// + /// 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. + /// Cancellation token. + /// + /// jsonString - jsonString + /// or + /// exchange - exchange + ValueTask SendBatchAsync( + IEnumerable> objs, + BasicProperties props, + string routingKey, + string? exchange = default, + CancellationToken cancellationToken = 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. - /// 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); + /// + /// Connect to RabbitMQ server and run publisher channel. + /// + /// Cancellation token. + /// + Task ConnectAsync(CancellationToken cancellationToken = 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. - /// 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 + /// + /// Shutdown RabbitMQ connection. + /// + /// + Task ShutdownAsync(); +} 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 bd6aab2..5fde531 100644 --- a/src/RabbitMQCoreClient/MessageHandlerJson.cs +++ b/src/RabbitMQCoreClient/MessageHandlerJson.cs @@ -1,76 +1,71 @@ -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; using RabbitMQCoreClient.Models; -using RabbitMQCoreClient.Serializers; -using System; using System.Text; -using System.Threading.Tasks; +using System.Text.Json.Serialization.Metadata; -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 + where TModel : class { /// - /// Handler for the message received from the queue. + /// The method will be called when there is an error parsing Json into the model. /// - /// The type of model that will be deserialized into. - /// - public abstract class MessageHandlerJson : IMessageHandler - { - /// - /// 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. - /// - /// 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); + /// The json. + /// The exception. + /// The instance containing the event data. + /// Handler configuration context. + /// + protected virtual ValueTask OnParseError(string json, + Exception e, + RabbitMessageEventArgs args, + MessageHandlerContext context) => default; - /// - /// Raw Json formatted message. - /// - protected string? RawJson { get; set; } + /// + /// 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, + MessageHandlerContext context); - /// - /// Gets the options. - /// - public ConsumerHandlerOptions? Options { get; set; } + /// + /// Raw Json formatted message. + /// + protected string? RawJson { get; set; } - /// - /// The default json serializer. - /// - public IMessageSerializer Serializer { get; set; } = new SystemTextJsonMessageSerializer(); + /// + /// You must provide TModel json serialization context. + /// + /// + 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 { - 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 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, context); + // Fall to the top-level exception handler. + throw; + } + + await HandleMessage(messageModel, args, context); } } diff --git a/src/RabbitMQCoreClient/Models/Exchange.cs b/src/RabbitMQCoreClient/Models/Exchange.cs index 8648c93..14a4bd6 100644 --- a/src/RabbitMQCoreClient/Models/Exchange.cs +++ b/src/RabbitMQCoreClient/Models/Exchange.cs @@ -1,50 +1,51 @@ -using RabbitMQ.Client; -using RabbitMQCoreClient.Configuration.DependencyInjection.Options; -using System; +using RabbitMQ.Client; +using RabbitMQCoreClient.DependencyInjection; -namespace RabbitMQCoreClient.Configuration.DependencyInjection +namespace RabbitMQCoreClient.Models; + +/// +/// 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($"Name in {nameof(options)} is null or empty.", nameof(options)); } + + /// + /// Starts the exchange. + /// + /// The channel. + /// 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, + cancellationToken: cancellationToken + ); } diff --git a/src/RabbitMQCoreClient/Models/Queue.cs b/src/RabbitMQCoreClient/Models/Queue.cs index 373ec6b..d44ef79 100644 --- a/src/RabbitMQCoreClient/Models/Queue.cs +++ b/src/RabbitMQCoreClient/Models/Queue.cs @@ -1,35 +1,46 @@ -using RabbitMQCoreClient.DependencyInjection.ConfigModels; -using System; -using System.Collections.Generic; +using RabbitMQCoreClient.Configuration; +using RabbitMQCoreClient.DependencyInjection; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Models; + +/// +/// Simple custom message queue. +/// +public sealed class Queue : QueueBase { /// - /// Simple custom message queue. + /// Create new object of . /// - public sealed class Queue : QueueBase + /// 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) { - 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(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 ?? [], + RoutingKeys = queueConfig.RoutingKeys ?? [] + }; } diff --git a/src/RabbitMQCoreClient/Models/QueueBase.cs b/src/RabbitMQCoreClient/Models/QueueBase.cs index 882993a..2c42696 100644 --- a/src/RabbitMQCoreClient/Models/QueueBase.cs +++ b/src/RabbitMQCoreClient/Models/QueueBase.cs @@ -1,117 +1,125 @@ -using RabbitMQ.Client; +using RabbitMQ.Client; using RabbitMQ.Client.Events; +using RabbitMQCoreClient.Configuration; using RabbitMQCoreClient.Exceptions; -using System; -using System.Collections.Generic; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Models; + +/// +/// Options to be applied to the message queue. +/// +public abstract class QueueBase { /// - /// Options to be applied to the message queue. + /// 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; + 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. + /// Except is true. Then the queue will be created with be created with header + /// + 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 abstract class QueueBase + 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; } = []; + + /// + /// The list of exchange points to which the queue is bound. + /// + public virtual HashSet Exchanges { get; set; } = []; + + /// + /// Declare the queue on and start consuming messages. + /// + public virtual async Task StartQueueAsync(IChannel channel, + AsyncEventingBasicConsumer consumer, + CancellationToken cancellationToken = default) + { + 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, + cancellationToken: cancellationToken) + ?? throw new QueueBindException("Queue is not properly bind."); + if (RoutingKeys.Count > 0) + foreach (var exchangeName in Exchanges) + { + await BindToExchangeAsync(channel, declaredQueue, exchangeName, cancellationToken); + } + + await channel.BasicConsumeAsync(queue: declaredQueue.QueueName, + autoAck: false, + consumer: consumer, + consumerTag: $"amq.{declaredQueue.QueueName}.{Guid.NewGuid()}", + cancellationToken: cancellationToken + ); + } + + async Task BindToExchangeAsync(IChannel channel, QueueDeclareOk declaredQueue, string exchangeName, CancellationToken cancellationToken = default) { - 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, + cancellationToken: cancellationToken + ); } } diff --git a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs index 9d0199f..f606616 100644 --- a/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs +++ b/src/RabbitMQCoreClient/Models/RabbitMessageEventArgs.cs @@ -1,27 +1,32 @@ -using System; +using RabbitMQ.Client.Events; -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; } +/// +/// Arguments that used in message consumer methods. +/// +public class RabbitMessageEventArgs +{ + /// + /// 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..55bc891 100644 --- a/src/RabbitMQCoreClient/Models/Routes.cs +++ b/src/RabbitMQCoreClient/Models/Routes.cs @@ -1,14 +1,16 @@ -namespace RabbitMQCoreClient.Models +namespace RabbitMQCoreClient.Models; + +/// +/// Errored messages processing types. +/// +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..bb10f68 100644 --- a/src/RabbitMQCoreClient/Models/Subscription.cs +++ b/src/RabbitMQCoreClient/Models/Subscription.cs @@ -1,29 +1,34 @@ -using RabbitMQCoreClient.DependencyInjection.ConfigModels; -using System; -using System.Collections.Generic; +using RabbitMQCoreClient.Configuration; +using RabbitMQCoreClient.DependencyInjection; -namespace RabbitMQCoreClient.Configuration.DependencyInjection.Options +namespace RabbitMQCoreClient.Models; + +/// +/// 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. + /// Create new object of . /// - public sealed class Subscription : QueueBase - { - public Subscription(bool useQuorum = false) - : base($"sub_{Guid.NewGuid().ToString()}", false, true, true, useQuorum) - { } + /// 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) + { } - public static Subscription Create(SubscriptionConfig queueConfig) - { - return new Subscription - { - Arguments = queueConfig.Arguments ?? new Dictionary(), - DeadLetterExchange = queueConfig.DeadLetterExchange, - UseQuorum = queueConfig.UseQuorum, - Exchanges = queueConfig.Exchanges ?? new HashSet(), - RoutingKeys = queueConfig.RoutingKeys ?? new HashSet() - }; - } - } + /// + /// Create new subscription from configuration. + /// + /// + /// + public static Subscription Create(SubscriptionConfig queueConfig) => new() + { + Arguments = queueConfig.Arguments ?? new Dictionary(), + DeadLetterExchange = queueConfig.DeadLetterExchange, + UseQuorum = queueConfig.UseQuorum, + Exchanges = queueConfig.Exchanges ?? [], + RoutingKeys = queueConfig.RoutingKeys ?? [] + }; } 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..407986f --- /dev/null +++ b/src/RabbitMQCoreClient/QueueService.cs @@ -0,0 +1,556 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQCoreClient.DependencyInjection; +using RabbitMQCoreClient.Events; +using RabbitMQCoreClient.Exceptions; +using RabbitMQCoreClient.Models; +using RabbitMQCoreClient.Serializers; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using static RabbitMQCoreClient.Configuration.AppConstants.RabbitMQHeaders; + +namespace RabbitMQCoreClient; + +/// +/// Implementation of the . +/// +/// +public sealed class QueueService : IQueueService +{ + readonly ILogger _log; + readonly IList _exchanges; + bool _connectionBlocked = false; + IConnection? _connection; + IChannel? _publishChannel; + + CancellationToken _serviceLifetimeToken = default; + + /// + public RabbitMQCoreClientOptions Options { get; } + + /// + public IMessageSerializer Serializer { get; } + + /// + public IConnection? Connection => _connection; + + /// + public IChannel? PublishChannel => _publishChannel; + + /// + 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 ?? new SystemTextJsonMessageSerializer(); + } + + /// + /// Connects this instance to RabbitMQ. + /// + async Task ConnectInternal() + { + // 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(_serviceLifetimeToken); + + _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(cancellationToken: _serviceLifetimeToken); + _publishChannel.CallbackExceptionAsync += Channel_CallbackException; + await _publishChannel.BasicQosAsync(0, Options.PrefetchCount, false, _serviceLifetimeToken); // Per consumer limit + + foreach (var exchange in _exchanges) + { + await exchange.StartExchangeAsync(_publishChannel, _serviceLifetimeToken); + } + _connectionBlocked = false; + + CheckSendChannelOpened(); + _log.LogInformation("Connected to RabbitMQ endpoint '{HostName}'", Options.HostName); + } + + /// + /// Close all connections and Cleans up. + /// + public async Task ShutdownAsync() + { + _log.LogInformation("Closing and cleaning up publisher 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) + } + } + + /// + 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 ShutdownAsync(); + } + + while (true) // loop until state is true, checking every Options.ReconnectionTimeout + { + cancellationToken.ThrowIfCancellationRequested(); + + 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 ConnectInternal(); + _reconnectAttemptsCount = 0; + + if (ReconnectedAsync != null) + await ReconnectedAsync.Invoke(this, new ReconnectEventArgs()); + + break; // state set to true - breaks out of loop + } + catch (Exception e) + { + _reconnectAttemptsCount++; + 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); + } + } + } + + /// + public async ValueTask AsyncDispose() => await ShutdownAsync(); + + #region Publish Single methods + + /// + public async ValueTask SendAsync( + ReadOnlyMemory obj, + BasicProperties props, + string routingKey, + string? exchange = default, + 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(); + + if (string.IsNullOrEmpty(exchange)) + throw new ArgumentException($"{nameof(exchange)} is null or empty.", nameof(exchange)); + + 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}"); + } + + CheckSendChannelOpened(); + + AddTtl(props, decreaseTtl); + + await _publishChannel.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); + } + + #endregion + + #region Publish Batch methods + + /// + public async ValueTask SendBatchAsync( + IEnumerable<(ReadOnlyMemory Body, BasicProperties Props)> objs, + string routingKey, + string? exchange = default, + bool decreaseTtl = true, + 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(); + + foreach (var (body, props) 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; + } + + 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, + 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); + } + + // 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 (var 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 ConnectAsync(); + } + + async Task Connection_ConnectionBlocked(object? sender, ConnectionBlockedEventArgs e) + { + if (e != null) + _log.LogError("Connection blocked! Reason: {Reason}", e.Reason); + _connectionBlocked = true; + await ConnectAsync(); + } + #endregion + + /// + /// Creates a object with Persistent = true. + /// + /// + public static BasicProperties CreateDefaultProperties() + { + var properties = new BasicProperties + { + Persistent = true + }; + return properties; + } + + void AddTtl(BasicProperties props, bool 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 + { + // В случае неправильного типа – оставляем 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; + } + + 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; + } + } + + [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. + var decodedStringLength = Math.Min(obj.Length, bufferSize); + var slice = obj.Span.Slice(0, decodedStringLength); + + // Find the index of the last complete character + 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), + // 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(); + 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 + { + // 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 9d55267..65b2c2d 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj +++ b/src/RabbitMQCoreClient/RabbitMQCoreClient.csproj @@ -1,11 +1,12 @@ - + - 6.1.1 + 7.0.0 $(VersionSuffix) $(Version)-$(VersionSuffix) true - net6.0;net7.0;net8.0 + true + net8.0;net9.0;net10.0 Sergey Pismennyi MONQ Digital lab RabbitMQCoreClient @@ -15,11 +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 @@ -27,9 +30,8 @@ - - - + + diff --git a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs index 23d7e48..2bb52c8 100644 --- a/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs +++ b/src/RabbitMQCoreClient/RabbitMQCoreClientConsumer.cs @@ -1,293 +1,314 @@ -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; + +/// +/// Default RabbitMQ consumer. +/// +public sealed class RabbitMQCoreClientConsumer : IQueueConsumer { - 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. + /// + 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."); - } + _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; + /// inheritdoc /> + public async Task StartAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken != default) + _serviceLifetimeToken = cancellationToken; - if (_queueService.Connection is null || !_queueService.Connection.IsOpen) - throw new NotConnectedException("Connection is not opened."); + if (_consumer != null && _consumer.IsRunning) + return; - if (_queueService.SendChannel is null || _queueService.SendChannel.IsClosed) - throw new NotConnectedException("Send channel is not opened."); + if (_queueService.Connection is null || !_queueService.Connection.IsOpen) + await _queueService.ConnectAsync(cancellationToken); - if (!_wasSubscribed) - { - _queueService.OnReconnected += QueueService_OnReconnected; - _queueService.OnConnectionShutdown += QueueService_OnConnectionShutdown; - _wasSubscribed = true; - } + if (_queueService.Connection is null || !_queueService.Connection.IsOpen) + throw new NotConnectedException("Connection is not opened."); - ConsumeChannel = _queueService.Connection.CreateModel(); - ConsumeChannel.CallbackException += Channel_CallbackException; - ConsumeChannel.BasicQos(0, _queueService.Options.PrefetchCount, false); // Per consumer limit + if (_queueService.PublishChannel is null || _queueService.PublishChannel.IsClosed) + throw new NotConnectedException("Send channel is not opened."); - ConnectToAllQueues(); - } - - /// inheritdoc /> - public void Shutdown() => StopAndClearConsumer(); - - 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(cancellationToken: cancellationToken); + ConsumeChannel.CallbackExceptionAsync += Channel_CallbackException; + await ConsumeChannel.BasicQosAsync(0, _queueService.Options.PrefetchCount, false, cancellationToken); // 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 ShutdownAsync() => 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.StartQueueAsync(ConsumeChannel, _consumer, _serviceLifetimeToken); + } + _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, _serviceLifetimeToken); + _log.LogDebug("Message was rejected due to low ttl."); + return; } - void QueueService_OnReconnected() + if (!_builder.RoutingHandlerTypes.TryGetValue(@event.RoutingKey, out var result)) { - StopAndClearConsumer(); - Start(); + await RejectDueToNoHandler(@event); + return; } + var handlerType = result.Type; + var handlerOptions = result.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() + 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, 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); + } + 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 (handlerContext.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, _serviceLifetimeToken); + _log.LogError(e, "Error message with deliveryTag='{DeliveryTag}'. " + + "Sent to dead letter exchange.", @event.DeliveryTag); + break; + case Routes.SourceQueue: + 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, + // 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; } } + } + + async Task QueueService_OnReconnected(object? sender, ReconnectEventArgs args) + { + await StopAndClearConsumer(); + await StartAsync(); + } - void RejectDueToNoHandler(BasicDeliverEventArgs ea) + Task QueueService_OnConnectionShutdown(object? sender, ShutdownEventArgs args) => + 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: 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. + await ConsumeChannel.QueueDeclareAsync(queue: deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: _serviceLifetimeToken); + 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, + cancellationToken: _serviceLifetimeToken + ); + + foreach (var route in allRoutingKeys) + await ConsumeChannel.QueueBindAsync( + queue: deadLetterQueueName, + exchange: deadLetterEx, + routingKey: route, + arguments: null, + cancellationToken: _serviceLifetimeToken + ); } + } + + 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, _serviceLifetimeToken); + } - void StopAndClearConsumer() + async Task StopAndClearConsumer() + { + _log.LogInformation("Closing and cleaning up consumer connection and channels."); + try { - try + if (_queueService != null) { - 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"); + _queueService.ReconnectedAsync -= QueueService_OnReconnected; + _queueService.ConnectionShutdownAsync -= QueueService_OnConnectionShutdown; } - 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..75926ae 100644 --- a/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/IMessageSerializer.cs @@ -1,26 +1,18 @@ -using System; +using System.Diagnostics.CodeAnalysis; -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); - - /// - /// 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); - } + /// The value type. + /// The object to serialize. + /// Serialized string. + [RequiresUnreferencedCode("Serialization might require types that cannot be statically analyzed.")] + ReadOnlyMemory Serialize(TValue 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..9c4357c 100644 --- a/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs +++ b/src/RabbitMQCoreClient/Serializers/SystemTextJsonMessageSerializer.cs @@ -1,48 +1,47 @@ -using RabbitMQCoreClient.Serializers.JsonConverters; -using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace RabbitMQCoreClient.Serializers +namespace RabbitMQCoreClient.Serializers; + +/// +/// System.Text.Json message serializer. +/// +public class SystemTextJsonMessageSerializer : IMessageSerializer { - public class SystemTextJsonMessageSerializer : IMessageSerializer - { - static readonly System.Text.Json.JsonSerializerOptions _defaultOptions = new System.Text.Json.JsonSerializerOptions + /// + /// Default serialization options. + /// + public static System.Text.Json.JsonSerializerOptions DefaultOptions { get; } = + new System.Text.Json.JsonSerializerOptions { - PropertyNameCaseInsensitive = true, - DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } }; - public static System.Text.Json.JsonSerializerOptions DefaultOptions => _defaultOptions; + /// + /// Current serialization options. + /// + public System.Text.Json.JsonSerializerOptions Options { get; } - static SystemTextJsonMessageSerializer() + /// + /// Creates new object of . + /// + /// Setup parameters. + public SystemTextJsonMessageSerializer(Action? setupAction = null) + { + if (setupAction is null) { - _defaultOptions.Converters.Add(new JsonStringEnumConverter()); - _defaultOptions.Converters.Add(new NewtonsoftJObjectConverter()); - _defaultOptions.Converters.Add(new NewtonsoftJArrayConverter()); - _defaultOptions.Converters.Add(new NewtonsoftJTokenConverter()); + Options = DefaultOptions; } - - public System.Text.Json.JsonSerializerOptions Options { get; } - - public SystemTextJsonMessageSerializer(Action? setupAction = null) + 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 TResult? Deserialize(ReadOnlyMemory value) => - System.Text.Json.JsonSerializer.Deserialize(value.Span, Options); } + + /// + [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); } diff --git a/v7-MIGRATION.md b/v7-MIGRATION.md new file mode 100644 index 0000000..30f7178 --- /dev/null +++ b/v7-MIGRATION.md @@ -0,0 +1,100 @@ +# Migrating to RabbitMQCoreClient 7.x + +This document outlines the major API changes in version 7 of this library. + +## Namespaces + +The namespace structure has changed. All service registration logic has been moved to `using RabbitMQCoreClient.DependencyInjection;`. + +## Start Methods + +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` + +Methods that perform queue, exchange configuration, and server connection have been renamed to follow the Async naming pattern. + +## `Send` Methods + +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. + +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` + +The method for setting up `BatchQueueSender` has changed. It is now configured via the RabbitMQCoreClient builder like this: + +```csharp +services + .AddRabbitMQCoreClient(config.GetSection("RabbitMQ")) + .AddBatchQueueSender(); +``` + +Previously, `AddBatchQueueSender` could be called directly on `IServiceCollection`, potentially forgetting to register `AddRabbitMQCoreClient`. + +`IQueueEventsBufferEngine` has been renamed to `IQueueBufferService`. + +## IMessageSerializer + +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` has changed. It now works with JSON serialization context and requires a context to be provided. Example: + +```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