From e20f4968761c336907854a4982c3bcc794d44645 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sun, 12 May 2024 17:27:01 +0200 Subject: [PATCH 01/21] [Core] Incorporate CancellationToken and IConsumerContext to the Consumer interfaces #246 Signed-off-by: Tomasz Maruszak --- README.md | 6 +- docs/intro.md | 78 +++-- docs/intro.t.md | 78 +++-- docs/plugin_asyncapi.md | 2 +- docs/plugin_asyncapi.t.md | 2 +- docs/plugin_fluent_validation.md | 18 +- docs/plugin_outbox.md | 2 +- docs/provider_azure_eventhubs.md | 16 +- docs/provider_azure_servicebus.md | 5 +- docs/provider_kafka.md | 2 +- docs/provider_memory.md | 2 +- docs/provider_memory.t.md | 2 +- docs/provider_rabbitmq.md | 2 +- docs/provider_redis.md | 12 +- docs/provider_sql.md | 14 +- src/Host.Plugin.Properties.xml | 2 +- .../Messages/CustomerCreatedEventConsumer.cs | 3 +- .../Messages/CustomerEventConsumer.cs | 3 +- .../DomainEventHandlers/AuditingHandler.cs | 2 +- .../OrderSubmittedHandler.cs | 4 +- .../CustomerChangedEventHandler.cs | 3 +- .../EmailService/SmtpEmailService.cs | 3 +- .../GenerateThumbnailRequestHandler.cs | 4 +- .../Sample.Nats.WebApi/PingConsumer.cs | 8 +- .../CreateCustomerCommandHandler.cs | 2 +- .../Program.cs | 9 +- .../Sample.Simple.ConsoleApp/Program.cs | 20 +- .../CreateCustomerCommandHandler.cs | 3 +- .../SearchCustomerQueryHandler.cs | 9 +- .../Builders/AbstractConsumerBuilder.cs | 19 +- .../Builders/ConsumerBuilder.cs | 101 ++++--- .../Builders/HandlerBuilder.cs | 120 +++++--- .../Builders/MessageConsumerContext.cs | 26 ++ .../Settings/ConsumerSettings.cs | 2 +- .../Settings/Delegates.cs | 4 +- .../IMessageTypeConsumerInvokerSettings.cs | 2 +- .../MessageTypeConsumerInvokerSettings.cs | 2 +- .../SlimMessageBus.Host.Configuration.csproj | 8 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- .../Collections/GenericTypeCache.cs | 10 +- .../Collections/GenericTypeCache2.cs | 11 +- .../Collections/IRuntimeTypeCache.cs | 2 +- .../Collections/RuntimeTypeCache.cs | 33 +-- .../MessageProcessors/MessageHandler.cs | 2 +- .../ConsumerMethodPostProcessor.cs | 6 +- .../Helpers/ReflectionUtils.cs | 57 +++- src/SlimMessageBus/IConsumer.cs | 5 +- src/SlimMessageBus/IConsumerContext.cs | 5 + src/SlimMessageBus/IConsumerWithContext.cs | 10 +- src/SlimMessageBus/IProducerContext.cs | 2 +- .../RequestResponse/IRequestHandler.cs | 10 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- .../EventHubMessageBusIt.cs | 21 +- .../ServiceBusMessageBusIt.cs | 6 +- .../ServiceBusTopologyServiceTests.cs | 2 +- .../ConsumerCallBenchmark.cs | 20 +- .../AbstractConsumerBuilderTest.cs | 39 +++ .../ConsumerBuilderTest.cs | 163 ++++++++--- .../HandlerBuilderTest.cs | 140 ++++++--- .../MessageConsumerContextTest.cs | 39 +++ .../SampleMessages.cs | 50 +++- .../Usings.cs | 2 + .../HybridTests.cs | 6 +- .../MessageBusCurrentTests.cs | 6 +- .../MessageScopeAccessorTests.cs | 2 +- .../KafkaPartitionConsumerForConsumersTest.cs | 2 +- .../KafkaMessageBusIt.cs | 272 +++++++++--------- .../PubSubBenchmark.cs | 4 +- .../ReqRespBenchmark.cs | 4 +- .../MemoryMessageBusBuilderTests.cs | 12 +- .../MemoryMessageBusIt.cs | 4 +- .../MemoryMessageBusTests.cs | 46 +-- .../MqttMessageBusIt.cs | 4 +- .../NatsMessageBusIt.cs | 10 +- .../OutboxTests.cs | 16 +- .../IntegrationTests/RabbitMqMessageBusIt.cs | 6 +- .../RedisMessageBusIt.cs | 4 +- .../RedisMessageBusTest.cs | 6 +- .../Collections/GenericTypeCacheTests.cs | 2 +- .../ConsumerInstanceMessageProcessorTest.cs | 72 ++--- .../Consumer/MessageHandlerTest.cs | 4 +- .../ConsumerMethodPostProcessorTest.cs | 53 ++++ .../Helpers/ReflectionUtilsTests.cs | 79 ++++- .../SampleMessages.cs | 6 +- 85 files changed, 1223 insertions(+), 638 deletions(-) create mode 100644 src/SlimMessageBus.Host.Configuration/Builders/MessageConsumerContext.cs create mode 100644 src/Tests/SlimMessageBus.Host.Configuration.Test/AbstractConsumerBuilderTest.cs create mode 100644 src/Tests/SlimMessageBus.Host.Configuration.Test/MessageConsumerContextTest.cs create mode 100644 src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ConsumerMethodPostProcessorTest.cs diff --git a/README.md b/README.md index f99e61d1..b316062e 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Another service (or application layer) handles the message: ```cs public class SomeMessageConsumer : IConsumer { - public async Task OnHandle(SomeMessage message) + public async Task OnHandle(SomeMessage message, CancellationToken cancellationToken) { // handle the message } @@ -134,7 +134,7 @@ The receiving side handles the request and replies: ```cs public class SomeRequestHandler : IRequestHandler { - public async Task OnHandle(SomeRequest request) + public async Task OnHandle(SomeRequest request, CancellationToken cancellationToken) { // handle the request message and return a response return new SomeResponse { /* ... */ }; @@ -213,7 +213,7 @@ The domain event handler implements the `IConsumer` interface: // domain event handler public class OrderSubmittedHandler : IConsumer { - public Task OnHandle(OrderSubmittedEvent e) + public Task OnHandle(OrderSubmittedEvent e, CancellationToken cancellationToken) { // ... } diff --git a/docs/intro.md b/docs/intro.md index 6744559e..36e6f675 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -18,7 +18,7 @@ - [Consume the request message (the request handler)](#consume-the-request-message-the-request-handler) - [Request without response](#request-without-response) - [Static accessor](#static-accessor) -- [Dependency resolver](#dependency-resolver) +- [Dependency Resolver](#dependency-resolver) - [Dependency auto-registration](#dependency-auto-registration) - [ASP.Net Core](#aspnet-core) - [Modularization of configuration](#modularization-of-configuration) @@ -152,6 +152,9 @@ await bus.Publish(msg); // OR delivered to the specified topic (or queue) await bus.Publish(msg, "other-topic"); + +// pass cancellation token +await bus.Publish(msg, cancellationToken: ct); ``` > The transport plugins might introduce additional configuration options. Please check the relevant provider docs. For example, Azure Service Bus, Azure Event Hub and Kafka allow setting the partitioning key for a given message type. @@ -180,7 +183,7 @@ mbb }) ``` -Finally, it is possible to specify a headers modifier for the entire bus: +Finally, it is possible to specify a headers modifier for the entire bus (it will apply to all outgoing messages): ```cs mbb @@ -200,7 +203,7 @@ mbb.Consume(x => x .WithConsumer() // (1) // if you do not want to implement the IConsumer interface // .WithConsumer(nameof(AddCommandConsumer.MyHandleMethod)) // (2) uses reflection - // .WithConsumer((consumer, message) => consumer.MyHandleMethod(message)) // (3) uses a delegate + // .WithConsumer((consumer, message, consumerContext, cancellationToken) => consumer.MyHandleMethod(message)) // (3) uses a delegate .Instances(1) //.KafkaGroup("some-consumer-group")) // Kafka provider specific extensions ``` @@ -210,7 +213,7 @@ When the consumer implements the `IConsumer` interface: ```cs public class SomeConsumer : IConsumer { - public async Task OnHandle(SomeMessage msg) + public async Task OnHandle(SomeMessage msg, CancellationToken cancellationToken) { // handle the msg } @@ -221,7 +224,7 @@ The `SomeConsumer` needs to be registered in the DI container. The SMB runtime w > When `.WithConsumer()` is not declared, then a default consumer of type `IConsumer` will be assumed (since v2.0.0). -Alternatively, if you do not want to implement the `IConsumer`, then you can provide the method name (2) or a delegate that calls the consumer method (3). +Alternatively, if you do not want to implement the `IConsumer`, then you can provide the method name _(2)_ or a delegate that calls the consumer method _(3)_. `IConsumerContext` and/or `CancellationToken` can optionally be included as parameters to be populated on invocation when taking this approach: ```cs @@ -261,8 +264,6 @@ await consumerControl.Stop(); #### Consumer context (additional message information) -> Changed in version 1.15.0 - The consumer can access the [`IConsumerContext`](../src/SlimMessageBus/IConsumerContext.cs) object which: - allows to access additional message information - topic (or queue) name the message arrived on, headers, cancellation token, @@ -270,18 +271,42 @@ The consumer can access the [`IConsumerContext`](../src/SlimMessageBus/IConsumer Examples of such transport specific information are the Azure Service Bus UserProperties, or Kafka Topic-Partition offset. -To use it the consumer has to implement the [`IConsumerWithContext`](../src/SlimMessageBus/IConsumerWithContext.cs) interface: +The recommended (and newer) approach is to define a consumer type that implements `IConsumer>`. +For example: + +```cs +// The consumer wraps the message type in IConsumerContext +public class PingConsumer : IConsumer> +{ + public Task OnHandle(IConsumerContext context, CancellationToken cancellationToken) + { + var message = context.Message; // the message (here PingMessage) + var topic = context.Path; // the topic or queue name + var headers = context.Headers; // message headers + // Kafka transport specific extension (requires SlimMessageBus.Host.Kafka package): + var transportMessage = context.GetTransportMessage(); + var partition = transportMessage.TopicPartition.Partition; + } +} + +// To declare the consumer type use the .WithConsumerOfContext() method +mbb.Consume(x => x + .Topic("some-topic") + .WithConsumerOfContext() + ); +``` + +The other approach is for the consumer to implement the [`IConsumerWithContext`](../src/SlimMessageBus/IConsumerWithContext.cs) interface: ```cs public class PingConsumer : IConsumer, IConsumerWithContext { public IConsumerContext Context { get; set; } - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { var topic = Context.Path; // the topic or queue name var headers = Context.Headers; // message headers - var cancellationToken = Context.CancellationToken; // Kafka transport specific extension (requires SlimMessageBus.Host.Kafka package): var transportMessage = Context.GetTransportMessage(); var partition = transportMessage.TopicPartition.Partition; @@ -457,7 +482,7 @@ The request handling micro-service needs to have a handler that implements `IReq ```cs public class SomeRequestHandler : IRequestHandler { - public async Task OnHandle(SomeRequest request) + public async Task OnHandle(SomeRequest request, CancellationToken cancellationToken) { // handle the request return new SomeResponse(); @@ -497,7 +522,7 @@ public class SomeRequest : IRequest // The handler has to use IRequestHandler interface public class SomeRequestHandler : IRequestHandler { - public async Task OnHandle(SomeRequest request) + public async Task OnHandle(SomeRequest request, CancellationToken cancellationToken) { // no response returned } @@ -530,7 +555,7 @@ This allows to easily look up the `IMessageBus` instance in the domain model lay See [`DomainEvents`](../src/Samples/Sample.DomainEvents.WebApi/Startup.cs#L79) sample it works per-request scope and how to use it for domain events. -## Dependency resolver +## Dependency Resolver SMB uses the [`Microsoft.Extensions.DependencyInjection`](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) container to obtain and manage instances of the declared consumers (class instances that implement `IConsumer<>` or `IRequestHandler<>`) or interceptors. @@ -623,9 +648,12 @@ services.AddSlimMessageBus(mbb => > Since version 2.0.0 -The SMB bus configuration can be split into modules. This allows to keep the bus configuration alongside the relevant application module (or layer). +The SMB bus configuration can be split into modules. This allows to keep the bus configuration alongside the relevant application module (or layer): -The `services.AddSlimMessageBus(mbb => { })` can be called multiple times. The end result will be a sum of the configurations (the supplied `MessageBusBuilder` instance will be the same). Consider the example: +- The `services.AddSlimMessageBus(mbb => { })` can be called multiple times. +- The end result will be a sum of the configurations (the supplied `MessageBusBuilder` instance will be the same). + +Consider the example: ```cs // Module 1 @@ -653,7 +681,8 @@ services.AddSlimMessageBus(mbb => }); ``` -Before version 2.0.0 there was support for modularity using `IMessageBusConfigurator` implementation. However, the interface was deprecated in favor of the `AddSlimMessageBus()` extension method that was made additive. +Before version 2.0.0 there was support for modularity using `IMessageBusConfigurator` implementation. +However, the interface was deprecated in favor of the `AddSlimMessageBus()` extension method that was made additive. ### Auto registration of consumers and interceptors @@ -716,12 +745,12 @@ mbb.Produce(x => x.DefaultTopic("events")); public class CustomerEventConsumer : IConsumer { - public Task OnHandle(CustomerEvent e) { } + public Task OnHandle(CustomerEvent e, CancellationToken cancellationToken) { } } public class OrderEventConsumer : IConsumer { - public Task OnHandle(OrderEvent e) { } + public Task OnHandle(OrderEvent e, CancellationToken cancellationToken) { } } // which consume from the same topic @@ -796,12 +825,12 @@ Given the following consumers: ```cs public class CustomerEventConsumer : IConsumer { - public Task OnHandle(CustomerEvent e) { } + public Task OnHandle(CustomerEvent e, CancellationToken cancellationToken) { } } public class CustomerCreatedEventConsumer : IConsumer { - public Task OnHandle(CustomerCreatedEvent e) { } + public Task OnHandle(CustomerCreatedEvent e, CancellationToken cancellationToken) { } } ``` @@ -1081,8 +1110,11 @@ For example, Apache Kafka requires `mbb.KafkaGroup(string)` for consumers to dec Providers: - [Apache Kafka](provider_kafka.md) -- [Azure Service Bus](provider_azure_servicebus.md) - [Azure Event Hubs](provider_azure_eventhubs.md) -- [Redis](provider_redis.md) +- [Azure Service Bus](provider_azure_servicebus.md) +- [Hybrid](provider_hybrid.md) +- [MQTT](provider_mqtt.md) - [Memory](provider_memory.md) -- [Hybrid](provider_hybrid.md) \ No newline at end of file +- [RabbitMQ](provider_rabbitmq.md) +- [Redis](provider_redis.md) +- [SQL](provider_sql.md) \ No newline at end of file diff --git a/docs/intro.t.md b/docs/intro.t.md index a948e273..c0cbfb00 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -18,7 +18,7 @@ - [Consume the request message (the request handler)](#consume-the-request-message-the-request-handler) - [Request without response](#request-without-response) - [Static accessor](#static-accessor) -- [Dependency resolver](#dependency-resolver) +- [Dependency Resolver](#dependency-resolver) - [Dependency auto-registration](#dependency-auto-registration) - [ASP.Net Core](#aspnet-core) - [Modularization of configuration](#modularization-of-configuration) @@ -152,6 +152,9 @@ await bus.Publish(msg); // OR delivered to the specified topic (or queue) await bus.Publish(msg, "other-topic"); + +// pass cancellation token +await bus.Publish(msg, cancellationToken: ct); ``` > The transport plugins might introduce additional configuration options. Please check the relevant provider docs. For example, Azure Service Bus, Azure Event Hub and Kafka allow setting the partitioning key for a given message type. @@ -180,7 +183,7 @@ mbb }) ``` -Finally, it is possible to specify a headers modifier for the entire bus: +Finally, it is possible to specify a headers modifier for the entire bus (it will apply to all outgoing messages): ```cs mbb @@ -200,7 +203,7 @@ mbb.Consume(x => x .WithConsumer() // (1) // if you do not want to implement the IConsumer interface // .WithConsumer(nameof(AddCommandConsumer.MyHandleMethod)) // (2) uses reflection - // .WithConsumer((consumer, message) => consumer.MyHandleMethod(message)) // (3) uses a delegate + // .WithConsumer((consumer, message, consumerContext, cancellationToken) => consumer.MyHandleMethod(message)) // (3) uses a delegate .Instances(1) //.KafkaGroup("some-consumer-group")) // Kafka provider specific extensions ``` @@ -210,7 +213,7 @@ When the consumer implements the `IConsumer` interface: ```cs public class SomeConsumer : IConsumer { - public async Task OnHandle(SomeMessage msg) + public async Task OnHandle(SomeMessage msg, CancellationToken cancellationToken) { // handle the msg } @@ -221,7 +224,7 @@ The `SomeConsumer` needs to be registered in the DI container. The SMB runtime w > When `.WithConsumer()` is not declared, then a default consumer of type `IConsumer` will be assumed (since v2.0.0). -Alternatively, if you do not want to implement the `IConsumer`, then you can provide the method name (2) or a delegate that calls the consumer method (3). +Alternatively, if you do not want to implement the `IConsumer`, then you can provide the method name _(2)_ or a delegate that calls the consumer method _(3)_. `IConsumerContext` and/or `CancellationToken` can optionally be included as parameters to be populated on invocation when taking this approach: ```cs @@ -261,8 +264,6 @@ await consumerControl.Stop(); #### Consumer context (additional message information) -> Changed in version 1.15.0 - The consumer can access the [`IConsumerContext`](../src/SlimMessageBus/IConsumerContext.cs) object which: - allows to access additional message information - topic (or queue) name the message arrived on, headers, cancellation token, @@ -270,18 +271,42 @@ The consumer can access the [`IConsumerContext`](../src/SlimMessageBus/IConsumer Examples of such transport specific information are the Azure Service Bus UserProperties, or Kafka Topic-Partition offset. -To use it the consumer has to implement the [`IConsumerWithContext`](../src/SlimMessageBus/IConsumerWithContext.cs) interface: +The recommended (and newer) approach is to define a consumer type that implements `IConsumer>`. +For example: + +```cs +// The consumer wraps the message type in IConsumerContext +public class PingConsumer : IConsumer> +{ + public Task OnHandle(IConsumerContext context, CancellationToken cancellationToken) + { + var message = context.Message; // the message (here PingMessage) + var topic = context.Path; // the topic or queue name + var headers = context.Headers; // message headers + // Kafka transport specific extension (requires SlimMessageBus.Host.Kafka package): + var transportMessage = context.GetTransportMessage(); + var partition = transportMessage.TopicPartition.Partition; + } +} + +// To declare the consumer type use the .WithConsumerOfContext() method +mbb.Consume(x => x + .Topic("some-topic") + .WithConsumerOfContext() + ); +``` + +The other approach is for the consumer to implement the [`IConsumerWithContext`](../src/SlimMessageBus/IConsumerWithContext.cs) interface: ```cs public class PingConsumer : IConsumer, IConsumerWithContext { public IConsumerContext Context { get; set; } - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { var topic = Context.Path; // the topic or queue name var headers = Context.Headers; // message headers - var cancellationToken = Context.CancellationToken; // Kafka transport specific extension (requires SlimMessageBus.Host.Kafka package): var transportMessage = Context.GetTransportMessage(); var partition = transportMessage.TopicPartition.Partition; @@ -457,7 +482,7 @@ The request handling micro-service needs to have a handler that implements `IReq ```cs public class SomeRequestHandler : IRequestHandler { - public async Task OnHandle(SomeRequest request) + public async Task OnHandle(SomeRequest request, CancellationToken cancellationToken) { // handle the request return new SomeResponse(); @@ -497,7 +522,7 @@ public class SomeRequest : IRequest // The handler has to use IRequestHandler interface public class SomeRequestHandler : IRequestHandler { - public async Task OnHandle(SomeRequest request) + public async Task OnHandle(SomeRequest request, CancellationToken cancellationToken) { // no response returned } @@ -530,7 +555,7 @@ This allows to easily look up the `IMessageBus` instance in the domain model lay See [`DomainEvents`](../src/Samples/Sample.DomainEvents.WebApi/Startup.cs#L79) sample it works per-request scope and how to use it for domain events. -## Dependency resolver +## Dependency Resolver SMB uses the [`Microsoft.Extensions.DependencyInjection`](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) container to obtain and manage instances of the declared consumers (class instances that implement `IConsumer<>` or `IRequestHandler<>`) or interceptors. @@ -623,9 +648,12 @@ services.AddSlimMessageBus(mbb => > Since version 2.0.0 -The SMB bus configuration can be split into modules. This allows to keep the bus configuration alongside the relevant application module (or layer). +The SMB bus configuration can be split into modules. This allows to keep the bus configuration alongside the relevant application module (or layer): + +- The `services.AddSlimMessageBus(mbb => { })` can be called multiple times. +- The end result will be a sum of the configurations (the supplied `MessageBusBuilder` instance will be the same). -The `services.AddSlimMessageBus(mbb => { })` can be called multiple times. The end result will be a sum of the configurations (the supplied `MessageBusBuilder` instance will be the same). Consider the example: +Consider the example: ```cs // Module 1 @@ -653,7 +681,8 @@ services.AddSlimMessageBus(mbb => }); ``` -Before version 2.0.0 there was support for modularity using `IMessageBusConfigurator` implementation. However, the interface was deprecated in favor of the `AddSlimMessageBus()` extension method that was made additive. +Before version 2.0.0 there was support for modularity using `IMessageBusConfigurator` implementation. +However, the interface was deprecated in favor of the `AddSlimMessageBus()` extension method that was made additive. ### Auto registration of consumers and interceptors @@ -716,12 +745,12 @@ mbb.Produce(x => x.DefaultTopic("events")); public class CustomerEventConsumer : IConsumer { - public Task OnHandle(CustomerEvent e) { } + public Task OnHandle(CustomerEvent e, CancellationToken cancellationToken) { } } public class OrderEventConsumer : IConsumer { - public Task OnHandle(OrderEvent e) { } + public Task OnHandle(OrderEvent e, CancellationToken cancellationToken) { } } // which consume from the same topic @@ -796,12 +825,12 @@ Given the following consumers: ```cs public class CustomerEventConsumer : IConsumer { - public Task OnHandle(CustomerEvent e) { } + public Task OnHandle(CustomerEvent e, CancellationToken cancellationToken) { } } public class CustomerCreatedEventConsumer : IConsumer { - public Task OnHandle(CustomerCreatedEvent e) { } + public Task OnHandle(CustomerCreatedEvent e, CancellationToken cancellationToken) { } } ``` @@ -1067,8 +1096,11 @@ For example, Apache Kafka requires `mbb.KafkaGroup(string)` for consumers to dec Providers: - [Apache Kafka](provider_kafka.md) -- [Azure Service Bus](provider_azure_servicebus.md) - [Azure Event Hubs](provider_azure_eventhubs.md) -- [Redis](provider_redis.md) -- [Memory](provider_memory.md) +- [Azure Service Bus](provider_azure_servicebus.md) - [Hybrid](provider_hybrid.md) +- [MQTT](provider_mqtt.md) +- [Memory](provider_memory.md) +- [RabbitMQ](provider_rabbitmq.md) +- [Redis](provider_redis.md) +- [SQL](provider_sql.md) diff --git a/docs/plugin_asyncapi.md b/docs/plugin_asyncapi.md index 637141ca..74235fcf 100644 --- a/docs/plugin_asyncapi.md +++ b/docs/plugin_asyncapi.md @@ -80,7 +80,7 @@ public class CustomerCreatedEventConsumer : IConsumer /// /// /// - public Task OnHandle(CustomerCreatedEvent message) { } + public Task OnHandle(CustomerCreatedEvent message, CancellationToken cancellationToken) { } } ``` diff --git a/docs/plugin_asyncapi.t.md b/docs/plugin_asyncapi.t.md index 83a155b4..56bb5f7a 100644 --- a/docs/plugin_asyncapi.t.md +++ b/docs/plugin_asyncapi.t.md @@ -64,7 +64,7 @@ public class CustomerCreatedEventConsumer : IConsumer /// /// /// - public Task OnHandle(CustomerCreatedEvent message) { } + public Task OnHandle(CustomerCreatedEvent message, CancellationToken cancellationToken) { } } ``` diff --git a/docs/plugin_fluent_validation.md b/docs/plugin_fluent_validation.md index 216f0ef3..b36686c8 100644 --- a/docs/plugin_fluent_validation.md +++ b/docs/plugin_fluent_validation.md @@ -9,7 +9,7 @@ Please read the [Introduction](intro.md) before reading this provider documentat - [Producer side validation](#producer-side-validation) - [Consumer side validation](#consumer-side-validation) - [Configuring without MSDI](#configuring-without-msdi) - + ## Introduction The [`SlimMessageBus.Host.FluentValidation`](https://www.nuget.org/packages/SlimMessageBus.Host.FluentValidation) introduces validation on the producer or consumer side by leveraging the [FluentValidation](https://www.nuget.org/packages/FluentValidation) library. @@ -66,7 +66,7 @@ builder.Services.AddSlimMessageBus(mbb => .WithProviderMemory() .AutoDeclareFrom(Assembly.GetExecutingAssembly()) .AddServicesFromAssembly(Assembly.GetExecutingAssembly()) - .AddFluentValidation(opts => + .AddFluentValidation(opts => { // SMB FluentValidation setup goes here }); @@ -84,7 +84,7 @@ It is possible to configure custom exception (or perhaps to supress the validati ```cs builder.Services.AddSlimMessageBus(mbb => { - mbb.AddFluentValidation(opts => + mbb.AddFluentValidation(opts => { // SMB FluentValidation setup goes here opts.AddValidationErrorsHandler(errors => new ApplicationException("Custom exception")); @@ -99,7 +99,7 @@ The `.AddProducerValidatorsFromAssemblyContaining()` will register an SMB interc ```cs builder.Services.AddSlimMessageBus(mbb => { - mbb.AddFluentValidation(opts => + mbb.AddFluentValidation(opts => { // Register validation interceptors for message (here command) producers inside message bus // Required Package: SlimMessageBus.Host.FluentValidation @@ -108,18 +108,18 @@ builder.Services.AddSlimMessageBus(mbb => }); ``` -For example given an ASP.NET Mimimal WebApi, the request can be delegated to SlimMessageBus in memory transport: +For example given an ASP.NET Minimal WebApi, the request can be delegated to SlimMessageBus in memory transport: ```cs // Using minimal APIs var app = builder.Build(); -app.MapPost("/customer", (CreateCustomerCommand command, IMessageBus bus) => bus.Send(command)); +app.MapPost("/customer", (CreateCustomerCommand command, IMessageBus bus) => bus.Send(command)); await app.RunAsync(); ``` -In the situation that the incomming HTTP request where to deliver an invalid command, the request will fail with `FluentValidation.ValidationException: Validation failed` exception. +In the situation that the incoming HTTP request where to deliver an invalid command, the request will fail with `FluentValidation.ValidationException: Validation failed` exception. For full example, please see the [Sample.ValidatingWebApi](../src/Samples/Sample.ValidatingWebApi/) sample. @@ -131,7 +131,7 @@ Such validation would be needed in scenarios when an external system delivers me ```cs builder.Services.AddSlimMessageBus(mbb => { - mbb.AddFluentValidation(opts => + mbb.AddFluentValidation(opts => { // Register validation interceptors for message (here command) consumers inside message bus // Required Package: SlimMessageBus.Host.FluentValidation @@ -150,5 +150,3 @@ If you are using another DI container than Microsoft.Extensions.DependencyInject - register the respective `ProducerValidationInterceptor` as `IProducerInterceptor` for each of the message type `T` that needs to be validated on producer side, - register the respective `ConsumerValidationInterceptor` as `IConsumerInterceptor` for each of the message type `T` that needs to be validated on consumer side, - the scope of can be anything that you need (scoped, transient, singleton) - -> Packages for other DI containers (Autofac/Unity) will likely also be created in the future. PRs are also welcome. diff --git a/docs/plugin_outbox.md b/docs/plugin_outbox.md index 97d75fef..13fcac99 100644 --- a/docs/plugin_outbox.md +++ b/docs/plugin_outbox.md @@ -101,7 +101,7 @@ Command handler: ```cs public record CreateCustomerCommandHandler(IMessageBus Bus, CustomerContext CustomerContext) : IRequestHandler { - public async Task OnHandle(CreateCustomerCommand request) + public async Task OnHandle(CreateCustomerCommand request, CancellationToken cancellationToken) { // Note: This handler will be already wrapped in a transaction: see Program.cs and .UseTransactionScope() / .UseSqlTransaction() diff --git a/docs/provider_azure_eventhubs.md b/docs/provider_azure_eventhubs.md index 04f0d840..f7e95b57 100644 --- a/docs/provider_azure_eventhubs.md +++ b/docs/provider_azure_eventhubs.md @@ -1,4 +1,4 @@ -# Azure Event Hub Provider for SlimMessageBus +# Azure Event Hub Provider for SlimMessageBus Please read the [Introduction](intro.md) before reading this provider documentation. @@ -20,13 +20,13 @@ var storageContainerName = ""; // Azure Blob Storage container name services.AddSlimMessageBus(mbb => { - // Use Azure Event Hub as provider + // Use Azure Event Hub as provider mbb.WithProviderEventHub(cfg => { cfg.ConnectionString = eventHubConnectionString; cfg.StorageConnectionString = storageConnectionString; cfg.StorageBlobContainerName = storageContainerName; - }); + }); mbb.AddJsonSerializer(); // ... @@ -72,14 +72,14 @@ services.AddSlimMessageBus(mbb => { Identifier = $"MyService_{Guid.NewGuid()}" }; - + cfg.EventHubProcessorClientOptionsFactory = (consumerParams) => new Azure.Messaging.EventHubs.EventProcessorClientOptions { // Force partition lease rebalancing to happen faster (if new consumers join they can quickly gain a partition lease) LoadBalancingUpdateInterval = TimeSpan.FromSeconds(2), PartitionOwnershipExpirationInterval = TimeSpan.FromSeconds(5), }; - }); + }); }); ``` @@ -89,7 +89,7 @@ To produce a given `TMessage` to an Azure Event Hub named `my-event-hub` use: ```cs // send TMessage to Azure SB queues -mbb.Produce(x => x.DefaultPath("my-event-hub")); +mbb.Produce(x => x.DefaultPath("my-event-hub")); ``` ### Selecting message partition @@ -102,7 +102,7 @@ Azure EventHub topics are broken into partitions: SMB Azure EventHub allows to set a provider (selector) that will assign the partition key for a given message. Here is an example: ```cs -mbb.Produce(x => +mbb.Produce(x => { x.DefaultPath("topic1"); // Message key could be set for the message @@ -135,7 +135,7 @@ mbb.Consume(x => x .Path(hubName) // hub name .Group(consumerGroupName) // consumer group name on the hub .WithConsumer() - .CheckpointAfter(TimeSpan.FromSeconds(10)) // trigger checkpoint after 10 seconds + .CheckpointAfter(TimeSpan.FromSeconds(10)) // trigger checkpoint after 10 seconds .CheckpointEvery(50)); // trigger checkpoint every 50 messages ``` diff --git a/docs/provider_azure_servicebus.md b/docs/provider_azure_servicebus.md index 8cab0796..9593bc83 100644 --- a/docs/provider_azure_servicebus.md +++ b/docs/provider_azure_servicebus.md @@ -16,6 +16,7 @@ Please read the [Introduction](intro.md) before reading this provider documentat - [Handle Request Messages](#handle-request-messages) - [ASB Sessions](#asb-sessions) - [Topology Provisioning](#topology-provisioning) + - [Validation of Topology](#validation-of-topology) - [Trigger Topology Provisioning](#trigger-topology-provisioning) ## Configuration @@ -178,7 +179,7 @@ public class PingConsumer : IConsumer, IConsumerWithContext { public IConsumerContext Context { get; set; } - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { // Azure SB transport specific extension: var transportMessage = Context.GetTransportMessage(); // Of type Azure.Messaging.ServiceBus.ServiceBusReceivedMessage @@ -458,7 +459,7 @@ mbb.WithProviderServiceBus(cfg => Enabled = true, CanConsumerCreateTopic = false, // the consumers will not be able to provision a missing topic CanConsumerCreateSubscription = true, // the consumers will not be able to add a missing subscription if needed - CanConsumerCreateSubscriptionFilter = true, // the consumers will not be able to add a missing filter on subscription + CanConsumerCreateSubscriptionFilter = true, // the consumers will not be able to add a missing filter on subscription CanConsumerValidateSubscriptionFilters = true, // any deviations from the expected will be logged }; diff --git a/docs/provider_kafka.md b/docs/provider_kafka.md index e6fc15cc..2d994b29 100644 --- a/docs/provider_kafka.md +++ b/docs/provider_kafka.md @@ -176,7 +176,7 @@ public class PingConsumer : IConsumer, IConsumerWithContext { public IConsumerContext Context { get; set; } - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { // SMB Kafka transport specific extension: var transportMessage = Context.GetTransportMessage(); diff --git a/docs/provider_memory.md b/docs/provider_memory.md index d90296cb..ac9777c8 100644 --- a/docs/provider_memory.md +++ b/docs/provider_memory.md @@ -129,7 +129,7 @@ For example, assuming this is the discovered handler type: ```cs public class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) { /* ... */ } + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { /* ... */ } } ``` diff --git a/docs/provider_memory.t.md b/docs/provider_memory.t.md index 30bb2a3c..347d678e 100644 --- a/docs/provider_memory.t.md +++ b/docs/provider_memory.t.md @@ -129,7 +129,7 @@ For example, assuming this is the discovered handler type: ```cs public class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) { /* ... */ } + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { /* ... */ } } ``` diff --git a/docs/provider_rabbitmq.md b/docs/provider_rabbitmq.md index e9ff12be..e9eab07b 100644 --- a/docs/provider_rabbitmq.md +++ b/docs/provider_rabbitmq.md @@ -350,7 +350,7 @@ services.AddSlimMessageBus((mbb) => channel.ExchangeDelete("test-ping", ifUnused: true); channel.ExchangeDelete("subscriber-dlq", ifUnused: true); - // apply default SMB infered topology + // apply default SMB inferred topology applyDefaultTopology(); }); }); diff --git a/docs/provider_redis.md b/docs/provider_redis.md index 5ad67387..58dd0438 100644 --- a/docs/provider_redis.md +++ b/docs/provider_redis.md @@ -49,7 +49,7 @@ To produce a given `TMessage` to Redis pub/sub topic (or queue implemented as a ```cs // send TMessage to Redis queues (lists) -mbb.Produce(x => x.UseQueue()); +mbb.Produce(x => x.UseQueue()); // send TMessage to Redis pub/sub topics mbb.Produce(x => x.UseTopic()); @@ -78,7 +78,7 @@ If you configure the default queue (or default topic) for a message type: ```cs mbb.Produce(x => x.DefaultTopic("some-topic")); // OR -mbb.Produce(x => x.DefaultQueue("some-queue")); +mbb.Produce(x => x.DefaultQueue("some-queue")); ``` and skip the second (name) parameter in `bus.Publish()`, then that default queue (or default topic) name is going to be used: @@ -148,17 +148,17 @@ Internally the queue is implemented in the following way: There is a chance that the consumer process dies after it performs `LPOP` and before it fully processes the message. Another implementation was also considered using [`RPOPLPUSH`](https://redis.io/commands/rpoplpush) that would allow for at-least-once guarantee. -However, that would require to manage individual per process instance local queues making the runtime and configuration not practical. +However, that would require to manage individual per process instance local queues - making that runtime and configuration not practical. ### Message Headers SMB uses headers to pass additional metadata information with the message. This includes the `MessageType` (of type `string`) or in the case of request/response messages the `RequestId` (of type `string`), `ReplyTo` (of type `string`) and `Expires` (of type `long`). Redis does not support headers natively hence SMB Redis transport emulates them. -The emulation works by using a message wrapper envelope (`MessageWithHeader`) that during serialization puts the headers first and then the actual message content after that. If you want to override that behaviour, you could provide another serializer as long as it is able to serialize the wrapper `MessageWithHeaders` type: +The emulation works by using a message wrapper envelope (`MessageWithHeader`) that during serialization puts the headers first and then the actual message content after that. If you want to override that behavior, you could provide another serializer as long as it is able to serialize the wrapper `MessageWithHeaders` type: ```cs -mbb.WithProviderRedis(cfg => -{ +mbb.WithProviderRedis(cfg => +{ cfg.EnvelopeSerializer = new MessageWithHeadersSerializer(); }); ``` diff --git a/docs/provider_sql.md b/docs/provider_sql.md index c35384df..5448e297 100644 --- a/docs/provider_sql.md +++ b/docs/provider_sql.md @@ -24,19 +24,14 @@ If you see an issue, please raise an github issue. ToDo: Finish -The configuration is arranged via the `.WithProviderMqtt(cfg => {})` method on the message bus builder. +The configuration is arranged via the `.WithProviderSql(cfg => {})` method on the message bus builder. ```cs services.AddSlimMessageBus(mbb => { - mbb.WithProviderMqtt(cfg => + mbb.WithProviderSql(cfg => { - cfg.ClientBuilder - .WithTcpServer(configuration["Mqtt:Server"], int.Parse(configuration["Mqtt:Port"])) - .WithTls() - .WithCredentials(configuration["Mqtt:Username"], configuration["Mqtt:Password"]) - // Use MQTTv5 to use message headers (if the broker supports it) - .WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V500); + // ToDo }); mbb.AddServicesFromAssemblyContaining(); @@ -44,9 +39,6 @@ services.AddSlimMessageBus(mbb => }); ``` -The `ClientBuilder` property (of type `MqttClientOptionsBuilder`) is used to configure the underlying [MQTTnet library client](https://github.com/dotnet/MQTTnet/wiki/Client). -Please consult the MQTTnet library docs for more configuration options. - ## How it works The same SQL database instance is required for all the producers and consumers to collaborate. diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index 2578f949..1228b730 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 2.5.4-rc1 + 3.0.0-rc6 \ No newline at end of file diff --git a/src/Samples/Sample.AsyncApi.Service/Messages/CustomerCreatedEventConsumer.cs b/src/Samples/Sample.AsyncApi.Service/Messages/CustomerCreatedEventConsumer.cs index 9da9b7f0..4d213bdb 100644 --- a/src/Samples/Sample.AsyncApi.Service/Messages/CustomerCreatedEventConsumer.cs +++ b/src/Samples/Sample.AsyncApi.Service/Messages/CustomerCreatedEventConsumer.cs @@ -6,8 +6,9 @@ public class CustomerCreatedEventConsumer : IConsumer /// Upon the will store it with the database. /// /// + /// /// - public Task OnHandle(CustomerCreatedEvent message) + public Task OnHandle(CustomerCreatedEvent message, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Samples/Sample.AsyncApi.Service/Messages/CustomerEventConsumer.cs b/src/Samples/Sample.AsyncApi.Service/Messages/CustomerEventConsumer.cs index a190f4f2..a9073b2f 100644 --- a/src/Samples/Sample.AsyncApi.Service/Messages/CustomerEventConsumer.cs +++ b/src/Samples/Sample.AsyncApi.Service/Messages/CustomerEventConsumer.cs @@ -9,8 +9,9 @@ public class CustomerEventConsumer : IConsumer /// This will create an customer entry in the local database for the created customer. /// /// + /// /// - public Task OnHandle(CustomerEvent message) + public Task OnHandle(CustomerEvent message, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/AuditingHandler.cs b/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/AuditingHandler.cs index 8474bb99..23c27230 100644 --- a/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/AuditingHandler.cs +++ b/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/AuditingHandler.cs @@ -10,5 +10,5 @@ /// public class AuditingHandler(IAuditService auditService) : IConsumer { - public Task OnHandle(OrderSubmittedEvent e) => auditService.Append(e.Order.Id, "The Order was submitted"); + public Task OnHandle(OrderSubmittedEvent e, CancellationToken cancellationToken) => auditService.Append(e.Order.Id, "The Order was submitted"); } diff --git a/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/OrderSubmittedHandler.cs b/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/OrderSubmittedHandler.cs index e18bd28e..d95748c6 100644 --- a/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/OrderSubmittedHandler.cs +++ b/src/Samples/Sample.DomainEvents.Application/DomainEventHandlers/OrderSubmittedHandler.cs @@ -11,7 +11,7 @@ /// public class OrderSubmittedHandler(ILogger logger) : IConsumer { - public Task OnHandle(OrderSubmittedEvent e) + public Task OnHandle(OrderSubmittedEvent e, CancellationToken cancellationToken) { logger.LogInformation("Customer {Firstname} {Lastname} just placed an order for:", e.Order.Customer.Firstname, e.Order.Customer.Lastname); foreach (var orderLine in e.Order.Lines) @@ -20,6 +20,6 @@ public Task OnHandle(OrderSubmittedEvent e) } logger.LogInformation("Generating a shipping order..."); - return Task.Delay(1000); + return Task.Delay(1000, cancellationToken); } } diff --git a/src/Samples/Sample.Hybrid.ConsoleApp/ApplicationLayer/CustomerChangedEventHandler.cs b/src/Samples/Sample.Hybrid.ConsoleApp/ApplicationLayer/CustomerChangedEventHandler.cs index 45f3a08a..f0871878 100644 --- a/src/Samples/Sample.Hybrid.ConsoleApp/ApplicationLayer/CustomerChangedEventHandler.cs +++ b/src/Samples/Sample.Hybrid.ConsoleApp/ApplicationLayer/CustomerChangedEventHandler.cs @@ -2,6 +2,7 @@ using Sample.Hybrid.ConsoleApp.Domain; using Sample.Hybrid.ConsoleApp.EmailService.Contract; + using SlimMessageBus; public class CustomerChangedEventHandler : IConsumer @@ -10,7 +11,7 @@ public class CustomerChangedEventHandler : IConsumer public CustomerChangedEventHandler(IMessageBus bus) => this.bus = bus; - public async Task OnHandle(CustomerEmailChangedEvent message) + public async Task OnHandle(CustomerEmailChangedEvent message, CancellationToken cancellationToken) { // Send confirmation email diff --git a/src/Samples/Sample.Hybrid.ConsoleApp/EmailService/SmtpEmailService.cs b/src/Samples/Sample.Hybrid.ConsoleApp/EmailService/SmtpEmailService.cs index 48be0c81..7b0d8cc4 100644 --- a/src/Samples/Sample.Hybrid.ConsoleApp/EmailService/SmtpEmailService.cs +++ b/src/Samples/Sample.Hybrid.ConsoleApp/EmailService/SmtpEmailService.cs @@ -1,11 +1,12 @@ namespace Sample.Hybrid.ConsoleApp.EmailService; using Sample.Hybrid.ConsoleApp.EmailService.Contract; + using SlimMessageBus; public class SmtpEmailService : IConsumer { - public Task OnHandle(SendEmailCommand message) + public Task OnHandle(SendEmailCommand message, CancellationToken cancellationToken) { // Sending email via SMTP... Console.WriteLine("--------------------------------------------"); diff --git a/src/Samples/Sample.Images.Worker/Handlers/GenerateThumbnailRequestHandler.cs b/src/Samples/Sample.Images.Worker/Handlers/GenerateThumbnailRequestHandler.cs index 87662a5d..779094ed 100644 --- a/src/Samples/Sample.Images.Worker/Handlers/GenerateThumbnailRequestHandler.cs +++ b/src/Samples/Sample.Images.Worker/Handlers/GenerateThumbnailRequestHandler.cs @@ -4,8 +4,10 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; + using Sample.Images.FileStore; using Sample.Images.Messages; + using SlimMessageBus; public class GenerateThumbnailRequestHandler : IRequestHandler @@ -21,7 +23,7 @@ public GenerateThumbnailRequestHandler(IFileStore fileStore, IThumbnailFileIdStr #region Implementation of IRequestHandler - public async Task OnHandle(GenerateThumbnailRequest request) + public async Task OnHandle(GenerateThumbnailRequest request, CancellationToken cancellationToken) { var image = await LoadImage(request.FileId).ConfigureAwait(false); if (image == null) diff --git a/src/Samples/Sample.Nats.WebApi/PingConsumer.cs b/src/Samples/Sample.Nats.WebApi/PingConsumer.cs index 62a7b9c6..a70766a7 100644 --- a/src/Samples/Sample.Nats.WebApi/PingConsumer.cs +++ b/src/Samples/Sample.Nats.WebApi/PingConsumer.cs @@ -4,13 +4,11 @@ namespace Sample.Nats.WebApi; public class PingConsumer(ILogger logger) : IConsumer, IConsumerWithContext { - private readonly ILogger _logger = logger; + public IConsumerContext? Context { get; set; } - public IConsumerContext Context { get; set; } = default!; - - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { - _logger.LogInformation("Got message {Counter} on topic {Path}", message.Counter, Context.Path); + logger.LogInformation("Got message {Counter} on topic {Path}", message.Counter, Context?.Path); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Samples/Sample.OutboxWebApi/Application/CreateCustomerCommandHandler.cs b/src/Samples/Sample.OutboxWebApi/Application/CreateCustomerCommandHandler.cs index b4bd409b..d2ca0553 100644 --- a/src/Samples/Sample.OutboxWebApi/Application/CreateCustomerCommandHandler.cs +++ b/src/Samples/Sample.OutboxWebApi/Application/CreateCustomerCommandHandler.cs @@ -5,7 +5,7 @@ // doc:fragment:Handler public record CreateCustomerCommandHandler(IMessageBus Bus, CustomerContext CustomerContext) : IRequestHandler { - public async Task OnHandle(CreateCustomerCommand request) + public async Task OnHandle(CreateCustomerCommand request, CancellationToken cancellationToken) { // Note: This handler will be already wrapped in a transaction: see Program.cs and .UseTransactionScope() / .UseSqlTransaction() diff --git a/src/Samples/Sample.Serialization.ConsoleApp/Program.cs b/src/Samples/Sample.Serialization.ConsoleApp/Program.cs index 3e3a75ec..1b3da382 100644 --- a/src/Samples/Sample.Serialization.ConsoleApp/Program.cs +++ b/src/Samples/Sample.Serialization.ConsoleApp/Program.cs @@ -11,7 +11,6 @@ using SlimMessageBus.Host; using SlimMessageBus.Host.Memory; using SlimMessageBus.Host.Redis; -using SlimMessageBus.Host.Serialization; using SlimMessageBus.Host.Serialization.Avro; using SlimMessageBus.Host.Serialization.Hybrid; using SlimMessageBus.Host.Serialization.Json; @@ -54,7 +53,7 @@ static async Task Main(string[] args) => await Host.CreateDefaultBuilder(args) builder .AsDefault() .AddJsonSerializer(); - + builder .For(typeof(AddCommand), typeof(MultiplyRequest), typeof(MultiplyResponse)) .AddAvroSerializer(); @@ -193,7 +192,7 @@ protected async Task MultiplyLoop() public class AddCommandConsumer : IConsumer { - public async Task OnHandle(AddCommand message) + public async Task OnHandle(AddCommand message, CancellationToken cancellationToken) { Console.WriteLine("Consumer: Adding {0} and {1} gives {2}", message.Left, message.Right, message.Left + message.Right); await Task.Delay(50); // Simulate some work @@ -202,7 +201,7 @@ public async Task OnHandle(AddCommand message) public class SubtractCommandConsumer : IConsumer { - public async Task OnHandle(SubtractCommand message) + public async Task OnHandle(SubtractCommand message, CancellationToken cancellationToken) { Console.WriteLine("Consumer: Subtracting {0} and {1} gives {2}", message.Left, message.Right, message.Left - message.Right); await Task.Delay(50); // Simulate some work @@ -211,7 +210,7 @@ public async Task OnHandle(SubtractCommand message) public class MultiplyRequestHandler : IRequestHandler { - public async Task OnHandle(MultiplyRequest request) + public async Task OnHandle(MultiplyRequest request, CancellationToken cancellationToken) { await Task.Delay(50); // Simulate some work return new MultiplyResponse { Result = request.Left * request.Right, OperationId = request.OperationId }; diff --git a/src/Samples/Sample.Simple.ConsoleApp/Program.cs b/src/Samples/Sample.Simple.ConsoleApp/Program.cs index bf7fc458..ee7910f0 100644 --- a/src/Samples/Sample.Simple.ConsoleApp/Program.cs +++ b/src/Samples/Sample.Simple.ConsoleApp/Program.cs @@ -68,8 +68,8 @@ internal class ApplicationService : IHostedService public Task StartAsync(CancellationToken cancellationToken) { - var addTask = Task.Factory.StartNew(AddLoop, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); - var multiplyTask = Task.Factory.StartNew(MultiplyLoop, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + var addTask = Task.Factory.StartNew(AddLoop, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + var multiplyTask = Task.Factory.StartNew(MultiplyLoop, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); return Task.CompletedTask; } @@ -170,7 +170,7 @@ private static void ConfigureMessageBus(MessageBusBuilder mbb, IConfiguration co .Produce(x => x.DefaultTopic(topicForAddCommand) .WithModifier((msg, nativeMsg) => nativeMsg.PartitionKey = msg.Left.ToString())) // By default AddCommand messages will go to event-hub/topic named 'add-command' .Consume(x => x.Topic(topicForAddCommand) - .WithConsumer() + .WithConsumerOfContext() //.WithConsumer(nameof(AddCommandConsumer.OnHandle)) //.WithConsumer((consumer, message, name) => consumer.OnHandle(message, name)) .KafkaGroup(consumerGroup) // for Apache Kafka @@ -347,15 +347,13 @@ public class AddCommand public int Right { get; set; } } -public class AddCommandConsumer : IConsumer, IConsumerWithContext +public class AddCommandConsumer : IConsumer> { - public IConsumerContext Context { get; set; } - - public async Task OnHandle(AddCommand message) + public async Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) { - Console.WriteLine("Consumer: Adding {0} and {1} gives {2}", message.Left, message.Right, message.Left + message.Right); + Console.WriteLine("Consumer: Adding {0} and {1} gives {2}", message.Message.Left, message.Message.Right, message.Message.Left + message.Message.Right); // Context.Headers -> has the headers - await Task.Delay(50); // Simulate some work + await Task.Delay(50, cancellationToken); // Simulate some work } } @@ -372,9 +370,9 @@ public class MultiplyResponse public class MultiplyRequestHandler : IRequestHandler { - public async Task OnHandle(MultiplyRequest request) + public async Task OnHandle(MultiplyRequest request, CancellationToken cancellationToken) { - await Task.Delay(50); // Simulate some work + await Task.Delay(50, cancellationToken); // Simulate some work return new MultiplyResponse { Result = request.Left * request.Right }; } } diff --git a/src/Samples/Sample.ValidatingWebApi/CommandHandlers/CreateCustomerCommandHandler.cs b/src/Samples/Sample.ValidatingWebApi/CommandHandlers/CreateCustomerCommandHandler.cs index fcb97155..bbeae364 100644 --- a/src/Samples/Sample.ValidatingWebApi/CommandHandlers/CreateCustomerCommandHandler.cs +++ b/src/Samples/Sample.ValidatingWebApi/CommandHandlers/CreateCustomerCommandHandler.cs @@ -1,11 +1,12 @@ namespace Sample.ValidatingWebApi.CommandHandlers; using Sample.ValidatingWebApi.Commands; + using SlimMessageBus; public class CreateCustomerCommandHandler : IRequestHandler { - public Task OnHandle(CreateCustomerCommand command) + public Task OnHandle(CreateCustomerCommand command, CancellationToken cancellationToken) { return Task.FromResult(new CommandResultWithId(Guid.NewGuid())); } diff --git a/src/Samples/Sample.ValidatingWebApi/QueryHandlers/SearchCustomerQueryHandler.cs b/src/Samples/Sample.ValidatingWebApi/QueryHandlers/SearchCustomerQueryHandler.cs index d55fe981..a435c354 100644 --- a/src/Samples/Sample.ValidatingWebApi/QueryHandlers/SearchCustomerQueryHandler.cs +++ b/src/Samples/Sample.ValidatingWebApi/QueryHandlers/SearchCustomerQueryHandler.cs @@ -2,16 +2,17 @@ using Sample.ValidatingWebApi.Commands; using Sample.ValidatingWebApi.Queries; + using SlimMessageBus; public class SearchCustomerQueryHandler : IRequestHandler { - public Task OnHandle(SearchCustomerQuery request) => Task.FromResult(new SearchCustomerResult + public Task OnHandle(SearchCustomerQuery request, CancellationToken cancellationToken) => Task.FromResult(new SearchCustomerResult { - Items = new[] - { + Items = + [ new CustomerModel(Guid.NewGuid(), "John", "Whick", "john@whick.com", null) - } + ] }); } diff --git a/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs index f573805b..42468da7 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs @@ -1,5 +1,7 @@ namespace SlimMessageBus.Host; +using System.Reflection; + public abstract class AbstractConsumerBuilder : IAbstractConsumerBuilder { public MessageBusSettings Settings { get; } @@ -35,13 +37,12 @@ static bool ParameterMatch(IMessageTypeConsumerInvokerSettings invoker, MethodIn { var parameters = new List(methodInfo.GetParameters().Select(x => x.ParameterType)); - var requiredParameters = new[] { invoker.MessageType }; - foreach (var parameter in requiredParameters) + var consumerContextOfMessageType = typeof(IConsumerContext<>).MakeGenericType(invoker.MessageType); + + if (!parameters.Remove(invoker.MessageType) + && !parameters.Remove(consumerContextOfMessageType)) { - if (!parameters.Remove(parameter)) - { - return false; - } + return false; } var allowedParameters = new[] { typeof(IConsumerContext), typeof(CancellationToken) }; @@ -64,11 +65,15 @@ static bool ParameterMatch(IMessageTypeConsumerInvokerSettings invoker, MethodIn return true; } +#if NETSTANDARD2_0 if (invoker == null) throw new ArgumentNullException(nameof(invoker)); +#else + ArgumentNullException.ThrowIfNull(invoker); +#endif methodName ??= nameof(IConsumer.OnHandle); - /// See and + /// See and var consumerOnHandleMethod = invoker.ConsumerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(x => x.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase) && ParameterMatch(invoker, x)) diff --git a/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs index 4572e7a6..219d9eda 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs @@ -1,31 +1,27 @@ namespace SlimMessageBus.Host; -public class ConsumerBuilder : AbstractConsumerBuilder +public class ConsumerBuilder : AbstractConsumerBuilder { public ConsumerBuilder(MessageBusSettings settings, Type messageType = null) - : base(settings, messageType ?? typeof(T)) + : base(settings, messageType ?? typeof(TMessage)) { ConsumerSettings.ConsumerMode = ConsumerMode.Consumer; } - public ConsumerBuilder Path(string path) + public ConsumerBuilder Path(string path, Action> pathConfig = null) { ConsumerSettings.Path = path; + pathConfig?.Invoke(this); return this; } - public ConsumerBuilder Path(string path, Action> pathConfig) - { - if (pathConfig is null) throw new ArgumentNullException(nameof(pathConfig)); - - var b = Path(path); - pathConfig(b); - return b; - } + public ConsumerBuilder Topic(string topic, Action> topicConfig = null) => Path(topic, topicConfig); - public ConsumerBuilder Topic(string topic) => Path(topic); + private static Task DefaultConsumerOnMethod(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) + => ((IConsumer)consumer).OnHandle((T)message, cancellationToken); - public ConsumerBuilder Topic(string topic, Action> topicConfig) => Path(topic, topicConfig); + private static Task DefaultConsumerOnMethodOfContext(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) + => ((IConsumer>)consumer).OnHandle(new MessageConsumerContext(consumerContext, (T)message), cancellationToken); /// /// Declares type as the consumer of messages . @@ -33,32 +29,30 @@ public ConsumerBuilder Path(string path, Action> pathConfi /// /// /// - public ConsumerBuilder WithConsumer() - where TConsumer : class, IConsumer + public ConsumerBuilder WithConsumer() + where TConsumer : class, IConsumer { ConsumerSettings.ConsumerType = typeof(TConsumer); - ConsumerSettings.ConsumerMethod = (consumer, message, _, _) => ((IConsumer)consumer).OnHandle((T)message); - + ConsumerSettings.ConsumerMethod = DefaultConsumerOnMethod; ConsumerSettings.Invokers.Add(ConsumerSettings); - return this; } /// - /// Declares type as the consumer of the derived message . + /// Declares type as the consumer of the derived message . /// The consumer type has to implement interface. /// /// /// - public ConsumerBuilder WithConsumer() - where TConsumer : class, IConsumer - where TMessage : T + public ConsumerBuilder WithConsumer() + where TConsumer : class, IConsumer + where TDerivedMessage : TMessage { - AssertInvokerUnique(derivedConsumerType: typeof(TConsumer), derivedMessageType: typeof(TMessage)); + AssertInvokerUnique(derivedConsumerType: typeof(TConsumer), derivedMessageType: typeof(TDerivedMessage)); - var invoker = new MessageTypeConsumerInvokerSettings(ConsumerSettings, messageType: typeof(TMessage), consumerType: typeof(TConsumer)) + var invoker = new MessageTypeConsumerInvokerSettings(ConsumerSettings, messageType: typeof(TDerivedMessage), consumerType: typeof(TConsumer)) { - ConsumerMethod = (consumer, message, _, _) => ((IConsumer)consumer).OnHandle((TMessage)message) + ConsumerMethod = DefaultConsumerOnMethod }; ConsumerSettings.Invokers.Add(invoker); @@ -71,7 +65,7 @@ public ConsumerBuilder WithConsumer() /// /// /// - public ConsumerBuilder WithConsumer(Type derivedConsumerType, Type derivedMessageType, string methodName = null) + public ConsumerBuilder WithConsumer(Type derivedConsumerType, Type derivedMessageType, string methodName = null) { AssertInvokerUnique(derivedConsumerType, derivedMessageType); @@ -93,14 +87,13 @@ public ConsumerBuilder WithConsumer(Type derivedConsumerType, Type derivedMes /// /// Specifies how to delegate messages to the consumer type. /// - public ConsumerBuilder WithConsumer(Func consumerMethod) + public ConsumerBuilder WithConsumer(Func consumerMethod) where TConsumer : class { if (consumerMethod == null) throw new ArgumentNullException(nameof(consumerMethod)); ConsumerSettings.ConsumerType = typeof(TConsumer); - ConsumerSettings.ConsumerMethod = (consumer, message, _, _) => consumerMethod((TConsumer)consumer, (T)message); - + ConsumerSettings.ConsumerMethod = (consumer, message, consumerContext, ct) => consumerMethod((TConsumer)consumer, (TMessage)message, consumerContext, ct); ConsumerSettings.Invokers.Add(ConsumerSettings); return this; @@ -113,7 +106,7 @@ public ConsumerBuilder WithConsumer(Func consu /// /// /// - public ConsumerBuilder WithConsumer(string consumerMethodName) + public ConsumerBuilder WithConsumer(string consumerMethodName) where TConsumer : class { if (consumerMethodName == null) throw new ArgumentNullException(nameof(consumerMethodName)); @@ -128,7 +121,7 @@ public ConsumerBuilder WithConsumer(string consumerMethodName) /// /// If null, will default to /// - public ConsumerBuilder WithConsumer(Type consumerType, string consumerMethodName = null) + public ConsumerBuilder WithConsumer(Type consumerType, string consumerMethodName = null) { _ = consumerType ?? throw new ArgumentNullException(nameof(consumerType)); @@ -142,13 +135,49 @@ public ConsumerBuilder WithConsumer(Type consumerType, string consumerMethodN return this; } + /// + /// Declares type as the consumer of messages . + /// The consumer type has to implement interface. + /// + /// + /// + public ConsumerBuilder WithConsumerOfContext() + where TConsumer : class, IConsumer> + { + ConsumerSettings.ConsumerType = typeof(TConsumer); + ConsumerSettings.ConsumerMethod = DefaultConsumerOnMethodOfContext; + ConsumerSettings.Invokers.Add(ConsumerSettings); + return this; + } + + /// + /// Declares type as the consumer of the derived message . + /// The consumer type has to implement interface. + /// + /// + /// + public ConsumerBuilder WithConsumerOfContext() + where TConsumer : class, IConsumer> + where TDerivedMessage : TMessage + { + AssertInvokerUnique(derivedConsumerType: typeof(TConsumer), derivedMessageType: typeof(TDerivedMessage)); + + var invoker = new MessageTypeConsumerInvokerSettings(ConsumerSettings, messageType: typeof(TDerivedMessage), consumerType: typeof(TConsumer)) + { + ConsumerMethod = DefaultConsumerOnMethodOfContext + }; + ConsumerSettings.Invokers.Add(invoker); + + return this; + } + /// /// Number of concurrent competing consumer instances that the bus is asking for the DI plugin. /// This dictates how many concurrent messages can be processed at a time. /// /// /// - public ConsumerBuilder Instances(int numberOfInstances) + public ConsumerBuilder Instances(int numberOfInstances) { ConsumerSettings.Instances = numberOfInstances; return this; @@ -159,7 +188,7 @@ public ConsumerBuilder Instances(int numberOfInstances) /// /// /// - public ConsumerBuilder PerMessageScopeEnabled(bool enabled) + public ConsumerBuilder PerMessageScopeEnabled(bool enabled) { ConsumerSettings.IsMessageScopeEnabled = enabled; return this; @@ -171,7 +200,7 @@ public ConsumerBuilder PerMessageScopeEnabled(bool enabled) /// This should be used in conjunction with . With per message scope enabled, the DI should dispose the consumer upon disposal of message scope. /// /// - public ConsumerBuilder DisposeConsumerEnabled(bool enabled) + public ConsumerBuilder DisposeConsumerEnabled(bool enabled) { ConsumerSettings.IsDisposeConsumerEnabled = enabled; return this; @@ -182,11 +211,11 @@ public ConsumerBuilder DisposeConsumerEnabled(bool enabled) /// /// /// - public ConsumerBuilder WhenUndeclaredMessageTypeArrives(Action action) + public ConsumerBuilder WhenUndeclaredMessageTypeArrives(Action action) { action(ConsumerSettings.UndeclaredMessageType); return this; } - public ConsumerBuilder Do(Action> action) => base.Do(action); + public ConsumerBuilder Do(Action> action) => base.Do(action); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs index 3c46b887..0cb8d5b6 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs @@ -14,53 +14,33 @@ protected AbstractHandlerBuilder(MessageBusSettings settings, Type messageType, } protected THandlerBuilder TypedThis => (THandlerBuilder)this; - - /// - /// Configure topic name (or queue name) that incoming requests () are expected on. - /// - /// Topic name - /// - public THandlerBuilder Topic(string path) => Path(path); /// /// Configure topic name (or queue name) that incoming requests () are expected on. /// /// Topic name + /// /// - public THandlerBuilder Path(string path) + public THandlerBuilder Path(string path, Action pathConfig = null) { var consumerSettingsExist = Settings.Consumers.Any(x => x.Path == path && x.ConsumerMode == ConsumerMode.RequestResponse && x != ConsumerSettings); if (consumerSettingsExist) { - throw new ConfigurationMessageBusException($"Attempted to configure request handler for topic/queue '{path}' when one was already configured. You can only have one request handler for a given topic/queue, otherwise which response would you send back?"); + throw new ConfigurationMessageBusException($"Attempted to configure request handler for path '{path}' when one was already configured. There can only be one request handler for a given path (topic/queue)"); } - ConsumerSettings.Path = path; + ConsumerSettings.Path = path; + pathConfig?.Invoke(TypedThis); return TypedThis; } /// - /// Configure topic name that incoming requests () are expected on. - /// - /// Topic name - /// - /// - public THandlerBuilder Path(string path, Action pathConfig) - { - if (pathConfig is null) throw new ArgumentNullException(nameof(pathConfig)); - - var b = Path(path); - pathConfig(b); - return b; - } - - /// - /// Configure topic name that incoming requests () are expected on. + /// Configure topic name (or queue name) that incoming requests () are expected on. /// /// Topic name /// /// - public THandlerBuilder Topic(string topic, Action topicConfig) => Path(topic, topicConfig); + public THandlerBuilder Topic(string topic, Action topicConfig = null) => Path(topic, topicConfig); public THandlerBuilder Instances(int numberOfInstances) { @@ -125,15 +105,19 @@ public HandlerBuilder(MessageBusSettings settings, Type requestType = null, Type throw new ConfigurationMessageBusException($"The {nameof(ConsumerSettings)}.{nameof(ConsumerSettings.ResponseType)} is not set"); } } + + private static Task DefaultHandlerOnMethod(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) + => ((IRequestHandler)consumer).OnHandle((TReq)message, cancellationToken); + + private static Task DefaultHandlerOnMethodOfContext(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) + => ((IRequestHandler, TRes>)consumer).OnHandle(new MessageConsumerContext(consumerContext, (TReq)message), cancellationToken); public HandlerBuilder WithHandler() where THandler : IRequestHandler { ConsumerSettings.ConsumerType = typeof(THandler); - ConsumerSettings.ConsumerMethod = (consumer, message, _, _) => ((THandler)consumer).OnHandle((TRequest)message); - + ConsumerSettings.ConsumerMethod = DefaultHandlerOnMethod; ConsumerSettings.Invokers.Add(ConsumerSettings); - return this; } @@ -152,7 +136,38 @@ public HandlerBuilder WithHandler ((IRequestHandler)consumer).OnHandle((TDerivedRequest)message) + ConsumerMethod = DefaultHandlerOnMethod + }; + ConsumerSettings.Invokers.Add(invoker); + + return this; + } + + public HandlerBuilder WithHandlerOfContext() + where THandler : IRequestHandler, TResponse> + { + ConsumerSettings.ConsumerType = typeof(THandler); + ConsumerSettings.ConsumerMethod = DefaultHandlerOnMethodOfContext; + ConsumerSettings.Invokers.Add(ConsumerSettings); + return this; + } + + /// + /// Declares type as the consumer of the derived message . + /// The consumer type has to implement interface. + /// + /// + /// + /// + public HandlerBuilder WithHandlerOfContext() + where THandler : class, IRequestHandler, TResponse> + where TDerivedRequest : TRequest + { + AssertInvokerUnique(derivedConsumerType: typeof(THandler), derivedMessageType: typeof(TDerivedRequest)); + + var invoker = new MessageTypeConsumerInvokerSettings(ConsumerSettings, messageType: typeof(TDerivedRequest), consumerType: typeof(THandler)) + { + ConsumerMethod = DefaultHandlerOnMethodOfContext }; ConsumerSettings.Invokers.Add(invoker); @@ -174,14 +189,18 @@ public HandlerBuilder(MessageBusSettings settings, Type requestType = null) ConsumerSettings.ResponseType = null; } + private static Task DefaultHandlerOnMethod(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) + => ((IRequestHandler)consumer).OnHandle((TReq)message, cancellationToken); + + private static Task DefaultHandlerOnMethodOfContext(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) + => ((IRequestHandler>)consumer).OnHandle(new MessageConsumerContext(consumerContext, (TReq)message), cancellationToken); + public HandlerBuilder WithHandler() where THandler : IRequestHandler { ConsumerSettings.ConsumerType = typeof(THandler); - ConsumerSettings.ConsumerMethod = (consumer, message, _, _) => ((THandler)consumer).OnHandle((TRequest)message); - + ConsumerSettings.ConsumerMethod = DefaultHandlerOnMethod; ConsumerSettings.Invokers.Add(ConsumerSettings); - return TypedThis; } @@ -200,7 +219,38 @@ public HandlerBuilder WithHandler() var invoker = new MessageTypeConsumerInvokerSettings(ConsumerSettings, messageType: typeof(TDerivedRequest), consumerType: typeof(THandler)) { - ConsumerMethod = (consumer, message, _, _) => ((IRequestHandler)consumer).OnHandle((TDerivedRequest)message) + ConsumerMethod = DefaultHandlerOnMethod + }; + ConsumerSettings.Invokers.Add(invoker); + + return this; + } + + public HandlerBuilder WithHandlerOfContext() + where THandler : IRequestHandler> + { + ConsumerSettings.ConsumerType = typeof(THandler); + ConsumerSettings.ConsumerMethod = DefaultHandlerOnMethodOfContext; + ConsumerSettings.Invokers.Add(ConsumerSettings); + return TypedThis; + } + + /// + /// Declares type as the consumer of the derived message . + /// The consumer type has to implement interface. + /// + /// + /// + /// + public HandlerBuilder WithHandlerOfContext() + where THandler : class, IRequestHandler> + where TDerivedRequest : TRequest + { + AssertInvokerUnique(derivedConsumerType: typeof(THandler), derivedMessageType: typeof(TDerivedRequest)); + + var invoker = new MessageTypeConsumerInvokerSettings(ConsumerSettings, messageType: typeof(TDerivedRequest), consumerType: typeof(THandler)) + { + ConsumerMethod = DefaultHandlerOnMethodOfContext }; ConsumerSettings.Invokers.Add(invoker); diff --git a/src/SlimMessageBus.Host.Configuration/Builders/MessageConsumerContext.cs b/src/SlimMessageBus.Host.Configuration/Builders/MessageConsumerContext.cs new file mode 100644 index 00000000..2af4bd3d --- /dev/null +++ b/src/SlimMessageBus.Host.Configuration/Builders/MessageConsumerContext.cs @@ -0,0 +1,26 @@ +namespace SlimMessageBus.Host; + +public class MessageConsumerContext : IConsumerContext +{ + private readonly IConsumerContext _target; + + public MessageConsumerContext(IConsumerContext consumerContext, T message) + { + _target = consumerContext; + Message = message; + } + + public string Path => _target.Path; + + public IReadOnlyDictionary Headers => _target.Headers; + + public CancellationToken CancellationToken => _target.CancellationToken; + + public IMessageBus Bus => _target.Bus; + + public IDictionary Properties => _target.Properties; + + public object Consumer => _target.Consumer; + + public T Message { get; } +} diff --git a/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs b/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs index efcd7e6b..218a52aa 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs @@ -28,7 +28,7 @@ private void CalculateResponseType() /// public Type ConsumerType { get; set; } /// - public Func ConsumerMethod { get; set; } + public ConsumerMethod ConsumerMethod { get; set; } /// public MethodInfo ConsumerMethodInfo { get; set; } /// diff --git a/src/SlimMessageBus.Host.Configuration/Settings/Delegates.cs b/src/SlimMessageBus.Host.Configuration/Settings/Delegates.cs index f8b6e087..ee677b77 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/Delegates.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/Delegates.cs @@ -1,3 +1,5 @@ namespace SlimMessageBus.Host; -public delegate void MessageHeaderModifier(IDictionary headers, T message); \ No newline at end of file +public delegate void MessageHeaderModifier(IDictionary headers, T message); + +public delegate Task ConsumerMethod(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken); diff --git a/src/SlimMessageBus.Host.Configuration/Settings/IMessageTypeConsumerInvokerSettings.cs b/src/SlimMessageBus.Host.Configuration/Settings/IMessageTypeConsumerInvokerSettings.cs index e7dce1a1..cab6bcee 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/IMessageTypeConsumerInvokerSettings.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/IMessageTypeConsumerInvokerSettings.cs @@ -14,7 +14,7 @@ public interface IMessageTypeConsumerInvokerSettings /// /// The delegate to the consumer method responsible for accepting messages. /// - Func ConsumerMethod { get; set; } + ConsumerMethod ConsumerMethod { get; set; } /// /// The consumer method. /// diff --git a/src/SlimMessageBus.Host.Configuration/Settings/MessageTypeConsumerInvokerSettings.cs b/src/SlimMessageBus.Host.Configuration/Settings/MessageTypeConsumerInvokerSettings.cs index ecb06aa6..9026b46e 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/MessageTypeConsumerInvokerSettings.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/MessageTypeConsumerInvokerSettings.cs @@ -11,7 +11,7 @@ public class MessageTypeConsumerInvokerSettings : IMessageTypeConsumerInvokerSet /// public Type ConsumerType { get; } /// - public Func ConsumerMethod { get; set; } + public ConsumerMethod ConsumerMethod { get; set; } /// public MethodInfo ConsumerMethodInfo { get; set; } diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index 920b1441..210e2384 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 2.5.2-rc1 + 3.0.0-rc6 @@ -21,4 +21,10 @@ + + + <_Parameter1>SlimMessageBus.Host.Configuration.Test + + + diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index dbd524c4..d866a6c8 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 2.0.4 + 3.0.0-rc6 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index b56af7cb..076a8825 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 2.0.4 + 3.0.0-rc6 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host/Collections/GenericTypeCache.cs b/src/SlimMessageBus.Host/Collections/GenericTypeCache.cs index f57f6bf2..d10902d5 100644 --- a/src/SlimMessageBus.Host/Collections/GenericTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/GenericTypeCache.cs @@ -35,8 +35,6 @@ public class GenericTypeCache : IGenericTypeCache { private readonly Type _openGenericType; private readonly string _methodName; - private readonly Func _returnTypeFunc; - private readonly Func _argumentTypesFunc; private readonly IReadOnlyCache.GenericInterfaceType> _messageTypeToGenericInterfaceType; private readonly SafeDictionaryWrapper _messageTypeToResolveCache; @@ -47,12 +45,10 @@ public class GenericTypeCache : IGenericTypeCache /// The method name on the open generic type. /// The return type of the method. /// Additional method arguments (in addition to the message type which is the open generic type param). - public GenericTypeCache(Type openGenericType, string methodName, Func returnTypeFunc, Func argumentTypesFunc = null) + public GenericTypeCache(Type openGenericType, string methodName) { _openGenericType = openGenericType; _methodName = methodName; - _returnTypeFunc = returnTypeFunc; - _argumentTypesFunc = argumentTypesFunc; _messageTypeToGenericInterfaceType = new SafeDictionaryWrapper.GenericInterfaceType>(CreateType); _messageTypeToResolveCache = new SafeDictionaryWrapper(); } @@ -61,9 +57,7 @@ private IGenericTypeCache.GenericInterfaceType CreateType(Type messageTyp { var genericType = _openGenericType.MakeGenericType(messageType); var method = genericType.GetMethod(_methodName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public) ?? throw new InvalidOperationException($"The method {_methodName} was not found on type {genericType}"); - var methodArguments = new[] { messageType }.Concat(_argumentTypesFunc?.Invoke(messageType) ?? Enumerable.Empty()).ToArray(); - var returnType = _returnTypeFunc(messageType); - var func = ReflectionUtils.GenerateMethodCallToFunc(method, genericType, returnType, methodArguments); + var func = ReflectionUtils.GenerateMethodCallToFunc(method); return new IGenericTypeCache.GenericInterfaceType(messageType, genericType, method, func); } diff --git a/src/SlimMessageBus.Host/Collections/GenericTypeCache2.cs b/src/SlimMessageBus.Host/Collections/GenericTypeCache2.cs index 2d212916..e2488aad 100644 --- a/src/SlimMessageBus.Host/Collections/GenericTypeCache2.cs +++ b/src/SlimMessageBus.Host/Collections/GenericTypeCache2.cs @@ -35,19 +35,15 @@ public class GenericTypeCache2 : IGenericTypeCache2 { private readonly Type _openGenericType; private readonly string _methodName; - private readonly Func _returnTypeFunc; - private readonly Func _argumentTypesFunc; private readonly IReadOnlyCache<(Type RequestType, Type ResponseType), IGenericTypeCache2.GenericInterfaceType> _messageTypeToGenericInterfaceType; private readonly SafeDictionaryWrapper<(Type RequestType, Type ResponseType), GenericTypeResolveCache> _messageTypeToResolveCache; public TFunc this[(Type RequestType, Type ResponseType) key] => _messageTypeToGenericInterfaceType[key].Func; - public GenericTypeCache2(Type openGenericType, string methodName, Func returnTypeFunc, Func argumentTypesFunc) + public GenericTypeCache2(Type openGenericType, string methodName) { _openGenericType = openGenericType; _methodName = methodName; - _returnTypeFunc = returnTypeFunc; - _argumentTypesFunc = argumentTypesFunc; _messageTypeToGenericInterfaceType = new SafeDictionaryWrapper<(Type RequestType, Type ResponseType), IGenericTypeCache2.GenericInterfaceType>(CreateType); _messageTypeToResolveCache = new SafeDictionaryWrapper<(Type RequestType, Type ResponseType), GenericTypeResolveCache>(); } @@ -56,13 +52,10 @@ private IGenericTypeCache2.GenericInterfaceType CreateType((Type RequestT { var genericType = _openGenericType.MakeGenericType(p.RequestType, p.ResponseType); var method = genericType.GetMethod(_methodName) ?? throw new InvalidOperationException($"The method {_methodName} was not found on type {genericType}"); - var methodArguments = new[] { p.RequestType }.Concat(_argumentTypesFunc(p.ResponseType)).ToArray(); - var returnType = _returnTypeFunc(p.ResponseType); - var func = ReflectionUtils.GenerateMethodCallToFunc(method, genericType, returnType, methodArguments); + var func = ReflectionUtils.GenerateMethodCallToFunc(method); return new IGenericTypeCache2.GenericInterfaceType(p.RequestType, p.ResponseType, genericType, method, func); } - /// /// Returns the resolved instances, or null if none are registered. /// diff --git a/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs b/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs index 9eb9524c..e24d3a5c 100644 --- a/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs @@ -7,7 +7,7 @@ public interface IRuntimeTypeCache /// /// Cache for generic methods that match this signature . /// - IReadOnlyCache<(Type ClassType, string MethodName, Type GenericArgument), Func> GenericMethod { get; } + IReadOnlyCache<(Type ClassType, string MethodName, Type GenericArgument), Func>> GenericMethod { get; } /// /// Provides a closed generic type for with as the generic parameter. diff --git a/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs b/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs index ddeb8722..36b60af8 100644 --- a/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs @@ -6,7 +6,7 @@ public class RuntimeTypeCache : IRuntimeTypeCache private readonly IReadOnlyCache _taskOfType; private readonly IReadOnlyCache<(Type OpenGenericType, Type GenericParameterType), Type> _closedGenericTypeOfOpenGenericType; - public IReadOnlyCache<(Type ClassType, string MethodName, Type GenericArgument), Func> GenericMethod { get; } + public IReadOnlyCache<(Type ClassType, string MethodName, Type GenericArgument), Func>> GenericMethod { get; } public IGenericTypeCache>, IProducerContext, Task>> ProducerInterceptorType { get; } public IGenericTypeCache, IProducerContext, Task>> PublishInterceptorType { get; } @@ -19,57 +19,42 @@ public class RuntimeTypeCache : IRuntimeTypeCache public RuntimeTypeCache() { - static Type ReturnTypeFunc(Type responseType) => typeof(Task<>).MakeGenericType(responseType); - static Type FuncTypeFunc(Type responseType) => typeof(Func<>).MakeGenericType(ReturnTypeFunc(responseType)); - _isAssignable = new SafeDictionaryWrapper<(Type From, Type To), bool>(x => x.To.IsAssignableFrom(x.From)); _taskOfType = new SafeDictionaryWrapper(type => new TaskOfTypeCache(type)); _closedGenericTypeOfOpenGenericType = new SafeDictionaryWrapper<(Type OpenGenericType, Type GenericPatameterType), Type>(x => x.OpenGenericType.MakeGenericType(x.GenericPatameterType)); - GenericMethod = new SafeDictionaryWrapper<(Type ClassType, string MethodName, Type GenericArgument), Func>(key => + GenericMethod = new SafeDictionaryWrapper<(Type ClassType, string MethodName, Type GenericArgument), Func>>(key => { var genericMethod = key.ClassType .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) .Single(x => x.ContainsGenericParameters && x.IsGenericMethodDefinition && x.GetGenericArguments().Length == 1 && x.Name == key.MethodName); - return ReflectionUtils.GenerateGenericMethodCallToFunc>(genericMethod, [key.GenericArgument], key.ClassType, typeof(Task)); + return ReflectionUtils.GenerateGenericMethodCallToFunc>>(genericMethod, [key.GenericArgument]); }); ProducerInterceptorType = new GenericTypeCache>, IProducerContext, Task>>( typeof(IProducerInterceptor<>), - nameof(IProducerInterceptor.OnHandle), - messageType => typeof(Task), - messageType => [typeof(Func>), typeof(IProducerContext)]); + nameof(IProducerInterceptor.OnHandle)); PublishInterceptorType = new GenericTypeCache, IProducerContext, Task>>( typeof(IPublishInterceptor<>), - nameof(IPublishInterceptor.OnHandle), - messageType => typeof(Task), - messageType => [typeof(Func), typeof(IProducerContext)]); + nameof(IPublishInterceptor.OnHandle)); SendInterceptorType = new GenericTypeCache2>( typeof(ISendInterceptor<,>), - nameof(ISendInterceptor.OnHandle), - ReturnTypeFunc, - responseType => [FuncTypeFunc(responseType), typeof(IProducerContext)]); + nameof(ISendInterceptor.OnHandle)); ConsumerInterceptorType = new GenericTypeCache>, IConsumerContext, Task>>( typeof(IConsumerInterceptor<>), - nameof(IConsumerInterceptor.OnHandle), - messageType => typeof(Task), - messageType => [typeof(Func>), typeof(IConsumerContext)]); + nameof(IConsumerInterceptor.OnHandle)); HandlerInterceptorType = new GenericTypeCache2>( typeof(IRequestHandlerInterceptor<,>), - nameof(IRequestHandlerInterceptor.OnHandle), - ReturnTypeFunc, - responseType => [FuncTypeFunc(responseType), typeof(IConsumerContext)]); + nameof(IRequestHandlerInterceptor.OnHandle)); ConsumerErrorHandlerType = new GenericTypeCache>, IConsumerContext, Exception, Task>>( typeof(IConsumerErrorHandler<>), - nameof(IConsumerErrorHandler.OnHandleError), - messageType => typeof(Task), - messageType => [typeof(Func>), typeof(IConsumerContext), typeof(Exception)]); + nameof(IConsumerErrorHandler.OnHandleError)); } public bool IsAssignableFrom(Type from, Type to) diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index 8768d007..40d594f0 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -107,7 +107,7 @@ public MessageHandler( } catch (Exception ex) { - // Give a chance to the consumer error handler to take action + // Give the consumer error handler a chance to take action var handleErrorResult = await DoHandleError(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext, ex).ConfigureAwait(false); if (!handleErrorResult.Handled) { diff --git a/src/SlimMessageBus.Host/DependencyResolver/ConsumerMethodPostProcessor.cs b/src/SlimMessageBus.Host/DependencyResolver/ConsumerMethodPostProcessor.cs index a4997b54..96144efd 100644 --- a/src/SlimMessageBus.Host/DependencyResolver/ConsumerMethodPostProcessor.cs +++ b/src/SlimMessageBus.Host/DependencyResolver/ConsumerMethodPostProcessor.cs @@ -5,10 +5,12 @@ public class ConsumerMethodPostProcessor : IMessageBusSettingsPostProcessor public void Run(MessageBusSettings settings) { var consumerInvokers = settings.Consumers.Concat(settings.Children.SelectMany(x => x.Consumers)) - .SelectMany(x => x.Invokers).ToList(); + .SelectMany(x => x.Invokers) + .ToList(); + foreach (var consumerInvoker in consumerInvokers.Where(x => x.ConsumerMethod == null && x.ConsumerMethodInfo != null)) { - consumerInvoker.ConsumerMethod = ReflectionUtils.GenerateMethodCallToFunc>(consumerInvoker.ConsumerMethodInfo, consumerInvoker.MessageType); + consumerInvoker.ConsumerMethod = ReflectionUtils.GenerateMethodCallToFunc(consumerInvoker.ConsumerMethodInfo, consumerInvoker.MessageType); } } } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Helpers/ReflectionUtils.cs b/src/SlimMessageBus.Host/Helpers/ReflectionUtils.cs index 0c289915..a7c45bda 100644 --- a/src/SlimMessageBus.Host/Helpers/ReflectionUtils.cs +++ b/src/SlimMessageBus.Host/Helpers/ReflectionUtils.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Linq.Expressions; +using System.Reflection; public static class ReflectionUtils { @@ -16,18 +17,54 @@ public static Func GenerateGetterFunc(PropertyInfo property) return Expression.Lambda>(propertyObjExpr, objInstanceExpr).Compile(); } - public static T GenerateMethodCallToFunc(MethodInfo method, Type instanceType, Type returnType, params Type[] argumentTypes) + /// + /// Compiles a delegate that invokes the specified method. The delegate paremeters must match the method signature in the following way: + /// - first parameter is the instance of the object containing the method + /// - next purameters have to be convertable (or be same type) to the method parameter + /// For example `Func` for method `Task OnHandle(SomeMessage message, CancellationToken ct)` + /// + /// + /// + /// + public static TDelegate GenerateMethodCallToFunc(MethodInfo method) where TDelegate : Delegate { - var objInstanceExpr = Expression.Parameter(typeof(object), "instance"); - var typedInstanceExpr = Expression.Convert(objInstanceExpr, instanceType); + static Expression ConvertIfNecessary(Expression expr, Type targetType) => expr.Type == targetType ? expr : Expression.Convert(expr, targetType); + + var delegateSignature = typeof(TDelegate).GetMethod("Invoke")!; + var delegateReturnType = delegateSignature.ReturnType; + var delegateArgumentTypes = delegateSignature.GetParameters().Select(x => x.ParameterType).ToArray(); + + var methodArgumentTypes = method.GetParameters().Select(x => x.ParameterType).ToArray(); + + if (delegateArgumentTypes.Length < 1) + { + throw new ConfigurationMessageBusException($"Delegate {typeof(TDelegate)} must have at least one argument"); + } + if (!delegateReturnType.IsAssignableFrom(method.ReturnType)) + { + throw new ConfigurationMessageBusException($"Return type mismatch for method {method.Name} and delegate {typeof(TDelegate)}"); + } + + // first argument of the delegate is the instance of the object containing the methid, need to skip it + var inputInstanceType = delegateArgumentTypes[0]; + var inputArgumentTypes = delegateArgumentTypes.Skip(1).ToArray(); + + if (methodArgumentTypes.Length != inputArgumentTypes.Length) + { + throw new ConfigurationMessageBusException($"Argument count mismatch between method {method.Name} and delegate {typeof(TDelegate)}"); + } + + var inputInstanceExpr = Expression.Parameter(inputInstanceType, "instance"); + var targetInstanceExpr = ConvertIfNecessary(inputInstanceExpr, method.DeclaringType); - var objArguments = argumentTypes.Select((x, i) => Expression.Parameter(typeof(object), $"arg{i + 1}")).ToArray(); - var typedArguments = argumentTypes.Select((x, i) => Expression.Convert(objArguments[i], x)).ToArray(); + var inputArguments = inputArgumentTypes.Select((argType, i) => Expression.Parameter(argType, $"arg{i + 1}")).ToArray(); + var methodArguments = methodArgumentTypes.Select((argType, i) => ConvertIfNecessary(inputArguments[i], argType)).ToArray(); - var methodResultExpr = Expression.Call(typedInstanceExpr, method, typedArguments); - var typedMethodResultExpr = Expression.Convert(methodResultExpr, returnType); + var targetMethodResultExpr = Expression.Call(targetInstanceExpr, method, methodArguments); + var targetMethodResultWithConvertExpr = ConvertIfNecessary(targetMethodResultExpr, delegateReturnType); - return Expression.Lambda(typedMethodResultExpr, new[] { objInstanceExpr }.Concat(objArguments)).Compile(); + var targetArguments = new[] { inputInstanceExpr }.Concat(inputArguments); + return Expression.Lambda(targetMethodResultWithConvertExpr, targetArguments).Compile(); } /// @@ -125,10 +162,10 @@ public static TDelegate GenerateMethodCallToFunc(MethodInfo methodInf return lambda.Compile(); } - public static T GenerateGenericMethodCallToFunc(MethodInfo genericMethod, Type[] genericTypeArguments, Type instanceType, Type returnType, params Type[] argumentTypes) + public static T GenerateGenericMethodCallToFunc(MethodInfo genericMethod, Type[] genericTypeArguments) where T : Delegate { var method = genericMethod.MakeGenericMethod(genericTypeArguments); - return GenerateMethodCallToFunc(method, instanceType, returnType, argumentTypes); + return GenerateMethodCallToFunc(method); } private static readonly Type taskOfObject = typeof(Task); diff --git a/src/SlimMessageBus/IConsumer.cs b/src/SlimMessageBus/IConsumer.cs index 4c364391..bda83754 100644 --- a/src/SlimMessageBus/IConsumer.cs +++ b/src/SlimMessageBus/IConsumer.cs @@ -9,7 +9,8 @@ public interface IConsumer /// /// Invoked when a message arrives of type . /// - /// The arriving message + /// The arriving message + /// The cancellation token /// - Task OnHandle(TMessage message); + Task OnHandle(TMessage message, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/SlimMessageBus/IConsumerContext.cs b/src/SlimMessageBus/IConsumerContext.cs index 0333f4df..d23c87e6 100644 --- a/src/SlimMessageBus/IConsumerContext.cs +++ b/src/SlimMessageBus/IConsumerContext.cs @@ -27,3 +27,8 @@ public interface IConsumerContext /// object Consumer { get; } } + +public interface IConsumerContext : IConsumerContext +{ + public TMessage Message { get; } +} \ No newline at end of file diff --git a/src/SlimMessageBus/IConsumerWithContext.cs b/src/SlimMessageBus/IConsumerWithContext.cs index 7e44d4fd..b580d0fb 100644 --- a/src/SlimMessageBus/IConsumerWithContext.cs +++ b/src/SlimMessageBus/IConsumerWithContext.cs @@ -1,12 +1,12 @@ namespace SlimMessageBus; - -/// -/// An extension point for to receive provider specific (for current message subject to processing). + +/// +/// An extension point for to recieve provider specific (for current message subject to processing). /// public interface IConsumerWithContext { - /// - /// Current message consumer context (injected by SMB prior message OnHandle). + /// + /// Current message consumer context (injected by SMB prior message OnHandle). /// IConsumerContext Context { get; set; } } diff --git a/src/SlimMessageBus/IProducerContext.cs b/src/SlimMessageBus/IProducerContext.cs index 0c6f7eaa..6458bf6d 100644 --- a/src/SlimMessageBus/IProducerContext.cs +++ b/src/SlimMessageBus/IProducerContext.cs @@ -18,7 +18,7 @@ public interface IProducerContext /// The bus that was used to produce the message. /// For hybrid bus this will the child bus that was identified as the one to handle the message. /// - public IMessageBus Bus { get; set; } + IMessageBus Bus { get; set; } /// /// Additional transport provider specific features or user custom data. /// diff --git a/src/SlimMessageBus/RequestResponse/IRequestHandler.cs b/src/SlimMessageBus/RequestResponse/IRequestHandler.cs index b7ad2ec0..9ee9e922 100644 --- a/src/SlimMessageBus/RequestResponse/IRequestHandler.cs +++ b/src/SlimMessageBus/RequestResponse/IRequestHandler.cs @@ -6,13 +6,14 @@ namespace SlimMessageBus; /// The request message type /// The response message type public interface IRequestHandler -{ +{ /// /// Handles the incoming request message. /// - /// The request message + /// The request message + /// The cancellation token /// - Task OnHandle(TRequest request); + Task OnHandle(TRequest request, CancellationToken cancellationToken); } /// @@ -25,6 +26,7 @@ public interface IRequestHandler /// Handles the incoming request message. /// /// The request message + /// The cancellation token /// - Task OnHandle(TRequest request); + Task OnHandle(TRequest request, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index 829be406..7715b625 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 2.0.4 + 3.0.0-rc6 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs index b80bf93f..2cd56421 100644 --- a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs @@ -62,7 +62,7 @@ public async Task BasicPubSub() .Produce(x => x.DefaultPath(hubName).KeyProvider(m => (m.Counter % 2) == 0 ? "even" : "odd")) .Consume(x => x.Path(hubName) .Group("subscriber") // ensure consumer group exists on the event hub - .WithConsumer() + .WithConsumerOfContext() .CheckpointAfter(TimeSpan.FromSeconds(10)) .CheckpointEvery(50) .Instances(2)); @@ -175,27 +175,20 @@ public class PingMessage #endregion } -public class PingConsumer(ILogger logger, ConcurrentBag messages) - : IConsumer, IConsumerWithContext +public class PingConsumer(ILogger logger, ConcurrentBag messages) : IConsumer> { private readonly ILogger _logger = logger; private readonly ConcurrentBag _messages = messages; - public IConsumerContext Context { get; set; } - - #region Implementation of IConsumer - - public Task OnHandle(PingMessage message) + public Task OnHandle(IConsumerContext context, CancellationToken cancellationToken) { - _messages.Add(message); + _messages.Add(context.Message); - var msg = Context.GetTransportMessage(); + var msg = context.GetTransportMessage(); - _logger.LogInformation("Got message {0:000} on topic {1} offset {2} partition key {3}.", message.Counter, Context.Path, msg.Offset, msg.PartitionKey); + _logger.LogInformation("Got message {Message:000} on topic {Path} offset {Offset} partition key {PartitionKey}.", context.Message.Counter, context.Path, msg.Offset, msg.PartitionKey); return Task.CompletedTask; } - - #endregion } public class EchoRequest : IRequest @@ -223,7 +216,7 @@ public class EchoResponse public class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { return Task.FromResult(new EchoResponse { Message = request.Message }); } diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs index fe01e732..8b8d487f 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs @@ -364,7 +364,7 @@ public PingConsumer(ILogger logger, TestEventCollector #region Implementation of IConsumer - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { var sbMessage = Context.GetTransportMessage(); @@ -393,7 +393,7 @@ public PingDerivedConsumer(ILogger logger, TestEventCollect #region Implementation of IConsumer - public Task OnHandle(PingDerivedMessage message) + public Task OnHandle(PingDerivedMessage message, CancellationToken cancellationToken) { var sbMessage = Context.GetTransportMessage(); @@ -458,7 +458,7 @@ public EchoRequestHandler(TestMetric testMetric) testMetric.OnCreatedConsumer(); } - public Task OnHandle(EchoRequest request) + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { return Task.FromResult(new EchoResponse(request.Message)); } diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs index b9149464..f0ad0e66 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs @@ -411,7 +411,7 @@ private record SampleMessage private class SampleConsumer : IConsumer { - public Task OnHandle(T message) + public Task OnHandle(T message, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Tests/SlimMessageBus.Host.Benchmark/ConsumerCallBenchmark.cs b/src/Tests/SlimMessageBus.Host.Benchmark/ConsumerCallBenchmark.cs index 16c1ce2f..31ede72c 100644 --- a/src/Tests/SlimMessageBus.Host.Benchmark/ConsumerCallBenchmark.cs +++ b/src/Tests/SlimMessageBus.Host.Benchmark/ConsumerCallBenchmark.cs @@ -18,38 +18,38 @@ public IEnumerable Scenarios { get { - var onHandleMethodInfo = typeof(SomeMessageConsumer).GetMethod(nameof(SomeMessageConsumer.OnHandle), new[] { typeof(SomeMessage) }); + var onHandleMethodInfo = typeof(SomeMessageConsumer).GetMethod(nameof(SomeMessageConsumer.OnHandle), [typeof(SomeMessage), typeof(CancellationToken)]); var message = new SomeMessage(); var consumer = new SomeMessageConsumer(); - return new[] - { + return + [ new Scenario("Reflection", message, consumer, - (target, message) => (Task)onHandleMethodInfo.Invoke(target, new[]{ message })), + (target, message, ct) => (Task)onHandleMethodInfo.Invoke(target, [message, ct])), new Scenario("CompiledExpression", message, consumer, - ReflectionUtils.GenerateMethodCallToFunc>(onHandleMethodInfo, typeof(SomeMessageConsumer), typeof(Task), typeof(SomeMessage))), + ReflectionUtils.GenerateMethodCallToFunc>(onHandleMethodInfo)), new Scenario("CompiledExpressionWithOptional", message, consumer, - ReflectionUtils.GenerateMethodCallToFunc>(onHandleMethodInfo, [typeof(SomeMessage)])) - }; + ReflectionUtils.GenerateMethodCallToFunc>(onHandleMethodInfo, [typeof(SomeMessage)])) + ]; } } [Benchmark] public void CallConsumerOnHandle() { - _ = scenario.OnHandle(scenario.Consumer, scenario.Message); + _ = scenario.OnHandle(scenario.Consumer, scenario.Message, default); } - public record Scenario(string Name, SomeMessage Message, SomeMessageConsumer Consumer, Func OnHandle) + public record Scenario(string Name, SomeMessage Message, SomeMessageConsumer Consumer, Func OnHandle) { public override string ToString() => Name; } @@ -58,6 +58,6 @@ public record SomeMessage; public class SomeMessageConsumer : IConsumer { - public Task OnHandle(SomeMessage message) => Task.CompletedTask; + public Task OnHandle(SomeMessage message, CancellationToken cancellationToken) => Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/AbstractConsumerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/AbstractConsumerBuilderTest.cs new file mode 100644 index 00000000..e90ba489 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/AbstractConsumerBuilderTest.cs @@ -0,0 +1,39 @@ +namespace SlimMessageBus.Host.Test.Config; + +public class AbstractConsumerBuilderTest +{ + [Theory] + [InlineData(typeof(SomeMessage), typeof(SomeMessageConsumer))] + [InlineData(typeof(SomeRequest), typeof(SomeRequestMessageHandler))] + [InlineData(typeof(SomeMessage), typeof(SomeMessageConsumerEx))] + [InlineData(typeof(SomeRequest), typeof(SomeRequestHandlerEx))] + [InlineData(typeof(SomeRequest), typeof(SomeRequestHandlerExWithResponse))] + public void When_SetupConsumerOnHandleMethod_Given_TypesThatImplementConsumerInterfaces_Then_AssignsConsumerMethodInfo(Type messageType, Type consumerType) + { + // arrange + var consumerSettings = new ConsumerSettings(); + var invoker = new MessageTypeConsumerInvokerSettings(consumerSettings, messageType, consumerType); + + // act + AbstractConsumerBuilder.SetupConsumerOnHandleMethod(invoker); + + // assert + invoker.ConsumerMethodInfo.Should().NotBeNull(); + invoker.ConsumerMethodInfo.Should().BeSameAs(consumerType.GetMethod(nameof(IConsumer.OnHandle))); + } + + private class SomeMessageConsumerEx : IConsumer> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(); + } + + private class SomeRequestHandlerEx : IRequestHandler> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(); + } + + private class SomeRequestHandlerExWithResponse : IRequestHandler, SomeResponse> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs index 7772e89c..19d8c1fe 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs @@ -2,11 +2,13 @@ public class ConsumerBuilderTest { - private readonly MessageBusSettings messageBusSettings; + private readonly MessageBusSettings _messageBusSettings; + private readonly string _path; public ConsumerBuilderTest() { - messageBusSettings = new MessageBusSettings(); + _messageBusSettings = new MessageBusSettings(); + _path = "topic"; } [Fact] @@ -15,7 +17,7 @@ public void Given_MessageType_When_Configured_Then_MessageType_ProperlySet_And_C // arrange // act - var subject = new ConsumerBuilder(messageBusSettings); + var subject = new ConsumerBuilder(_messageBusSettings); // assert subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); @@ -23,43 +25,51 @@ public void Given_MessageType_When_Configured_Then_MessageType_ProperlySet_And_C subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeMessage)); } - [Fact] - public void Given_Path_Set_When_Configured_Then_Path_ProperlySet() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Given_Path_Set_When_Configured_Then_Path_ProperlySet(bool delegatePassed) { // arrange - var path = "topic"; + var pathDelegate = new Mock>>(); // act - var subject = new ConsumerBuilder(messageBusSettings) - .Path(path); + var subject = new ConsumerBuilder(_messageBusSettings).Path(_path, delegatePassed ? pathDelegate.Object : null); // assert - subject.ConsumerSettings.Path.Should().Be(path); + subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.PathKind.Should().Be(PathKind.Topic); + if (delegatePassed) + { + pathDelegate.Verify(x => x.Invoke(subject), Times.Once); + } } - [Fact] - public void Given_Topic_Set_When_Configured_Then_Topic_ProperlySet() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Given_Topic_Set_When_Configured_Then_Topic_ProperlySet(bool delegatePassed) { // arrange - var topic = "topic"; + var pathDelegate = new Mock>>(); // act - var subject = new ConsumerBuilder(messageBusSettings) - .Topic(topic); + var subject = new ConsumerBuilder(_messageBusSettings).Topic(_path, delegatePassed ? pathDelegate.Object : null); // assert - subject.ConsumerSettings.Path.Should().Be(topic); + subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.PathKind.Should().Be(PathKind.Topic); + if (delegatePassed) + { + pathDelegate.Verify(x => x.Invoke(subject), Times.Once); + } } [Fact] public void Given_Instances_Set_When_Configured_Then_Instances_ProperlySet() { - // arrange - // act - var subject = new ConsumerBuilder(messageBusSettings) + var subject = new ConsumerBuilder(_messageBusSettings) .Instances(3); // assert @@ -70,14 +80,12 @@ public void Given_Instances_Set_When_Configured_Then_Instances_ProperlySet() public void Given_BaseMessageType_And_ItsHierarchy_When_WithConsumer_ForTheBaseTypeAndDerivedTypes_Then_TheConsumerSettingsAreCorrect() { // arrange - var topic = "topic"; - var consumerContextMock = new Mock(); consumerContextMock.SetupGet(x => x.CancellationToken).Returns(new CancellationToken()); // act - var subject = new ConsumerBuilder(messageBusSettings) - .Topic(topic) + var subject = new ConsumerBuilder(_messageBusSettings) + .Topic(_path) .WithConsumer() .WithConsumer() .WithConsumer() @@ -123,17 +131,69 @@ public void Given_BaseMessageType_And_ItsHierarchy_When_WithConsumer_ForTheBaseT } [Fact] - public void Given_BaseRequestType_And_ItsHierarchy_When_WithConsumer_ForTheBaseTypeAndDerivedTypes_Then_TheConsumerSettingsAreCorrect() + public void Given_BaseMessageType_And_ItsHierarchy_And_ConsumerOfContext_When_WithConsumer_ForTheBaseTypeAndDerivedTypes_Then_TheConsumerSettingsAreCorrect() { // arrange - var topic = "topic"; + var consumerContextMock = new Mock(); + consumerContextMock.SetupGet(x => x.CancellationToken).Returns(new CancellationToken()); + + // act + var subject = new ConsumerBuilder(_messageBusSettings) + .Topic(_path) + .WithConsumerOfContext() + .WithConsumerOfContext() + .WithConsumerOfContext() + .WithConsumerOfContext(); + + // assert + subject.ConsumerSettings.ResponseType.Should().BeNull(); + + subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); + subject.ConsumerSettings.ConsumerType.Should().Be(typeof(BaseMessageConsumerOfContext)); + Func call = () => subject.ConsumerSettings.ConsumerMethod(new BaseMessageConsumerOfContext(), new BaseMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(BaseMessage)); + + subject.ConsumerSettings.Invokers.Count.Should().Be(4); + + var consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(BaseMessage)); + consumerInvokerSettings.MessageType.Should().Be(typeof(BaseMessage)); + consumerInvokerSettings.ConsumerType.Should().Be(typeof(BaseMessageConsumerOfContext)); + consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); + call = () => consumerInvokerSettings.ConsumerMethod(new BaseMessageConsumerOfContext(), new BaseMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(BaseMessage)); + + consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedAMessage)); + consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedAMessage)); + consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedAMessageConsumerOfContext)); + consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); + call = () => consumerInvokerSettings.ConsumerMethod(new DerivedAMessageConsumerOfContext(), new DerivedAMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(DerivedAMessage)); + + consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedBMessage)); + consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedBMessage)); + consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedBMessageConsumerOfContext)); + consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); + call = () => consumerInvokerSettings.ConsumerMethod(new DerivedBMessageConsumerOfContext(), new DerivedBMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(DerivedBMessage)); + consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(Derived2AMessage)); + consumerInvokerSettings.MessageType.Should().Be(typeof(Derived2AMessage)); + consumerInvokerSettings.ConsumerType.Should().Be(typeof(Derived2AMessageConsumerOfContext)); + consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); + call = () => consumerInvokerSettings.ConsumerMethod(new Derived2AMessageConsumerOfContext(), new Derived2AMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(Derived2AMessage)); + } + + [Fact] + public void Given_BaseRequestType_And_ItsHierarchy_When_WithConsumer_ForTheBaseTypeAndDerivedTypes_Then_TheConsumerSettingsAreCorrect() + { + // arrange var consumerContextMock = new Mock(); consumerContextMock.SetupGet(x => x.CancellationToken).Returns(new CancellationToken()); // act - var subject = new ConsumerBuilder(messageBusSettings) - .Topic(topic) + var subject = new ConsumerBuilder(_messageBusSettings) + .Topic(_path) .WithConsumer() .WithConsumer(); @@ -162,6 +222,25 @@ public void Given_BaseRequestType_And_ItsHierarchy_When_WithConsumer_ForTheBaseT call.Should().ThrowAsync().WithMessage(nameof(DerivedRequest)); } + [Fact] + public void When_WithConsumer_Given_CustomDelegateOverloadUsed_Then_ConsumerMethodSet() + { + // arrange + var message = new SomeMessage(); + var consumerMock = new Mock>(); + var subject = new ConsumerBuilder(_messageBusSettings); + var ct = new CancellationToken(); + + // act + subject.WithConsumer>((c, m, context, ct) => c.OnHandle(m, ct)); + + // assert + subject.ConsumerSettings.Invokers.Count.Should().Be(1); + subject.ConsumerSettings.ConsumerMethod(consumerMock.Object, message, null, ct); + + consumerMock.Verify(x => x.OnHandle(message, ct), Times.Once); + } + public class BaseMessage { } @@ -180,22 +259,42 @@ public class Derived2AMessage : DerivedAMessage public class BaseMessageConsumer : IConsumer { - public Task OnHandle(BaseMessage message) => throw new NotImplementedException(nameof(BaseMessage)); + public Task OnHandle(BaseMessage message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(BaseMessage)); } public class DerivedAMessageConsumer : IConsumer { - public Task OnHandle(DerivedAMessage message) => throw new NotImplementedException(nameof(DerivedAMessage)); + public Task OnHandle(DerivedAMessage message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(DerivedAMessage)); } public class DerivedBMessageConsumer : IConsumer { - public Task OnHandle(DerivedBMessage message) => throw new NotImplementedException(nameof(DerivedBMessage)); + public Task OnHandle(DerivedBMessage message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(DerivedBMessage)); } public class Derived2AMessageConsumer : IConsumer { - public Task OnHandle(Derived2AMessage message) => throw new NotImplementedException(nameof(Derived2AMessage)); + public Task OnHandle(Derived2AMessage message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(Derived2AMessage)); + } + + public class BaseMessageConsumerOfContext : IConsumer> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(BaseMessage)); + } + + public class DerivedAMessageConsumerOfContext : IConsumer> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(DerivedAMessage)); + } + + public class DerivedBMessageConsumerOfContext : IConsumer> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(DerivedBMessage)); + } + + public class Derived2AMessageConsumerOfContext : IConsumer> + { + public Task OnHandle(IConsumerContext message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(Derived2AMessage)); } public class BaseResponse @@ -212,11 +311,11 @@ public class DerivedRequest : BaseRequest public class BaseRequestConsumer : IConsumer { - public Task OnHandle(BaseRequest message) => throw new NotImplementedException(nameof(BaseRequest)); + public Task OnHandle(BaseRequest message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(BaseRequest)); } public class DerivedRequestConsumer : IConsumer { - public Task OnHandle(DerivedRequest message) => throw new NotImplementedException(nameof(DerivedRequest)); + public Task OnHandle(DerivedRequest message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(DerivedRequest)); } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs index 0214eb03..bbb0fbfb 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs @@ -2,108 +2,176 @@ public class HandlerBuilderTest { - private readonly MessageBusSettings messageBusSettings; + private readonly Fixture _fixture; + private readonly MessageBusSettings _messageBusSettings; + private readonly string _path; public HandlerBuilderTest() { - messageBusSettings = new MessageBusSettings(); + _fixture = new Fixture(); + _messageBusSettings = new MessageBusSettings(); + _path = _fixture.Create(); } [Fact] public void When_Created_Given_RequestAndResposeType_Then_MessageType_And_ResponseType_And_DefaultHandlerTypeSet_ProperlySet() { - // arrange + // act + var subject = new HandlerBuilder(_messageBusSettings); + // assert + subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); + subject.ConsumerSettings.ResponseType.Should().Be(typeof(SomeResponse)); + subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); + subject.ConsumerSettings.ConsumerType.Should().BeNull(); + subject.ConsumerSettings.Invokers.Should().BeEmpty(); + } + + [Fact] + public void When_Created_Given_RequestWithoutResposeType_Then_MessageType_And_DefaultHandlerTypeSet_ProperlySet() + { // act - var subject = new HandlerBuilder(messageBusSettings); + var subject = new HandlerBuilder(_messageBusSettings); // assert + subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequestWithoutResponse)); + subject.ConsumerSettings.ResponseType.Should().BeNull(); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); subject.ConsumerSettings.ConsumerType.Should().BeNull(); - subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); - subject.ConsumerSettings.ResponseType.Should().Be(typeof(SomeResponse)); + subject.ConsumerSettings.Invokers.Should().BeEmpty(); } [Fact] public void When_PathSet_Given_Path_Then_Path_ProperlySet() { // arrange - var path = "topic"; - var subject = new HandlerBuilder(messageBusSettings); + var pathConfig = new Mock>>(); + var subject = new HandlerBuilder(_messageBusSettings); // act - subject.Path(path); + subject.Path(_path, pathConfig.Object); // assert - subject.ConsumerSettings.Path.Should().Be(path); + subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.PathKind.Should().Be(PathKind.Topic); + pathConfig.Verify(x => x(subject), Times.Once); } [Fact] - public void When_Configured_Given_RequestResponse_Then_ProperSettings() + public void When_PathSet_Given_ThePathWasUsedBeforeOnAnotherHandler_Then_ExceptionIsRaised() { // arrange - var path = "topic"; + var otherHandlerBuilder = new HandlerBuilder(_messageBusSettings).Path(_path); + var subject = new HandlerBuilder(_messageBusSettings); + // act + var act = () => subject.Path(_path); + + // assert + act.Should() + .Throw() + .WithMessage($"Attempted to configure request handler for path '*' when one was already configured. There can only be one request handler for a given path (topic/queue)"); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void When_Configured_Given_RequestResponse_Then_ProperSettings(bool ofContext) + { + // arrange var consumerContextMock = new Mock(); consumerContextMock.SetupGet(x => x.CancellationToken).Returns(new CancellationToken()); + var consumerType = ofContext ? typeof(SomeRequestMessageHandlerOfContext) : typeof(SomeRequestMessageHandler); + // act - var subject = new HandlerBuilder(messageBusSettings) - .Topic(path) - .Instances(3) - .WithHandler(); + var subject = new HandlerBuilder(_messageBusSettings) + .Topic(_path) + .Instances(3); + + if (ofContext) + { + subject.WithHandlerOfContext(); + subject.WithHandlerOfContext(); + } + else + { + subject.WithHandler(); + subject.WithHandler(); + } // assert subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); - subject.ConsumerSettings.Path.Should().Be(path); + subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.Instances.Should().Be(3); - subject.ConsumerSettings.ConsumerType.Should().Be(typeof(SomeRequestMessageHandler)); + subject.ConsumerSettings.ConsumerType.Should().Be(consumerType); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); subject.ConsumerSettings.ResponseType.Should().Be(typeof(SomeResponse)); - subject.ConsumerSettings.Invokers.Count.Should().Be(1); + subject.ConsumerSettings.Invokers.Count.Should().Be(2); var consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(SomeRequest)); - consumerInvokerSettings.MessageType.Should().Be(typeof(SomeRequest)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(SomeRequestMessageHandler)); - Func call = () => consumerInvokerSettings.ConsumerMethod(new SomeRequestMessageHandler(), new SomeRequest(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + consumerInvokerSettings.Should().NotBeNull(); + consumerInvokerSettings.ConsumerType.Should().Be(consumerType); + Func call = () => consumerInvokerSettings.ConsumerMethod(ofContext ? new SomeRequestMessageHandlerOfContext() : new SomeRequestMessageHandler(), new SomeRequest(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(SomeRequest)); + + consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(SomeDerivedRequest)); + consumerInvokerSettings.Should().NotBeNull(); + consumerInvokerSettings.ConsumerType.Should().Be(ofContext ? typeof(SomeDerivedRequestMessageHandlerOfContext) : typeof(SomeDerivedRequestMessageHandler)); + call = () => consumerInvokerSettings.ConsumerMethod(ofContext ? new SomeDerivedRequestMessageHandlerOfContext() : new SomeDerivedRequestMessageHandler(), new SomeDerivedRequest(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(SomeRequest)); } - [Fact] - public void When_Configured_Given_RequestWithoutResponse_Then_ProperSettings() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void When_Configured_Given_RequestWithoutResponse_And_HandlersWithDerivedMessageType_Then_ProperSettings(bool ofContext) { // arrange - var path = "topic"; - var consumerContextMock = new Mock(); consumerContextMock.SetupGet(x => x.CancellationToken).Returns(new CancellationToken()); + var consumerType = ofContext ? typeof(SomeRequestWithoutResponseHandlerOfContext) : typeof(SomeRequestWithoutResponseHandler); + // act - var subject = new HandlerBuilder(messageBusSettings) - .Topic(path) - .Instances(3) - .WithHandler(); + var subject = new HandlerBuilder(_messageBusSettings) + .Topic(_path) + .Instances(3); + + if (ofContext) + { + subject.WithHandlerOfContext(); + subject.WithHandlerOfContext(); + } + else + { + subject.WithHandler(); + subject.WithHandler(); + } // assert subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequestWithoutResponse)); - subject.ConsumerSettings.Path.Should().Be(path); + subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.Instances.Should().Be(3); - subject.ConsumerSettings.ConsumerType.Should().Be(typeof(SomeRequestWithoutResponseHandler)); + subject.ConsumerSettings.ConsumerType.Should().Be(consumerType); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); subject.ConsumerSettings.ResponseType.Should().BeNull(); - subject.ConsumerSettings.Invokers.Count.Should().Be(1); + subject.ConsumerSettings.Invokers.Count.Should().Be(2); var consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(SomeRequestWithoutResponse)); - consumerInvokerSettings.MessageType.Should().Be(typeof(SomeRequestWithoutResponse)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(SomeRequestWithoutResponseHandler)); - Func call = () => consumerInvokerSettings.ConsumerMethod(new SomeRequestWithoutResponseHandler(), new SomeRequestWithoutResponse(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + consumerInvokerSettings.ConsumerType.Should().Be(consumerType); + Func call = () => consumerInvokerSettings.ConsumerMethod(ofContext ? new SomeRequestWithoutResponseHandlerOfContext() : new SomeRequestWithoutResponseHandler(), new SomeRequestWithoutResponse(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(SomeRequestWithoutResponse)); + + consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(SomeDerivedRequestWithoutResponse)); + consumerInvokerSettings.ConsumerType.Should().Be(ofContext ? typeof(SomeDerivedRequestWithoutResponseHandlerOfContext) : typeof(SomeDerivedRequestWithoutResponseHandler)); + call = () => consumerInvokerSettings.ConsumerMethod(ofContext ? new SomeDerivedRequestWithoutResponseHandlerOfContext() : new SomeDerivedRequestWithoutResponseHandler(), new SomeDerivedRequestWithoutResponse(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); + call.Should().ThrowAsync().WithMessage(nameof(SomeDerivedRequestWithoutResponse)); } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageConsumerContextTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageConsumerContextTest.cs new file mode 100644 index 00000000..07fed3ff --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageConsumerContextTest.cs @@ -0,0 +1,39 @@ +namespace SlimMessageBus.Host.Configuration.Test; + +using SlimMessageBus.Host.Test; + +public class MessageConsumerContextTest +{ + [Fact] + public void When_Constructor_Then_PropertiesUseTargetConsumerContext_And_ProvidedMessage_Given_MessageAndAnotherConsumerContext() + { + // arrange + var message = new Mock(); + var consumer = new Mock>(); + var headers = new Dictionary(); + var properties = new Dictionary(); + var path = "path"; + var bus = Mock.Of(); + var ct = new CancellationToken(); + + var untypedConsumerContext = new Mock(); + untypedConsumerContext.SetupGet(x => x.Consumer).Returns(consumer.Object); + untypedConsumerContext.SetupGet(x => x.Headers).Returns(headers); + untypedConsumerContext.SetupGet(x => x.Properties).Returns(properties); + untypedConsumerContext.SetupGet(x => x.Path).Returns(path); + untypedConsumerContext.SetupGet(x => x.Bus).Returns(bus); + untypedConsumerContext.SetupGet(x => x.CancellationToken).Returns(ct); + + // act + var subject = new MessageConsumerContext(untypedConsumerContext.Object, message.Object); + + // assert + subject.Message.Should().BeSameAs(message.Object); + subject.Consumer.Should().BeSameAs(consumer.Object); + subject.Headers.Should().BeSameAs(headers); + subject.Properties.Should().BeSameAs(properties); + subject.Path.Should().Be(path); + subject.Bus.Should().BeSameAs(bus); + subject.CancellationToken.Should().Be(ct); + } +} diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/SampleMessages.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/SampleMessages.cs index 459c2215..5dc3bae0 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/SampleMessages.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/SampleMessages.cs @@ -12,8 +12,16 @@ public record SomeRequest : IRequest, ISomeMessageMarkerInterface { } +public record SomeDerivedRequest : SomeRequest +{ +} + public record SomeRequestWithoutResponse : IRequest { +} + +public record SomeDerivedRequestWithoutResponse : SomeRequestWithoutResponse +{ } public record SomeResponse @@ -22,17 +30,53 @@ public record SomeResponse public class SomeMessageConsumer : IConsumer { - public Task OnHandle(SomeMessage message) => throw new NotImplementedException(); + public Task OnHandle(SomeMessage message, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(SomeMessage)); } public class SomeRequestMessageHandler : IRequestHandler { - public Task OnHandle(SomeRequest request) + public Task OnHandle(SomeRequest request, CancellationToken cancellationToken) + => throw new NotImplementedException(nameof(SomeRequest)); +} + +public class SomeDerivedRequestMessageHandler : IRequestHandler +{ + public Task OnHandle(SomeDerivedRequest request, CancellationToken cancellationToken) + => throw new NotImplementedException(nameof(SomeDerivedRequest)); +} + +public class SomeRequestMessageHandlerOfContext : IRequestHandler, SomeResponse> +{ + public Task OnHandle(IConsumerContext request, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(SomeRequest)); } +public class SomeDerivedRequestMessageHandlerOfContext : IRequestHandler, SomeResponse> +{ + public Task OnHandle(IConsumerContext request, CancellationToken cancellationToken) + => throw new NotImplementedException(nameof(SomeDerivedRequest)); +} + public class SomeRequestWithoutResponseHandler : IRequestHandler { - public Task OnHandle(SomeRequestWithoutResponse request) + public Task OnHandle(SomeRequestWithoutResponse request, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(SomeRequestWithoutResponse)); } + +public class SomeDerivedRequestWithoutResponseHandler : IRequestHandler +{ + public Task OnHandle(SomeDerivedRequestWithoutResponse request, CancellationToken cancellationToken) + => throw new NotImplementedException(nameof(SomeDerivedRequestWithoutResponse)); +} + +public class SomeRequestWithoutResponseHandlerOfContext : IRequestHandler> +{ + public Task OnHandle(IConsumerContext request, CancellationToken cancellationToken) + => throw new NotImplementedException(nameof(SomeRequestWithoutResponse)); +} + +public class SomeDerivedRequestWithoutResponseHandlerOfContext : IRequestHandler> +{ + public Task OnHandle(IConsumerContext request, CancellationToken cancellationToken) + => throw new NotImplementedException(nameof(SomeDerivedRequestWithoutResponse)); +} diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/Usings.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/Usings.cs index 849728a8..824a36fa 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/Usings.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/Usings.cs @@ -1,3 +1,5 @@ +global using AutoFixture; + global using FluentAssertions; global using Moq; diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs index 26b8016d..a9bd4975 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs @@ -154,7 +154,7 @@ public class ExternalMessageConsumer(IMessageBus bus, UnitOfWork unitOfWork, Lis { public IConsumerContext Context { get; set; } - public async Task OnHandle(ExternalMessage message) + public async Task OnHandle(ExternalMessage message, CancellationToken cancellationToken) { lock (store) { @@ -162,7 +162,7 @@ public async Task OnHandle(ExternalMessage message) } // some processing - await bus.Publish(new InternalMessage(message.CustomerId)); + await bus.Publish(new InternalMessage(message.CustomerId), cancellationToken: cancellationToken); // some processing @@ -174,7 +174,7 @@ public class InternalMessageConsumer(UnitOfWork unitOfWork, List stor { public IConsumerContext Context { get; set; } - public Task OnHandle(InternalMessage message) + public Task OnHandle(InternalMessage message, CancellationToken cancellationToken) { lock (store) { diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs index c04f67e4..0777772c 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs @@ -47,12 +47,12 @@ public record SetValueCommand(Guid Value); public class SetValueCommandHandler : IRequestHandler { - public async Task OnHandle(SetValueCommand request) + public async Task OnHandle(SetValueCommand request, CancellationToken cancellationToken) { // Some other logic here ... // and then notify about the value change using the MessageBus.Current accessor which should look up in the current message scope - await MessageBus.Current.Publish(new ValueChangedEvent(request.Value)); + await MessageBus.Current.Publish(new ValueChangedEvent(request.Value), cancellationToken: cancellationToken); } } @@ -60,7 +60,7 @@ public record ValueChangedEvent(Guid Value); public class ValueChangedEventHandler(ValueHolder valueHolder) : IRequestHandler { - public Task OnHandle(ValueChangedEvent request) + public Task OnHandle(ValueChangedEvent request, CancellationToken cancellationToken) { valueHolder.Value = request.Value; return Task.CompletedTask; diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs index 6db21509..40261fe9 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs @@ -49,7 +49,7 @@ public record TestMessage(Guid Value); public class TestMessageConsumer(TestValueHolder holder, IServiceProvider serviceProvider, IMessageScopeAccessor messageScopeAccessor) : IRequestHandler { - public Task OnHandle(TestMessage request) + public Task OnHandle(TestMessage request, CancellationToken cancellationToken) { holder.ServiceProvider = serviceProvider; holder.MessageScopeAccessorServiceProvider = messageScopeAccessor.Current; diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs index 53fa9eb0..87e27784 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs @@ -120,5 +120,5 @@ public class SomeMessage public class SomeMessageConsumer : IConsumer { - public Task OnHandle(SomeMessage message) => Task.CompletedTask; + public Task OnHandle(SomeMessage message, CancellationToken cancellationToken) => Task.CompletedTask; } diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs index c4e94b73..e435442c 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs @@ -12,23 +12,23 @@ namespace SlimMessageBus.Host.Kafka.Test; using SlimMessageBus.Host.Serialization.Json; using SlimMessageBus.Host.Test.Common.IntegrationTest; -/// -/// Performs basic integration test to verify that pub/sub and request-response communication works while concurrent producers pump data. -/// -/// Ensure the topics used in this test (test-ping and test-echo) have 2 partitions, otherwise you will get an exception (Confluent.Kafka.KafkaException : Local: Unknown partition) -/// See https://kafka.apache.org/quickstart#quickstart_createtopic -/// bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --partitions 2 --replication-factor 1 --topic test-ping -/// bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --partitions 2 --replication-factor 1 --topic test-echo -/// -/// +/// +/// Performs basic integration test to verify that pub/sub and request-response communication works while concurrent producers pump data. +/// +/// Ensure the topics used in this test (test-ping and test-echo) have 2 partitions, otherwise you will get an exception (Confluent.Kafka.KafkaException : Local: Unknown partition) +/// See https://kafka.apache.org/quickstart#quickstart_createtopic +/// bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --partitions 2 --replication-factor 1 --topic test-ping +/// bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --partitions 2 --replication-factor 1 --topic test-echo +/// +/// [Trait("Category", "Integration")] -[Trait("Transport", "Kafka")] +[Trait("Transport", "Kafka")] public class KafkaMessageBusIt(ITestOutputHelper testOutputHelper) - : BaseIntegrationTest(testOutputHelper) + : BaseIntegrationTest(testOutputHelper) { private const int NumberOfMessages = 77; private string TopicPrefix { get; set; } - + private static void AddSsl(string username, string password, ClientConfig c) { // cloudkarafka.com uses SSL with SASL authentication @@ -37,12 +37,12 @@ private static void AddSsl(string username, string password, ClientConfig c) c.SaslPassword = password; c.SaslMechanism = SaslMechanism.ScramSha256; c.SslCaLocation = "cloudkarafka_2023-10.pem"; - } + } protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { - var kafkaBrokers = Secrets.Service.PopulateSecrets(configuration["Kafka:Brokers"]); - var kafkaUsername = Secrets.Service.PopulateSecrets(configuration["Kafka:Username"]); + var kafkaBrokers = Secrets.Service.PopulateSecrets(configuration["Kafka:Brokers"]); + var kafkaUsername = Secrets.Service.PopulateSecrets(configuration["Kafka:Username"]); var kafkaPassword = Secrets.Service.PopulateSecrets(configuration["Kafka:Password"]); var kafkaSecure = Convert.ToBoolean(Secrets.Service.PopulateSecrets(configuration["Kafka:Secure"])); @@ -82,27 +82,27 @@ protected override void SetupServices(ServiceCollection services, IConfiguration mbb.AddServicesFromAssemblyContaining(); mbb.AddJsonSerializer(); - ApplyBusConfiguration(mbb); + ApplyBusConfiguration(mbb); }); services.AddSingleton>(); } public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); - - [Fact] - public async Task BasicPubSub() + + [Fact] + public async Task BasicPubSub() { // arrange AddBusConfiguration(mbb => { - var topic = $"{TopicPrefix}test-ping"; - mbb.Produce(x => - { - x.DefaultTopic(topic); - // Partition #0 for even counters - // Partition #1 for odd counters - x.PartitionProvider((m, t) => m.Counter % 2); + var topic = $"{TopicPrefix}test-ping"; + mbb.Produce(x => + { + x.DefaultTopic(topic); + // Partition #0 for even counters + // Partition #1 for odd counters + x.PartitionProvider((m, t) => m.Counter % 2); }); // doc:fragment:ExampleCheckpointConfig mbb.Consume(x => @@ -117,100 +117,100 @@ public async Task BasicPubSub() // doc:fragment:ExampleCheckpointConfig }); - var consumedMessages = ServiceProvider.GetRequiredService>(); - var messageBus = MessageBus; - - // act + var consumedMessages = ServiceProvider.GetRequiredService>(); + var messageBus = MessageBus; + + // act // consume all messages that might be on the queue/subscription await consumedMessages.WaitUntilArriving(newMessagesTimeout: 5); consumedMessages.Clear(); - - // publish - var stopwatch = Stopwatch.StartNew(); - - var messages = Enumerable - .Range(0, NumberOfMessages) - .Select(i => new PingMessage(DateTime.UtcNow, i)) - .ToList(); - - await Task.WhenAll(messages.Select(m => messageBus.Publish(m))); - - stopwatch.Stop(); - Logger.LogInformation("Published {MessageCount} messages in {PublishTime}", messages.Count, stopwatch.Elapsed); - - // consume + + // publish + var stopwatch = Stopwatch.StartNew(); + + var messages = Enumerable + .Range(0, NumberOfMessages) + .Select(i => new PingMessage(DateTime.UtcNow, i)) + .ToList(); + + await Task.WhenAll(messages.Select(m => messageBus.Publish(m))); + + stopwatch.Stop(); + Logger.LogInformation("Published {MessageCount} messages in {PublishTime}", messages.Count, stopwatch.Elapsed); + + // consume stopwatch.Restart(); - await consumedMessages.WaitUntilArriving(newMessagesTimeout: 5); - - stopwatch.Stop(); - Logger.LogInformation("Consumed {MessageCount} messages in {ConsumedTime}", consumedMessages.Count, stopwatch.Elapsed); - - // assert - - // all messages got back - consumedMessages.Count.Should().Be(messages.Count); - - // Partition #0 => Messages with even counter - consumedMessages.Snapshot() - .Where(x => x.Partition == 0) - .All(x => x.Message.Counter % 2 == 0) - .Should().BeTrue(); - - // Partition #1 => Messages with odd counter - consumedMessages.Snapshot() - .Where(x => x.Partition == 1) - .All(x => x.Message.Counter % 2 == 1) - .Should().BeTrue(); - } - - [Fact] - public async Task BasicReqResp() - { - // arrange - - // ensure the topic has 2 partitions - + await consumedMessages.WaitUntilArriving(newMessagesTimeout: 5); + + stopwatch.Stop(); + Logger.LogInformation("Consumed {MessageCount} messages in {ConsumedTime}", consumedMessages.Count, stopwatch.Elapsed); + + // assert + + // all messages got back + consumedMessages.Count.Should().Be(messages.Count); + + // Partition #0 => Messages with even counter + consumedMessages.Snapshot() + .Where(x => x.Partition == 0) + .All(x => x.Message.Counter % 2 == 0) + .Should().BeTrue(); + + // Partition #1 => Messages with odd counter + consumedMessages.Snapshot() + .Where(x => x.Partition == 1) + .All(x => x.Message.Counter % 2 == 1) + .Should().BeTrue(); + } + + [Fact] + public async Task BasicReqResp() + { + // arrange + + // ensure the topic has 2 partitions + AddBusConfiguration(mbb => { var topic = $"{TopicPrefix}test-echo"; mbb - .Produce(x => - { - x.DefaultTopic(topic); - // Partition #0 for even indices - // Partition #1 for odd indices - x.PartitionProvider((m, t) => m.Index % 2); - }) - .Handle(x => x.Topic(topic) - .WithHandler() - .KafkaGroup("handler") - .Instances(2) + .Produce(x => + { + x.DefaultTopic(topic); + // Partition #0 for even indices + // Partition #1 for odd indices + x.PartitionProvider((m, t) => m.Index % 2); + }) + .Handle(x => x.Topic(topic) + .WithHandler() + .KafkaGroup("handler") + .Instances(2) .CheckpointEvery(100) - .CheckpointAfter(TimeSpan.FromSeconds(10))) - .ExpectRequestResponses(x => - { - x.ReplyToTopic($"{TopicPrefix}test-echo-resp"); - x.KafkaGroup("response-reader"); - // for subsequent test runs allow enough time for kafka to reassign the partitions + .CheckpointAfter(TimeSpan.FromSeconds(10))) + .ExpectRequestResponses(x => + { + x.ReplyToTopic($"{TopicPrefix}test-echo-resp"); + x.KafkaGroup("response-reader"); + // for subsequent test runs allow enough time for kafka to reassign the partitions x.DefaultTimeout(TimeSpan.FromSeconds(60)); x.CheckpointEvery(100); - x.CheckpointAfter(TimeSpan.FromSeconds(10)); + x.CheckpointAfter(TimeSpan.FromSeconds(10)); }); - }); - + }); + var kafkaMessageBus = MessageBus; - // act - - var requests = Enumerable - .Range(0, NumberOfMessages) - .Select(i => new EchoRequest(i, $"Echo {i}")) - .ToList(); - - var responses = new ConcurrentBag>(); - await Task.WhenAll(requests.Select(async req => + // act + + var requests = Enumerable + .Range(0, NumberOfMessages) + .Select(i => new EchoRequest(i, $"Echo {i}")) + .ToList(); + + var responses = new ConcurrentBag>(); + await Task.WhenAll(requests.Select(async req => { try { @@ -223,43 +223,43 @@ await Task.WhenAll(requests.Select(async req => } })); - await responses.WaitUntilArriving(newMessagesTimeout: 5); - - // assert - - // all messages got back - responses.Count.Should().Be(NumberOfMessages); - responses.All(x => x.Item1.Message == x.Item2.Message).Should().BeTrue(); - } - + await responses.WaitUntilArriving(newMessagesTimeout: 5); + + // assert + + // all messages got back + responses.Count.Should().Be(NumberOfMessages); + responses.All(x => x.Item1.Message == x.Item2.Message).Should().BeTrue(); + } + private record PingMessage(DateTime Timestamp, int Counter); record struct ConsumedMessage(PingMessage Message, int Partition); - + private class PingConsumer(ILogger logger, TestEventCollector messages) - : IConsumer, IConsumerWithContext - { - public IConsumerContext Context { get; set; } - - public Task OnHandle(PingMessage message) - { - var transportMessage = Context.GetTransportMessage(); + : IConsumer, IConsumerWithContext + { + public IConsumerContext Context { get; set; } + + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) + { + var transportMessage = Context.GetTransportMessage(); var partition = transportMessage.TopicPartition.Partition; messages.Add(new ConsumedMessage(message, partition)); - - logger.LogInformation("Got message {MessageCounter:000} on topic {TopicName}.", message.Counter, Context.Path); - return Task.CompletedTask; - } - } - - private record EchoRequest(int Index, string Message); - - private record EchoResponse(string Message); - - private class EchoRequestHandler : IRequestHandler - { - public Task OnHandle(EchoRequest request) - => Task.FromResult(new EchoResponse(request.Message)); - } + + logger.LogInformation("Got message {MessageCounter:000} on topic {TopicName}.", message.Counter, Context.Path); + return Task.CompletedTask; + } + } + + private record EchoRequest(int Index, string Message); + + private record EchoResponse(string Message); + + private class EchoRequestHandler : IRequestHandler + { + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) + => Task.FromResult(new EchoResponse(request.Message)); + } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs index efc46876..d9ae5834 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs @@ -1,7 +1,9 @@ namespace SlimMessageBus.Host.Memory.Benchmark; using BenchmarkDotNet.Attributes; + using Microsoft.Extensions.DependencyInjection; + using SlimMessageBus.Host.Interceptor; public abstract class PubSubBaseBenchmark : AbstractMemoryBenchmark @@ -89,7 +91,7 @@ public record SomeEvent(DateTimeOffset Timestamp, long Id); public record SomeEventConsumer(TestResult TestResult) : IConsumer { - public Task OnHandle(SomeEvent message) + public Task OnHandle(SomeEvent message, CancellationToken cancellationToken) { TestResult.OnArrived(); return Task.CompletedTask; diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs index 650cd713..c3dfeb82 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs @@ -1,7 +1,9 @@ namespace SlimMessageBus.Host.Memory.Benchmark; using BenchmarkDotNet.Attributes; + using Microsoft.Extensions.DependencyInjection; + using SlimMessageBus.Host.Interceptor; public abstract class ReqRespBaseBenchmark : AbstractMemoryBenchmark @@ -106,7 +108,7 @@ public record SomeResponse(DateTimeOffset Timestamp, long Id); public record SomeRequestHandler(TestResult TestResult) : IRequestHandler { - public Task OnHandle(SomeRequest request) + public Task OnHandle(SomeRequest request, CancellationToken cancellationToken) { TestResult.OnArrived(); return Task.FromResult(new SomeResponse(DateTimeOffset.Now, request.Id)); diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusBuilderTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusBuilderTests.cs index 698b1881..8c9a5b1b 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusBuilderTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusBuilderTests.cs @@ -1,9 +1,9 @@ namespace SlimMessageBus.Host.Memory.Test; -using SlimMessageBus.Host; - using System.Reflection; +using SlimMessageBus.Host; + using static SlimMessageBus.Host.Memory.Test.MemoryMessageBusIt; public class MemoryMessageBusBuilderTests @@ -198,20 +198,20 @@ public record OrderShipped : OrderEvent; public class CustomerEventConsumer : IConsumer { - public Task OnHandle(CustomerEvent message) => throw new NotImplementedException(); + public Task OnHandle(CustomerEvent message, CancellationToken cancellationToken) => throw new NotImplementedException(); } public class CustomerCreatedCustomer : IConsumer { - public Task OnHandle(CustomerCreated message) => throw new NotImplementedException(); + public Task OnHandle(CustomerCreated message, CancellationToken cancellationToken) => throw new NotImplementedException(); } public class CustomerDeletedCustomer : IConsumer { - public Task OnHandle(CustomerDeleted message) => throw new NotImplementedException(); + public Task OnHandle(CustomerDeleted message, CancellationToken cancellationToken) => throw new NotImplementedException(); } public class OrderShippedConsumer : IConsumer { - public Task OnHandle(OrderShipped message) => throw new NotImplementedException(); + public Task OnHandle(OrderShipped message, CancellationToken cancellationToken) => throw new NotImplementedException(); } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs index 1b8d0904..057da73b 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs @@ -242,7 +242,7 @@ internal record PingMessage internal record PingConsumer(TestEventCollector Messages, SafeCounter SafeCounter) : IConsumer { - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { message.ConsumerCounter = SafeCounter.NextValue(); Messages.Add(message); @@ -265,7 +265,7 @@ internal record EchoResponse internal class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) => + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) => Task.FromResult(new EchoResponse { Message = request.Message }); } } diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index 6b640745..ca5952e7 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -113,11 +113,11 @@ public async Task When_Publish_Given_MessageSerializationSetting_Then_DeliversMe // assert if (enableMessageSerialization) { - aConsumerMock.Verify(x => x.OnHandle(It.Is(a => a.Equals(m))), Times.Once); + aConsumerMock.Verify(x => x.OnHandle(It.Is(a => a.Equals(m)), It.IsAny()), Times.Once); } else { - aConsumerMock.Verify(x => x.OnHandle(m), Times.Once); + aConsumerMock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); } aConsumerMock.VerifySet(x => x.Context = It.IsAny(), Times.Once); @@ -134,10 +134,10 @@ public async Task When_Publish_Given_MessageSerializationSetting_Then_DeliversMe aConsumerMock.Object.Context.Headers.Should().BeNull(); } - aConsumer2Mock.Verify(x => x.OnHandle(It.IsAny()), Times.Never); + aConsumer2Mock.Verify(x => x.OnHandle(It.IsAny(), It.IsAny()), Times.Never); aConsumer2Mock.VerifyNoOtherCalls(); - bConsumerMock.Verify(x => x.OnHandle(It.IsAny()), Times.Never); + bConsumerMock.Verify(x => x.OnHandle(It.IsAny(), It.IsAny()), Times.Never); bConsumerMock.VerifyNoOtherCalls(); } @@ -148,7 +148,7 @@ public async Task When_Publish_Given_PerMessageScopeEnabled_Then_TheScopeIsCreat var m = new SomeMessageA(Guid.NewGuid()); var consumerMock = new Mock(); - consumerMock.Setup(x => x.OnHandle(m)).Returns(() => Task.CompletedTask); + consumerMock.Setup(x => x.OnHandle(m, It.IsAny())).Returns(() => Task.CompletedTask); Mock scopeProviderMock = null; Mock scopeMock = null; @@ -195,7 +195,7 @@ public async Task When_Publish_Given_PerMessageScopeEnabled_Then_TheScopeIsCreat scopeProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); consumerMock.VerifySet(x => x.Context = It.IsAny(), Times.Once); - consumerMock.Verify(x => x.OnHandle(m), Times.Once); + consumerMock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumerMock.Verify(x => x.Dispose(), Times.Never); consumerMock.VerifyNoOtherCalls(); } @@ -233,8 +233,8 @@ public async Task When_Publish_Given_PerMessageScopeDisabled_Then_TheScopeIsNotC _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); + consumerMock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumerMock.VerifySet(x => x.Context = It.IsAny(), Times.Once); - consumerMock.Verify(x => x.OnHandle(m), Times.Once); consumerMock.Verify(x => x.Dispose(), Times.Once); consumerMock.VerifyNoOtherCalls(); } @@ -280,7 +280,7 @@ public async Task When_ProducePublish_Given_PerMessageScopeDisabledOrEnabled_And MessageScope.Current.Should().BeNull(); consumerMock.VerifySet(x => x.Context = It.IsAny(), Times.Once); - consumerMock.Verify(x => x.OnHandle(m), Times.Once); + consumerMock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumerMock.Verify(x => x.Dispose(), Times.Once); consumerMock.VerifyNoOtherCalls(); @@ -344,10 +344,10 @@ public async Task When_Publish_Given_TwoConsumersOnSameTopic_Then_BothAreInvoked _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumer1Mock.VerifySet(x => x.Context = It.IsAny(), Times.Once); - consumer1Mock.Verify(x => x.OnHandle(m), Times.Once); + consumer1Mock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumer1Mock.VerifyNoOtherCalls(); - consumer2Mock.Verify(x => x.OnHandle(m), Times.Once); + consumer2Mock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumer2Mock.VerifyNoOtherCalls(); } @@ -361,10 +361,10 @@ public async Task When_Send_Given_AConsumersAndHandlerOnSameTopic_Then_BothAreIn var sequenceOfConsumption = new MockSequence(); var consumer1Mock = new Mock(MockBehavior.Strict); - consumer1Mock.InSequence(sequenceOfConsumption).Setup(x => x.OnHandle(m)).CallBase(); + consumer1Mock.InSequence(sequenceOfConsumption).Setup(x => x.OnHandle(m, It.IsAny())).CallBase(); var consumer2Mock = new Mock(MockBehavior.Strict); - consumer2Mock.InSequence(sequenceOfConsumption).Setup(x => x.OnHandle(m)).CallBase(); + consumer2Mock.InSequence(sequenceOfConsumption).Setup(x => x.OnHandle(m, It.IsAny())).CallBase(); _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(SomeRequestConsumer))).Returns(() => consumer1Mock.Object); _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(SomeRequestHandler))).Returns(() => consumer2Mock.Object); @@ -393,10 +393,10 @@ public async Task When_Send_Given_AConsumersAndHandlerOnSameTopic_Then_BothAreIn _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); - consumer2Mock.Verify(x => x.OnHandle(m), Times.Once); + consumer2Mock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumer2Mock.VerifyNoOtherCalls(); - consumer1Mock.Verify(x => x.OnHandle(m), Times.Once); + consumer1Mock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); consumer1Mock.VerifyNoOtherCalls(); } @@ -412,7 +412,7 @@ public async Task When_Publish_Given_AConsumersThatThrowsException_Then_Exceptio var consumerMock = new Mock>(); consumerMock - .Setup(x => x.OnHandle(m)) + .Setup(x => x.OnHandle(m, It.IsAny())) .ThrowsAsync(new ApplicationException("Bad Request")); var consumerErrorHandlerMock = new Mock>(); @@ -460,7 +460,7 @@ public async Task When_Send_Given_AHandlerThatThrowsException_Then_ExceptionIsBu var consumerMock = new Mock>(); consumerMock - .Setup(x => x.OnHandle(m)) + .Setup(x => x.OnHandle(m, It.IsAny())) .ThrowsAsync(new ApplicationException("Bad Request")); var consumerErrorHandlerMock = new Mock>(); @@ -511,22 +511,22 @@ public virtual void Dispose() GC.SuppressFinalize(this); } - public virtual Task OnHandle(SomeMessageA messageA) => Task.CompletedTask; + public virtual Task OnHandle(SomeMessageA messageA, CancellationToken cancellationToken) => Task.CompletedTask; } public class GenericConsumer : IConsumer { - public Task OnHandle(T message) => Task.CompletedTask; + public Task OnHandle(T message, CancellationToken cancellationToken) => Task.CompletedTask; } public class SomeMessageAConsumer2 : IConsumer { - public virtual Task OnHandle(SomeMessageA messageA) => Task.CompletedTask; + public virtual Task OnHandle(SomeMessageA messageA, CancellationToken cancellationToken) => Task.CompletedTask; } public class SomeMessageBConsumer : IConsumer { - public virtual Task OnHandle(SomeMessageB message) => Task.CompletedTask; + public virtual Task OnHandle(SomeMessageB message, CancellationToken cancellationToken) => Task.CompletedTask; } public record SomeRequest(Guid Id) : IRequest; @@ -535,17 +535,17 @@ public record SomeResponse(Guid Id); public class SomeRequestHandler : IRequestHandler { - public virtual Task OnHandle(SomeRequest request) => Task.FromResult(new SomeResponse(request.Id)); + public virtual Task OnHandle(SomeRequest request, CancellationToken cancellationToken) => Task.FromResult(new SomeResponse(request.Id)); } public class SomeRequestConsumer : IConsumer { - public virtual Task OnHandle(SomeRequest message) => Task.CompletedTask; + public virtual Task OnHandle(SomeRequest message, CancellationToken cancellationToken) => Task.CompletedTask; } public record SomeRequestWithoutResponse(Guid Id) : IRequest; public class SomeRequestWithoutResponseHandler : IRequestHandler { - public virtual Task OnHandle(SomeRequestWithoutResponse request) => Task.CompletedTask; + public virtual Task OnHandle(SomeRequestWithoutResponse request, CancellationToken cancellationToken) => Task.CompletedTask; } diff --git a/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs index b52a2f32..df90624b 100644 --- a/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs @@ -190,7 +190,7 @@ private class PingConsumer(ILogger logger, TestEventCollector - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { _messages.Add(message); @@ -207,7 +207,7 @@ private record EchoResponse(string Message); private class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { return Task.FromResult(new EchoResponse(request.Message)); } diff --git a/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs index 31ec1a46..87341802 100644 --- a/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs @@ -165,7 +165,7 @@ private async Task BasicReqResp() private async Task WaitUntilConnected() { // Wait until connected - var natsMessageBus = (NatsMessageBus) ServiceProvider.GetRequiredService(); + var natsMessageBus = (NatsMessageBus)ServiceProvider.GetRequiredService(); while (!natsMessageBus.IsConnected) { await Task.Delay(200); @@ -176,15 +176,13 @@ private record PingMessage(int Counter, Guid Value); private class PingConsumer(ILogger logger, TestEventCollector messages) : IConsumer, IConsumerWithContext { - private readonly ILogger _logger = logger; - public IConsumerContext Context { get; set; } - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { messages.Add(message); - _logger.LogInformation("Got message {Counter} on topic {Path}", message.Counter, Context.Path); + logger.LogInformation("Got message {Counter} on topic {Path}", message.Counter, Context.Path); return Task.CompletedTask; } } @@ -195,7 +193,7 @@ private record EchoResponse(string Message); private class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { return Task.FromResult(new EchoResponse(request.Message)); } diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs index 56e9d42f..4f7fe49e 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs @@ -280,21 +280,21 @@ public record CreateCustomerCommand(string Firstname, string Lastname) : IReques public class CreateCustomerCommandHandler(IMessageBus Bus, CustomerContext CustomerContext) : IRequestHandler { - public async Task OnHandle(CreateCustomerCommand request) + public async Task OnHandle(CreateCustomerCommand request, CancellationToken cancellationToken) { // Note: This handler will be already wrapped in a transaction: see Program.cs and .UseTransactionScope() / .UseSqlTransaction() - var uniqueId = await Bus.Send(new GenerateCustomerIdCommand(request.Firstname, request.Lastname)); + var uniqueId = await Bus.Send(new GenerateCustomerIdCommand(request.Firstname, request.Lastname), cancellationToken: cancellationToken); var customer = new Customer(request.Firstname, request.Lastname, uniqueId); - await CustomerContext.Customers.AddAsync(customer); - await CustomerContext.SaveChangesAsync(); + await CustomerContext.Customers.AddAsync(customer, cancellationToken); + await CustomerContext.SaveChangesAsync(cancellationToken); // Announce to anyone outside of this micro-service that a customer has been created (this will go out via an transactional outbox) - await Bus.Publish(new CustomerCreatedEvent(customer.Id, customer.Firstname, customer.Lastname), headers: new Dictionary { ["CustomerId"] = customer.Id }); + await Bus.Publish(new CustomerCreatedEvent(customer.Id, customer.Firstname, customer.Lastname), headers: new Dictionary { ["CustomerId"] = customer.Id }, cancellationToken: cancellationToken); // Simulate some variable processing time - await Task.Delay(Random.Shared.Next(10, 250)); + await Task.Delay(Random.Shared.Next(10, 250), cancellationToken); if (request.Lastname == OutboxTests.InvalidLastname) { @@ -310,7 +310,7 @@ public record GenerateCustomerIdCommand(string Firstname, string Lastname) : IRe public class GenerateCustomerIdCommandHandler : IRequestHandler { - public Task OnHandle(GenerateCustomerIdCommand request) + public async Task OnHandle(GenerateCustomerIdCommand request, CancellationToken cancellationToken) { // Note: This handler will be already wrapped in a transaction: see Program.cs and .UseTransactionScope() / .UseSqlTransaction() @@ -330,7 +330,7 @@ public class CustomerCreatedEventConsumer(TestEventCollector logger, TestEventCollector public IConsumerContext Context { get; set; } - public async Task OnHandle(PingMessage message) + public async Task OnHandle(PingMessage message, CancellationToken cancellationToken) { var transportMessage = Context.GetTransportMessage(); @@ -311,7 +311,7 @@ public PingDerivedConsumer(ILogger logger, TestEventCollect #region Implementation of IConsumer - public async Task OnHandle(PingDerivedMessage message) + public async Task OnHandle(PingDerivedMessage message, CancellationToken cancellationToken) { var transportMessage = Context.GetTransportMessage(); @@ -340,7 +340,7 @@ public EchoRequestHandler(TestMetric testMetric) testMetric.OnCreatedConsumer(); } - public Task OnHandle(EchoRequest request) + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { return Task.FromResult(new EchoResponse(request.Message)); } diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs index 2a7e44ce..f0f2e8c6 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs @@ -249,7 +249,7 @@ private class PingConsumer(ILogger logger, TestEventCollector - public Task OnHandle(PingMessage message) + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { _messages.Add(message); @@ -275,7 +275,7 @@ private class EchoResponse private class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request) + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) { return Task.FromResult(new EchoResponse { Message = request.Message }); } diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs index d3faafc9..fb1deae3 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs @@ -171,7 +171,7 @@ public virtual void Dispose() #region Implementation of IConsumer - public virtual Task OnHandle(SomeMessageA messageA) + public virtual Task OnHandle(SomeMessageA messageA, CancellationToken cancellationToken) { return Task.CompletedTask; } @@ -183,7 +183,7 @@ public class SomeMessageAConsumer2 : IConsumer { #region Implementation of IConsumer - public virtual Task OnHandle(SomeMessageA messageA) + public virtual Task OnHandle(SomeMessageA messageA, CancellationToken cancellationToken) { return Task.CompletedTask; } @@ -195,7 +195,7 @@ public class SomeMessageBConsumer : IConsumer { #region Implementation of IConsumer - public virtual Task OnHandle(SomeMessageB message) + public virtual Task OnHandle(SomeMessageB message, CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/src/Tests/SlimMessageBus.Host.Test/Collections/GenericTypeCacheTests.cs b/src/Tests/SlimMessageBus.Host.Test/Collections/GenericTypeCacheTests.cs index 2c7bab60..53d26540 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Collections/GenericTypeCacheTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Collections/GenericTypeCacheTests.cs @@ -16,7 +16,7 @@ public GenericTypeCacheTests() scopeMock = new Mock(); scopeMock.Setup(x => x.GetService(typeof(IEnumerable>))).Returns(() => new[] { consumerInterceptorMock.Object }); - subject = new GenericTypeCache>(typeof(IConsumerInterceptor<>), nameof(IConsumerInterceptor.OnHandle), mt => typeof(Task), mt => new[] { typeof(Func>), typeof(IConsumerContext) }); + subject = new GenericTypeCache>(typeof(IConsumerInterceptor<>), nameof(IConsumerInterceptor.OnHandle)); } [Fact] diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs index 50a6c09e..93cf7edf 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs @@ -94,7 +94,7 @@ public async Task When_ProcessMessage_Given_ExpiredRequest_Then_HandlerNeverCall object MessageProvider(Type messageType, byte[] payload) => request; - var p = new MessageProcessor(new[] { _handlerSettings }, _busMock.Bus, MessageProvider, "path", responseProducer: _busMock.Bus); + var p = new MessageProcessor([_handlerSettings], _busMock.Bus, MessageProvider, "path", responseProducer: _busMock.Bus); _busMock.SerializerMock.Setup(x => x.Deserialize(typeof(SomeRequest), It.IsAny())).Returns(request); @@ -102,7 +102,7 @@ public async Task When_ProcessMessage_Given_ExpiredRequest_Then_HandlerNeverCall await p.ProcessMessage(_transportMessage, headers, default); // assert - _busMock.HandlerMock.Verify(x => x.OnHandle(It.IsAny()), Times.Never); // the handler should not be called + _busMock.HandlerMock.Verify(x => x.OnHandle(It.IsAny(), It.IsAny()), Times.Never); // the handler should not be called _busMock.HandlerMock.VerifyNoOtherCalls(); VerifyProduceResponseNeverCalled(); @@ -121,12 +121,12 @@ public async Task When_ProcessMessage_Given_FailedRequest_Then_ErrorResponseIsSe headers.SetHeader(ReqRespMessageHeaders.ReplyTo, replyTo); object MessageProvider(Type messageType, byte[] payload) => request; - var p = new MessageProcessor(new[] { _handlerSettings }, _busMock.Bus, MessageProvider, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([_handlerSettings], _busMock.Bus, MessageProvider, _topic, responseProducer: _busMock.Bus); _busMock.SerializerMock.Setup(x => x.Deserialize(typeof(SomeRequest), It.IsAny())).Returns(request); var ex = new Exception("Something went bad"); - _busMock.HandlerMock.Setup(x => x.OnHandle(request)).Returns(Task.FromException(ex)); + _busMock.HandlerMock.Setup(x => x.OnHandle(request, It.IsAny())).Returns(Task.FromException(ex)); // act var result = await p.ProcessMessage(_transportMessage, headers, default); @@ -135,7 +135,7 @@ public async Task When_ProcessMessage_Given_FailedRequest_Then_ErrorResponseIsSe result.Exception.Should().BeNull(); result.Response.Should().BeNull(); - _busMock.HandlerMock.Verify(x => x.OnHandle(request), Times.Once); // handler called once + _busMock.HandlerMock.Verify(x => x.OnHandle(request, It.IsAny()), Times.Once); // handler called once _busMock.HandlerMock.VerifyNoOtherCalls(); _busMock.BusMock.Verify( @@ -157,9 +157,9 @@ public async Task When_ProcessMessage_Given_FailedMessage_Then_ExceptionReturned _messageProviderMock.Setup(x => x(message.GetType(), It.IsAny())).Returns(message); var ex = new Exception("Something went bad"); - _busMock.ConsumerMock.Setup(x => x.OnHandle(message)).ThrowsAsync(ex); + _busMock.ConsumerMock.Setup(x => x.OnHandle(message, It.IsAny())).ThrowsAsync(ex); - var p = new MessageProcessor(new[] { _consumerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([_consumerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); // act var result = await p.ProcessMessage(_transportMessage, messageHeaders, default); @@ -170,7 +170,7 @@ public async Task When_ProcessMessage_Given_FailedMessage_Then_ExceptionReturned result.Exception.Should().BeSameAs(ex); result.ConsumerSettings.Should().BeSameAs(_consumerSettings); - _busMock.ConsumerMock.Verify(x => x.OnHandle(message), Times.Once); // handler called once + _busMock.ConsumerMock.Verify(x => x.OnHandle(message, It.IsAny()), Times.Once); // handler called once _busMock.ConsumerMock.VerifyNoOtherCalls(); VerifyProduceResponseNeverCalled(); @@ -195,9 +195,9 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_Then_MessageConsumerI var message = new SomeMessage(); _messageProviderMock.Setup(x => x(message.GetType(), It.IsAny())).Returns(message); - _busMock.ConsumerMock.Setup(x => x.OnHandle(message)).Returns(Task.CompletedTask); + _busMock.ConsumerMock.Setup(x => x.OnHandle(message, It.IsAny())).Returns(Task.CompletedTask); - var p = new MessageProcessor(new[] { _consumerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([_consumerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); // act var result = await p.ProcessMessage(_transportMessage, new Dictionary(), default); @@ -206,7 +206,7 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_Then_MessageConsumerI result.Exception.Should().BeNull(); result.Response.Should().BeNull(); - _busMock.ConsumerMock.Verify(x => x.OnHandle(message), Times.Once); // handler called once + _busMock.ConsumerMock.Verify(x => x.OnHandle(message, It.IsAny()), Times.Once); // handler called once _busMock.ConsumerMock.VerifyNoOtherCalls(); VerifyProduceResponseNeverCalled(); @@ -228,9 +228,9 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_Then_ConsumerIntercep .Returns(new[] { messageConsumerInterceptor.Object }); _messageProviderMock.Setup(x => x(message.GetType(), It.IsAny())).Returns(message); - _busMock.ConsumerMock.Setup(x => x.OnHandle(message)).Returns(Task.CompletedTask); + _busMock.ConsumerMock.Setup(x => x.OnHandle(message, It.IsAny())).Returns(Task.CompletedTask); - var p = new MessageProcessor(new[] { _consumerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([_consumerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); // act var result = await p.ProcessMessage(Array.Empty(), new Dictionary(), default); @@ -239,7 +239,7 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_Then_ConsumerIntercep result.Exception.Should().BeNull(); result.Response.Should().BeNull(); - _busMock.ConsumerMock.Verify(x => x.OnHandle(message), Times.Once); // handler called once + _busMock.ConsumerMock.Verify(x => x.OnHandle(message, It.IsAny()), Times.Once); // handler called once _busMock.ConsumerMock.VerifyNoOtherCalls(); messageConsumerInterceptor.Verify(x => x.OnHandle(message, It.IsAny>>(), It.IsAny()), Times.Once); @@ -256,7 +256,7 @@ public async Task When_ProcessMessage_Given_RequestArrived_Then_RequestHandlerIn var handlerMock = new Mock>(); handlerMock - .Setup(x => x.OnHandle(request)) + .Setup(x => x.OnHandle(request, It.IsAny())) .Returns(Task.FromResult(response)); var requestHandlerInterceptor = new Mock>(); @@ -278,7 +278,7 @@ public async Task When_ProcessMessage_Given_RequestArrived_Then_RequestHandlerIn _messageProviderMock.Setup(x => x(request.GetType(), requestPayload)).Returns(request); - var p = new MessageProcessor(new[] { _handlerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([_handlerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); // act var result = await p.ProcessMessage(requestPayload, new Dictionary(), default); @@ -290,7 +290,7 @@ public async Task When_ProcessMessage_Given_RequestArrived_Then_RequestHandlerIn requestHandlerInterceptor.Verify(x => x.OnHandle(request, It.IsAny>>(), It.IsAny()), Times.Once); requestHandlerInterceptor.VerifyNoOtherCalls(); - handlerMock.Verify(x => x.OnHandle(request), Times.Once); // handler called once + handlerMock.Verify(x => x.OnHandle(request, It.IsAny()), Times.Once); // handler called once handlerMock.VerifyNoOtherCalls(); } @@ -303,7 +303,7 @@ public async Task When_ProcessMessage_Given_ArrivedRequestWithoutResponse_Then_R var handlerMock = new Mock>(); handlerMock - .Setup(x => x.OnHandle(request)) + .Setup(x => x.OnHandle(request, It.IsAny())) .Returns(Task.CompletedTask); var requestHandlerInterceptor = new Mock>(); @@ -327,7 +327,7 @@ public async Task When_ProcessMessage_Given_ArrivedRequestWithoutResponse_Then_R _messageProviderMock.Setup(x => x(request.GetType(), requestPayload)).Returns(request); - var p = new MessageProcessor(new[] { consumerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([consumerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); // act var result = await p.ProcessMessage(requestPayload, new Dictionary(), default); @@ -339,7 +339,7 @@ public async Task When_ProcessMessage_Given_ArrivedRequestWithoutResponse_Then_R requestHandlerInterceptor.Verify(x => x.OnHandle(request, It.IsAny>>(), It.IsAny()), Times.Once); requestHandlerInterceptor.VerifyNoOtherCalls(); - handlerMock.Verify(x => x.OnHandle(request), Times.Once); // handler called once + handlerMock.Verify(x => x.OnHandle(request, It.IsAny()), Times.Once); // handler called once handlerMock.VerifyNoOtherCalls(); } @@ -347,7 +347,7 @@ public class SomeMessageConsumerWithContext : IConsumer, IConsumerW { public virtual IConsumerContext Context { get; set; } - public virtual Task OnHandle(SomeMessage message) => Task.CompletedTask; + public virtual Task OnHandle(SomeMessage message, CancellationToken cancellationToken) => Task.CompletedTask; } [Fact] @@ -360,7 +360,7 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_ConsumerWithConte CancellationToken cancellationToken = default; var consumerMock = new Mock(); - consumerMock.Setup(x => x.OnHandle(message)).Returns(Task.CompletedTask); + consumerMock.Setup(x => x.OnHandle(message, It.IsAny())).Returns(Task.CompletedTask); consumerMock.SetupSet(x => x.Context = It.IsAny()) .Callback(p => context = p) .Verifiable(); @@ -371,13 +371,13 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_ConsumerWithConte _messageProviderMock.Setup(x => x(message.GetType(), _transportMessage)).Returns(message); - var p = new MessageProcessor(new[] { consumerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([consumerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); // act await p.ProcessMessage(_transportMessage, headers, cancellationToken: cancellationToken); // assert - consumerMock.Verify(x => x.OnHandle(message), Times.Once); // handler called once + consumerMock.Verify(x => x.OnHandle(message, It.IsAny()), Times.Once); // handler called once consumerMock.VerifySet(x => x.Context = It.IsAny()); consumerMock.VerifyNoOtherCalls(); @@ -398,9 +398,9 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_MessageScopeEnabl var message = new SomeMessage(); _messageProviderMock.Setup(x => x(message.GetType(), It.IsAny())).Returns(message); - _busMock.ConsumerMock.Setup(x => x.OnHandle(message)).Returns(Task.CompletedTask); + _busMock.ConsumerMock.Setup(x => x.OnHandle(message, It.IsAny())).Returns(Task.CompletedTask); - var p = new MessageProcessor(new[] { consumerSettings }, _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); + var p = new MessageProcessor([consumerSettings], _busMock.Bus, _messageProviderMock.Object, _topic, responseProducer: _busMock.Bus); Mock childScopeMock = null; @@ -413,7 +413,7 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_MessageScopeEnabl await p.ProcessMessage(_transportMessage, new Dictionary(), default); // assert - _busMock.ConsumerMock.Verify(x => x.OnHandle(message), Times.Once); // handler called once + _busMock.ConsumerMock.Verify(x => x.OnHandle(message, It.IsAny()), Times.Once); // handler called once _busMock.ServiceProviderMock.Verify(x => x.GetService(typeof(IServiceScopeFactory)), Times.Once); _busMock.ChildDependencyResolverMocks.Count.Should().Be(0); // it has been disposed childScopeMock.Should().NotBeNull(); @@ -463,7 +463,7 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_SeveralConsumersO }; var p = new MessageProcessor( - new[] { consumerSettingsForSomeMessage, consumerSettingsForSomeRequest, consumerSettingsForSomeMessageInterface }, + [consumerSettingsForSomeMessage, consumerSettingsForSomeRequest, consumerSettingsForSomeMessageInterface], _busMock.Bus, messageWithHeaderProviderMock.Object, _topic, @@ -482,10 +482,10 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_SeveralConsumersO _busMock.ServiceProviderMock.Setup(x => x.GetService(typeof(IConsumer))).Returns(someDerivedMessageConsumerMock.Object); _busMock.ServiceProviderMock.Setup(x => x.GetService(typeof(IRequestHandler))).Returns(someRequestMessageHandlerMock.Object); - someMessageConsumerMock.Setup(x => x.OnHandle(It.IsAny())).Returns(Task.CompletedTask); - someMessageInterfaceConsumerMock.Setup(x => x.OnHandle(It.IsAny())).Returns(Task.CompletedTask); - someDerivedMessageConsumerMock.Setup(x => x.OnHandle(It.IsAny())).Returns(Task.CompletedTask); - someRequestMessageHandlerMock.Setup(x => x.OnHandle(It.IsAny())).Returns(Task.FromResult(new SomeResponse())); + someMessageConsumerMock.Setup(x => x.OnHandle(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + someMessageInterfaceConsumerMock.Setup(x => x.OnHandle(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + someDerivedMessageConsumerMock.Setup(x => x.OnHandle(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + someRequestMessageHandlerMock.Setup(x => x.OnHandle(It.IsAny(), It.IsAny())).Returns(Task.FromResult(new SomeResponse())); // act var result = await p.ProcessMessage(_transportMessage, mesageHeaders, default); @@ -510,25 +510,25 @@ public async Task When_ProcessMessage_Given_ArrivedMessage_And_SeveralConsumersO if (message is SomeMessage someMessage) { - someMessageConsumerMock.Verify(x => x.OnHandle(someMessage), Times.Once); + someMessageConsumerMock.Verify(x => x.OnHandle(someMessage, It.IsAny()), Times.Once); } someMessageConsumerMock.VerifyNoOtherCalls(); if (message is ISomeMessageMarkerInterface someMessageInterface) { - someMessageInterfaceConsumerMock.Verify(x => x.OnHandle(someMessageInterface), Times.Once); + someMessageInterfaceConsumerMock.Verify(x => x.OnHandle(someMessageInterface, It.IsAny()), Times.Once); } someMessageInterfaceConsumerMock.VerifyNoOtherCalls(); if (message is SomeDerivedMessage someDerivedMessage) { - someDerivedMessageConsumerMock.Verify(x => x.OnHandle(someDerivedMessage), Times.Once); + someDerivedMessageConsumerMock.Verify(x => x.OnHandle(someDerivedMessage, It.IsAny()), Times.Once); } someDerivedMessageConsumerMock.VerifyNoOtherCalls(); if (message is SomeRequest someRequest) { - someRequestMessageHandlerMock.Verify(x => x.OnHandle(someRequest), Times.Once); + someRequestMessageHandlerMock.Verify(x => x.OnHandle(someRequest, It.IsAny()), Times.Once); } someRequestMessageHandlerMock.VerifyNoOtherCalls(); } diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs index c80e2fc4..f80aa052 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs @@ -11,7 +11,7 @@ public class MessageHandlerTest private readonly Mock messageHeaderFactoryMock; private readonly Mock consumerContextMock; private readonly Mock consumerInvokerMock; - private readonly Mock> consumerMethodMock; + private readonly Mock consumerMethodMock; private readonly MessageHandler subject; private readonly Fixture fixture = new(); @@ -30,7 +30,7 @@ public MessageHandlerTest() consumerContextMock = new Mock(); consumerInvokerMock = new Mock(); - consumerMethodMock = new Mock>(); + consumerMethodMock = new Mock(); consumerInvokerMock.SetupGet(x => x.ConsumerMethod).Returns(consumerMethodMock.Object); subject = new MessageHandler( diff --git a/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ConsumerMethodPostProcessorTest.cs b/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ConsumerMethodPostProcessorTest.cs new file mode 100644 index 00000000..aea45650 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ConsumerMethodPostProcessorTest.cs @@ -0,0 +1,53 @@ +namespace SlimMessageBus.Host.Test.DependencyResolver; + +using SlimMessageBus.Host.Test; + +public class ConsumerMethodPostProcessorTest +{ + private readonly ConsumerMethodPostProcessor _processor; + + public ConsumerMethodPostProcessorTest() + { + _processor = new ConsumerMethodPostProcessor(); + } + + [Fact] + public void When_Run_Given_ConsumerInvokerWithConsumerMethodInfo_And_WithoutConsumerMethod_Then_ConsumerMethodIsGenerated() + { + // arrange + var settings = new MessageBusSettings(); + var consumerSettings = new ConsumerSettings(); + + var invokerWithoutConsumerMethod = new MessageTypeConsumerInvokerSettings(consumerSettings, typeof(SomeMessage), typeof(IConsumer)) + { + ConsumerMethodInfo = typeof(IConsumer).GetMethod(nameof(IConsumer.OnHandle)), + }; + var existingConsumerMethod = Mock.Of(); + var invokerWithConsumerMethod = new MessageTypeConsumerInvokerSettings(consumerSettings, typeof(SomeMessage), typeof(IConsumer)) + { + ConsumerMethodInfo = typeof(IConsumer).GetMethod(nameof(IConsumer.OnHandle)), + ConsumerMethod = existingConsumerMethod + }; + + consumerSettings.Invokers.Add(invokerWithoutConsumerMethod); + consumerSettings.Invokers.Add(invokerWithConsumerMethod); + + settings.Consumers.Add(consumerSettings); + + var consumerMock = new Mock>(); + var message = new SomeMessage(); + var consumerContextMock = new Mock(); + var cancellationToken = default(CancellationToken); + + // act + _processor.Run(settings); + + // assert + invokerWithoutConsumerMethod.ConsumerMethod.Should().NotBeNull(); + invokerWithoutConsumerMethod.ConsumerMethod(consumerMock.Object, message, consumerContextMock.Object, cancellationToken); + consumerMock.Verify(x => x.OnHandle(message, cancellationToken), Times.Once); + + invokerWithConsumerMethod.ConsumerMethod.Should().NotBeNull(); + invokerWithConsumerMethod.ConsumerMethod.Should().Be(existingConsumerMethod); + } +} diff --git a/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs b/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs index 9c29d055..2f8dfb37 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs @@ -25,23 +25,69 @@ public async Task When_GenerateMethodCallToFunc_Given_ConsumerWithOnHandlerAsync { // arrange var message = new SomeMessage(); + var cancellationToken = new CancellationToken(); var instanceType = typeof(IConsumer); - var consumerOnHandleMethodInfo = instanceType.GetMethod(nameof(IConsumer.OnHandle), [typeof(SomeMessage)]); + var consumerOnHandleMethodInfo = instanceType.GetMethod(nameof(IConsumer.OnHandle), [typeof(SomeMessage), typeof(CancellationToken)]); var consumerMock = new Mock>(); - consumerMock.Setup(x => x.OnHandle(message)).Returns(Task.CompletedTask); + consumerMock.Setup(x => x.OnHandle(message, It.IsAny())).Returns(Task.CompletedTask); // act - var callAsyncMethodFunc = ReflectionUtils.GenerateMethodCallToFunc>(consumerOnHandleMethodInfo, instanceType, typeof(Task), typeof(SomeMessage)); + var callAsyncMethodFunc = ReflectionUtils.GenerateMethodCallToFunc>(consumerOnHandleMethodInfo); - await callAsyncMethodFunc(consumerMock.Object, message); + await callAsyncMethodFunc(consumerMock.Object, message, cancellationToken); // assert - consumerMock.Verify(x => x.OnHandle(message), Times.Once); + consumerMock.Verify(x => x.OnHandle(message, cancellationToken), Times.Once); consumerMock.VerifyNoOtherCalls(); } + [Fact] + public void When_GenerateMethodCallToFunc_Given_DelegateLessThanOneException_Then_ThrowException() + { + var instanceType = typeof(ICustomConsumer); + var consumerHandleAMessageMethodInfo = instanceType.GetMethod(nameof(ICustomConsumer.MethodThatHasParamatersThatCannotBeSatisfied)); + + // act + var act = () => ReflectionUtils.GenerateMethodCallToFunc(consumerHandleAMessageMethodInfo); + + // assert + act.Should() + .Throw() + .WithMessage("Delegate * must have at least one argument"); + } + + [Fact] + public void When_GenerateMethodCallToFunc_Given_MethodReturnTypeNotConvertableToDelegateReturnType_Then_ThrowException() + { + var instanceType = typeof(ICustomConsumer); + var consumerHandleAMessageMethodInfo = instanceType.GetMethod(nameof(ICustomConsumer.MethodThatHasParamatersThatCannotBeSatisfied)); + + // act + var act = () => ReflectionUtils.GenerateMethodCallToFunc>(consumerHandleAMessageMethodInfo); + + // assert + act.Should() + .Throw() + .WithMessage("Return type mismatch for method * and delegate *"); + } + + [Fact] + public void When_GenerateMethodCallToFunc_Given_MethodAndDelegateParamCountMismatch_Then_ThrowException() + { + var instanceType = typeof(ICustomConsumer); + var consumerHandleAMessageMethodInfo = instanceType.GetMethod(nameof(ICustomConsumer.MethodThatHasParamatersThatCannotBeSatisfied)); + + // act + var act = () => ReflectionUtils.GenerateMethodCallToFunc>(consumerHandleAMessageMethodInfo); + + // assert + act.Should() + .Throw() + .WithMessage("Argument count mismatch between method * and delegate *"); + } + internal record ClassWithGenericMethod(object Value) { public T GenericMethod() => (T)Value; @@ -55,11 +101,15 @@ public void When_GenerateGenericMethodCallToFunc_Given_GenericMethid_Then_Method var genericMethod = typeof(ClassWithGenericMethod).GetMethods().FirstOrDefault(x => x.Name == nameof(ClassWithGenericMethod.GenericMethod)); // act - var methodOfTypeBoolFunc = ReflectionUtils.GenerateGenericMethodCallToFunc>(genericMethod, [typeof(bool)], obj.GetType(), typeof(object)); - var result = methodOfTypeBoolFunc(obj); + var methodOfTypeObjectFunc = ReflectionUtils.GenerateGenericMethodCallToFunc>(genericMethod, [typeof(bool)]); + var methodOfTypeBoolFunc = ReflectionUtils.GenerateGenericMethodCallToFunc>(genericMethod, [typeof(bool)]); + + var resultObject = methodOfTypeObjectFunc(obj); + var resultBool = methodOfTypeBoolFunc(obj); // assert - result.Should().Be(true); + resultObject.Should().Be(true); + resultBool.Should().Be(true); } [Fact] @@ -89,23 +139,24 @@ public async Task When_TaskOfObjectContinueWithTaskOfTypeFunc_Given_TaskOfObject public async Task When_GenerateMethodCallToFunc_Given_Delegate_Then_InstanceTypeIsInferred() { var message = new SomeMessage(); + var cancellationToken = new CancellationToken(); var instanceType = typeof(IConsumer); - var consumerOnHandleMethodInfo = instanceType.GetMethod(nameof(IConsumer.OnHandle), [typeof(SomeMessage)]); + var consumerOnHandleMethodInfo = instanceType.GetMethod(nameof(IConsumer.OnHandle), [typeof(SomeMessage), typeof(CancellationToken)]); var consumerMock = new Mock>(); - consumerMock.Setup(x => x.OnHandle(message)).Returns(Task.CompletedTask); + consumerMock.Setup(x => x.OnHandle(message, It.IsAny())).Returns(Task.CompletedTask); // act (positive) - var callAsyncMethodFunc = ReflectionUtils.GenerateMethodCallToFunc>(consumerOnHandleMethodInfo, typeof(SomeMessage)); - await callAsyncMethodFunc(consumerMock.Object, message); + var callAsyncMethodFunc = ReflectionUtils.GenerateMethodCallToFunc>(consumerOnHandleMethodInfo, typeof(SomeMessage)); + await callAsyncMethodFunc(consumerMock.Object, message, cancellationToken); // assert (positive) - consumerMock.Verify(x => x.OnHandle(message), Times.Once); + consumerMock.Verify(x => x.OnHandle(message, It.IsAny()), Times.Once); consumerMock.VerifyNoOtherCalls(); // act (negative) - var act = async () => await callAsyncMethodFunc(1, message); + var act = async () => await callAsyncMethodFunc(1, message, cancellationToken); // assertion (negative) await act.Should().ThrowAsync(); diff --git a/src/Tests/SlimMessageBus.Host.Test/SampleMessages.cs b/src/Tests/SlimMessageBus.Host.Test/SampleMessages.cs index 41808898..315bd4d4 100644 --- a/src/Tests/SlimMessageBus.Host.Test/SampleMessages.cs +++ b/src/Tests/SlimMessageBus.Host.Test/SampleMessages.cs @@ -23,19 +23,19 @@ public record SomeResponse public class SomeMessageConsumer : IConsumer { - public Task OnHandle(SomeMessage message) + public Task OnHandle(SomeMessage message, CancellationToken cancellationToken) => throw new NotImplementedException(); } public class SomeRequestMessageHandler : IRequestHandler { - public Task OnHandle(SomeRequest request) + public Task OnHandle(SomeRequest request, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(SomeRequest)); } public class SomeRequestWithoutResponseHandler : IRequestHandler { - public Task OnHandle(SomeRequestWithoutResponse request) + public Task OnHandle(SomeRequestWithoutResponse request, CancellationToken cancellationToken) => throw new NotImplementedException(nameof(SomeRequestWithoutResponse)); } From c2cbd1f039017587d7f9f7fdd9c0e9ca56f190ea Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Wed, 21 Aug 2024 00:36:37 +0200 Subject: [PATCH 02/21] [Host.Configuration] Backward compatibility with 2.x Signed-off-by: Tomasz Maruszak --- src/Host.Plugin.Properties.xml | 2 +- .../Builders/ConsumerBuilder.cs | 14 ++++++- .../Builders/HandlerBuilder.cs | 40 ++++++++++++++----- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- .../HandlerBuilderTest.cs | 2 +- .../OutboxTests.cs | 2 +- 9 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index 1228b730..fc8c3714 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc6 + 3.0.0-rc11 \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs index 219d9eda..48589242 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/ConsumerBuilder.cs @@ -8,14 +8,24 @@ public ConsumerBuilder(MessageBusSettings settings, Type messageType = null) ConsumerSettings.ConsumerMode = ConsumerMode.Consumer; } - public ConsumerBuilder Path(string path, Action> pathConfig = null) + public ConsumerBuilder Path(string path) { ConsumerSettings.Path = path; + return this; + } + + public ConsumerBuilder Path(string path, Action> pathConfig) + { + Path(path); pathConfig?.Invoke(this); return this; } - public ConsumerBuilder Topic(string topic, Action> topicConfig = null) => Path(topic, topicConfig); + public ConsumerBuilder Topic(string topic) + => Path(topic); + + public ConsumerBuilder Topic(string topic, Action> topicConfig) + => Path(topic, topicConfig); private static Task DefaultConsumerOnMethod(object consumer, object message, IConsumerContext consumerContext, CancellationToken cancellationToken) => ((IConsumer)consumer).OnHandle((T)message, cancellationToken); diff --git a/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs index 0cb8d5b6..d7573f45 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/HandlerBuilder.cs @@ -14,6 +14,23 @@ protected AbstractHandlerBuilder(MessageBusSettings settings, Type messageType, } protected THandlerBuilder TypedThis => (THandlerBuilder)this; + + /// + /// Configure topic name (or queue name) that incoming requests () are expected on. + /// + /// Topic name + /// + public THandlerBuilder Path(string path) + { + var consumerSettingsExist = Settings.Consumers.Any(x => x.Path == path && x.ConsumerMode == ConsumerMode.RequestResponse && x != ConsumerSettings); + if (consumerSettingsExist) + { + throw new ConfigurationMessageBusException($"Attempted to configure request handler for path '{path}' when one was already configured. There can only be one request handler for a given path."); + } + + ConsumerSettings.Path = path; + return TypedThis; + } /// /// Configure topic name (or queue name) that incoming requests () are expected on. @@ -21,26 +38,29 @@ protected AbstractHandlerBuilder(MessageBusSettings settings, Type messageType, /// Topic name /// /// - public THandlerBuilder Path(string path, Action pathConfig = null) - { - var consumerSettingsExist = Settings.Consumers.Any(x => x.Path == path && x.ConsumerMode == ConsumerMode.RequestResponse && x != ConsumerSettings); - if (consumerSettingsExist) - { - throw new ConfigurationMessageBusException($"Attempted to configure request handler for path '{path}' when one was already configured. There can only be one request handler for a given path (topic/queue)"); - } - - ConsumerSettings.Path = path; + public THandlerBuilder Path(string path, Action pathConfig) + { + Path(path); pathConfig?.Invoke(TypedThis); return TypedThis; } + /// + /// Configure topic name (or queue name) that incoming requests () are expected on. + /// + /// Topic name + /// + public THandlerBuilder Topic(string topic) + => Path(topic); + /// /// Configure topic name (or queue name) that incoming requests () are expected on. /// /// Topic name /// /// - public THandlerBuilder Topic(string topic, Action topicConfig = null) => Path(topic, topicConfig); + public THandlerBuilder Topic(string topic, Action topicConfig) + => Path(topic, topicConfig); public THandlerBuilder Instances(int numberOfInstances) { diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index 210e2384..a1b9b632 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc6 + 3.0.0-rc11 diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index d866a6c8..0bd8e2de 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc6 + 3.0.0-rc11 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index 076a8825..43251dd3 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc6 + 3.0.0-rc11 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index 7715b625..37c72979 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc6 + 3.0.0-rc11 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs index bbb0fbfb..82e95fed 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs @@ -70,7 +70,7 @@ public void When_PathSet_Given_ThePathWasUsedBeforeOnAnotherHandler_Then_Excepti // assert act.Should() .Throw() - .WithMessage($"Attempted to configure request handler for path '*' when one was already configured. There can only be one request handler for a given path (topic/queue)"); + .WithMessage($"Attempted to configure request handler for path '*' when one was already configured. There can only be one request handler for a given path."); } [Theory] diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs index 4f7fe49e..ca7c45f3 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs @@ -310,7 +310,7 @@ public record GenerateCustomerIdCommand(string Firstname, string Lastname) : IRe public class GenerateCustomerIdCommandHandler : IRequestHandler { - public async Task OnHandle(GenerateCustomerIdCommand request, CancellationToken cancellationToken) + public Task OnHandle(GenerateCustomerIdCommand request, CancellationToken cancellationToken) { // Note: This handler will be already wrapped in a transaction: see Program.cs and .UseTransactionScope() / .UseSqlTransaction() From ee36ea4ddf89f2872d6f0a26dda532da0802b3cb Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sun, 6 Oct 2024 15:07:56 +0200 Subject: [PATCH 03/21] [Host.Outbox] Support for UUIDv7 values and MSSQL side ID generation, flexible PK types, refactor Signed-off-by: Tomasz Maruszak --- .github/workflows/build.yml | 4 +- README.md | 4 +- build/tasks.ps1 | 2 +- docs/plugin_outbox.md | 19 +-- docs/plugin_outbox.t.md | 21 +-- docs/provider_kafka.md | 9 +- src/Host.Plugin.Properties.xml | 2 +- src/Samples/Sample.OutboxWebApi/Program.cs | 2 +- .../Sample.OutboxWebApi.csproj | 4 +- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../MessageBusBuilderExtensions.cs | 20 --- .../DbContextOutboxRepository.cs | 24 --- .../DbContextTransactionService.cs | 34 ---- .../MessageBusBuilderExtensions.cs | 28 ++++ .../DbContextOutboxRepository.cs | 29 ++++ .../DbContextTransactionService.cs | 34 ++++ .../GlobalUsings.cs | 8 + ...ssageBus.Host.Outbox.Sql.DbContext.csproj} | 0 .../MessageBusBuilderExtensions.cs | 52 ++++-- .../SqlOutboxMessageIdGenerationMode.cs | 20 +++ .../SqlOutboxMessageIdGenerationSettings.cs | 22 +++ .../Configuration/SqlOutboxSettings.cs | 11 ++ .../ISqlMessageOutboxRepository.cs | 5 + .../ISqlOutboxRepository.cs | 5 - .../SqlTransactionConsumerInterceptor.cs | 2 +- ...ringSqlMessageOutboxRepositoryDecorator.cs | 59 +++++++ .../SqlOutboxMessage.cs | 11 ++ ...itory.cs => SqlOutboxMessageRepository.cs} | 153 ++++++++++++------ .../SqlOutboxMigrationService.cs | 13 +- .../SqlOutboxTemplate.cs | 13 +- .../MessageBusBuilderExtensions.cs | 11 +- .../Configuration/OutboxSettings.cs | 2 +- .../IOutboxNotificationService.cs | 4 +- .../OutboxForwardingPublishInterceptor.cs | 49 +++--- .../Repositories/IHasId.cs | 11 ++ .../Repositories/IOutboxMessageFactory.cs | 26 +++ .../Repositories/IOutboxMessageRepository.cs | 13 ++ .../Repositories/IOutboxRepository.cs | 12 -- .../Repositories/OutboxMessage.cs | 14 +- .../IOutboxLockRenewalTimerFactory.cs | 2 - .../Services/OutboxLockRenewalTimer.cs | 13 +- .../Services/OutboxLockRenewalTimerFactory.cs | 20 +-- .../Services/OutboxSendingTask.cs | 80 ++++----- .../SlimMessageBus.Host.Outbox.csproj | 5 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- .../CommonSqlRepository.cs | 11 +- .../SqlDialect.cs | 2 +- .../AbstractSqlTransactionService.cs | 11 +- .../MessageProcessors/MessageProcessor.cs | 2 +- .../ServiceCollectionExtensions.cs | 7 + src/SlimMessageBus.Host/MessageBusBase.cs | 16 +- .../Providers/GuidGenerator/GuidGenerator.cs | 9 ++ .../Providers/GuidGenerator/IGuidGenerator.cs | 10 ++ .../TimeProvider/CurrentTimeProvider.cs | 6 + .../TimeProvider}/ICurrentTimeProvider.cs | 0 src/SlimMessageBus.sln | 4 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- .../ServiceBusMessageBusTests.cs | 1 + .../KafkaMessageBusTest.cs | 1 + .../MessageBusMock.cs | 15 +- .../MemoryMessageBusTests.cs | 6 + .../BusType.cs | 8 - .../BaseOutboxIntegrationTest.cs | 4 +- .../BusType.cs | 8 + .../DataAccess/Customer.cs | 2 +- .../DataAccess/CustomerContext.cs | 2 +- .../DatabaseFacadeExtensions.cs | 6 +- .../20240617163727_AssemblySchema.Designer.cs | 10 +- .../20240617163727_AssemblySchema.cs | 5 +- .../CustomerContextModelSnapshot.cs | 6 +- .../OutboxBenchmarkTests.cs | 14 +- .../OutboxTests.cs | 70 +++++--- ...Bus.Host.Outbox.Sql.DbContext.Test.csproj} | 2 +- .../TransactionType.cs | 2 +- .../Usings.cs | 3 +- .../appsettings.json | 0 .../BaseSqlOutboxRepositoryTest.cs | 74 +++++---- .../GlobalUsings.cs | 13 +- ...SlimMessageBus.Host.Outbox.Sql.Test.csproj | 18 +-- .../SqlOutboxRepositoryTests.cs | 31 ++-- ...OutboxForwardingPublishInterceptorTests.cs | 93 +++++++---- .../OutboxLockRenewalTimerTests.cs | 16 +- .../Services/OutboxSendingTaskTests.cs | 28 ++-- .../RedisMessageBusTest.cs | 1 + .../CurrentTimeProviderFake.cs | 6 + .../Consumer/MessageBusMock.cs | 4 +- .../Consumer/MessageHandlerTest.cs | 2 +- .../Hybrid/HybridMessageBusTest.cs | 1 + .../MessageBusBaseTests.cs | 12 +- .../MessageBusTested.cs | 9 +- 91 files changed, 919 insertions(+), 512 deletions(-) delete mode 100644 src/SlimMessageBus.Host.Outbox.DbContext/Configuration/MessageBusBuilderExtensions.cs delete mode 100644 src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs delete mode 100644 src/SlimMessageBus.Host.Outbox.DbContext/DbContextTransactionService.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql.DbContext/Configuration/MessageBusBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextOutboxRepository.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextTransactionService.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql.DbContext/GlobalUsings.cs rename src/{SlimMessageBus.Host.Outbox.DbContext/SlimMessageBus.Host.Outbox.DbContext.csproj => SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj} (100%) create mode 100644 src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationMode.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationSettings.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql/ISqlMessageOutboxRepository.cs delete mode 100644 src/SlimMessageBus.Host.Outbox.Sql/ISqlOutboxRepository.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs create mode 100644 src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessage.cs rename src/SlimMessageBus.Host.Outbox.Sql/{SqlOutboxRepository.cs => SqlOutboxMessageRepository.cs} (57%) create mode 100644 src/SlimMessageBus.Host.Outbox/Repositories/IHasId.cs create mode 100644 src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageFactory.cs create mode 100644 src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageRepository.cs delete mode 100644 src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs create mode 100644 src/SlimMessageBus.Host/Providers/GuidGenerator/GuidGenerator.cs create mode 100644 src/SlimMessageBus.Host/Providers/GuidGenerator/IGuidGenerator.cs create mode 100644 src/SlimMessageBus.Host/Providers/TimeProvider/CurrentTimeProvider.cs rename src/SlimMessageBus.Host/{ => Providers/TimeProvider}/ICurrentTimeProvider.cs (100%) delete mode 100644 src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BusType.cs rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/BaseOutboxIntegrationTest.cs (82%) create mode 100644 src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BusType.cs rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/DataAccess/Customer.cs (84%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/DataAccess/CustomerContext.cs (88%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/DatabaseFacadeExtensions.cs (99%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/Migrations/20240617163727_AssemblySchema.Designer.cs (92%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/Migrations/20240617163727_AssemblySchema.cs (90%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/Migrations/CustomerContextModelSnapshot.cs (89%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/OutboxBenchmarkTests.cs (93%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/OutboxTests.cs (82%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj => SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj} (94%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/TransactionType.cs (52%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/Usings.cs (82%) rename src/Tests/{SlimMessageBus.Host.Outbox.DbContext.Test => SlimMessageBus.Host.Outbox.Sql.DbContext.Test}/appsettings.json (100%) create mode 100644 src/Tests/SlimMessageBus.Host.Test.Common/CurrentTimeProviderFake.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df4da77e..45655843 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,7 +103,7 @@ jobs: --verbosity normal \ --logger "trx;LogFilePrefix=Integration" \ --collect:"XPlat Code Coverage;Format=opencover" \ - --filter "Category=Integration" + --filter "Category=Integration&Transport=Outbox" working-directory: ./src env: # Connects to the Azure cloud @@ -185,7 +185,7 @@ jobs: name: .NET Tests path: ./test-results/*.trx reporter: dotnet-trx - fail-on-error: false + fail-on-error: true - name: Copy NuGet packages shell: bash diff --git a/README.md b/README.md index b316062e..0afa8dac 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i | `.Host.AspNetCore` | Integration for ASP.NET Core | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AspNetCore.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AspNetCore) | | `.Host.Interceptor` | Core interface for interceptors | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Interceptor.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Interceptor) | | `.Host.FluentValidation` | Validation for messages based on [FluentValidation](https://www.nuget.org/packages/FluentValidation) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.FluentValidation.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.FluentValidation) | -| `.Host.Outbox.Sql` | Transactional Outbox using SQL | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.Sql.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) | -| `.Host.Outbox.DbContext` | Transactional Outbox using EF DbContext | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.DbContext.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) | +| `.Host.Outbox.Sql` | Transactional Outbox using MSSQL | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.Sql.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) | +| `.Host.Outbox.Sql.DbContext` | Transactional Outbox using MSSQL with EF DataContext integration | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.Sql.DbContext.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) | | `.Host.AsyncApi` | [AsyncAPI](https://www.asyncapi.com/) specification generation via [Saunter](https://github.com/tehmantra/saunter) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AsyncApi.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AsyncApi) | Typically the application layers (domain model, business logic) only need to depend on `SlimMessageBus` which is the facade, and ultimately the application hosting layer (ASP.NET, Console App, Windows Service) will reference and configure the other packages (`SlimMessageBus.Host.*`) which are the messaging transport providers and additional plugins. diff --git a/build/tasks.ps1 b/build/tasks.ps1 index c2e8813a..8eb62a4c 100644 --- a/build/tasks.ps1 +++ b/build/tasks.ps1 @@ -40,7 +40,7 @@ $projects = @( "SlimMessageBus.Host.Outbox", "SlimMessageBus.Host.Outbox.Sql", - "SlimMessageBus.Host.Outbox.DbContext", + "SlimMessageBus.Host.Outbox.Sql.DbContext", "SlimMessageBus.Host.AsyncApi" ) diff --git a/docs/plugin_outbox.md b/docs/plugin_outbox.md index 13fcac99..8b713174 100644 --- a/docs/plugin_outbox.md +++ b/docs/plugin_outbox.md @@ -12,14 +12,15 @@ Please read the [Introduction](intro.md) before reading this provider documentat - [UseTransactionScope](#usetransactionscope) - [UseSqlTransaction](#usesqltransaction) - [How it works](#how-it-works) +- [Important note](#important-note) ## Introduction The [`Host.Outbox`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox) introduces [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) pattern to the SlimMessageBus. It comes in two flavors: -- [`Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) as integration with the System.Data.Sql client -- [`Host.Outbox.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) as integration with Entity Framework Core +- [`Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) as integration with the System.Data.Sql client (MSSQL) +- [`Host.Outbox.Sql.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) as integration with Entity Framework Core Outbox plugin can work in combination with any transport provider. @@ -27,15 +28,15 @@ Outbox plugin can work in combination with any transport provider. ### Entity Framework -> Required: [`SlimMessageBus.Host.Outbox.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) +> Required: [`SlimMessageBus.Host.Outbox.Sql.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) ```cs -using SlimMessageBus.Host.Outbox.DbContext; +using SlimMessageBus.Host.Outbox.Sql.DbContext; ``` Consider the following example (from [Samples](../src/Samples/Sample.OutboxWebApi/Program.cs)): -- `services.AddOutboxUsingDbContext(...)` is used to add the [Outbox.DbContext](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) plugin to the container. +- `services.AddOutboxUsingDbContext(...)` is used to add the [Outbox.DbContext](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) plugin to the container. - `CustomerContext` is the application specific Entity Framework `DbContext`. - `CustomerCreatedEvent` is produced on the `AzureSB` child bus, the bus will deliver these events via outbox - see `.UseOutbox()` - `CreateCustomerCommand` is consumed on the `Memory` child bus, each command is wrapped in an SQL transaction - see `UseSqlTransaction()` @@ -169,10 +170,10 @@ There are two types of transaction support: #### UseTransactionScope -> Required: [`Host.Outbox`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox) +> Required: [`SlimMessageBus.Host.Outbox`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox) ```cs -using SlimMessageBus.Host.Outbox.Sql; +using SlimMessageBus.Host.Outbox; ``` `.UseTransactionScope()` can be used on consumers (or handlers) declaration to force the consumer to start a `TransactionScope` prior the message `OnHandle` and to complete that transaction after it. Any exception raised by the consumer would cause the transaction to be rolled back. @@ -181,7 +182,7 @@ When applied on the (child) bus level then all consumers (or handlers) will inhe #### UseSqlTransaction -> Required: [`SlimMessageBus.Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) or [`SlimMessageBus.Host.Outbox.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) +> Required: [`SlimMessageBus.Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) or [`SlimMessageBus.Host.Outbox.Sql.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) ```cs using SlimMessageBus.Host.Outbox.Sql; @@ -218,6 +219,6 @@ When applied on the (child) bus level then all consumers (or handlers) will inhe ## Important note -As the outbox can be processed by instance of the application that did not originally process it, it is important to ensure that all active instances maintian the same message registrations (and compatible JSON schema definitions). +As the outbox can be processed by instance of the application that did not originally process it, it is important to ensure that all active instances maintain the same message registrations (and compatible JSON schema definitions). A message that fails to deserialize will be flagged as invalid by setting the associated `DeliveryAborted` field in the `Outbox` table, to `1`. It is safe to manually reset this field value to `0` once the version incompatibility has been resolved. \ No newline at end of file diff --git a/docs/plugin_outbox.t.md b/docs/plugin_outbox.t.md index fa7dd5db..b9df20d1 100644 --- a/docs/plugin_outbox.t.md +++ b/docs/plugin_outbox.t.md @@ -12,14 +12,15 @@ Please read the [Introduction](intro.md) before reading this provider documentat - [UseTransactionScope](#usetransactionscope) - [UseSqlTransaction](#usesqltransaction) - [How it works](#how-it-works) +- [Important note](#important-note) ## Introduction The [`Host.Outbox`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox) introduces [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) pattern to the SlimMessageBus. It comes in two flavors: -- [`Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) as integration with the System.Data.Sql client -- [`Host.Outbox.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) as integration with Entity Framework Core +- [`Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) as integration with the System.Data.Sql client (MSSQL) +- [`Host.Outbox.Sql.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) as integration with Entity Framework Core Outbox plugin can work in combination with any transport provider. @@ -27,15 +28,15 @@ Outbox plugin can work in combination with any transport provider. ### Entity Framework -> Required: [`SlimMessageBus.Host.Outbox.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) +> Required: [`SlimMessageBus.Host.Outbox.Sql.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) ```cs -using SlimMessageBus.Host.Outbox.DbContext; +using SlimMessageBus.Host.Outbox.Sql.DbContext; ``` Consider the following example (from [Samples](../src/Samples/Sample.OutboxWebApi/Program.cs)): -- `services.AddOutboxUsingDbContext(...)` is used to add the [Outbox.DbContext](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) plugin to the container. +- `services.AddOutboxUsingDbContext(...)` is used to add the [Outbox.DbContext](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) plugin to the container. - `CustomerContext` is the application specific Entity Framework `DbContext`. - `CustomerCreatedEvent` is produced on the `AzureSB` child bus, the bus will deliver these events via outbox - see `.UseOutbox()` - `CreateCustomerCommand` is consumed on the `Memory` child bus, each command is wrapped in an SQL transaction - see `UseSqlTransaction()` @@ -100,10 +101,10 @@ There are two types of transaction support: #### UseTransactionScope -> Required: [`Host.Outbox`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox) +> Required: [`SlimMessageBus.Host.Outbox`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox) ```cs -using SlimMessageBus.Host.Outbox.Sql; +using SlimMessageBus.Host.Outbox; ``` `.UseTransactionScope()` can be used on consumers (or handlers) declaration to force the consumer to start a `TransactionScope` prior the message `OnHandle` and to complete that transaction after it. Any exception raised by the consumer would cause the transaction to be rolled back. @@ -112,7 +113,7 @@ When applied on the (child) bus level then all consumers (or handlers) will inhe #### UseSqlTransaction -> Required: [`SlimMessageBus.Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) or [`SlimMessageBus.Host.Outbox.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) +> Required: [`SlimMessageBus.Host.Outbox.Sql`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) or [`SlimMessageBus.Host.Outbox.Sql.DbContext`](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) ```cs using SlimMessageBus.Host.Outbox.Sql; @@ -149,6 +150,6 @@ When applied on the (child) bus level then all consumers (or handlers) will inhe ## Important note -As the outbox can be processed by instance of the application that did not originally process it, it is important to ensure that all active instances maintian the same message registrations (and compatible JSON schema definitions). +As the outbox can be processed by instance of the application that did not originally process it, it is important to ensure that all active instances maintain the same message registrations (and compatible JSON schema definitions). -A message that fails to deserialize will be flagged as invalid by setting the associated `DeliveryAborted` field in the `Outbox` table, to `1`. It is safe to manually reset this field value to `0` once the version incompatibility has been resolved. \ No newline at end of file +A message that fails to deserialize will be flagged as invalid by setting the associated `DeliveryAborted` field in the `Outbox` table, to `1`. It is safe to manually reset this field value to `0` once the version incompatibility has been resolved. diff --git a/docs/provider_kafka.md b/docs/provider_kafka.md index 2d994b29..14c22f51 100644 --- a/docs/provider_kafka.md +++ b/docs/provider_kafka.md @@ -29,7 +29,7 @@ When troubleshooting or fine tuning it is worth reading the `librdkafka` and `co ## Configuration properties -Producer, consumer and global configuration properties are described [here](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md). +Producer, consumer and global configuration properties are described [here](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md). The configuration on the underlying Kafka client can be adjusted like so: ```cs @@ -53,7 +53,7 @@ services.AddSlimMessageBus(mbb => ### Minimizing message latency -There is a good description [here](https://github.com/edenhill/librdkafka/wiki/How-to-decrease-message-latency) on improving the latency by applying producer/consumer settings on librdkafka. Here is how you enter the settings using SlimMessageBus: +There is a good description [here](https://github.com/confluentinc/librdkafka/wiki/How-to-decrease-message-latency) on improving the latency by applying producer/consumer settings on librdkafka. Here is how you enter the settings using SlimMessageBus: ```cs services.AddSlimMessageBus(mbb => @@ -176,7 +176,7 @@ public class PingConsumer : IConsumer, IConsumerWithContext { public IConsumerContext Context { get; set; } - public Task OnHandle(PingMessage message, CancellationToken cancellationToken) + public Task OnHandle(PingMessage message) { // SMB Kafka transport specific extension: var transportMessage = Context.GetTransportMessage(); @@ -211,7 +211,8 @@ mbb ### Offset Commit -In the current Kafka provider implementation, SMB handles the manual commit of topic-partition offsets for the consumer. This configuration is controlled through the following methods on the consumer builder: +In the current Kafka provider implementation, SMB handles the manual commit of topic-partition offsets for the consumer.Th +is configuration is controlled through the following methods on the consumer builder: - `CheckpointEvery(int)` – Commits the offset after a specified number of processed messages. - `CheckpointAfter(TimeSpan)` – Commits the offset after a specified time interval. diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index fc8c3714..6a16e12a 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc11 + 3.0.0-rc12 \ No newline at end of file diff --git a/src/Samples/Sample.OutboxWebApi/Program.cs b/src/Samples/Sample.OutboxWebApi/Program.cs index 87e3509e..4ac65808 100644 --- a/src/Samples/Sample.OutboxWebApi/Program.cs +++ b/src/Samples/Sample.OutboxWebApi/Program.cs @@ -12,8 +12,8 @@ using SlimMessageBus.Host.AzureServiceBus; using SlimMessageBus.Host.Memory; using SlimMessageBus.Host.Outbox; -using SlimMessageBus.Host.Outbox.DbContext; using SlimMessageBus.Host.Outbox.Sql; +using SlimMessageBus.Host.Outbox.Sql.DbContext; using SlimMessageBus.Host.Serialization.Json; // Local file with secrets diff --git a/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj b/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj index f6d3de10..c4ef505f 100644 --- a/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj +++ b/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj @@ -20,9 +20,7 @@ - - - + diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index f6114bee..64248c7a 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc11 + 3.0.0-rc12 diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index 0bd8e2de..1b44979d 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc11 + 3.0.0-rc12 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Outbox.DbContext/Configuration/MessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.Outbox.DbContext/Configuration/MessageBusBuilderExtensions.cs deleted file mode 100644 index 2ee8bbc0..00000000 --- a/src/SlimMessageBus.Host.Outbox.DbContext/Configuration/MessageBusBuilderExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace SlimMessageBus.Host.Outbox.DbContext; - -using Microsoft.Extensions.DependencyInjection.Extensions; - -using SlimMessageBus.Host; -using SlimMessageBus.Host.Outbox.Sql; -using SlimMessageBus.Host.Sql.Common; - -public static class MessageBusBuilderExtensions -{ - public static MessageBusBuilder AddOutboxUsingDbContext(this MessageBusBuilder mbb, Action configure) - where TDbContext : Microsoft.EntityFrameworkCore.DbContext - { - mbb.PostConfigurationActions.Add(services => - { - services.TryAddScoped>(); - }); - return mbb.AddOutboxUsingSql>(configure); - } -} diff --git a/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs deleted file mode 100644 index d9deb1b7..00000000 --- a/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace SlimMessageBus.Host.Outbox.DbContext; - -using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using SlimMessageBus.Host.Outbox.Sql; -using SlimMessageBus.Host.Sql.Common; - -public class DbContextOutboxRepository : SqlOutboxRepository where TDbContext : DbContext -{ - public TDbContext DbContext { get; } - - public DbContextOutboxRepository( - ILogger logger, - SqlOutboxSettings settings, - SqlOutboxTemplate sqlOutboxTemplate, - TDbContext dbContext, - ISqlTransactionService transactionService) - : base(logger, settings, sqlOutboxTemplate, (SqlConnection)dbContext.Database.GetDbConnection(), transactionService) - { - DbContext = dbContext; - } -} diff --git a/src/SlimMessageBus.Host.Outbox.DbContext/DbContextTransactionService.cs b/src/SlimMessageBus.Host.Outbox.DbContext/DbContextTransactionService.cs deleted file mode 100644 index 1a1ade5f..00000000 --- a/src/SlimMessageBus.Host.Outbox.DbContext/DbContextTransactionService.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace SlimMessageBus.Host.Outbox.DbContext; - -using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; - -using SlimMessageBus.Host.Sql.Common; - -public class DbContextTransactionService(TDbContext dbContext, ISqlSettings sqlSettings) - : AbstractSqlTransactionService((SqlConnection)dbContext.Database.GetDbConnection()) - where TDbContext : DbContext -{ - public TDbContext DbContext { get; } = dbContext; - - public override SqlTransaction CurrentTransaction => (SqlTransaction)DbContext.Database.CurrentTransaction?.GetDbTransaction(); - - protected override Task OnBeginTransaction() - { - return DbContext.Database.BeginTransactionAsync(sqlSettings.TransactionIsolationLevel); - } - - protected override Task OnCompleteTransaction(bool transactionFailed) - { - if (transactionFailed) - { - DbContext.Database.RollbackTransaction(); - } - else - { - DbContext.Database.CommitTransaction(); - } - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql.DbContext/Configuration/MessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/Configuration/MessageBusBuilderExtensions.cs new file mode 100644 index 00000000..1407132c --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/Configuration/MessageBusBuilderExtensions.cs @@ -0,0 +1,28 @@ +namespace SlimMessageBus.Host.Outbox.Sql.DbContext; + +public static class MessageBusBuilderExtensions +{ + public static MessageBusBuilder AddOutboxUsingDbContext(this MessageBusBuilder mbb, Action configure) + where TDbContext : Microsoft.EntityFrameworkCore.DbContext + { + mbb.PostConfigurationActions.Add(services => + { + services.TryAddScoped>(); + services.TryAddScoped(svp => + { + var settings = svp.GetRequiredService(); + return new DbContextOutboxRepository( + svp.GetRequiredService>>(), + settings, + svp.GetRequiredService(), + settings.IdGeneration.GuidGenerator ?? (IGuidGenerator)svp.GetRequiredService(settings.IdGeneration.GuidGeneratorType), + svp.GetRequiredService(), + svp.GetRequiredService(), + svp.GetRequiredService(), + svp.GetRequiredService() + ); + }); + }); + return mbb.AddOutboxUsingSql>(configure); + } +} diff --git a/src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextOutboxRepository.cs new file mode 100644 index 00000000..6c373f07 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextOutboxRepository.cs @@ -0,0 +1,29 @@ +namespace SlimMessageBus.Host.Outbox.Sql.DbContext; + +public class DbContextOutboxRepository : SqlOutboxMessageRepository + where TDbContext : Microsoft.EntityFrameworkCore.DbContext +{ + public TDbContext DbContext { get; } + + public DbContextOutboxRepository( + ILogger> logger, + SqlOutboxSettings settings, + SqlOutboxTemplate sqlOutboxTemplate, + IGuidGenerator guidGenerator, + ICurrentTimeProvider currentTimeProvider, + IInstanceIdProvider instanceIdProvider, + TDbContext dbContext, + ISqlTransactionService transactionService) + : base( + logger, + settings, + sqlOutboxTemplate, + guidGenerator, + currentTimeProvider, + instanceIdProvider, + (SqlConnection)dbContext.Database.GetDbConnection(), + transactionService) + { + DbContext = dbContext; + } +} diff --git a/src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextTransactionService.cs b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextTransactionService.cs new file mode 100644 index 00000000..5bda0ecc --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/DbContextTransactionService.cs @@ -0,0 +1,34 @@ +namespace SlimMessageBus.Host.Outbox.Sql.DbContext; + +public class DbContextTransactionService(TDbContext dbContext, ISqlSettings sqlSettings) + : AbstractSqlTransactionService((SqlConnection)dbContext.Database.GetDbConnection()) + where TDbContext : Microsoft.EntityFrameworkCore.DbContext +{ + private IDbContextTransaction _currentTransaction; + + public TDbContext DbContext { get; } = dbContext; + + public override SqlTransaction CurrentTransaction => _currentTransaction?.GetDbTransaction() as SqlTransaction; + + protected override async Task OnBeginTransaction() + { + _currentTransaction = await DbContext.Database.BeginTransactionAsync(sqlSettings.TransactionIsolationLevel); + } + + protected override async Task OnCompleteTransaction(bool transactionFailed) + { + if (_currentTransaction != null) + { + if (transactionFailed) + { + await _currentTransaction.RollbackAsync(); + } + else + { + await _currentTransaction.CommitAsync(); + } + _currentTransaction.Dispose(); + _currentTransaction = null; + } + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql.DbContext/GlobalUsings.cs b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/GlobalUsings.cs new file mode 100644 index 00000000..1fb51166 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using Microsoft.Data.SqlClient; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Storage; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; + +global using SlimMessageBus.Host.Sql.Common; diff --git a/src/SlimMessageBus.Host.Outbox.DbContext/SlimMessageBus.Host.Outbox.DbContext.csproj b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj similarity index 100% rename from src/SlimMessageBus.Host.Outbox.DbContext/SlimMessageBus.Host.Outbox.DbContext.csproj rename to src/SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs index 19676168..eacf2e76 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs @@ -3,10 +3,8 @@ public static class MessageBusBuilderExtensions { public static MessageBusBuilder AddOutboxUsingSql(this MessageBusBuilder mbb, Action configure) - where TOutboxRepository : class, ISqlOutboxRepository + where TOutboxRepository : class, ISqlMessageOutboxRepository { - mbb.AddOutbox(); - mbb.PostConfigurationActions.Add(services => { var settings = new[] { mbb.Settings }.Concat(mbb.Children.Values.Select(x => x.Settings)).ToList(); @@ -17,8 +15,7 @@ public static MessageBusBuilder AddOutboxUsingSql(this Messag configure?.Invoke(settings); return settings; }); - services.Replace(ServiceDescriptor.Transient(svp => svp.GetRequiredService())); - + services.TryAddTransient(svp => svp.GetRequiredService()); services.TryAddSingleton(svp => svp.GetRequiredService().SqlSettings); // Optimization: only register generic interceptors in the DI for particular message types that have opted in for transaction scope @@ -33,18 +30,51 @@ public static MessageBusBuilder AddOutboxUsingSql(this Messag services.TryAddEnumerable(ServiceDescriptor.Transient(serviceType, implementationType)); } - services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(svp => + { + var outboxRepository = svp.GetRequiredService(); + var settings = svp.GetRequiredService(); + if (settings.MeasureSqlOperations) + { + return new MeasuringSqlMessageOutboxRepositoryDecorator(outboxRepository, svp.GetRequiredService>()); + } + return outboxRepository; + }); - services.Replace(ServiceDescriptor.Scoped(svp => svp.GetRequiredService())); - services.Replace(ServiceDescriptor.Scoped(svp => svp.GetRequiredService())); + services.TryAddScoped(svp => svp.GetRequiredService()); + services.TryAddScoped>(svp => svp.GetRequiredService()); services.TryAddSingleton(); - services.TryAddTransient(); + services.TryAddTransient(svp => new SqlOutboxMigrationService( + svp.GetRequiredService>(), + svp.GetRequiredService(), + svp.GetRequiredService(), + svp.GetRequiredService())); }); - return mbb; + return mbb.AddOutbox(); } public static MessageBusBuilder AddOutboxUsingSql(this MessageBusBuilder mbb, Action configure) - => mbb.AddOutboxUsingSql(configure); + { + mbb.PostConfigurationActions.Add(services => + { + services.TryAddScoped(svp => + { + var settings = svp.GetRequiredService(); + + return new SqlOutboxMessageRepository( + svp.GetRequiredService>(), + settings, + svp.GetRequiredService(), + settings.IdGeneration.GuidGenerator ?? (IGuidGenerator)svp.GetRequiredService(settings.IdGeneration.GuidGeneratorType), + svp.GetRequiredService(), + svp.GetRequiredService(), + svp.GetRequiredService(), + svp.GetRequiredService() + ); + }); + }); + return mbb.AddOutboxUsingSql(configure); + } } diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationMode.cs b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationMode.cs new file mode 100644 index 00000000..f398e349 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationMode.cs @@ -0,0 +1,20 @@ +namespace SlimMessageBus.Host.Outbox.Sql; + +public enum SqlOutboxMessageIdGenerationMode +{ + /// + /// The database is responsible for generating the message id, using NEWID(). + /// See https://learn.microsoft.com/en-us/sql/t-sql/functions/newid-transact-sql?view=sql-server-ver16 + /// + DatabaseGeneratedGuid, + /// + /// The database is responsible for generating the message id, using NEWSEQUENTIALID(). + /// See https://learn.microsoft.com/en-us/sql/t-sql/functions/newsequentialid-transact-sql?view=sql-server-ver16 + /// + DatabaseGeneratedSequentialGuid, + /// + /// The client is responsible for generating the message id, using . + /// + ClientGuidGenerator, +} + diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationSettings.cs b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationSettings.cs new file mode 100644 index 00000000..331d378d --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxMessageIdGenerationSettings.cs @@ -0,0 +1,22 @@ +namespace SlimMessageBus.Host.Outbox.Sql; + +public class SqlOutboxMessageIdGenerationSettings +{ + /// + /// The mode to use for generating the . + /// + public SqlOutboxMessageIdGenerationMode Mode { get; set; } = SqlOutboxMessageIdGenerationMode.ClientGuidGenerator; + /// + /// The type to resolve from MSDI that implementes the . + /// Default is . + /// Guid generator is used to generate unique identifiers for the outbox messages. + /// + public Type GuidGeneratorType { get; set; } = typeof(IGuidGenerator); + /// + /// The instance of to use (if specified). + /// Default is null. + /// Guid generator is used to generate unique identifiers for the outbox messages. + /// + public IGuidGenerator GuidGenerator { get; set; } = null; +} + diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs index cd6be80c..487a5813 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs @@ -3,4 +3,15 @@ public class SqlOutboxSettings : OutboxSettings { public SqlSettings SqlSettings { get; set; } = new(); + + /// + /// Control how the is being generated. + /// + public SqlOutboxMessageIdGenerationSettings IdGeneration { get; set; } = new(); + + /// + /// When enabled the SQL operation execution time will be measured and logged. + /// This will cause a bit of overhead, so it should be used for debugging purposes only. + /// + public bool MeasureSqlOperations { get; set; } = false; } diff --git a/src/SlimMessageBus.Host.Outbox.Sql/ISqlMessageOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.Sql/ISqlMessageOutboxRepository.cs new file mode 100644 index 00000000..5696fa9b --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql/ISqlMessageOutboxRepository.cs @@ -0,0 +1,5 @@ +namespace SlimMessageBus.Host.Outbox.Sql; + +public interface ISqlMessageOutboxRepository : IOutboxMessageRepository, IOutboxMessageFactory +{ +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql/ISqlOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.Sql/ISqlOutboxRepository.cs deleted file mode 100644 index cb124ccb..00000000 --- a/src/SlimMessageBus.Host.Outbox.Sql/ISqlOutboxRepository.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace SlimMessageBus.Host.Outbox.Sql; - -public interface ISqlOutboxRepository : IOutboxRepository -{ -} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs b/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs index 083e6f2e..57fed3a6 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs @@ -4,7 +4,7 @@ public abstract class SqlTransactionConsumerInterceptor { } -/// +/// s /// Wraps the consumer in an (conditionally). /// /// diff --git a/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs b/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs new file mode 100644 index 00000000..de2d5f05 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs @@ -0,0 +1,59 @@ +namespace SlimMessageBus.Host.Outbox.Sql; + +using System.Diagnostics; + +public class MeasuringSqlMessageOutboxRepositoryDecorator( + ISqlMessageOutboxRepository target, + ILogger logger) + : ISqlMessageOutboxRepository +{ + private async Task MeasureMethod(string name, Func> action) + { + var sw = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + LogTime(name, sw); + } + } + + private void LogTime(string name, Stopwatch sw) + => logger.LogInformation("Method {MethodName} took {Elapsed}", name, sw.Elapsed); + + private async Task MeasureMethod(string name, Func action) + { + var sw = Stopwatch.StartNew(); + try + { + await action(); + } + finally + { + LogTime(name, sw); + } + } + + public Task AbortDelivery(IReadOnlyCollection ids, CancellationToken cancellationToken) + => MeasureMethod(nameof(AbortDelivery), () => target.AbortDelivery(ids, cancellationToken)); + + public Task Create(string busName, IDictionary headers, string path, string messageType, byte[] messagePayload, CancellationToken cancellationToken) + => MeasureMethod(nameof(Create), () => target.Create(busName, headers, path, messageType, messagePayload, cancellationToken)); + + public Task DeleteSent(DateTime olderThan, CancellationToken cancellationToken) + => MeasureMethod(nameof(DeleteSent), () => target.DeleteSent(olderThan, cancellationToken)); + + public Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int maxDeliveryAttempts, CancellationToken cancellationToken) + => MeasureMethod(nameof(IncrementDeliveryAttempt), () => target.IncrementDeliveryAttempt(ids, maxDeliveryAttempts, cancellationToken)); + + public Task> LockAndSelect(string instanceId, int batchSize, bool tableLock, TimeSpan lockDuration, CancellationToken cancellationToken) + => MeasureMethod(nameof(LockAndSelect), () => target.LockAndSelect(instanceId, batchSize, tableLock, lockDuration, cancellationToken)); + + public Task RenewLock(string instanceId, TimeSpan lockDuration, CancellationToken cancellationToken) + => MeasureMethod(nameof(RenewLock), () => target.RenewLock(instanceId, lockDuration, cancellationToken)); + + public Task UpdateToSent(IReadOnlyCollection ids, CancellationToken cancellationToken) + => MeasureMethod(nameof(UpdateToSent), () => target.UpdateToSent(ids, cancellationToken)); +} diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessage.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessage.cs new file mode 100644 index 00000000..ce7e4bda --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessage.cs @@ -0,0 +1,11 @@ +namespace SlimMessageBus.Host.Outbox.Sql; + +public class SqlOutboxMessage : OutboxMessage +{ + public DateTime Timestamp { get; set; } + public string LockInstanceId { get; set; } = string.Empty; + public DateTime? LockExpiresOn { get; set; } = null; + public int DeliveryAttempt { get; set; } = 0; + public bool DeliveryComplete { get; set; } = false; + public bool DeliveryAborted { get; set; } = false; +} diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessageRepository.cs similarity index 57% rename from src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs rename to src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessageRepository.cs index 6501bb99..091128eb 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMessageRepository.cs @@ -1,45 +1,96 @@ namespace SlimMessageBus.Host.Outbox.Sql; -public class SqlOutboxRepository : CommonSqlRepository, ISqlOutboxRepository +/// +/// The MS SQL implmentation of the +/// +public class SqlOutboxMessageRepository : CommonSqlRepository, ISqlMessageOutboxRepository { + /// + /// Used to serialize the headers dictionary to JSON + /// + private static readonly JsonSerializerOptions _jsonOptions = new() + { + Converters = { new ObjectToInferredTypesConverter() } + }; + + private static readonly DateTime _defaultExpiresOn = new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private readonly SqlOutboxTemplate _sqlTemplate; - private readonly JsonSerializerOptions _jsonOptions; + private readonly IGuidGenerator _guidGenerator; + private readonly ICurrentTimeProvider _currentTimeProvider; + private readonly IInstanceIdProvider _instanceIdProvider; + private readonly bool _idDatabaseGenerated; protected SqlOutboxSettings Settings { get; } - public SqlOutboxRepository(ILogger logger, SqlOutboxSettings settings, SqlOutboxTemplate sqlOutboxTemplate, SqlConnection connection, ISqlTransactionService transactionService) + public SqlOutboxMessageRepository( + ILogger logger, + SqlOutboxSettings settings, + SqlOutboxTemplate sqlOutboxTemplate, + IGuidGenerator guidGenerator, + ICurrentTimeProvider currentTimeProvider, + IInstanceIdProvider instanceIdProvider, + SqlConnection connection, + ISqlTransactionService transactionService) : base(logger, settings.SqlSettings, connection, transactionService) { _sqlTemplate = sqlOutboxTemplate; - _jsonOptions = new(); - _jsonOptions.Converters.Add(new ObjectToInferredTypesConverter()); - + _guidGenerator = guidGenerator; + _currentTimeProvider = currentTimeProvider; + _instanceIdProvider = instanceIdProvider; + _idDatabaseGenerated = settings.IdGeneration.Mode == SqlOutboxMessageIdGenerationMode.DatabaseGeneratedSequentialGuid; Settings = settings; } - public async virtual Task Save(OutboxMessage message, CancellationToken token) + public virtual async Task Create(string busName, IDictionary headers, string path, string messageType, byte[] messagePayload, CancellationToken cancellationToken) { + var om = new SqlOutboxMessage + { + Timestamp = _currentTimeProvider.CurrentTime.DateTime, + InstanceId = _instanceIdProvider.GetInstanceId(), + + BusName = busName, + Headers = headers, + Path = path, + MessageType = messageType, + MessagePayload = messagePayload, + }; + + if (!_idDatabaseGenerated) + { + om.Id = _guidGenerator.NewGuid(); + } + + var template = _idDatabaseGenerated + ? _sqlTemplate.SqlOutboxMessageInsertWithDatabaseId + : _sqlTemplate.SqlOutboxMessageInsertWithClientId; + await EnsureConnection(); - await ExecuteNonQuery(Settings.SqlSettings.OperationRetry, _sqlTemplate.SqlOutboxMessageInsert, cmd => + om.Id = (Guid)await ExecuteScalarAsync(Settings.SqlSettings.OperationRetry, template, cmd => { - cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = message.Id; - cmd.Parameters.Add("@Timestamp", SqlDbType.DateTime2).Value = message.Timestamp; - cmd.Parameters.Add("@BusName", SqlDbType.NVarChar).Value = message.BusName; - cmd.Parameters.Add("@MessageType", SqlDbType.NVarChar).Value = message.MessageType; - cmd.Parameters.Add("@MessagePayload", SqlDbType.VarBinary).Value = message.MessagePayload; - cmd.Parameters.Add("@Headers", SqlDbType.NVarChar).Value = message.Headers != null ? JsonSerializer.Serialize(message.Headers, _jsonOptions) : DBNull.Value; - cmd.Parameters.Add("@Path", SqlDbType.NVarChar).Value = message.Path; - cmd.Parameters.Add("@InstanceId", SqlDbType.NVarChar).Value = message.InstanceId; - cmd.Parameters.Add("@LockInstanceId", SqlDbType.NVarChar).Value = message.LockInstanceId; - cmd.Parameters.Add("@LockExpiresOn", SqlDbType.DateTime2).Value = message.LockExpiresOn ?? new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); - cmd.Parameters.Add("@DeliveryAttempt", SqlDbType.Int).Value = message.DeliveryAttempt; - cmd.Parameters.Add("@DeliveryComplete", SqlDbType.Bit).Value = message.DeliveryComplete; - cmd.Parameters.Add("@DeliveryAborted", SqlDbType.Bit).Value = message.DeliveryAborted; - }, token); + if (!_idDatabaseGenerated) + { + cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = om.Id; + } + cmd.Parameters.Add("@Timestamp", SqlDbType.DateTime2).Value = om.Timestamp; + cmd.Parameters.Add("@BusName", SqlDbType.NVarChar).Value = om.BusName; + cmd.Parameters.Add("@MessageType", SqlDbType.NVarChar).Value = om.MessageType; + cmd.Parameters.Add("@MessagePayload", SqlDbType.VarBinary).Value = om.MessagePayload; + cmd.Parameters.Add("@Headers", SqlDbType.NVarChar).Value = om.Headers != null ? JsonSerializer.Serialize(om.Headers, _jsonOptions) : DBNull.Value; + cmd.Parameters.Add("@Path", SqlDbType.NVarChar).Value = om.Path; + cmd.Parameters.Add("@InstanceId", SqlDbType.NVarChar).Value = om.InstanceId; + cmd.Parameters.Add("@LockInstanceId", SqlDbType.NVarChar).Value = om.LockInstanceId; + cmd.Parameters.Add("@LockExpiresOn", SqlDbType.DateTime2).Value = om.LockExpiresOn ?? _defaultExpiresOn; + cmd.Parameters.Add("@DeliveryAttempt", SqlDbType.Int).Value = om.DeliveryAttempt; + cmd.Parameters.Add("@DeliveryComplete", SqlDbType.Bit).Value = om.DeliveryComplete; + cmd.Parameters.Add("@DeliveryAborted", SqlDbType.Bit).Value = om.DeliveryAborted; + }, cancellationToken); + + return om; } - public async Task> LockAndSelect(string instanceId, int batchSize, bool tableLock, TimeSpan lockDuration, CancellationToken token) + public async Task> LockAndSelect(string instanceId, int batchSize, bool tableLock, TimeSpan lockDuration, CancellationToken cancellationToken) { await EnsureConnection(); @@ -49,10 +100,10 @@ public async Task> LockAndSelect(string insta cmd.Parameters.Add("@BatchSize", SqlDbType.Int).Value = batchSize; cmd.Parameters.Add("@LockDuration", SqlDbType.Int).Value = lockDuration.TotalSeconds; - return await ReadMessages(cmd, token).ConfigureAwait(false); + return await ReadMessages(cmd, cancellationToken).ConfigureAwait(false); } - public async Task AbortDelivery(IReadOnlyCollection ids, CancellationToken token) + public async Task AbortDelivery(IReadOnlyCollection ids, CancellationToken cancellationToken) { if (ids.Count == 0) { @@ -67,7 +118,7 @@ public async Task AbortDelivery(IReadOnlyCollection ids, CancellationToken { cmd.Parameters.AddWithValue("@Ids", ToIdsString(ids)); }, - token: token); + token: cancellationToken); if (affected != ids.Count) { @@ -75,7 +126,7 @@ public async Task AbortDelivery(IReadOnlyCollection ids, CancellationToken } } - public async Task UpdateToSent(IReadOnlyCollection ids, CancellationToken token) + public async Task UpdateToSent(IReadOnlyCollection ids, CancellationToken cancellationToken) { if (ids.Count == 0) { @@ -90,7 +141,7 @@ public async Task UpdateToSent(IReadOnlyCollection ids, CancellationToken { cmd.Parameters.AddWithValue("@Ids", ToIdsString(ids)); }, - token: token); + token: cancellationToken); if (affected != ids.Count) { @@ -100,7 +151,7 @@ public async Task UpdateToSent(IReadOnlyCollection ids, CancellationToken private string ToIdsString(IReadOnlyCollection ids) => string.Join(_sqlTemplate.InIdsSeparator, ids); - public async Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int maxDeliveryAttempts, CancellationToken token) + public async Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int maxDeliveryAttempts, CancellationToken cancellationToken) { if (ids.Count == 0) { @@ -121,7 +172,7 @@ public async Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int ma cmd.Parameters.AddWithValue("@Ids", ToIdsString(ids)); cmd.Parameters.AddWithValue("@MaxDeliveryAttempts", maxDeliveryAttempts); }, - token: token); + token: cancellationToken); if (affected != ids.Count) { @@ -129,20 +180,20 @@ public async Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int ma } } - public async Task DeleteSent(DateTime olderThan, CancellationToken token) + public async Task DeleteSent(DateTime olderThan, CancellationToken cancellationToken) { await EnsureConnection(); var affected = await ExecuteNonQuery( Settings.SqlSettings.OperationRetry, _sqlTemplate.SqlOutboxMessageDeleteSent, - cmd => cmd.Parameters.Add("@Timestamp", SqlDbType.DateTime2).Value = olderThan, - token); + cmd => cmd.Parameters.Add("@Timestamp", SqlDbType.DateTimeOffset).Value = olderThan, + cancellationToken); Logger.Log(affected > 0 ? LogLevel.Information : LogLevel.Debug, "Removed {MessageCount} sent messages from outbox table", affected); } - public async Task RenewLock(string instanceId, TimeSpan lockDuration, CancellationToken token) + public async Task RenewLock(string instanceId, TimeSpan lockDuration, CancellationToken cancellationToken) { await EnsureConnection(); @@ -151,10 +202,10 @@ public async Task RenewLock(string instanceId, TimeSpan lockDuration, Canc cmd.Parameters.Add("@InstanceId", SqlDbType.NVarChar).Value = instanceId; cmd.Parameters.Add("@LockDuration", SqlDbType.Int).Value = lockDuration.TotalSeconds; - return await cmd.ExecuteNonQueryAsync(token) > 0; + return await cmd.ExecuteNonQueryAsync(cancellationToken) > 0; } - internal async Task> GetAllMessages(CancellationToken cancellationToken) + internal async Task> GetAllMessages(CancellationToken cancellationToken) { await EnsureConnection(); @@ -164,7 +215,7 @@ internal async Task> GetAllMessages(Cancellat return await ReadMessages(cmd, cancellationToken).ConfigureAwait(false); } - private async Task> ReadMessages(SqlCommand cmd, CancellationToken cancellationToken) + private static async Task> ReadMessages(SqlCommand cmd, CancellationToken cancellationToken) { using var reader = await cmd.ExecuteReaderAsync(cancellationToken); @@ -182,23 +233,33 @@ private async Task> ReadMessages(SqlCommand c var deliveryCompleteOrdinal = reader.GetOrdinal("DeliveryComplete"); var deliveryAbortedOrdinal = reader.GetOrdinal("DeliveryAborted"); - var items = new List(); + var items = new List(); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { - var id = reader.GetGuid(idOrdinal); - var headers = reader.IsDBNull(headersOrdinal) ? null : reader.GetString(headersOrdinal); - var message = new OutboxMessage + var headers = reader.IsDBNull(headersOrdinal) + ? null + : reader.GetString(headersOrdinal); + + var message = new SqlOutboxMessage { - Id = id, + Id = reader.GetGuid(idOrdinal), Timestamp = reader.GetDateTime(timestampOrdinal), BusName = reader.GetString(busNameOrdinal), MessageType = reader.GetString(typeOrdinal), MessagePayload = reader.GetSqlBinary(payloadOrdinal).Value, - Headers = headers == null ? null : JsonSerializer.Deserialize>(headers, _jsonOptions), - Path = reader.IsDBNull(pathOrdinal) ? null : reader.GetString(pathOrdinal), + Headers = headers == null + ? null + : JsonSerializer.Deserialize>(headers, _jsonOptions), + Path = reader.IsDBNull(pathOrdinal) + ? null + : reader.GetString(pathOrdinal), InstanceId = reader.GetString(instanceIdOrdinal), - LockInstanceId = reader.IsDBNull(lockInstanceIdOrdinal) ? null : reader.GetString(lockInstanceIdOrdinal), - LockExpiresOn = reader.IsDBNull(lockExpiresOnOrdinal) ? null : reader.GetDateTime(lockExpiresOnOrdinal), + LockInstanceId = reader.IsDBNull(lockInstanceIdOrdinal) + ? null + : reader.GetString(lockInstanceIdOrdinal), + LockExpiresOn = reader.IsDBNull(lockExpiresOnOrdinal) + ? null + : reader.GetDateTime(lockExpiresOnOrdinal), DeliveryAttempt = reader.GetInt32(deliveryAttemptOrdinal), DeliveryComplete = reader.GetBoolean(deliveryCompleteOrdinal), DeliveryAborted = reader.GetBoolean(deliveryAbortedOrdinal) diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs index dbcea818..f8e2f892 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs @@ -1,12 +1,13 @@ namespace SlimMessageBus.Host.Outbox.Sql; -public class SqlOutboxMigrationService : CommonSqlMigrationService, IOutboxMigrationService +public class SqlOutboxMigrationService( + ILogger logger, + ISqlMessageOutboxRepository repository, + ISqlTransactionService transactionService, + SqlOutboxSettings settings) + : CommonSqlMigrationService(logger, (CommonSqlRepository)repository, transactionService, settings.SqlSettings), + IOutboxMigrationService { - public SqlOutboxMigrationService(ILogger logger, ISqlOutboxRepository repository, ISqlTransactionService transactionService, SqlOutboxSettings settings) - : base(logger, (CommonSqlRepository)repository, transactionService, settings.SqlSettings) - { - } - protected override async Task OnMigrate(CancellationToken token) { var qualifiedTableName = Repository.GetQualifiedName(Settings.DatabaseTableName); diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs index 60da6780..a0a8d6cd 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs @@ -4,7 +4,9 @@ public class SqlOutboxTemplate { public string TableNameQualified { get; } public string MigrationsTableNameQualified { get; } - public string SqlOutboxMessageInsert { get; } + public string SqlOutboxMessageInsertWithClientId { get; } + public string SqlOutboxMessageInsertWithDatabaseId { get; } + public string SqlOutboxMessageInsertWithDatabaseIdSequential { get; } public string SqlOutboxMessageDeleteSent { get; } public string SqlOutboxMessageLockAndSelect { get; } public string SqlOutboxMessageLockTableAndSelect { get; } @@ -25,12 +27,17 @@ public SqlOutboxTemplate(SqlOutboxSettings settings) TableNameQualified = $"[{settings.SqlSettings.DatabaseSchemaName}].[{settings.SqlSettings.DatabaseTableName}]"; MigrationsTableNameQualified = $"[{settings.SqlSettings.DatabaseSchemaName}].[{settings.SqlSettings.DatabaseMigrationsTableName}]"; - SqlOutboxMessageInsert = $""" + string insertWith(string idFunc) => $""" INSERT INTO {TableNameQualified} ([Id], [Timestamp], [BusName], [MessageType], [MessagePayload], [Headers], [Path], [InstanceId], [LockInstanceId], [LockExpiresOn], [DeliveryAttempt], [DeliveryComplete], [DeliveryAborted]) - VALUES (@Id, @Timestamp, @BusName, @MessageType, @MessagePayload, @Headers, @Path, @InstanceId, @LockInstanceId, @LockExpiresOn, @DeliveryAttempt, @DeliveryComplete, @DeliveryAborted) + OUTPUT INSERTED.[Id] + VALUES ({idFunc}, @Timestamp, @BusName, @MessageType, @MessagePayload, @Headers, @Path, @InstanceId, @LockInstanceId, @LockExpiresOn, @DeliveryAttempt, @DeliveryComplete, @DeliveryAborted) """; + SqlOutboxMessageInsertWithClientId = insertWith("@Id"); + SqlOutboxMessageInsertWithDatabaseId = insertWith("NEWID()"); + SqlOutboxMessageInsertWithDatabaseIdSequential = insertWith("NEWSEQUENTIALID()"); + SqlOutboxMessageDeleteSent = $""" DELETE FROM {TableNameQualified} WHERE [DeliveryComplete] = 1 diff --git a/src/SlimMessageBus.Host.Outbox/Configuration/MessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.Outbox/Configuration/MessageBusBuilderExtensions.cs index 0c9368db..7ec18862 100644 --- a/src/SlimMessageBus.Host.Outbox/Configuration/MessageBusBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.Outbox/Configuration/MessageBusBuilderExtensions.cs @@ -4,7 +4,8 @@ public static class MessageBusBuilderExtensions { - public static MessageBusBuilder AddOutbox(this MessageBusBuilder mbb, Action configure = null) + public static MessageBusBuilder AddOutbox(this MessageBusBuilder mbb, Action configure = null) + where TOutboxMessage : OutboxMessage { mbb.PostConfigurationActions.Add(services => { @@ -33,12 +34,12 @@ public static MessageBusBuilder AddOutbox(this MessageBusBuilder mbb, Action(); - services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); - services.TryAdd(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + services.AddSingleton>(); + services.TryAddEnumerable(ServiceDescriptor.Singleton>(sp => sp.GetRequiredService>())); + services.TryAddSingleton(sp => sp.GetRequiredService>()); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton>(); services.TryAddSingleton(svp => { diff --git a/src/SlimMessageBus.Host.Outbox/Configuration/OutboxSettings.cs b/src/SlimMessageBus.Host.Outbox/Configuration/OutboxSettings.cs index a1c46f86..a4facc2f 100644 --- a/src/SlimMessageBus.Host.Outbox/Configuration/OutboxSettings.cs +++ b/src/SlimMessageBus.Host.Outbox/Configuration/OutboxSettings.cs @@ -36,9 +36,9 @@ public class OutboxSettings /// Sent message cleanup settings. /// public OutboxMessageCleanupSettings MessageCleanup { get; set; } = new(); - /// /// Type resolver which is responsible for converting message type into the Outbox table column MessageType /// public IMessageTypeResolver MessageTypeResolver { get; set; } = new AssemblyQualifiedNameMessageTypeResolver(); } + diff --git a/src/SlimMessageBus.Host.Outbox/IOutboxNotificationService.cs b/src/SlimMessageBus.Host.Outbox/IOutboxNotificationService.cs index 1bc9414f..1ff002f0 100644 --- a/src/SlimMessageBus.Host.Outbox/IOutboxNotificationService.cs +++ b/src/SlimMessageBus.Host.Outbox/IOutboxNotificationService.cs @@ -1,5 +1,5 @@ -namespace SlimMessageBus.Host.Outbox.Services; - +namespace SlimMessageBus.Host.Outbox; + public interface IOutboxNotificationService { /// diff --git a/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs b/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs index 40921b8d..552a6780 100644 --- a/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs +++ b/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs @@ -1,9 +1,5 @@ namespace SlimMessageBus.Host.Outbox; -using Microsoft.Extensions.Logging; - -using SlimMessageBus.Host.Outbox.Services; - public abstract class OutboxForwardingPublishInterceptor { } @@ -14,20 +10,14 @@ public abstract class OutboxForwardingPublishInterceptor /// public sealed class OutboxForwardingPublishInterceptor( ILogger logger, - IOutboxRepository outboxRepository, - IInstanceIdProvider instanceIdProvider, + IOutboxMessageFactory outboxMessageFactory, IOutboxNotificationService outboxNotificationService, OutboxSettings outboxSettings) - : OutboxForwardingPublishInterceptor, IInterceptorWithOrder, IPublishInterceptor, IDisposable where T : class + : OutboxForwardingPublishInterceptor, IInterceptorWithOrder, IPublishInterceptor, IDisposable + where T : class { internal const string SkipOutboxHeader = "__SkipOutbox"; - private readonly ILogger _logger = logger; - private readonly IOutboxRepository _outboxRepository = outboxRepository; - private readonly IInstanceIdProvider _instanceIdProvider = instanceIdProvider; - private readonly IOutboxNotificationService _outboxNotificationService = outboxNotificationService; - private readonly OutboxSettings _outboxSettings = outboxSettings; - private bool _notifyOutbox = false; public int Order => int.MaxValue; @@ -36,7 +26,7 @@ public void Dispose() { if (_notifyOutbox) { - _outboxNotificationService.Notify(); + outboxNotificationService.Notify(); } GC.SuppressFinalize(this); @@ -58,28 +48,25 @@ public async Task OnHandle(T message, Func next, IProducerContext context) return; } - // Forward to outbox - var messageType = message.GetType(); - - _logger.LogDebug("Forwarding published message of type {MessageType} to the outbox", messageType.Name); + // Forward to outbox - do not call next() + var messageType = message.GetType(); // Take the proper serializer (meant for the bus) var messagePayload = busMaster.Serializer?.Serialize(messageType, message) - ?? throw new PublishMessageBusException($"The {busMaster.Name} bus has no configured serializer, so it cannot be used with the outbox plugin"); + ?? throw new PublishMessageBusException($"The {busMaster.Name} bus has no configured serializer, so it cannot be used with the outbox plugin"); - // Add message to the database, do not call next() - var outboxMessage = new OutboxMessage - { - BusName = busMaster.Name, - Headers = context.Headers, - Path = context.Path, - MessageType = _outboxSettings.MessageTypeResolver.ToName(messageType), - MessagePayload = messagePayload, - InstanceId = _instanceIdProvider.GetInstanceId() - }; - await _outboxRepository.Save(outboxMessage, context.CancellationToken); + var outboxMessageEntity = await outboxMessageFactory.Create( + busName: busMaster.Name, + headers: context.Headers, + path: context.Path, + messageType: outboxSettings.MessageTypeResolver.ToName(messageType), + messagePayload: messagePayload, + cancellationToken: context.CancellationToken + ); + + logger.LogDebug("Forwarding published message of type {MessageType} to the outbox with Id {OutboxMessageId}", messageType.Name, outboxMessageEntity.Id); - // a message was sent, notify outbox service to poll on dispose (post transaction) + // A message was sent, notify outbox service to poll on dispose (post transaction) _notifyOutbox = true; } } diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/IHasId.cs b/src/SlimMessageBus.Host.Outbox/Repositories/IHasId.cs new file mode 100644 index 00000000..a53e6487 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox/Repositories/IHasId.cs @@ -0,0 +1,11 @@ +namespace SlimMessageBus.Host.Outbox; + +public interface IHasId +{ + object Id { get; } +} + +public interface IHasId: IHasId +{ + new TOutboxMessageKey Id { get; } +} diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageFactory.cs b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageFactory.cs new file mode 100644 index 00000000..56e25f8e --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageFactory.cs @@ -0,0 +1,26 @@ +namespace SlimMessageBus.Host.Outbox; + + +/// +/// Factory for creating outbox messages +/// +public interface IOutboxMessageFactory +{ + /// + /// Create a new outbox message and store it using the underlying store + /// + /// + /// + /// + /// + /// + /// + /// ID of the outbox message. + Task Create( + string busName, + IDictionary headers, + string path, + string messageType, + byte[] messagePayload, + CancellationToken cancellationToken); +} diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageRepository.cs b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageRepository.cs new file mode 100644 index 00000000..44686d80 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMessageRepository.cs @@ -0,0 +1,13 @@ +namespace SlimMessageBus.Host.Outbox; + +public interface IOutboxMessageRepository + where TOutboxMessage : class +{ + Task> LockAndSelect(string instanceId, int batchSize, bool tableLock, TimeSpan lockDuration, CancellationToken cancellationToken); + Task AbortDelivery(IReadOnlyCollection ids, CancellationToken cancellationToken); + Task UpdateToSent(IReadOnlyCollection ids, CancellationToken cancellationToken); + Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int maxDeliveryAttempts, CancellationToken cancellationToken); + Task DeleteSent(DateTime olderThan, CancellationToken cancellationToken); + Task RenewLock(string instanceId, TimeSpan lockDuration, CancellationToken cancellationToken); +} + diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs deleted file mode 100644 index a8772916..00000000 --- a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SlimMessageBus.Host.Outbox; - -public interface IOutboxRepository -{ - Task Save(OutboxMessage message, CancellationToken token); - Task> LockAndSelect(string instanceId, int batchSize, bool tableLock, TimeSpan lockDuration, CancellationToken token); - Task AbortDelivery (IReadOnlyCollection ids, CancellationToken token); - Task UpdateToSent(IReadOnlyCollection ids, CancellationToken token); - Task IncrementDeliveryAttempt(IReadOnlyCollection ids, int maxDeliveryAttempts, CancellationToken token); - Task DeleteSent(DateTime olderThan, CancellationToken token); - Task RenewLock(string instanceId, TimeSpan lockDuration, CancellationToken token); -} diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/OutboxMessage.cs b/src/SlimMessageBus.Host.Outbox/Repositories/OutboxMessage.cs index 01aedcdb..0cd30e26 100644 --- a/src/SlimMessageBus.Host.Outbox/Repositories/OutboxMessage.cs +++ b/src/SlimMessageBus.Host.Outbox/Repositories/OutboxMessage.cs @@ -1,18 +1,16 @@ namespace SlimMessageBus.Host.Outbox; -public class OutboxMessage + + +public class OutboxMessage : IHasId { - public Guid Id { get; set; } = Guid.NewGuid(); - public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public TOutboxMessageKey Id { get; set; } public string BusName { get; set; } public string MessageType { get; set; } public byte[] MessagePayload { get; set; } public string Path { get; set; } public IDictionary Headers { get; set; } public string InstanceId { get; set; } - public string LockInstanceId { get; set; } = string.Empty; - public DateTime? LockExpiresOn { get; set; } = null; - public int DeliveryAttempt { get; set; } = 0; - public bool DeliveryComplete { get; set; } = false; - public bool DeliveryAborted { get; set; } = false; + + object IHasId.Id => Id; } diff --git a/src/SlimMessageBus.Host.Outbox/Services/IOutboxLockRenewalTimerFactory.cs b/src/SlimMessageBus.Host.Outbox/Services/IOutboxLockRenewalTimerFactory.cs index e568913a..29284a6c 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/IOutboxLockRenewalTimerFactory.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/IOutboxLockRenewalTimerFactory.cs @@ -1,7 +1,5 @@ namespace SlimMessageBus.Host.Outbox.Services; -using System.Threading; - public interface IOutboxLockRenewalTimerFactory { IOutboxLockRenewalTimer CreateRenewalTimer(TimeSpan lockDuration, TimeSpan interval, Action lockLost, CancellationToken cancellationToken); diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs index 16ccae19..1527475d 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs @@ -1,20 +1,19 @@ namespace SlimMessageBus.Host.Outbox.Services; -using SlimMessageBus.Host.Outbox; - -public sealed class OutboxLockRenewalTimer : IOutboxLockRenewalTimer + +internal sealed class OutboxLockRenewalTimer : IOutboxLockRenewalTimer + where TOutboxMessage : OutboxMessage { private readonly object _lock; private readonly Timer _timer; - private readonly ILogger _logger; - private readonly IOutboxRepository _outboxRepository; + private readonly ILogger> _logger; + private readonly IOutboxMessageRepository _outboxRepository; private readonly CancellationToken _cancellationToken; private readonly Action _lockLost; private bool _active; private bool _renewingLock; - public OutboxLockRenewalTimer(ILogger logger, IOutboxRepository outboxRepository, IInstanceIdProvider instanceIdProvider, TimeSpan lockDuration, TimeSpan lockRenewalInterval, Action lockLost, CancellationToken cancellationToken) + public OutboxLockRenewalTimer(ILogger> logger, IOutboxMessageRepository outboxRepository, IInstanceIdProvider instanceIdProvider, TimeSpan lockDuration, TimeSpan lockRenewalInterval, Action lockLost, CancellationToken cancellationToken) { - Debug.Assert(lockRenewalInterval < lockDuration); _logger = logger; diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs index 84df1ccb..56254736 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs @@ -1,19 +1,15 @@ namespace SlimMessageBus.Host.Outbox.Services; -public class OutboxLockRenewalTimerFactory : IOutboxLockRenewalTimerFactory, IAsyncDisposable + +public class OutboxLockRenewalTimerFactory(IServiceProvider serviceProvider) + : IOutboxLockRenewalTimerFactory, IAsyncDisposable + where TOutboxMessage : OutboxMessage { - private readonly IServiceScope _scope; + private readonly IServiceScope _scope = serviceProvider.CreateScope(); private bool _isDisposed = false; - - public OutboxLockRenewalTimerFactory(IServiceProvider serviceProvider) - { - _scope = serviceProvider.CreateScope(); - } - - public IOutboxLockRenewalTimer CreateRenewalTimer(TimeSpan lockDuration, TimeSpan interval, Action lockLost, CancellationToken cancellationToken) - { - return (OutboxLockRenewalTimer)ActivatorUtilities.CreateInstance(_scope.ServiceProvider, typeof(OutboxLockRenewalTimer), lockDuration, interval, lockLost, cancellationToken); - } + + public IOutboxLockRenewalTimer CreateRenewalTimer(TimeSpan lockDuration, TimeSpan interval, Action lockLost, CancellationToken cancellationToken) + => (OutboxLockRenewalTimer)ActivatorUtilities.CreateInstance(_scope.ServiceProvider, typeof(OutboxLockRenewalTimer), lockDuration, interval, lockLost, cancellationToken); public async ValueTask DisposeAsync() { diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs index 843a3f0f..e1b79b6f 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs @@ -1,16 +1,14 @@ namespace SlimMessageBus.Host.Outbox.Services; - -using SlimMessageBus; -using SlimMessageBus.Host; -using SlimMessageBus.Host.Outbox; - -internal class OutboxSendingTask( + +internal class OutboxSendingTask( ILoggerFactory loggerFactory, - OutboxSettings outboxSettings, + OutboxSettings outboxSettings, + ICurrentTimeProvider currentTimeProvider, IServiceProvider serviceProvider) - : IMessageBusLifecycleInterceptor, IOutboxNotificationService, IAsyncDisposable + : IMessageBusLifecycleInterceptor, IOutboxNotificationService, IAsyncDisposable + where TOutboxMessage : OutboxMessage { - private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly ILogger> _logger = loggerFactory.CreateLogger>(); private readonly OutboxSettings _outboxSettings = outboxSettings; private readonly IServiceProvider _serviceProvider = serviceProvider; @@ -25,16 +23,17 @@ internal class OutboxSendingTask( private int _busStartCount; - private DateTime? _cleanupNextRun; + private DateTimeOffset? _cleanupNextRun; private bool ShouldRunCleanup() { if (_outboxSettings.MessageCleanup?.Enabled == true) - { - var trigger = !_cleanupNextRun.HasValue || DateTime.UtcNow > _cleanupNextRun.Value; + { + var currentTime = currentTimeProvider.CurrentTime; + var trigger = !_cleanupNextRun.HasValue || currentTime > _cleanupNextRun.Value; if (trigger) { - _cleanupNextRun = DateTime.UtcNow.Add(_outboxSettings.MessageCleanup.Interval); + _cleanupNextRun = currentTime.Add(_outboxSettings.MessageCleanup.Interval); } return trigger; @@ -136,8 +135,15 @@ private static async Task MigrateSchema(IServiceProvider serviceProvider, Cancel throw new MessageBusException("Outbox schema migration failed", e); } finally - { - await ((IAsyncDisposable)scope).DisposeAsync(); + { + if (scope is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + scope.Dispose(); + } } } @@ -160,7 +166,7 @@ private async Task Run() try { await EnsureMigrateSchema(scope.ServiceProvider, _loopCts.Token); - var outboxRepository = scope.ServiceProvider.GetRequiredService(); + var outboxRepository = scope.ServiceProvider.GetRequiredService>(); do { if (_loopCts.Token.IsCancellationRequested) @@ -175,7 +181,7 @@ private async Task Run() if (!_loopCts.IsCancellationRequested && ShouldRunCleanup()) { _logger.LogTrace("Running cleanup of sent messages"); - await outboxRepository.DeleteSent(DateTime.UtcNow.Add(-_outboxSettings.MessageCleanup.Age), _loopCts.Token).ConfigureAwait(false); + await outboxRepository.DeleteSent(currentTimeProvider.CurrentTime.DateTime.Add(-_outboxSettings.MessageCleanup.Age), _loopCts.Token).ConfigureAwait(false); } } catch (Exception e) @@ -208,7 +214,7 @@ private async Task Run() } } - async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxRepository outboxRepository, CancellationToken cancellationToken) + async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxMessageRepository outboxRepository, CancellationToken cancellationToken) { var lockDuration = TimeSpan.FromSeconds(Math.Min(Math.Max(_outboxSettings.LockExpiration.TotalSeconds, 5), 30)); if (lockDuration != _outboxSettings.LockExpiration) @@ -252,14 +258,14 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxR return count; } - async internal Task<(bool RunAgain, int Count)> ProcessMessages(IOutboxRepository outboxRepository, IReadOnlyCollection outboxMessages, ICompositeMessageBus compositeMessageBus, IMessageBusTarget messageBusTarget, CancellationToken cancellationToken) + async internal Task<(bool RunAgain, int Count)> ProcessMessages(IOutboxMessageRepository outboxRepository, IReadOnlyCollection outboxMessages, ICompositeMessageBus compositeMessageBus, IMessageBusTarget messageBusTarget, CancellationToken cancellationToken) { const int defaultBatchSize = 50; var runAgain = outboxMessages.Count == _outboxSettings.PollBatchSize; var count = 0; - var abortedIds = new List(_outboxSettings.PollBatchSize); + var abortedIds = new List(_outboxSettings.PollBatchSize); foreach (var busGroup in outboxMessages.GroupBy(x => x.BusName)) { var busName = busGroup.Key; @@ -291,20 +297,20 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxR } var path = pathGroup.Key; - var batches = pathGroup.Select( - outboxMessage => - { - var messageType = _outboxSettings.MessageTypeResolver.ToType(outboxMessage.MessageType); - if (messageType == null) - { - abortedIds.Add(outboxMessage.Id); - _logger.LogError("Outbox message with Id {Id} - the MessageType {MessageType} is not recognized. The type might have been renamed or moved namespaces.", outboxMessage.Id, outboxMessage.MessageType); - return null; - } - - var message = bus.Serializer.Deserialize(messageType, outboxMessage.MessagePayload); - return new OutboxBulkMessage(outboxMessage.Id, message, messageType, outboxMessage.Headers ?? new Dictionary()); - }) + var batches = pathGroup + .Select(outboxMessage => + { + var messageType = _outboxSettings.MessageTypeResolver.ToType(outboxMessage.MessageType); + if (messageType == null) + { + abortedIds.Add(outboxMessage.Id); + _logger.LogError("Outbox message with Id {Id} - the MessageType {MessageType} is not recognized. The type might have been renamed or moved namespaces.", outboxMessage.Id, outboxMessage.MessageType); + return null; + } + + var message = bus.Serializer.Deserialize(messageType, outboxMessage.MessagePayload); + return new OutboxBulkMessage(outboxMessage.Id, message, messageType, outboxMessage.Headers ?? new Dictionary()); + }) .Where(x => x != null) .Batch(bulkProducer.MaxMessagesPerTransaction ?? defaultBatchSize); @@ -325,7 +331,7 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxR return (runAgain, count); } - async internal Task<(bool Success, int Published)> DispatchBatch(IOutboxRepository outboxRepository, IMessageBusBulkProducer producer, IMessageBusTarget messageBusTarget, IReadOnlyCollection batch, string busName, string path, CancellationToken cancellationToken) + async internal Task<(bool Success, int Published)> DispatchBatch(IOutboxMessageRepository outboxRepository, IMessageBusBulkProducer producer, IMessageBusTarget messageBusTarget, IReadOnlyCollection batch, string busName, string path, CancellationToken cancellationToken) { _logger.LogDebug("Publishing batch of {MessageCount} messages to pathGroup {Path} on {BusName} bus", batch.Count, path, busName); @@ -371,9 +377,9 @@ private static IMasterMessageBus GetBus(ICompositeMessageBus compositeMessageBus public record OutboxBulkMessage : BulkMessageEnvelope { - public Guid Id { get; } + public TOutboxMessageKey Id { get; } - public OutboxBulkMessage(Guid id, object message, Type messageType, IDictionary headers) + public OutboxBulkMessage(TOutboxMessageKey id, object message, Type messageType, IDictionary headers) : base(message, messageType, headers) { Id = id; diff --git a/src/SlimMessageBus.Host.Outbox/SlimMessageBus.Host.Outbox.csproj b/src/SlimMessageBus.Host.Outbox/SlimMessageBus.Host.Outbox.csproj index 1ff619c3..359edb06 100644 --- a/src/SlimMessageBus.Host.Outbox/SlimMessageBus.Host.Outbox.csproj +++ b/src/SlimMessageBus.Host.Outbox/SlimMessageBus.Host.Outbox.csproj @@ -14,7 +14,8 @@ - - + + + diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index 43251dd3..a7ac5286 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc11 + 3.0.0-rc12 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs b/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs index 8f3f1a94..f30fab98 100644 --- a/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs +++ b/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs @@ -46,6 +46,15 @@ public Task ExecuteNonQuery(SqlRetrySettings retrySettings, string sql, Act using var cmd = CreateCommand(); cmd.CommandText = sql; setParameters?.Invoke(cmd); - return await cmd.ExecuteNonQueryAsync(); + return await cmd.ExecuteNonQueryAsync(token); + }, token); + + public Task ExecuteScalarAsync(SqlRetrySettings retrySettings, string sql, Action setParameters = null, CancellationToken token = default) => + SqlHelper.RetryIfTransientError(Logger, retrySettings, async () => + { + using var cmd = CreateCommand(); + cmd.CommandText = sql; + setParameters?.Invoke(cmd); + return await cmd.ExecuteScalarAsync(token); }, token); } diff --git a/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs b/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs index 268385a6..beb552d2 100644 --- a/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs +++ b/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs @@ -2,6 +2,6 @@ public enum SqlDialect { - SqlServer = 1 + SqlServer = 1, // More to come } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Sql.Common/Transactions/AbstractSqlTransactionService.cs b/src/SlimMessageBus.Host.Sql.Common/Transactions/AbstractSqlTransactionService.cs index 675abd43..b2ab93ff 100644 --- a/src/SlimMessageBus.Host.Sql.Common/Transactions/AbstractSqlTransactionService.cs +++ b/src/SlimMessageBus.Host.Sql.Common/Transactions/AbstractSqlTransactionService.cs @@ -31,13 +31,19 @@ protected async virtual ValueTask DisposeAsyncCore() public async virtual Task BeginTransaction() { - if (_transactionCount++ == 0) + if (_transactionCompleted) + { + throw new MessageBusException("Transaction was completed already"); + } + + if (_transactionCount == 0) { // Start transaction + await OnBeginTransaction(); _transactionFailed = false; _transactionCompleted = false; - await OnBeginTransaction(); } + _transactionCount++; } private async Task TryCompleteTransaction(bool transactionFailed = false) @@ -58,6 +64,7 @@ private async Task TryCompleteTransaction(bool transactionFailed = false) if (!_transactionCompleted && (_transactionCount == 0 || transactionFailed)) { _transactionCompleted = true; + _transactionCount = 0; await OnCompleteTransaction(_transactionFailed); } } diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs index dcf7af8a..ac37cad8 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs @@ -37,7 +37,7 @@ public MessageProcessor( messageTypeResolver: messageBus.MessageTypeResolver, messageHeadersFactory: messageBus, runtimeTypeCache: messageBus.RuntimeTypeCache, - currentTimeProvider: messageBus, + currentTimeProvider: messageBus.CurrentTimeProvider, path: path, consumerErrorHandlerOpenGenericType) { diff --git a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs index d8fbaac4..8c658dd8 100644 --- a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs +++ b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs @@ -94,6 +94,13 @@ public static IServiceCollection AddSlimMessageBus(this IServiceCollection servi services.AddHostedService(); + // Register the default providers + services.TryAddSingleton(); + services.TryAddSingleton(svp => svp.GetRequiredService()); + + services.TryAddSingleton(); + services.TryAddSingleton(svp => svp.GetRequiredService()); + return services; } diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index b4b4cf00..3fc9223f 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -15,13 +15,13 @@ protected MessageBusBase(MessageBusSettings settings, TProviderSettings provider } } -public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, IMessageHeadersFactory, ICurrentTimeProvider, IResponseProducer, IResponseConsumer, IMessageBusBulkProducer +public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, IMessageHeadersFactory, IResponseProducer, IResponseConsumer, IMessageBusBulkProducer { private readonly ILogger _logger; private CancellationTokenSource _cancellationTokenSource = new(); private IMessageSerializer _serializer; private readonly MessageHeaderService _headerService; - private readonly List _consumers = []; + private readonly List _consumers = []; /// /// Special market reference that signifies a dummy producer settings for response types. @@ -101,7 +101,9 @@ protected MessageBusBase(MessageBusSettings settings) RuntimeTypeCache = new RuntimeTypeCache(); - MessageBusTarget = new MessageBusProxy(this, Settings.ServiceProvider); + MessageBusTarget = new MessageBusProxy(this, Settings.ServiceProvider); + + CurrentTimeProvider = settings.ServiceProvider.GetRequiredService(); } protected void AddInit(Task task) @@ -174,7 +176,7 @@ protected virtual void Build() protected virtual void BuildPendingRequestStore() { PendingRequestStore = new InMemoryPendingRequestStore(); - PendingRequestManager = new PendingRequestManager(PendingRequestStore, () => CurrentTime, TimeSpan.FromSeconds(1), LoggerFactory); + PendingRequestManager = new PendingRequestManager(PendingRequestStore, () => CurrentTimeProvider.CurrentTime, TimeSpan.FromSeconds(1), LoggerFactory); PendingRequestManager.Start(); } @@ -387,7 +389,7 @@ protected async virtual Task DestroyConsumers() protected void AddConsumer(AbstractConsumer consumer) => _consumers.Add(consumer); - public virtual DateTimeOffset CurrentTime => DateTimeOffset.UtcNow; + public ICurrentTimeProvider CurrentTimeProvider { get; protected set; } public virtual int? MaxMessagesPerTransaction => null; @@ -513,7 +515,7 @@ public virtual Task ProduceSend(object request, string pat path ??= GetDefaultPath(requestType, producerSettings); timeout ??= GetDefaultRequestTimeout(requestType, producerSettings); - var created = CurrentTime; + var created = CurrentTimeProvider.CurrentTime; var expires = created.Add(timeout.Value); // generate the request guid @@ -670,7 +672,7 @@ public virtual Task OnResponseArrived(byte[] responsePayload, string { if (_logger.IsEnabled(LogLevel.Debug)) { - var tookTimespan = CurrentTime.Subtract(requestState.Created); + var tookTimespan = CurrentTimeProvider.CurrentTime.Subtract(requestState.Created); _logger.LogDebug("Response arrived for {Request} on path {Path} (time: {RequestTime} ms)", requestState, path, tookTimespan); } diff --git a/src/SlimMessageBus.Host/Providers/GuidGenerator/GuidGenerator.cs b/src/SlimMessageBus.Host/Providers/GuidGenerator/GuidGenerator.cs new file mode 100644 index 00000000..d52495e4 --- /dev/null +++ b/src/SlimMessageBus.Host/Providers/GuidGenerator/GuidGenerator.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host; + +public class GuidGenerator : IGuidGenerator +{ + /// + /// Generate a new Guid (UUID v4) using + /// + public Guid NewGuid() => Guid.NewGuid(); +} diff --git a/src/SlimMessageBus.Host/Providers/GuidGenerator/IGuidGenerator.cs b/src/SlimMessageBus.Host/Providers/GuidGenerator/IGuidGenerator.cs new file mode 100644 index 00000000..e891fc89 --- /dev/null +++ b/src/SlimMessageBus.Host/Providers/GuidGenerator/IGuidGenerator.cs @@ -0,0 +1,10 @@ +namespace SlimMessageBus.Host; + +public interface IGuidGenerator +{ + /// + /// Generate a new Guid + /// + /// + Guid NewGuid(); +} diff --git a/src/SlimMessageBus.Host/Providers/TimeProvider/CurrentTimeProvider.cs b/src/SlimMessageBus.Host/Providers/TimeProvider/CurrentTimeProvider.cs new file mode 100644 index 00000000..6ddd90fd --- /dev/null +++ b/src/SlimMessageBus.Host/Providers/TimeProvider/CurrentTimeProvider.cs @@ -0,0 +1,6 @@ +namespace SlimMessageBus.Host; + +public class CurrentTimeProvider : ICurrentTimeProvider +{ + public DateTimeOffset CurrentTime => DateTimeOffset.UtcNow; +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/ICurrentTimeProvider.cs b/src/SlimMessageBus.Host/Providers/TimeProvider/ICurrentTimeProvider.cs similarity index 100% rename from src/SlimMessageBus.Host/ICurrentTimeProvider.cs rename to src/SlimMessageBus.Host/Providers/TimeProvider/ICurrentTimeProvider.cs diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index 303fee7c..97de35bb 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -205,9 +205,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.OutboxWebApi", "Samp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.Sql", "SlimMessageBus.Host.Outbox.Sql\SlimMessageBus.Host.Outbox.Sql.csproj", "{F4A692F5-0FAE-46DE-A267-DAC2B71BF026}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.DbContext", "SlimMessageBus.Host.Outbox.DbContext\SlimMessageBus.Host.Outbox.DbContext.csproj", "{EC14A729-8DF5-448D-944F-32C0BA7A4B6E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.Sql.DbContext", "SlimMessageBus.Host.Outbox.Sql.DbContext\SlimMessageBus.Host.Outbox.Sql.DbContext.csproj", "{EC14A729-8DF5-448D-944F-32C0BA7A4B6E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.DbContext.Test", "Tests\SlimMessageBus.Host.Outbox.DbContext.Test\SlimMessageBus.Host.Outbox.DbContext.Test.csproj", "{2DD467F8-F06F-4B7C-A46F-1EC5BAE325A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.Sql.DbContext.Test", "Tests\SlimMessageBus.Host.Outbox.Sql.DbContext.Test\SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj", "{2DD467F8-F06F-4B7C-A46F-1EC5BAE325A5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Serialization.SystemTextJson.Test", "Tests\SlimMessageBus.Host.Serialization.SystemTextJson.Test\SlimMessageBus.Host.Serialization.SystemTextJson.Test.csproj", "{0155628A-7B0A-4461-B3BE-4CEBE4CD77F1}" EndProject diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index 37c72979..ed798b6d 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc11 + 3.0.0-rc12 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs index cd52a4cb..5f50e183 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs @@ -24,6 +24,7 @@ public ServiceBusMessageBusTests() serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(new Mock().Object); serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); serviceProviderMock.Setup(x => x.GetService(typeof(IEnumerable))).Returns(Array.Empty()); + serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); BusBuilder.WithDependencyResolver(serviceProviderMock.Object); diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs index 759ea2e3..278267f7 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs @@ -17,6 +17,7 @@ public KafkaMessageBusTest() var serviceProviderMock = new Mock(); serviceProviderMock.Setup(x => x.GetService(typeof(ILogger))).CallBase(); serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); + serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); MbSettings = new MessageBusSettings { diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs index b11c5391..d100af1f 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs @@ -9,28 +9,31 @@ public class MessageBusMock public Mock SerializerMock { get; } public MessageBusSettings BusSettings { get; } - public DateTimeOffset CurrentTime { get; set; } + public CurrentTimeProviderFake CurrentTimeProvider { get; set; } public Mock BusMock { get; } public MessageBusBase Bus => BusMock.Object; public MessageBusMock() { + CurrentTimeProvider = new CurrentTimeProviderFake + { + CurrentTime = DateTimeOffset.UtcNow + }; + SerializerMock = new Mock(); ServiceProviderMock = new ServiceProviderMock(); ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(SerializerMock.Object); ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); + ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(CurrentTimeProvider); BusSettings = new MessageBusSettings { ServiceProvider = ServiceProviderMock.ProviderMock.Object, }; - - CurrentTime = DateTimeOffset.UtcNow; - + BusMock = new Mock(BusSettings); - BusMock.SetupGet(x => x.Settings).Returns(BusSettings); - BusMock.SetupGet(x => x.CurrentTime).Returns(() => CurrentTime); + BusMock.SetupGet(x => x.Settings).Returns(BusSettings); } } diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index ca5952e7..2640acb2 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -36,6 +36,7 @@ public MemoryMessageBusTests() _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(_messageSerializerMock.Object); _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); _serviceProviderMock.ProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Enumerable.Empty()); + _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); _messageSerializerMock .Setup(x => x.Serialize(It.IsAny(), It.IsAny())) @@ -182,6 +183,7 @@ public async Task When_Publish_Given_PerMessageScopeEnabled_Then_TheScopeIsCreat _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); scopeProviderMock.Should().NotBeNull(); @@ -231,6 +233,7 @@ public async Task When_Publish_Given_PerMessageScopeDisabled_Then_TheScopeIsNotC _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumerMock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); @@ -306,6 +309,7 @@ public async Task When_ProducePublish_Given_PerMessageScopeDisabledOrEnabled_And _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); } @@ -341,6 +345,7 @@ public async Task When_Publish_Given_TwoConsumersOnSameTopic_Then_BothAreInvoked _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumer1Mock.VerifySet(x => x.Context = It.IsAny(), Times.Once); @@ -391,6 +396,7 @@ public async Task When_Send_Given_AConsumersAndHandlerOnSameTopic_Then_BothAreIn _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumer2Mock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BusType.cs b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BusType.cs deleted file mode 100644 index afdd0067..00000000 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BusType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test; - -public enum BusType -{ - AzureSB, - Kafka, - RabbitMQ -} diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BaseOutboxIntegrationTest.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs similarity index 82% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BaseOutboxIntegrationTest.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs index 58ed5773..98728288 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/BaseOutboxIntegrationTest.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs @@ -1,4 +1,6 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test; + +using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; public abstract class BaseOutboxIntegrationTest(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) { diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BusType.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BusType.cs new file mode 100644 index 00000000..38b936e7 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BusType.cs @@ -0,0 +1,8 @@ +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test; + +public enum BusType +{ + AzureSB, + Kafka, + RabbitMQ +} diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DataAccess/Customer.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DataAccess/Customer.cs similarity index 84% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DataAccess/Customer.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DataAccess/Customer.cs index ce4184e3..b45693bd 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DataAccess/Customer.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DataAccess/Customer.cs @@ -1,4 +1,4 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test.DataAccess; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; public class Customer { diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DataAccess/CustomerContext.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DataAccess/CustomerContext.cs similarity index 88% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DataAccess/CustomerContext.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DataAccess/CustomerContext.cs index 68bd8f28..0347d38f 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DataAccess/CustomerContext.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DataAccess/CustomerContext.cs @@ -1,4 +1,4 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test.DataAccess; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; using Microsoft.EntityFrameworkCore; diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DatabaseFacadeExtensions.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DatabaseFacadeExtensions.cs similarity index 99% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DatabaseFacadeExtensions.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DatabaseFacadeExtensions.cs index 9f4ee91b..22f4a545 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DatabaseFacadeExtensions.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/DatabaseFacadeExtensions.cs @@ -1,9 +1,9 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - +using Microsoft.EntityFrameworkCore.Infrastructure; + public static class DatabaseFacadeExtensions { public static async Task DropSchemaIfExistsAsync(this DatabaseFacade database, string schema, CancellationToken cancellationToken = default) diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/20240617163727_AssemblySchema.Designer.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/20240617163727_AssemblySchema.Designer.cs similarity index 92% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/20240617163727_AssemblySchema.Designer.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/20240617163727_AssemblySchema.Designer.cs index 69b24a4c..b6543b14 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/20240617163727_AssemblySchema.Designer.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/20240617163727_AssemblySchema.Designer.cs @@ -1,15 +1,17 @@ // -using System; +using System; + using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SlimMessageBus.Host.Outbox.DbContext.Test.DataAccess; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; #nullable disable -namespace SlimMessageBus.Host.Outbox.DbContext.Test.Migrations +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test.Migrations { [DbContext(typeof(CustomerContext))] [Migration("20240617163727_AssemblySchema")] diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/20240617163727_AssemblySchema.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/20240617163727_AssemblySchema.cs similarity index 90% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/20240617163727_AssemblySchema.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/20240617163727_AssemblySchema.cs index 340a543c..b40c4726 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/20240617163727_AssemblySchema.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/20240617163727_AssemblySchema.cs @@ -1,9 +1,8 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace SlimMessageBus.Host.Outbox.DbContext.Test.Migrations +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test.Migrations { /// public partial class AssemblySchema : Migration diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/CustomerContextModelSnapshot.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/CustomerContextModelSnapshot.cs similarity index 89% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/CustomerContextModelSnapshot.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/CustomerContextModelSnapshot.cs index b8096700..d93e6516 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Migrations/CustomerContextModelSnapshot.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Migrations/CustomerContextModelSnapshot.cs @@ -1,14 +1,16 @@ // using System; + using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SlimMessageBus.Host.Outbox.DbContext.Test.DataAccess; + +using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; #nullable disable -namespace SlimMessageBus.Host.Outbox.DbContext.Test.Migrations +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test.Migrations { [DbContext(typeof(CustomerContext))] partial class CustomerContextModelSnapshot : ModelSnapshot diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxBenchmarkTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs similarity index 93% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxBenchmarkTests.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs index b8e291c1..4b5f3caa 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxBenchmarkTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs @@ -1,13 +1,17 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test; using System.Net.Mime; using Microsoft.EntityFrameworkCore.Migrations; +using SlimMessageBus; +using SlimMessageBus.Host; +using SlimMessageBus.Host.Outbox; using SlimMessageBus.Host.Outbox.Services; +using SlimMessageBus.Host.Outbox.Sql.DbContext; +using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; using SlimMessageBus.Host.RabbitMQ; - /// /// This test should help to understand the runtime performance and overhead of the outbox feature. /// It will generate the time measurements for a given transport (Azure DB + Azure SQL instance) as the baseline, @@ -151,10 +155,10 @@ await PerformDbOperation(async (context, outboxMigrationService) => var events = Enumerable.Range(0, messageCount).Select(x => new CustomerCreatedEvent(Guid.NewGuid(), $"John {x:000}", surnames[x % surnames.Length])).ToList(); var store = ServiceProvider!.GetRequiredService>(); - OutboxSendingTask outboxSendingTask = null; + OutboxSendingTask outboxSendingTask = null; if (_useOutbox) { - outboxSendingTask = ServiceProvider.GetRequiredService(); + outboxSendingTask = ServiceProvider.GetRequiredService>(); // migrate data context await outboxSendingTask.OnBusLifecycle(Interceptor.MessageBusLifecycleEventType.Created, null); @@ -197,7 +201,7 @@ await PerformDbOperation(async (context, outboxMigrationService) => var outboxPublishTimerElapsed = TimeSpan.Zero; if (_useOutbox) { - var outputRepository = ServiceProvider.GetRequiredService(); + var outputRepository = ServiceProvider.GetRequiredService>(); var outboxTimer = Stopwatch.StartNew(); var publishCount = await outboxSendingTask.SendMessages(ServiceProvider, outputRepository, CancellationToken.None); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs similarity index 82% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs index 924fb70a..fa6578d8 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs @@ -1,7 +1,13 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Migrations; +using SlimMessageBus; +using SlimMessageBus.Host; +using SlimMessageBus.Host.Outbox; +using SlimMessageBus.Host.Outbox.Sql.DbContext; +using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; using SlimMessageBus.Host.Sql.Common; [Trait("Category", "Integration")] @@ -12,6 +18,7 @@ public class OutboxTests(ITestOutputHelper testOutputHelper) : BaseOutboxIntegra private bool _testParamUseHybridBus; private TransactionType _testParamTransactionType; private BusType _testParamBusType; + private SqlOutboxMessageIdGenerationMode _testParamIdGenerationMode; protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { @@ -98,40 +105,43 @@ void ConfigureExternalBus(MessageBusBuilder mbb) opts.MessageCleanup.Interval = TimeSpan.FromSeconds(10); opts.MessageCleanup.Age = TimeSpan.FromMinutes(1); opts.SqlSettings.DatabaseSchemaName = CustomerContext.Schema; + opts.IdGeneration.Mode = _testParamIdGenerationMode; + // We want to see the time output in the logs + opts.MeasureSqlOperations = true; }); }); services.AddSingleton>(); // Entity Framework setup - application specific EF DbContext - services.AddDbContext( - options => options.UseSqlServer( + services.AddDbContext(options => options + .UseSqlServer( Secrets.Service.PopulateSecrets(Configuration.GetConnectionString("DefaultConnection")), - x => x.MigrationsHistoryTable(HistoryRepository.DefaultTableName, CustomerContext.Schema))); + x => x.MigrationsHistoryTable(HistoryRepository.DefaultTableName, CustomerContext.Schema)) + .ConfigureWarnings(w => w.Ignore(SqlServerEventId.SavepointsDisabledBecauseOfMARS))); } public const string InvalidLastname = "Exception"; + private readonly string[] _surnames = ["Doe", "Smith", InvalidLastname]; + [Theory] - [InlineData([TransactionType.SqlTransaction, BusType.AzureSB, 100])] - [InlineData([TransactionType.TransactionScope, BusType.AzureSB, 100])] - [InlineData([TransactionType.SqlTransaction, BusType.Kafka, 100])] - public async Task Given_CommandHandlerInTransaction_When_ExceptionThrownDuringHandlingRaisedAtTheEnd_Then_TransactionIsRolledBack_And_NoDataSaved_And_NoEventRaised(TransactionType transactionType, BusType busType, int messageCount) + [InlineData([TransactionType.SqlTransaction, BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.ClientGuidGenerator])] + [InlineData([TransactionType.SqlTransaction, BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.DatabaseGeneratedSequentialGuid])] + [InlineData([TransactionType.SqlTransaction, BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.DatabaseGeneratedGuid])] + [InlineData([TransactionType.TransactionScope, BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.ClientGuidGenerator])] + [InlineData([TransactionType.SqlTransaction, BusType.Kafka, 100, SqlOutboxMessageIdGenerationMode.ClientGuidGenerator])] + public async Task Given_CommandHandlerInTransaction_When_ExceptionThrownDuringHandlingRaisedAtTheEnd_Then_TransactionIsRolledBack_And_NoDataSaved_And_NoEventRaised(TransactionType transactionType, BusType busType, int messageCount, SqlOutboxMessageIdGenerationMode mode) { // arrange _testParamUseHybridBus = true; _testParamTransactionType = transactionType; _testParamBusType = busType; + _testParamIdGenerationMode = mode; - await PerformDbOperation(async (context, _) => - { - // migrate db - await context.Database.DropSchemaIfExistsAsync(context.Model.GetDefaultSchema()); - await context.Database.MigrateAsync(); - }); + await PrepareDatabase(); - var surnames = new[] { "Doe", "Smith", InvalidLastname }; - var commands = Enumerable.Range(0, messageCount).Select(x => new CreateCustomerCommand($"John {x:000}", surnames[x % surnames.Length])); + var commands = Enumerable.Range(0, messageCount).Select(x => new CreateCustomerCommand($"John {x:000}", _surnames[x % _surnames.Length])).ToList(); var validCommands = commands.Where(x => !string.Equals(x.Lastname, InvalidLastname, StringComparison.InvariantCulture)).ToList(); await EnsureConsumersStarted(); @@ -182,25 +192,33 @@ await PerformDbOperation(async (context, _) => customerCountWithValidLastname.Should().Be(validCommands.Count); } + private Task PrepareDatabase() + => PerformDbOperation(async (context, _) => + { + // migrate db + await context.Database.MigrateAsync(); + await context.Customers.ExecuteDeleteAsync(); +#pragma warning disable EF1002 // Risk of vulnerability to SQL injection. + await context.Database.ExecuteSqlRawAsync($"delete from {context.Model.GetDefaultSchema()}.Outbox"); +#pragma warning restore EF1002 // Risk of vulnerability to SQL injection. + }); + [Theory] - [InlineData([BusType.AzureSB, 100])] - [InlineData([BusType.Kafka, 100])] - public async Task Given_PublishExternalEventInTransaction_When_ExceptionThrownDuringHandlingRaisedAtTheEnd_Then_TransactionIsRolledBack_And_NoEventRaised(BusType busType, int messageCount) + [InlineData([BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.ClientGuidGenerator])] + [InlineData([BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.DatabaseGeneratedGuid])] + [InlineData([BusType.AzureSB, 100, SqlOutboxMessageIdGenerationMode.DatabaseGeneratedSequentialGuid])] + [InlineData([BusType.Kafka, 100, SqlOutboxMessageIdGenerationMode.ClientGuidGenerator])] + public async Task Given_PublishExternalEventInTransaction_When_ExceptionThrownDuringHandlingRaisedAtTheEnd_Then_TransactionIsRolledBack_And_NoEventRaised(BusType busType, int messageCount, SqlOutboxMessageIdGenerationMode mode) { // arrange _testParamUseHybridBus = false; _testParamTransactionType = TransactionType.TransactionScope; _testParamBusType = busType; - await PerformDbOperation(async (context, _) => - { - // migrate db - await context.Database.DropSchemaIfExistsAsync(context.Model.GetDefaultSchema()); - await context.Database.MigrateAsync(); - }); + await PrepareDatabase(); var surnames = new[] { "Doe", "Smith", InvalidLastname }; - var events = Enumerable.Range(0, messageCount).Select(x => new CustomerCreatedEvent(Guid.NewGuid(), $"John {x:000}", surnames[x % surnames.Length])); + var events = Enumerable.Range(0, messageCount).Select(x => new CustomerCreatedEvent(Guid.NewGuid(), $"John {x:000}", surnames[x % surnames.Length])).ToList(); var validEvents = events.Where(x => !string.Equals(x.Lastname, InvalidLastname, StringComparison.InvariantCulture)).ToList(); await EnsureConsumersStarted(); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj similarity index 94% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj index 91f43ffb..86a731ac 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/TransactionType.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/TransactionType.cs similarity index 52% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/TransactionType.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/TransactionType.cs index 1be79f15..51a1bfb8 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/TransactionType.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/TransactionType.cs @@ -1,4 +1,4 @@ -namespace SlimMessageBus.Host.Outbox.DbContext.Test; +namespace SlimMessageBus.Host.Outbox.Sql.DbContext.Test; public enum TransactionType { diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Usings.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Usings.cs similarity index 82% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Usings.cs rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Usings.cs index 1c22dc7b..e8b66f07 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/Usings.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/Usings.cs @@ -15,8 +15,7 @@ global using SlimMessageBus.Host.AzureServiceBus; global using SlimMessageBus.Host.Kafka; global using SlimMessageBus.Host.Memory; -global using SlimMessageBus.Host.Outbox.DbContext.Test.DataAccess; -global using SlimMessageBus.Host.Outbox.Sql; +global using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; global using SlimMessageBus.Host.Serialization.SystemTextJson; global using SlimMessageBus.Host.Test.Common.IntegrationTest; diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/appsettings.json similarity index 100% rename from src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/appsettings.json rename to src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/appsettings.json diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/BaseSqlOutboxRepositoryTest.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/BaseSqlOutboxRepositoryTest.cs index cb51dc21..fb29e65d 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/BaseSqlOutboxRepositoryTest.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/BaseSqlOutboxRepositoryTest.cs @@ -1,5 +1,5 @@ -namespace SlimMessageBus.Host.Outbox.Sql.Test; - +namespace SlimMessageBus.Host.Outbox.Sql.Test; + public class BaseSqlOutboxRepositoryTest : BaseSqlTest { protected readonly Fixture _fixture = new(); @@ -7,9 +7,10 @@ public class BaseSqlOutboxRepositoryTest : BaseSqlTest protected SqlConnection _connection; protected SqlOutboxMigrationService _migrationService; protected SqlOutboxSettings _settings; - protected SqlOutboxRepository _target; + protected SqlOutboxMessageRepository _target; protected SqlOutboxTemplate _template; - protected ISqlTransactionService _transactionService; + protected ISqlTransactionService _transactionService; + protected CurrentTimeProviderFake _currentTimeProvider; public override async Task InitializeAsync() { @@ -19,8 +20,9 @@ public override async Task InitializeAsync() _connection = new SqlConnection(GetConnectionString()); _transactionService = new SqlTransactionService(_connection, _settings.SqlSettings); _template = new SqlOutboxTemplate(_settings); - _target = new SqlOutboxRepository(NullLogger.Instance, _settings, _template, _connection, _transactionService); - _migrationService = new SqlOutboxMigrationService(NullLogger.Instance, _target, _transactionService, _settings); + _currentTimeProvider = new(); + _target = new SqlOutboxMessageRepository(NullLogger.Instance, _settings, _template, new GuidGenerator(), _currentTimeProvider, new DefaultInstanceIdProvider(), _connection, _transactionService); + _migrationService = new SqlOutboxMigrationService(NullLogger.Instance, _target, _transactionService, _settings); await _migrationService.Migrate(CancellationToken.None); } @@ -31,49 +33,45 @@ public override Task DisposeAsync() return base.DisposeAsync(); } - protected async Task> SeedOutbox(int count, Action action = null, CancellationToken cancellationToken = default) + protected async Task> SeedOutbox(int count, Action action = null, CancellationToken cancellationToken = default) { var messages = CreateOutboxMessages(count); for (var i = 0; i < messages.Count; i++) { var message = messages[i]; action?.Invoke(i, message); - await _target.Save(message, cancellationToken); + message.Id = (Guid)(await _target.Create(message.BusName, message.Headers, message.Path, message.MessageType, message.MessagePayload, cancellationToken)).Id; } - return messages; } - protected IReadOnlyList CreateOutboxMessages(int count) - { - return Enumerable - .Range(0, count) - .Select(_ => - { - // Create a sample object for MessagePayload - var samplePayload = new { Key = _fixture.Create(), Number = _fixture.Create() }; - var jsonPayload = JsonSerializer.SerializeToUtf8Bytes(samplePayload); + protected IReadOnlyList CreateOutboxMessages(int count) => Enumerable + .Range(0, count) + .Select(_ => + { + // Create a sample object for MessagePayload + var samplePayload = new { Key = _fixture.Create(), Number = _fixture.Create() }; + var jsonPayload = JsonSerializer.SerializeToUtf8Bytes(samplePayload); - // Generate Headers dictionary with simple types - var headers = new Dictionary - { - { "Header1", _fixture.Create() }, - { "Header2", _fixture.Create() }, - { "Header3", _fixture.Create() } - }; + // Generate Headers dictionary with simple types + var headers = new Dictionary + { + { "Header1", _fixture.Create() }, + { "Header2", _fixture.Create() }, + { "Header3", _fixture.Create() } + }; - // Configure fixture to use the generated values - _fixture.Customize(om => om - .With(x => x.MessagePayload, jsonPayload) - .With(x => x.Headers, headers) - .With(x => x.LockExpiresOn, DateTime.MinValue) - .With(x => x.LockInstanceId, string.Empty) - .With(x => x.DeliveryAborted, false) - .With(x => x.DeliveryAttempt, 0) - .With(x => x.DeliveryComplete, false)); + // Configure fixture to use the generated values + _fixture.Customize(om => om + .With(x => x.MessagePayload, jsonPayload) + .With(x => x.Headers, headers) + .With(x => x.LockExpiresOn, DateTime.MinValue) + .With(x => x.LockInstanceId, string.Empty) + .With(x => x.DeliveryAborted, false) + .With(x => x.DeliveryAttempt, 0) + .With(x => x.DeliveryComplete, false)); - return _fixture.Create(); - }) - .ToList(); - } + return _fixture.Create(); + }) + .ToList(); } diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/GlobalUsings.cs index dc6a3e07..74fbfdb3 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/GlobalUsings.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/GlobalUsings.cs @@ -1,15 +1,16 @@ global using System.Text.Json; global using System.Threading.Tasks; - + global using AutoFixture; - + global using FluentAssertions; - + global using Microsoft.Data.SqlClient; global using Microsoft.Extensions.Logging.Abstractions; - + global using SlimMessageBus.Host.Sql.Common; - +global using SlimMessageBus.Host.Test.Common; + global using Testcontainers.MsSql; - + global using Xunit; diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj index bee621cc..c6fcffff 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj @@ -2,20 +2,18 @@ - - - - + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs index 381e1199..f14b2630 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs @@ -1,5 +1,4 @@ -namespace SlimMessageBus.Host.Outbox.Sql.Test; - +namespace SlimMessageBus.Host.Outbox.Sql.Test; public static class SqlOutboxRepositoryTests { public class SaveTests : BaseSqlOutboxRepositoryTest @@ -11,12 +10,18 @@ public async Task SavedMessage_IsPersisted() var message = CreateOutboxMessages(1).Single(); // act - await _target.Save(message, CancellationToken.None); - var actual = await _target.GetAllMessages(CancellationToken.None); - - // assert - actual.Count.Should().Be(1); - actual.Single().Should().BeEquivalentTo(message); + message.Id = (Guid)(await _target.Create(message.BusName, message.Headers, message.Path, message.MessageType, message.MessagePayload, CancellationToken.None)).Id; + var messages = await _target.GetAllMessages(CancellationToken.None); + + // assert + messages.Count.Should().Be(1); + var actual = messages.Single(); + actual.Id.Should().Be(message.Id); + actual.BusName.Should().Be(message.BusName); + actual.Headers.Should().BeEquivalentTo(message.Headers); + actual.Path.Should().Be(message.Path); + actual.MessageType.Should().Be(message.MessageType); + actual.MessagePayload.Should().BeEquivalentTo(message.MessagePayload); } } @@ -50,11 +55,13 @@ public async Task ExpiredItems_AreDeleted() var seedMessages = await SeedOutbox(10, (i, x) => { - x.DeliveryAttempt = 1; - x.DeliveryComplete = true; - x.Timestamp = i < 5 ? expired : active; + // affect the timestamp to make the message expired + _currentTimeProvider.CurrentTime = i < 5 ? expired : active; }); - + + // mark the first 5 messages as sent + await _target.UpdateToSent(seedMessages.Select(x => x.Id).Take(5).ToList(), CancellationToken.None); + // act await _target.DeleteSent(active, CancellationToken.None); var messages = await _target.GetAllMessages(CancellationToken.None); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Test/Interceptors/OutboxForwardingPublishInterceptorTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Test/Interceptors/OutboxForwardingPublishInterceptorTests.cs index b13f9dd8..8695e9c3 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Test/Interceptors/OutboxForwardingPublishInterceptorTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Test/Interceptors/OutboxForwardingPublishInterceptorTests.cs @@ -1,6 +1,6 @@ -namespace SlimMessageBus.Host.Outbox.Test.Interceptors; - -using SlimMessageBus.Host.Outbox.Services; +namespace SlimMessageBus.Host.Outbox.Test.Interceptors; + +using SlimMessageBus.Host.Outbox; using SlimMessageBus.Host.Serialization; public static class OutboxForwardingPublishInterceptorTests @@ -14,13 +14,13 @@ public void OutboxForwardingPublisher_MustBeLastInPipeline() var expected = int.MaxValue; var mockLogger = new Mock>(); - var mockOutboxRepository = new Mock(); - var mockInstanceIdProvider = new Mock(); + var mockOutboxRepository = new Mock, Guid>>(); var mockOutboxNotificationService = new Mock(); - var mockOutboxSettings = new Mock(); + var mockOutboxSettings = new Mock(); + var mockOutboxMessageFactory = new Mock(); // act - var target = new OutboxForwardingPublishInterceptor(mockLogger.Object, mockOutboxRepository.Object, mockInstanceIdProvider.Object, mockOutboxNotificationService.Object, mockOutboxSettings.Object); + var target = new OutboxForwardingPublishInterceptor(mockLogger.Object, mockOutboxMessageFactory.Object, mockOutboxNotificationService.Object, mockOutboxSettings.Object); var actual = target.Order; // assert @@ -41,20 +41,21 @@ public void OutboxForwardingPublisher_MustImplement_IInterceptorWithOrder() public class OnHandleTests { private readonly Mock> _mockLogger; - private readonly Mock _mockOutboxRepository; - private readonly Mock _mockInstanceIdProvider; + private readonly Mock, Guid>> _mockOutboxRepository; + private readonly Mock _mockOutboxFactory; private readonly Mock _mockSerializer; private readonly Mock _mockMasterMessageBus; private readonly Mock _mockOutboxNotificationService; private readonly Mock _mockOutboxSettings; - private Mock _mockTargetBus; - private Mock _mockProducerContext; + private readonly Mock _mockTargetBus; + private readonly Mock _mockProducerContext; + private readonly Mock _mockOutboxMessageFactory; public OnHandleTests() { _mockLogger = new Mock>(); - _mockOutboxRepository = new Mock(); - _mockInstanceIdProvider = new Mock(); + _mockOutboxRepository = new Mock, Guid>>(); + _mockOutboxFactory = new Mock(); _mockOutboxNotificationService = new Mock(); _mockOutboxSettings = new Mock(); @@ -67,7 +68,12 @@ public OnHandleTests() _mockTargetBus.SetupGet(x => x.Target).Returns(_mockMasterMessageBus.Object); _mockProducerContext = new Mock(); - _mockProducerContext.SetupGet(x => x.Bus).Returns(_mockTargetBus.Object); + _mockProducerContext.SetupGet(x => x.Bus).Returns(_mockTargetBus.Object); + + _mockOutboxMessageFactory = new Mock(); + _mockOutboxMessageFactory + .Setup(x => x.Create(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync( new OutboxMessage { Id = Guid.NewGuid() }); } [Fact] @@ -75,7 +81,8 @@ public async Task SkipOutboxHeader_IsPresent_PromoteToNext() { // arrange _mockProducerContext.SetupGet(x => x.Headers).Returns(new Dictionary { { OutboxForwardingPublishInterceptor.SkipOutboxHeader, true } }); - _mockOutboxRepository.Setup(x => x.Save(It.IsAny(), It.IsAny())).Verifiable(); + _mockOutboxFactory.Setup(x => x.Create(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); + var nextCalled = 0; var next = () => @@ -85,14 +92,22 @@ public async Task SkipOutboxHeader_IsPresent_PromoteToNext() }; // act - var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxRepository.Object, _mockInstanceIdProvider.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); + var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxMessageFactory.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); await target.OnHandle(new object(), next, _mockProducerContext.Object); target.Dispose(); // assert _mockProducerContext.VerifyGet(x => x.Bus, Times.AtLeastOnce); _mockProducerContext.VerifyGet(x => x.Headers, Times.AtLeastOnce); - _mockOutboxRepository.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Never); + _mockOutboxFactory + .Verify(x => x.Create( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); nextCalled.Should().Be(1); } @@ -103,8 +118,8 @@ public async Task SkipOutboxHeader_IsNotPresent_DoNotPromoteToNext() // arrange var message = new object(); - _mockSerializer.Setup(x => x.Serialize(typeof(object), message)).Verifiable(); - _mockOutboxRepository.Setup(x => x.Save(It.IsAny(), It.IsAny())).Verifiable(); + _mockSerializer.Setup(x => x.Serialize(typeof(object), message)).Verifiable(); + _mockOutboxFactory.Setup(x => x.Create(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); var nextCalled = 0; var next = () => @@ -114,13 +129,21 @@ public async Task SkipOutboxHeader_IsNotPresent_DoNotPromoteToNext() }; // act - var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxRepository.Object, _mockInstanceIdProvider.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); + var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxMessageFactory.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); await target.OnHandle(new object(), next, _mockProducerContext.Object); target.Dispose(); // assert nextCalled.Should().Be(0); - _mockOutboxRepository.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Once); + _mockOutboxFactory + .Verify(x => x.Create( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); } [Fact] @@ -128,16 +151,24 @@ public async Task SkipOutboxHeader_IsPresent_DoNotRaiseOutboxNotification() { // arrange _mockProducerContext.SetupGet(x => x.Headers).Returns(new Dictionary { { OutboxForwardingPublishInterceptor.SkipOutboxHeader, true } }); - _mockOutboxRepository.Setup(x => x.Save(It.IsAny(), It.IsAny())).Verifiable(); + _mockOutboxFactory.Setup(x => x.Create(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); _mockOutboxNotificationService.Setup(x => x.Notify()).Verifiable(); // act - var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxRepository.Object, _mockInstanceIdProvider.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); + var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxMessageFactory.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); await target.OnHandle(new object(), () => Task.CompletedTask, _mockProducerContext.Object); target.Dispose(); // assert - _mockOutboxRepository.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Never); + _mockOutboxFactory + .Verify(x => x.Create( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); _mockOutboxNotificationService.Verify(x => x.Notify(), Times.Never); } @@ -148,16 +179,24 @@ public async Task SkipOutboxHeader_IsNotPresent_RaiseOutboxNotification() var message = new object(); _mockSerializer.Setup(x => x.Serialize(typeof(object), message)).Verifiable(); - _mockOutboxRepository.Setup(x => x.Save(It.IsAny(), It.IsAny())).Verifiable(); + _mockOutboxFactory.Setup(x => x.Create(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); _mockOutboxNotificationService.Setup(x => x.Notify()).Verifiable(); // act - var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxRepository.Object, _mockInstanceIdProvider.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); + var target = new OutboxForwardingPublishInterceptor(_mockLogger.Object, _mockOutboxMessageFactory.Object, _mockOutboxNotificationService.Object, _mockOutboxSettings.Object); await target.OnHandle(new object(), () => Task.CompletedTask, _mockProducerContext.Object); target.Dispose(); // assert - _mockOutboxRepository.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Once); + _mockOutboxFactory + .Verify(x => x.Create( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); _mockOutboxNotificationService.Verify(x => x.Notify(), Times.Once); } } diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs index ffc9d15d..0ca55039 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs @@ -2,8 +2,8 @@ public class OutboxLockRenewalTimerTests { - private readonly Mock> _loggerMock; - private readonly Mock _outboxRepositoryMock; + private readonly Mock, Guid>>> _loggerMock; + private readonly Mock, Guid>> _outboxRepositoryMock; private readonly Mock _instanceIdProviderMock; private readonly CancellationTokenSource _cancellationTokenSource; private readonly Action _lockLostAction; @@ -13,8 +13,8 @@ public class OutboxLockRenewalTimerTests public OutboxLockRenewalTimerTests() { - _loggerMock = new Mock>(); - _outboxRepositoryMock = new Mock(); + _loggerMock = new Mock, Guid>>>(); + _outboxRepositoryMock = new Mock, Guid>>(); _instanceIdProviderMock = new Mock(); _cancellationTokenSource = new CancellationTokenSource(); _lockLostAction = Mock.Of>(); @@ -111,9 +111,9 @@ public async Task CallbackAsync_ShouldReturnGracefullyIfTokenCancelled() lockLostActionMock.Verify(a => a(It.IsAny()), Times.Never); } - private OutboxLockRenewalTimer CreateTimer(Action lockLostAction = null) + private OutboxLockRenewalTimer, Guid> CreateTimer(Action lockLostAction = null) { - return new OutboxLockRenewalTimer( + return new OutboxLockRenewalTimer, Guid>( _loggerMock.Object, _outboxRepositoryMock.Object, _instanceIdProviderMock.Object, @@ -123,9 +123,9 @@ private OutboxLockRenewalTimer CreateTimer(Action lockLostAction = nu _cancellationTokenSource.Token); } - private static async Task InvokeCallbackAsync(OutboxLockRenewalTimer timer) + private static async Task InvokeCallbackAsync(OutboxLockRenewalTimer, Guid> timer) { - var callbackMethod = typeof(OutboxLockRenewalTimer).GetMethod("CallbackAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var callbackMethod = typeof(OutboxLockRenewalTimer, Guid>).GetMethod("CallbackAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); await (Task)callbackMethod.Invoke(timer, null); } } diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs index e4f7a680..515e9a10 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs @@ -1,29 +1,29 @@ -namespace SlimMessageBus.Host.Outbox.Test.Services; - -using static SlimMessageBus.Host.Outbox.Services.OutboxSendingTask; - +namespace SlimMessageBus.Host.Outbox.Test.Services; + +using static SlimMessageBus.Host.Outbox.Services.OutboxSendingTask, Guid>; + public sealed class OutboxSendingTaskTests { public class DispatchBatchTests { private readonly ILoggerFactory _loggerFactory; - private readonly Mock _outboxRepositoryMock; + private readonly Mock, Guid>> _outboxRepositoryMock; private readonly Mock _producerMock; private readonly Mock _messageBusTargetMock; private readonly OutboxSettings _outboxSettings; private readonly IServiceProvider _serviceProvider; - private readonly OutboxSendingTask _sut; + private readonly OutboxSendingTask, Guid> _sut; public DispatchBatchTests() { - _outboxRepositoryMock = new Mock(); + _outboxRepositoryMock = new Mock, Guid>>(); _producerMock = new Mock(); _messageBusTargetMock = new Mock(); _outboxSettings = new OutboxSettings { MaxDeliveryAttempts = 5 }; _serviceProvider = Mock.Of(); _loggerFactory = new NullLoggerFactory(); - _sut = new OutboxSendingTask(_loggerFactory, _outboxSettings, _serviceProvider); + _sut = new OutboxSendingTask, Guid>(_loggerFactory, _outboxSettings, new CurrentTimeProvider(), _serviceProvider); } [Fact] @@ -88,17 +88,17 @@ public async Task DispatchBatch_ShouldIncrementDeliveryAttempts_WhenNotAllMessag public class ProcessMessagesTests { - private readonly Mock _mockOutboxRepository; + private readonly Mock, Guid>> _mockOutboxRepository; private readonly Mock _mockCompositeMessageBus; private readonly Mock _mockMessageBusTarget; private readonly Mock _mockMasterMessageBus; private readonly Mock _mockMessageBusBulkProducer; private readonly OutboxSettings _outboxSettings; - private readonly OutboxSendingTask _sut; + private readonly OutboxSendingTask, Guid> _sut; public ProcessMessagesTests() { - _mockOutboxRepository = new Mock(); + _mockOutboxRepository = new Mock, Guid>>(); _mockCompositeMessageBus = new Mock(); _mockMessageBusTarget = new Mock(); _mockMasterMessageBus = new Mock(); @@ -110,7 +110,7 @@ public ProcessMessagesTests() MessageTypeResolver = new Mock().Object }; - _sut = new OutboxSendingTask(NullLoggerFactory.Instance, _outboxSettings, null); + _sut = new OutboxSendingTask, Guid>(NullLoggerFactory.Instance, _outboxSettings, new CurrentTimeProvider(), null); } [Fact] @@ -235,12 +235,12 @@ public async Task ProcessMessages_ShouldAbortDelivery_WhenMessageTypeIsNotRecogn result.Count.Should().Be(knownMessageCount); } - private static List CreateOutboxMessages(int count) + private static List> CreateOutboxMessages(int count) { return Enumerable .Range(0, count) .Select( - _ => new OutboxMessage + _ => new OutboxMessage { Id = Guid.NewGuid(), MessageType = "TestType", diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs index fb1deae3..beced739 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs @@ -25,6 +25,7 @@ public RedisMessageBusTest() { _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(_messageSerializerMock.Object); _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); + _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); _serviceProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns(Enumerable.Empty()); _settings.ServiceProvider = _serviceProviderMock.Object; diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/CurrentTimeProviderFake.cs b/src/Tests/SlimMessageBus.Host.Test.Common/CurrentTimeProviderFake.cs new file mode 100644 index 00000000..61c3a953 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Test.Common/CurrentTimeProviderFake.cs @@ -0,0 +1,6 @@ +namespace SlimMessageBus.Host.Test.Common; + +public class CurrentTimeProviderFake : ICurrentTimeProvider +{ + public DateTimeOffset CurrentTime { get; set; } = DateTimeOffset.Now; +} \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs index b8f68ad7..60c1f7c6 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs @@ -5,7 +5,7 @@ namespace SlimMessageBus.Host.Test; using SlimMessageBus.Host.Interceptor; using SlimMessageBus.Host.Serialization; -public class MessageBusMock +public class MessageBusMock : ICurrentTimeProvider { public Mock ServiceProviderMock { get; } public IList> ChildDependencyResolverMocks { get; } @@ -63,6 +63,7 @@ void SetupDependencyResolver(Mock mock) where T : class, IServiceProvider ServiceProviderMock.Setup(x => x.GetService(typeof(IServiceScopeFactory))).Returns(serviceScopeFactoryMock.Object); ServiceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(SerializerMock.Object); + ServiceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(this); var mbSettings = new MessageBusSettings { @@ -75,7 +76,6 @@ void SetupDependencyResolver(Mock mock) where T : class, IServiceProvider BusMock.SetupGet(x => x.Settings).Returns(mbSettings); BusMock.SetupGet(x => x.Serializer).CallBase(); BusMock.SetupGet(x => x.MessageBusTarget).CallBase(); - BusMock.SetupGet(x => x.CurrentTime).Returns(() => CurrentTime); BusMock.Setup(x => x.CreateHeaders()).CallBase(); BusMock.Setup(x => x.CreateMessageScope(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).CallBase(); } diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs index f80aa052..6c63213f 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs @@ -39,7 +39,7 @@ public MessageHandlerTest() messageTypeResolver: messageTypeResolverMock.Object, messageHeadersFactory: messageHeaderFactoryMock.Object, runtimeTypeCache: new Host.Collections.RuntimeTypeCache(), - currentTimeProvider: busMock.Bus, + currentTimeProvider: busMock, path: "topic1"); } diff --git a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs index f6a18972..30140f62 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs @@ -39,6 +39,7 @@ public HybridMessageBusTest() _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); _serviceProviderMock.Setup(x => x.GetService(typeof(ILoggerFactory))).Returns(_loggerFactoryMock.Object); _serviceProviderMock.Setup(x => x.GetService(typeof(IEnumerable))).Returns(Array.Empty()); + _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); _loggerFactoryMock.Setup(x => x.CreateLogger(It.IsAny())).Returns(_loggerMock.Object); diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs index 0b0fea6f..57c27fbb 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs @@ -1,7 +1,5 @@ namespace SlimMessageBus.Host.Test; -using System.Threading; - using Moq.Protected; using SlimMessageBus.Host.Test.Common; @@ -29,9 +27,13 @@ public MessageBusBaseTests() _producedMessages = []; + var currentTimeProviderMock = new Mock(); + currentTimeProviderMock.SetupGet(x => x.CurrentTime).Returns(() => _timeNow); + _serviceProviderMock = new Mock(); _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(new JsonMessageSerializer()); _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); + _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); _serviceProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Array.CreateInstance(t.GetGenericArguments()[0], 0)); BusBuilder = MessageBusBuilder.Create() @@ -52,10 +54,9 @@ public MessageBusBaseTests() .WithDependencyResolver(_serviceProviderMock.Object) .WithProvider(s => { - return new MessageBusTested(s) + return new MessageBusTested(s, currentTimeProviderMock.Object) { // provide current time - CurrentTimeProvider = () => _timeNow, OnProduced = (mt, n, m) => _producedMessages.Add(new(mt, n, m)) }; }); @@ -497,6 +498,7 @@ public async Task When_Publish_Given_InterceptorsInDI_Then_InterceptorInfluenceI _serviceProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageSerializer)), Times.Between(0, 1, Moq.Range.Inclusive)); _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.VerifyNoOtherCalls(); if (producerInterceptorCallsNext != null) @@ -596,6 +598,7 @@ public async Task When_Send_Given_InterceptorsInDI_Then_InterceptorInfluenceIfTh _serviceProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageSerializer)), Times.Between(0, 1, Moq.Range.Inclusive)); _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); _serviceProviderMock.VerifyNoOtherCalls(); if (producerInterceptorCallsNext != null) @@ -679,6 +682,7 @@ public async Task When_Given_NoReplyToHeader_DoNothing() var mockServiceProvider = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(mockMessageTypeResolver.Object); + mockServiceProvider.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); var mockMessageTypeConsumerInvokerSettings = new Mock(); mockMessageTypeConsumerInvokerSettings.SetupGet(x => x.ParentSettings).Returns(() => new ConsumerSettings() { ResponseType = response.GetType() }); diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs index 0e3e427c..e2696828 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs @@ -5,12 +5,13 @@ public class MessageBusTested : MessageBusBase internal int _startedCount; internal int _stoppedCount; - public MessageBusTested(MessageBusSettings settings) + public MessageBusTested(MessageBusSettings settings, ICurrentTimeProvider currentTimeProvider) : base(settings) { // by default no responses will arrive OnReply = (type, payload, req) => null; - + + CurrentTimeProvider = currentTimeProvider; OnBuildProvider(); } @@ -78,12 +79,8 @@ protected override async Task> ProduceToTranspor return new(dispatched, null); } - public override DateTimeOffset CurrentTime => CurrentTimeProvider(); - #endregion - public Func CurrentTimeProvider { get; set; } = () => DateTimeOffset.UtcNow; - public void TriggerPendingRequestCleanup() { PendingRequestManager.CleanPendingRequests(); From 834044d91d119ffef056f384d7a5428392bbef87 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Mon, 4 Nov 2024 18:22:00 +0100 Subject: [PATCH 04/21] [Host] Batch produce API Signed-off-by: Tomasz Maruszak --- docs/NuGet.md | 1 + docs/intro.md | 25 ++ docs/intro.t.md | 25 ++ .../EventHubMessageBus.cs | 105 +++-- .../ServiceBusMessageBus.cs | 122 +++--- .../KafkaMessageBus.cs | 106 +++-- .../MemoryMessageBus.cs | 13 +- .../MqttMessageBus.cs | 81 ++-- .../NatsMessageBus.cs | 83 ++-- ...ringSqlMessageOutboxRepositoryDecorator.cs | 6 +- .../Services/LockLostException.cs | 14 + .../Services/OutboxLockRenewalTimer.cs | 15 +- .../Services/OutboxSendingTask.cs | 4 +- .../RabbitMqMessageBus.cs | 121 +++--- .../RedisMessageBus.cs | 51 +-- src/SlimMessageBus.Host.Sql/SqlMessageBus.cs | 7 +- .../Collections/CollectionTypeInfo.cs | 7 + .../Collections/IRuntimeTypeCache.cs | 2 + .../Collections/ProducerByMessageTypeCache.cs | 20 +- .../Collections/RuntimeTypeCache.cs | 29 ++ .../MessageProcessors/IResponseProducer.cs | 2 +- .../MessageProcessors/MessageProcessor.cs | 4 +- .../ServiceCollectionExtensions.cs | 5 + .../Hybrid/HybridMessageBus.cs | 2 +- ...kProducer.cs => ITransportBulkProducer.cs} | 10 +- src/SlimMessageBus.Host/ITransportProducer.cs | 12 + src/SlimMessageBus.Host/MessageBusBase.cs | 395 ++++++++++-------- .../ProducerInterceptorPipeline.cs | 8 +- .../PublishInterceptorPipeline.cs | 10 +- .../SendInterceptorPipeline.cs | 4 +- .../RequestResponse/IPendingRequestManager.cs | 7 + .../InMemoryPendingRequestStore.cs | 2 +- .../RequestResponse/PendingRequestManager.cs | 21 +- src/SlimMessageBus.sln | 12 +- .../EventHubMessageBusIt.cs | 23 +- .../GlobalUsings.cs | 12 +- .../ServiceBusMessageBusIt.cs | 52 ++- .../ServiceBusMessageBusTests.cs | 3 + .../HybridTests.cs | 6 +- .../MessageBusCurrentTests.cs | 4 +- .../MessageScopeAccessorTests.cs | 5 +- .../KafkaPartitionConsumerForConsumersTest.cs | 1 + .../KafkaMessageBusIt.cs | 36 +- .../KafkaMessageBusTest.cs | 7 +- .../MessageBusMock.cs | 4 + .../MemoryMessageBusIt.cs | 2 +- .../MemoryMessageBusTests.cs | 40 +- .../MqttMessageBusIt.cs | 29 +- .../NatsMessageBusIt.cs | 27 +- .../BaseOutboxIntegrationTest.cs | 2 +- .../OutboxBenchmarkTests.cs | 4 +- .../OutboxTests.cs | 3 +- .../Services/OutboxSendingTaskTests.cs | 8 +- .../IntegrationTests/RabbitMqMessageBusIt.cs | 3 +- .../GlobalUsings.cs | 1 + .../RedisMessageBusIt.cs | 63 +-- .../RedisMessageBusTest.cs | 6 + .../GlobalUsings.cs | 17 +- .../IntegrationTest/BaseIntegrationTest.cs | 17 +- .../XunitLogger.cs | 8 +- .../XunitLoggerFactory.cs | 3 +- .../ProducerByMessageTypeCacheTests.cs | 60 +-- .../Collections/RuntimeTypeCacheTests.cs | 37 ++ .../ConsumerInstanceMessageProcessorTest.cs | 12 +- .../Consumer/MessageBusMock.cs | 8 +- .../Hybrid/HybridMessageBusTest.cs | 3 + .../PublishInterceptorPipelineTests.cs | 11 +- .../MessageBusBaseTests.cs | 51 +-- .../MessageBusTested.cs | 71 ++-- .../PendingRequestManagerTest.cs | 7 +- 70 files changed, 1110 insertions(+), 867 deletions(-) create mode 100644 src/SlimMessageBus.Host.Outbox/Services/LockLostException.cs create mode 100644 src/SlimMessageBus.Host/Collections/CollectionTypeInfo.cs rename src/SlimMessageBus.Host/{IMessageBusBulkProducer.cs => ITransportBulkProducer.cs} (67%) create mode 100644 src/SlimMessageBus.Host/ITransportProducer.cs create mode 100644 src/SlimMessageBus.Host/RequestResponse/IPendingRequestManager.cs diff --git a/docs/NuGet.md b/docs/NuGet.md index b088e300..df5f1c9f 100644 --- a/docs/NuGet.md +++ b/docs/NuGet.md @@ -12,6 +12,7 @@ Transports: - Hybrid (composition of the bus out of many transports) - In-Memory transport (domain events, mediator) - MQTT / Azure IoT Hub +- NATS - RabbitMQ - Redis - SQL (MS SQL, PostgreSql) diff --git a/docs/intro.md b/docs/intro.md index 36e6f675..e415e8e6 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -3,6 +3,7 @@ - [Configuration](#configuration) - [Pub/Sub communication](#pubsub-communication) - [Producer](#producer) + - [Bulk (Batch) Publish](#bulk-batch-publish) - [Set message headers](#set-message-headers) - [Consumer](#consumer) - [Start or Stop message consumption](#start-or-stop-message-consumption) @@ -159,6 +160,30 @@ await bus.Publish(msg, cancellationToken: ct); > The transport plugins might introduce additional configuration options. Please check the relevant provider docs. For example, Azure Service Bus, Azure Event Hub and Kafka allow setting the partitioning key for a given message type. +#### Bulk (Batch) Publish + +Several transports support publishing messages in bulk, including: + +- **Azure Service Bus** +- **Azure Event Hub** +- **RabbitMQ** + +To publish messages in bulk, pass a collection of message instances of the specified type, as shown below: + +```csharp +// Assuming IMessageBus bus; +IEnumerable messages = [ ]; +await bus.Publish(messages); +``` + +Any collection type that can be converted to `IEnumerable` is supported. + +While there’s no upper limit enforced by SMB, be aware that the underlying transport may split messages into chunks to avoid exceeding its payload limits in a single publish operation. These chunks will retain the original message order. + +For transports that don’t natively support bulk publishing, messages are published individually in sequence. + +> **Note:** The [producer interceptor](#interceptors) pipeline is not executed during bulk publishing. This behavior may change in future updates. + #### Set message headers > Since version 1.15.0 diff --git a/docs/intro.t.md b/docs/intro.t.md index c0cbfb00..31a7bf0c 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -3,6 +3,7 @@ - [Configuration](#configuration) - [Pub/Sub communication](#pubsub-communication) - [Producer](#producer) + - [Bulk (Batch) Publish](#bulk-batch-publish) - [Set message headers](#set-message-headers) - [Consumer](#consumer) - [Start or Stop message consumption](#start-or-stop-message-consumption) @@ -159,6 +160,30 @@ await bus.Publish(msg, cancellationToken: ct); > The transport plugins might introduce additional configuration options. Please check the relevant provider docs. For example, Azure Service Bus, Azure Event Hub and Kafka allow setting the partitioning key for a given message type. +#### Bulk (Batch) Publish + +Several transports support publishing messages in bulk, including: + +- **Azure Service Bus** +- **Azure Event Hub** +- **RabbitMQ** + +To publish messages in bulk, pass a collection of message instances of the specified type, as shown below: + +```csharp +// Assuming IMessageBus bus; +IEnumerable messages = [ ]; +await bus.Publish(messages); +``` + +Any collection type that can be converted to `IEnumerable` is supported. + +While there’s no upper limit enforced by SMB, be aware that the underlying transport may split messages into chunks to avoid exceeding its payload limits in a single publish operation. These chunks will retain the original message order. + +For transports that don’t natively support bulk publishing, messages are published individually in sequence. + +> **Note:** The [producer interceptor](#interceptors) pipeline is not executed during bulk publishing. This behavior may change in future updates. + #### Set message headers > Since version 1.15.0 diff --git a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs index 4d2eb13c..d8a79cbe 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs @@ -104,68 +104,84 @@ protected override async Task OnStart() } } - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + private EventData GetTransportMessage(object message, Type messageType, IDictionary messageHeaders, string path, out string partitionKey) + { + OnProduceToTransport(message, messageType, path, messageHeaders); + + var messagePayload = message != null + ? Serializer.Serialize(messageType, message) + : null; + + var transportMessage = message != null + ? new EventData(messagePayload) + : new EventData(); + + if (messageHeaders != null) + { + foreach (var header in messageHeaders) + { + transportMessage.Properties.Add(header.Key, header.Value); + } + } + + partitionKey = messageType != null + ? GetPartitionKey(messageType, message) + : null; + + return transportMessage; + } + + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + try + { + var transportMessage = GetTransportMessage(message, messageType, messageHeaders, path, out var partitionKey); + var producer = _producerByPath[path]; + await producer.SendAsync([transportMessage], new SendEventOptions { PartitionKey = partitionKey }, cancellationToken); + } + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) + { + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); + } + } + + public override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) { AssertActive(); var dispatched = new List(envelopes.Count); try { + var producer = _producerByPath[path]; + var messagesByPartition = envelopes .Where(x => x.Message != null) .Select(envelope => { - var messageType = envelope.Message?.GetType(); - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); - - _logger.LogDebug("Producing message {Message} of Type {MessageType} on Path {Path} with Size {MessageSize}", envelope.Message, messageType?.Name, path, messagePayload?.Length ?? 0); - - var ev = envelope.Message != null ? new EventData(messagePayload) : new EventData(); - - if (envelope.Headers != null) - { - foreach (var header in envelope.Headers) - { - ev.Properties.Add(header.Key, header.Value); - } - } - - var partitionKey = messageType != null - ? GetPartitionKey(messageType, envelope.Message) - : null; - - return (Envelope: envelope, Message: ev, PartitionKey: partitionKey); + var transportMessage = GetTransportMessage(envelope.Message, envelope.MessageType, envelope.Headers, path, out var partitionKey); + return (Envelope: envelope, TransportMessage: transportMessage, PartitionKey: partitionKey); }) .GroupBy(x => x.PartitionKey); - var producer = _producerByPath[path]; + var inBatch = new List(envelopes.Count); foreach (var partition in messagesByPartition) { + var batchOptions = new CreateBatchOptions { PartitionKey = partition.Key }; EventDataBatch batch = null; try { - var items = partition.ToList(); - if (items.Count == 1) + using var it = partition.GetEnumerator(); + var advance = it.MoveNext(); + while (advance) { - // only one item - quicker to send on its own - var item = items.Single(); - await producer.SendAsync([item.Message], new SendEventOptions { PartitionKey = partition.Key }, cancellationToken); - - dispatched.Add(item.Envelope); - continue; - } + var item = it.Current; - // multiple items - send in batches - var inBatch = new List(items.Count); - var i = 0; - while (i < items.Count) - { - var item = items[i]; - batch ??= await producer.CreateBatchAsync(new CreateBatchOptions { PartitionKey = partition.Key }, cancellationToken); - if (batch.TryAdd(item.Message)) + batch ??= await producer.CreateBatchAsync(batchOptions, cancellationToken); + if (batch.TryAdd(item.TransportMessage)) { inBatch.Add(item.Envelope); - if (++i < items.Count) + advance = it.MoveNext(); + if (advance) { continue; } @@ -173,30 +189,29 @@ protected override async Task> ProduceToTranspor if (batch.Count == 0) { - throw new ProducerMessageBusException($"Failed to add message {item.Envelope.Message} of Type {item.Envelope.MessageType?.Name} on Path {path} to an empty batch"); + throw new ProducerMessageBusException($"Failed to add message {item.Envelope.Message} of type {item.Envelope.MessageType?.Name} on path {path} to an empty batch"); } + advance = false; await producer.SendAsync(batch, cancellationToken).ConfigureAwait(false); dispatched.AddRange(inBatch); inBatch.Clear(); + batch.Dispose(); batch = null; } - - return new(dispatched, null); } finally { batch?.Dispose(); } } + return new(dispatched, null); } catch (Exception ex) { return new(dispatched, ex); } - - return new(dispatched, null); } #endregion diff --git a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs index 0371664e..156efc4e 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs @@ -117,16 +117,33 @@ void AddConsumerFrom(TopicSubscriptionParams topicSubscription, IMessageProcesso } } - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { + try + { + var transportMessage = GetTransportMessage(message, messageType, messageHeaders, path); + var senderClient = _producerByPath.GetOrAdd(path); + await senderClient.SendMessageAsync(transportMessage, cancellationToken).ConfigureAwait(false); + _logger.LogDebug("Delivered item {Message} of type {MessageType} to {Path}", message, messageType?.Name, path); + } + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) + { + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); + } + } + + public override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + AssertActive(); + Task SendBatchAsync(ServiceBusSender senderClient, ServiceBusMessageBatch batch, CancellationToken cancellationToken) => Retry.WithDelay( - async cancellationToken => + operation: async cancellationToken => { await senderClient.SendMessagesAsync(batch, cancellationToken).ConfigureAwait(false); _logger.LogDebug("Batch of {BatchSize} message(s) dispatched to {Path} ({SizeInBytes} bytes)", batch.Count, path, batch.SizeInBytes); }, - (exception, attempt) => + shouldRetry: (exception, attempt) => { if (attempt < 3 && exception is ServiceBusException ex @@ -141,78 +158,35 @@ Task SendBatchAsync(ServiceBusSender senderClient, ServiceBusMessageBatch batch, jitter: TimeSpan.FromSeconds(1), cancellationToken); - AssertActive(); - var messages = envelopes .Select(envelope => { - var messageType = envelope.Message?.GetType(); - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); - - _logger.LogDebug("Producing item {Message} of type {MessageType} to path {Path} with size {MessageSize}", envelope.Message, messageType?.Name, path, messagePayload?.Length ?? 0); - - var m = messagePayload != null ? new ServiceBusMessage(messagePayload) : new ServiceBusMessage(); - - // add headers - if (envelope.Headers != null) - { - foreach (var header in envelope.Headers) - { - m.ApplicationProperties.Add(header.Key, header.Value); - } - } - - // global modifier first - InvokeMessageModifier(envelope.Message, messageType, m, ProviderSettings); - if (messageType != null) - { - // local producer modifier second - var producerSettings = GetProducerSettings(messageType); - InvokeMessageModifier(envelope.Message, messageType, m, producerSettings); - } - - return (Envelope: envelope, ServiceBusMessage: m); + var m = GetTransportMessage(envelope.Message, envelope.MessageType, envelope.Headers, path); + return (Envelope: envelope, TransportMessage: m); }) .ToList(); var senderClient = _producerByPath.GetOrAdd(path); - await EnsureInitFinished(); - - if (messages.Count == 1) - { - // only one item - quicker to send on its own - var item = messages.Single(); - try - { - await senderClient.SendMessageAsync(item.ServiceBusMessage, cancellationToken: cancellationToken).ConfigureAwait(false); - _logger.LogDebug("Delivered item {Message} of type {MessageType} to {Path}", item.Envelope.Message, item.Envelope.MessageType?.Name, path); - - return new([item.Envelope], null); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Producing message {Message} of type {MessageType} to path {Path} resulted in error {Error}", item.Envelope.Message, item.Envelope.MessageType?.Name, path, ex.Message); - return new([], ex); - } - } - // multiple items - send in batches var dispatched = new List(envelopes.Count); + var inBatch = new List(envelopes.Count); ServiceBusMessageBatch batch = null; try { // multiple items - send in batches - var inBatch = new List(envelopes.Count); - var i = 0; - while (i < messages.Count) + using var it = messages.GetEnumerator(); + var advance = it.MoveNext(); + while (advance) { - var item = messages[i]; + var item = it.Current; + batch ??= await senderClient.CreateMessageBatchAsync(cancellationToken); - if (batch.TryAddMessage(item.ServiceBusMessage)) + if (batch.TryAddMessage(item.TransportMessage)) { inBatch.Add(item.Envelope); - if (++i < messages.Count) + advance = it.MoveNext(); + if (advance) { continue; } @@ -223,10 +197,11 @@ Task SendBatchAsync(ServiceBusSender senderClient, ServiceBusMessageBatch batch, throw new ProducerMessageBusException($"Failed to add message {item.Envelope.Message} of Type {item.Envelope.MessageType?.Name} on Path {path} to an empty batch"); } + advance = false; await SendBatchAsync(senderClient, batch, cancellationToken).ConfigureAwait(false); - dispatched.AddRange(inBatch); inBatch.Clear(); + batch.Dispose(); batch = null; } @@ -244,6 +219,37 @@ Task SendBatchAsync(ServiceBusSender senderClient, ServiceBusMessageBatch batch, } } + private ServiceBusMessage GetTransportMessage(object message, Type messageType, IDictionary messageHeaders, string path) + { + var messagePayload = Serializer.Serialize(messageType, message); + + OnProduceToTransport(message, messageType, path, messageHeaders); + + var m = messagePayload != null + ? new ServiceBusMessage(messagePayload) + : new ServiceBusMessage(); + + // add headers + if (messageHeaders != null) + { + foreach (var header in messageHeaders) + { + m.ApplicationProperties.Add(header.Key, header.Value); + } + } + + // global modifier first + InvokeMessageModifier(message, messageType, m, ProviderSettings); + if (messageType != null) + { + // local producer modifier second + var producerSettings = GetProducerSettings(messageType); + InvokeMessageModifier(message, messageType, m, producerSettings); + } + + return m; + } + private void InvokeMessageModifier(object message, Type messageType, ServiceBusMessage m, HasProviderExtensions settings) { try diff --git a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs index 0a6c5580..11e58306 100644 --- a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs +++ b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs @@ -1,4 +1,4 @@ -namespace SlimMessageBus.Host.Kafka; +namespace SlimMessageBus.Host.Kafka; using IProducer = Confluent.Kafka.IProducer; using Message = Confluent.Kafka.Message; @@ -114,75 +114,65 @@ protected override async ValueTask DisposeAsyncCore() } } - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) - { - AssertActive(); - - var dispatched = new List(envelopes.Count); + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { try { - foreach (var envelope in envelopes) - { - var messageType = envelope.Message?.GetType(); - var producerSettings = messageType != null ? GetProducerSettings(messageType) : null; - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); + var producerSettings = messageType != null ? GetProducerSettings(messageType) : null; + var messagePayload = Serializer.Serialize(messageType, message); - // calculate message key - var key = GetMessageKey(producerSettings, messageType, envelope.Message, path); - var kafkaMessage = new Message { Key = key, Value = messagePayload }; + // calculate message key + var key = GetMessageKey(producerSettings, messageType, message, path); + var kafkaMessage = new Message { Key = key, Value = messagePayload }; - if (envelope.Headers != null && envelope.Headers.Count > 0) - { - kafkaMessage.Headers = []; - - foreach (var keyValue in envelope.Headers) - { - var valueBytes = HeaderSerializer.Serialize(typeof(object), keyValue.Value); - kafkaMessage.Headers.Add(keyValue.Key, valueBytes); - } - } + if (messageHeaders != null && messageHeaders.Count > 0) + { + kafkaMessage.Headers = []; - // calculate partition - var partition = producerSettings != null - ? GetMessagePartition(producerSettings, messageType, envelope.Message, path) - : NoPartition; - - _logger.LogDebug("Producing message {Message} of type {MessageType}, topic {Topic}, partition {Partition}, key size {KeySize}, payload size {MessageSize}, headers count {MessageHeaderCount}", - envelope.Message, - messageType?.Name, - path, - partition, - key?.Length ?? 0, - messagePayload?.Length ?? 0, - kafkaMessage.Headers?.Count ?? 0); - - // send the message to topic - var task = partition == NoPartition - ? _producer.ProduceAsync(path, kafkaMessage, cancellationToken: cancellationToken) - : _producer.ProduceAsync(new TopicPartition(path, new Partition(partition)), kafkaMessage, cancellationToken: cancellationToken); - - // ToDo: Introduce support for not awaited produce - - var deliveryResult = await task.ConfigureAwait(false); - if (deliveryResult.Status == PersistenceStatus.NotPersisted) + foreach (var keyValue in messageHeaders) { - throw new ProducerMessageBusException($"Error while publish message {envelope.Message} of type {messageType?.Name} to topic {path}. Kafka persistence status: {deliveryResult.Status}"); + var valueBytes = HeaderSerializer.Serialize(typeof(object), keyValue.Value); + kafkaMessage.Headers.Add(keyValue.Key, valueBytes); } + } - dispatched.Add(envelope); - - // log some debug information - _logger.LogDebug("Message {Message} of type {MessageType} delivered to topic {Topic}, partition {Partition}, offset: {Offset}", - envelope.Message, messageType?.Name, deliveryResult.Topic, deliveryResult.Partition, deliveryResult.Offset); + // calculate partition + var partition = producerSettings != null + ? GetMessagePartition(producerSettings, messageType, message, path) + : NoPartition; + + _logger.LogDebug("Producing message {Message} of type {MessageType}, topic {Topic}, partition {Partition}, key size {KeySize}, payload size {MessageSize}, headers count {MessageHeaderCount}", + message, + messageType?.Name, + path, + partition, + key?.Length ?? 0, + messagePayload?.Length ?? 0, + kafkaMessage.Headers?.Count ?? 0); + + // send the message to topic + var task = partition == NoPartition + ? _producer.ProduceAsync(path, kafkaMessage, cancellationToken: cancellationToken) + : _producer.ProduceAsync(new TopicPartition(path, new Partition(partition)), kafkaMessage, cancellationToken: cancellationToken); + + // ToDo: Introduce support for not awaited produce + + var deliveryResult = await task.ConfigureAwait(false); + if (deliveryResult.Status == PersistenceStatus.NotPersisted) + { + throw new ProducerMessageBusException($"Error while publish message {message} of type {messageType?.Name} to topic {path}. Kafka persistence status: {deliveryResult.Status}"); } + + // log some debug information + _logger.LogDebug("Message {Message} of type {MessageType} delivered to topic {Topic}, partition {Partition}, offset: {Offset}", + message, messageType?.Name, deliveryResult.Topic, deliveryResult.Partition, deliveryResult.Offset); + } - catch (Exception ex) + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) { - return new(dispatched, ex); + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); } - - return new(dispatched, null); - } + } protected byte[] GetMessageKey(ProducerSettings producerSettings, Type messageType, object message, string topic) { diff --git a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs index b79a7541..20b36e1a 100644 --- a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs +++ b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs @@ -48,11 +48,6 @@ protected override IMessageSerializer GetSerializer() return new NullMessageSerializer(); } - protected override void BuildPendingRequestStore() - { - // Do not built it. Memory bus does not need it. - } - public override IDictionary CreateHeaders() { if (ProviderSettings.EnableMessageHeaders) @@ -123,13 +118,7 @@ private IMessageProcessorQueue CreateMessageProcessorQueue(IMessageProcessor(), CancellationToken); } - protected override Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) - => Task.FromResult>(new([], null)); // Not used - - public override Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker) - => Task.CompletedTask; // Not used to responses - - protected override Task PublishInternal(object message, string path, IDictionary messageHeaders, CancellationToken cancellationToken, ProducerSettings producerSettings, IMessageBusTarget targetBus) + public override Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) => ProduceInternal(message, path, messageHeaders, targetBus, isPublish: true, cancellationToken); protected override Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) diff --git a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs index 37390c7d..a876e219 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs @@ -1,5 +1,8 @@ namespace SlimMessageBus.Host.Mqtt; +using System.Collections.Generic; +using System.Threading; + using MQTTnet.Extensions.ManagedClient; public class MqttMessageBus : MessageBusBase @@ -113,63 +116,51 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg) return Task.CompletedTask; } - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { - var messages = envelopes - .Select(envelope => - { - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); + try + { + OnProduceToTransport(message, messageType, path, messageHeaders); - var m = new MqttApplicationMessage - { - PayloadSegment = new ArraySegment(messagePayload), - Topic = path - }; + var messagePayload = Serializer.Serialize(messageType, message); - if (envelope.Headers != null) - { - m.UserProperties = new List(envelope.Headers.Count); - foreach (var header in envelope.Headers) - { - m.UserProperties.Add(new(header.Key, header.Value.ToString())); - } - } + var m = new MqttApplicationMessage + { + PayloadSegment = new ArraySegment(messagePayload), + Topic = path + }; - var messageType = envelope.Message?.GetType(); - try - { - var messageModifier = Settings.GetMessageModifier(); - messageModifier?.Invoke(envelope.Message, m); - - if (messageType != null) - { - var producerSettings = GetProducerSettings(messageType); - messageModifier = producerSettings.GetMessageModifier(); - messageModifier?.Invoke(envelope.Message, m); - } - } - catch (Exception e) + if (messageHeaders != null) + { + m.UserProperties = new List(messageHeaders.Count); + foreach (var header in messageHeaders) { - _logger.LogWarning(e, "The configured message modifier failed for message type {MessageType} and message {Message}", messageType, envelope.Message); + m.UserProperties.Add(new(header.Key, header.Value.ToString())); } + } - return (Envelope: envelope, Message: m); - }); - - var dispatched = new List(envelopes.Count); - foreach (var item in messages) - { try { - await _mqttClient.EnqueueAsync(item.Message); - dispatched.Add(item.Envelope); + var messageModifier = Settings.GetMessageModifier(); + messageModifier?.Invoke(message, m); + + if (messageType != null) + { + var producerSettings = GetProducerSettings(messageType); + messageModifier = producerSettings.GetMessageModifier(); + messageModifier?.Invoke(message, m); + } } - catch (Exception ex) + catch (Exception e) { - return new(dispatched, ex); + _logger.LogWarning(e, "The configured message modifier failed for message type {MessageType} and message {Message}", messageType, message); } - } - return new(dispatched, null); + await _mqttClient.EnqueueAsync(m); + } + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) + { + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); + } } } diff --git a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs index 53fef490..79af07d8 100644 --- a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs +++ b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs @@ -1,5 +1,8 @@ -namespace SlimMessageBus.Host.Nats; - +namespace SlimMessageBus.Host.Nats; + +using System.Collections.Generic; +using System.Threading; + using Microsoft.Extensions.Primitives; public class NatsMessageBus : MessageBusBase @@ -90,54 +93,38 @@ protected override async ValueTask DisposeAsyncCore() _connection = null; } } - - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, - CancellationToken cancellationToken) + + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { + try + { + OnProduceToTransport(message, messageType, path, messageHeaders); - await EnsureInitFinished(); - - if (_connection == null) - { - throw new ProducerMessageBusException("The connection is not available at this time"); + var messagePayload = Serializer.Serialize(messageType, message); + + var replyTo = messageHeaders.TryGetValue("ReplyTo", out var replyToValue) + ? replyToValue.ToString() + : null; + + NatsMsg m = new() + { + Data = messagePayload, + Subject = path, + Headers = [], + ReplyTo = replyTo + }; + + foreach (var header in messageHeaders) + { + m.Headers.Add(new KeyValuePair(header.Key, header.Value.ToString())); + } + + await _connection.PublishAsync(m, cancellationToken: cancellationToken); + } + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) + { + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); } - var messages = envelopes.Select(envelope => - { - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); - - var replyTo = envelope.Headers.TryGetValue("ReplyTo", out var replyToValue) ? replyToValue.ToString() : null; - - NatsMsg m = new() - { - Data = messagePayload, - Subject = path, - Headers = [], - ReplyTo = replyTo - }; - - foreach (var header in envelope.Headers) - { - m.Headers.Add(new KeyValuePair(header.Key, header.Value.ToString())); - } - - return (Envelope: envelope, Message: m); - }); - - var dispatched = new List(envelopes.Count); - foreach (var item in messages) - { - try - { - await _connection.PublishAsync(item.Message, cancellationToken: cancellationToken); - dispatched.Add(item.Envelope); - } - catch (Exception ex) - { - return new ProduceToTransportBulkResult(dispatched, ex); - } - } - - return new ProduceToTransportBulkResult(dispatched, null); - } + } } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs b/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs index de2d5f05..c18e9b30 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/MeasuringSqlMessageOutboxRepositoryDecorator.cs @@ -20,9 +20,6 @@ private async Task MeasureMethod(string name, Func> action) } } - private void LogTime(string name, Stopwatch sw) - => logger.LogInformation("Method {MethodName} took {Elapsed}", name, sw.Elapsed); - private async Task MeasureMethod(string name, Func action) { var sw = Stopwatch.StartNew(); @@ -36,6 +33,9 @@ private async Task MeasureMethod(string name, Func action) } } + private void LogTime(string name, Stopwatch sw) + => logger.LogInformation("Method {MethodName} took {Elapsed}", name, sw.Elapsed); + public Task AbortDelivery(IReadOnlyCollection ids, CancellationToken cancellationToken) => MeasureMethod(nameof(AbortDelivery), () => target.AbortDelivery(ids, cancellationToken)); diff --git a/src/SlimMessageBus.Host.Outbox/Services/LockLostException.cs b/src/SlimMessageBus.Host.Outbox/Services/LockLostException.cs new file mode 100644 index 00000000..a33fd111 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox/Services/LockLostException.cs @@ -0,0 +1,14 @@ +namespace SlimMessageBus.Host.Outbox.Services; + +public class LockLostException : Exception +{ + public LockLostException(string message) + : base(message) + { + } + + public LockLostException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs index 1527475d..701ab8eb 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimer.cs @@ -131,17 +131,4 @@ private async Task CallbackAsync() _renewingLock = false; } } - - public class LockLostException : Exception - { - public LockLostException(string message) - : base(message) - { - } - - public LockLostException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} \ No newline at end of file +} diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs index e1b79b6f..7698a37a 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs @@ -270,7 +270,7 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxM { var busName = busGroup.Key; var bus = GetBus(compositeMessageBus, messageBusTarget, busName); - if (bus == null || bus is not IMessageBusBulkProducer bulkProducer) + if (bus == null || bus is not ITransportBulkProducer bulkProducer) { foreach (var outboxMessage in busGroup) { @@ -331,7 +331,7 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxM return (runAgain, count); } - async internal Task<(bool Success, int Published)> DispatchBatch(IOutboxMessageRepository outboxRepository, IMessageBusBulkProducer producer, IMessageBusTarget messageBusTarget, IReadOnlyCollection batch, string busName, string path, CancellationToken cancellationToken) + async internal Task<(bool Success, int Published)> DispatchBatch(IOutboxMessageRepository outboxRepository, ITransportBulkProducer producer, IMessageBusTarget messageBusTarget, IReadOnlyCollection batch, string busName, string path, CancellationToken cancellationToken) { _logger.LogDebug("Publishing batch of {MessageCount} messages to pathGroup {Path} on {BusName} bus", batch.Count, path, busName); diff --git a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs index 528cd38f..e8207191 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs @@ -1,5 +1,4 @@ namespace SlimMessageBus.Host.RabbitMQ; - public class RabbitMqMessageBus : MessageBusBase, IRabbitMqChannel { private readonly ILogger _logger; @@ -62,10 +61,8 @@ private async Task CreateConnection() { try { - var retryCount = 3; - for (var retry = 0; _connection == null && retry < retryCount; retry++) - { - try + await Retry.WithDelay( + operation: (ct) => { ProviderSettings.ConnectionFactory.AutomaticRecoveryEnabled = true; ProviderSettings.ConnectionFactory.DispatchConsumersAsync = true; @@ -73,13 +70,15 @@ private async Task CreateConnection() _connection = ProviderSettings.Endpoints != null && ProviderSettings.Endpoints.Count > 0 ? ProviderSettings.ConnectionFactory.CreateConnection(ProviderSettings.Endpoints) : ProviderSettings.ConnectionFactory.CreateConnection(); - } - catch (global::RabbitMQ.Client.Exceptions.BrokerUnreachableException e) + + return Task.CompletedTask; + }, + shouldRetry: (e, retry) => { - _logger.LogInformation(e, "Retrying {Retry} of {RetryCount} connection to RabbitMQ...", retry, retryCount); - await Task.Delay(ProviderSettings.ConnectionFactory.NetworkRecoveryInterval); - } - } + _logger.LogInformation(e, "Retrying {Retry} connection to RabbitMQ...", retry); + return e is global::RabbitMQ.Client.Exceptions.BrokerUnreachableException && retry < 3; + }, + delay: ProviderSettings.ConnectionFactory.NetworkRecoveryInterval); lock (_channelLock) { @@ -128,59 +127,83 @@ protected override async ValueTask DisposeAsyncCore() } } - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + public override Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { - await EnsureInitFinished(); + EnsureChannel(); + try + { + OnProduceToTransport(message, messageType, path, messageHeaders); - if (_channel == null) + lock (_channelLock) + { + GetTransportMessage(message, messageType, messageHeaders, out var messagePayload, out var messageProperties, out var routingKey); + _channel.BasicPublish(path, routingKey: routingKey, mandatory: false, basicProperties: messageProperties, body: messagePayload); + } + + return Task.CompletedTask; + } + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) { - throw new ProducerMessageBusException("The Channel is not available at this time"); + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); } + } + + public override Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + EnsureChannel(); - var dispatched = new List(envelopes.Count); try { - foreach (var envelope in envelopes) + lock (_channelLock) { - cancellationToken.ThrowIfCancellationRequested(); - - var producer = GetProducerSettings(envelope.Message.GetType()); - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); - - lock (_channelLock) + var batch = _channel.CreateBasicPublishBatch(); + foreach (var envelope in envelopes) { - var messageProperties = _channel.CreateBasicProperties(); - if (envelope.Headers != null) - { - messageProperties.Headers ??= new Dictionary(); - foreach (var header in envelope.Headers) - { - messageProperties.Headers[header.Key] = ProviderSettings.HeaderValueConverter.ConvertTo(header.Value); - } - } - - // Calculate the routing key for the message (if provider set) - var routingKeyProvider = producer.GetMessageRoutingKeyProvider(ProviderSettings); - var routingKey = routingKeyProvider?.Invoke(envelope.Message, messageProperties) ?? string.Empty; - - // Invoke the bus level modifier - var messagePropertiesModifier = ProviderSettings.GetMessagePropertiesModifier(); - messagePropertiesModifier?.Invoke(envelope.Message, messageProperties); - - // Invoke the producer level modifier - messagePropertiesModifier = producer.GetMessagePropertiesModifier(); - messagePropertiesModifier?.Invoke(envelope.Message, messageProperties); - - _channel.BasicPublish(path, routingKey: routingKey, mandatory: false, basicProperties: messageProperties, body: messagePayload); - dispatched.Add(envelope); + GetTransportMessage(envelope.Message, envelope.MessageType, envelope.Headers, out var messagePayload, out var messageProperties, out var routingKey); + batch.Add(path, routingKey: routingKey, mandatory: false, properties: messageProperties, body: messagePayload.AsMemory()); } + batch.Publish(); } + return Task.FromResult(new ProduceToTransportBulkResult(envelopes, null)); } catch (Exception ex) { - return new(dispatched, ex); + return Task.FromResult(new ProduceToTransportBulkResult([], ex)); } + } + + private void EnsureChannel() + { + if (_channel == null) + { + throw new ProducerMessageBusException("The Channel is not available at this time"); + } + } + + private void GetTransportMessage(object message, Type messageType, IDictionary messageHeaders, out byte[] messagePayload, out IBasicProperties messageProperties, out string routingKey) + { + var producer = GetProducerSettings(messageType); + messagePayload = Serializer.Serialize(messageType, message); + messageProperties = _channel.CreateBasicProperties(); + if (messageHeaders != null) + { + messageProperties.Headers ??= new Dictionary(); + foreach (var header in messageHeaders) + { + messageProperties.Headers[header.Key] = ProviderSettings.HeaderValueConverter.ConvertTo(header.Value); + } + } + + // Calculate the routing key for the message (if provider set) + var routingKeyProvider = producer.GetMessageRoutingKeyProvider(ProviderSettings); + routingKey = routingKeyProvider?.Invoke(message, messageProperties) ?? string.Empty; + + // Invoke the bus level modifier + var messagePropertiesModifier = ProviderSettings.GetMessagePropertiesModifier(); + messagePropertiesModifier?.Invoke(message, messageProperties); - return new(dispatched, null); + // Invoke the producer level modifier + messagePropertiesModifier = producer.GetMessagePropertiesModifier(); + messagePropertiesModifier?.Invoke(message, messageProperties); } } diff --git a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs index 5f8656cb..29ba4efd 100644 --- a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs +++ b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs @@ -159,51 +159,34 @@ void AddTopicConsumer(string topic, ISubscriber subscriber, IMessageProcessor> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { -#if NETSTANDARD2_0 - if (envelopes is null) throw new ArgumentNullException(nameof(envelopes)); -#else - ArgumentNullException.ThrowIfNull(envelopes); -#endif - - AssertActive(); - - var dispatched = new List(envelopes.Count); try { - foreach (var envelope in envelopes) - { - var messageType = envelope.Message.GetType(); - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); + var messagePayload = Serializer.Serialize(messageType, message); - // determine the SMB topic name if its a Azure SB queue or topic - var kind = _kindMapping.GetKind(messageType, path); + // determine the SMB topic name if its a Azure SB queue or topic + var kind = _kindMapping.GetKind(messageType, path); - var messageWithHeaders = new MessageWithHeaders(messagePayload, envelope.Headers); - var messageWithHeadersBytes = ProviderSettings.EnvelopeSerializer.Serialize(typeof(MessageWithHeaders), messageWithHeaders); + var messageWithHeaders = new MessageWithHeaders(messagePayload, messageHeaders); + var messageWithHeadersBytes = ProviderSettings.EnvelopeSerializer.Serialize(typeof(MessageWithHeaders), messageWithHeaders); - _logger.LogDebug( - "Producing message {Message} of type {MessageType} to redis {PathKind} {Path} with size {MessageSize}", - envelope.Message, messageType.Name, GetPathKindString(kind), path, messageWithHeadersBytes.Length); + _logger.LogDebug( + "Producing message {Message} of type {MessageType} to redis {PathKind} {Path} with size {MessageSize}", + message, messageType.Name, GetPathKindString(kind), path, messageWithHeadersBytes.Length); - var result = kind == PathKind.Topic - ? await Database.PublishAsync(RedisUtils.ToRedisChannel(path), messageWithHeadersBytes).ConfigureAwait(false) // Use Redis Pub/Sub - : await Database.ListRightPushAsync(path, messageWithHeadersBytes).ConfigureAwait(false); // Use Redis List Type (append on the right side/end of list) + var result = kind == PathKind.Topic + ? await Database.PublishAsync(RedisUtils.ToRedisChannel(path), messageWithHeadersBytes).ConfigureAwait(false) // Use Redis Pub/Sub + : await Database.ListRightPushAsync(path, messageWithHeadersBytes).ConfigureAwait(false); // Use Redis List Type (append on the right side/end of list) - dispatched.Add(envelope); - - _logger.LogDebug( - "Produced message {Message} of type {MessageType} to redis channel {PathKind} {Path} with result {RedisResult}", - envelope.Message, messageType, GetPathKindString(kind), path, result); - } + _logger.LogDebug( + "Produced message {Message} of type {MessageType} to redis channel {PathKind} {Path} with result {RedisResult}", + message, messageType, GetPathKindString(kind), path, result); } - catch (Exception ex) + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) { - return new(dispatched, ex); + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); } - - return new(dispatched, null); } #endregion diff --git a/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs b/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs index 727ef033..305bd7a8 100644 --- a/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs +++ b/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs @@ -25,7 +25,12 @@ public override async Task ProvisionTopology() await provisioningService.Migrate(CancellationToken); // provisioning happens asynchronously } - protected override Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + public override Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) { var sqlRepository = targetBus.ServiceProvider.GetService(); diff --git a/src/SlimMessageBus.Host/Collections/CollectionTypeInfo.cs b/src/SlimMessageBus.Host/Collections/CollectionTypeInfo.cs new file mode 100644 index 00000000..054f1d21 --- /dev/null +++ b/src/SlimMessageBus.Host/Collections/CollectionTypeInfo.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host.Collections; + +public class CollectionTypeInfo +{ + public Type ItemType { get; init; } + public Func> ToCollection { get; init; } +} diff --git a/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs b/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs index e24d3a5c..6c7443d5 100644 --- a/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/IRuntimeTypeCache.cs @@ -16,4 +16,6 @@ public interface IRuntimeTypeCache /// /// Type GetClosedGenericType(Type openGenericType, Type genericParameterType); + + CollectionTypeInfo GetCollectionTypeInfo(Type type); } diff --git a/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs b/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs index a41a36bc..ff6e2bcb 100644 --- a/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs @@ -9,15 +9,15 @@ public class ProducerByMessageTypeCache : IReadOnlyCache _producerByBaseType; private readonly IRuntimeTypeCache _runtimeTypeCache; + private readonly IDictionary _producerByBaseType; private readonly IReadOnlyCache _producerByType; public ProducerByMessageTypeCache(ILogger logger, IDictionary producerByBaseType, IRuntimeTypeCache runtimeTypeCache) { _logger = logger; - _producerByBaseType = producerByBaseType; _runtimeTypeCache = runtimeTypeCache; + _producerByBaseType = producerByBaseType; _producerByType = new SafeDictionaryWrapper(CalculateProducer); } @@ -30,7 +30,10 @@ public ProducerByMessageTypeCache(ILogger logger, IDictionary p private TProducer CalculateProducer(Type messageType) { - var assignableProducers = _producerByBaseType.Where(x => _runtimeTypeCache.IsAssignableFrom(messageType, x.Key)).OrderBy(x => CalculateBaseClassDistance(messageType, x.Key)); + var assignableProducers = _producerByBaseType + .Where(x => _runtimeTypeCache.IsAssignableFrom(messageType, x.Key)) + .OrderBy(x => CalculateBaseClassDistance(messageType, x.Key)); + var assignableProducer = assignableProducers.FirstOrDefault(); if (assignableProducer.Key != null) { @@ -38,6 +41,17 @@ private TProducer CalculateProducer(Type messageType) return assignableProducer.Value; } + // Is is collection of message types? + var collectionInfo = _runtimeTypeCache.GetCollectionTypeInfo(messageType); + if (collectionInfo != null) + { + var innerProducer = CalculateProducer(collectionInfo.ItemType); + if (innerProducer != null) + { + return innerProducer; + } + } + _logger.LogDebug("Unable to match any declared producer for dispatched message type {MessageType}", messageType); // Note: Nulls are also added to dictionary, so that we don't look them up using reflection next time (cached). diff --git a/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs b/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs index 36b60af8..76c8813a 100644 --- a/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs @@ -5,6 +5,7 @@ public class RuntimeTypeCache : IRuntimeTypeCache private readonly IReadOnlyCache<(Type From, Type To), bool> _isAssignable; private readonly IReadOnlyCache _taskOfType; private readonly IReadOnlyCache<(Type OpenGenericType, Type GenericParameterType), Type> _closedGenericTypeOfOpenGenericType; + private readonly IReadOnlyCache _collectionInfoByType; public IReadOnlyCache<(Type ClassType, string MethodName, Type GenericArgument), Func>> GenericMethod { get; } @@ -22,6 +23,31 @@ public RuntimeTypeCache() _isAssignable = new SafeDictionaryWrapper<(Type From, Type To), bool>(x => x.To.IsAssignableFrom(x.From)); _taskOfType = new SafeDictionaryWrapper(type => new TaskOfTypeCache(type)); _closedGenericTypeOfOpenGenericType = new SafeDictionaryWrapper<(Type OpenGenericType, Type GenericPatameterType), Type>(x => x.OpenGenericType.MakeGenericType(x.GenericPatameterType)); + _collectionInfoByType = new SafeDictionaryWrapper(type => + { + var enumerableType = type.GetInterfaces().Concat([type]) + .SingleOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableType != null) + { + return new CollectionTypeInfo + { + ItemType = enumerableType.GetGenericArguments()[0], + ToCollection = x => (IEnumerable)x + }; + } + + if (type.IsArray) + { + return new CollectionTypeInfo + { + ItemType = type.GetElementType(), + ToCollection = x => (IEnumerable)x + }; + } + + return null; + }); GenericMethod = new SafeDictionaryWrapper<(Type ClassType, string MethodName, Type GenericArgument), Func>>(key => { @@ -65,4 +91,7 @@ public TaskOfTypeCache GetTaskOfType(Type type) public Type GetClosedGenericType(Type openGenericType, Type genericParameterType) => _closedGenericTypeOfOpenGenericType[(openGenericType, genericParameterType)]; + + public CollectionTypeInfo GetCollectionTypeInfo(Type type) + => _collectionInfoByType[type]; } diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseProducer.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseProducer.cs index c7006f35..c2fa25bb 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseProducer.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseProducer.cs @@ -2,5 +2,5 @@ public interface IResponseProducer { - Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker); + Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker, CancellationToken cancellationToken); } diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs index ac37cad8..46e4fa8f 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs @@ -1,7 +1,5 @@ namespace SlimMessageBus.Host; -using System; - /// /// Implementation of that performs orchestration around processing of a new message using an instance of the declared consumer ( or interface). /// @@ -103,7 +101,7 @@ public async virtual Task ProcessMessage(TTransportMessage if (!ReferenceEquals(ResponseForExpiredRequest, lastResponse)) { // We discard expired requests, so there is no response to provide - await _responseProducer.ProduceResponse(requestId, message, messageHeaders, lastResponse, lastException, consumerInvoker).ConfigureAwait(false); + await _responseProducer.ProduceResponse(requestId, message, messageHeaders, lastResponse, lastException, consumerInvoker, cancellationToken).ConfigureAwait(false); } // Clear the exception as it will be returned to the sender. lastException = null; diff --git a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs index 8c658dd8..7378e100 100644 --- a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs +++ b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs @@ -101,6 +101,11 @@ public static IServiceCollection AddSlimMessageBus(this IServiceCollection servi services.TryAddSingleton(); services.TryAddSingleton(svp => svp.GetRequiredService()); + services.TryAddSingleton(); + + services.TryAddTransient(); + services.TryAddSingleton(); + return services; } diff --git a/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs b/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs index 20ad2cfb..0cffd609 100644 --- a/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs +++ b/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs @@ -53,7 +53,7 @@ public HybridMessageBus(MessageBusSettings settings, HybridMessageBusSettings pr throw new ConfigurationMessageBusException($"Found request messages that are handled by more than one child bus: {string.Join(", ", requestTypesWithMoreThanOneBus)}. Double check your Produce configuration."); } - var runtimeTypeCache = new RuntimeTypeCache(); + var runtimeTypeCache = settings.ServiceProvider.GetRequiredService(); _busesByMessageType = new ProducerByMessageTypeCache(_logger, busesByMessageType, runtimeTypeCache); _undeclaredMessageType = new(); diff --git a/src/SlimMessageBus.Host/IMessageBusBulkProducer.cs b/src/SlimMessageBus.Host/ITransportBulkProducer.cs similarity index 67% rename from src/SlimMessageBus.Host/IMessageBusBulkProducer.cs rename to src/SlimMessageBus.Host/ITransportBulkProducer.cs index a941a3a7..0e7c5e7c 100644 --- a/src/SlimMessageBus.Host/IMessageBusBulkProducer.cs +++ b/src/SlimMessageBus.Host/ITransportBulkProducer.cs @@ -3,19 +3,17 @@ /// /// Interface for bulk publishing of messages directly to a queue. /// -/// -/// The Interface is intended for outbox publishing where messages have already flowed through the interceptor pipeline. -/// -public interface IMessageBusBulkProducer +public interface ITransportBulkProducer { /// /// The maximum number of messages that can take part in a . Null if transaction scopes are not supported by the message bus. /// int? MaxMessagesPerTransaction { get; } - Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) where T : BulkMessageEnvelope; + Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + where T : BulkMessageEnvelope; } public record BulkMessageEnvelope(object Message, Type MessageType, IDictionary Headers); -public record ProduceToTransportBulkResult(IReadOnlyCollection Dispatched, Exception Exception); \ No newline at end of file +public record ProduceToTransportBulkResult(IReadOnlyCollection Dispatched, Exception Exception); diff --git a/src/SlimMessageBus.Host/ITransportProducer.cs b/src/SlimMessageBus.Host/ITransportProducer.cs new file mode 100644 index 00000000..036f1a69 --- /dev/null +++ b/src/SlimMessageBus.Host/ITransportProducer.cs @@ -0,0 +1,12 @@ +namespace SlimMessageBus.Host; + +public interface ITransportProducer +{ + Task ProduceToTransport( + object message, + Type messageType, + string path, + IDictionary messageHeaders, + IMessageBusTarget targetBus, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index 3fc9223f..d023c2ff 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -1,7 +1,8 @@ namespace SlimMessageBus.Host; using System.Globalization; - +using System.Runtime.ExceptionServices; + using SlimMessageBus.Host.Consumer; using SlimMessageBus.Host.Services; @@ -15,7 +16,14 @@ protected MessageBusBase(MessageBusSettings settings, TProviderSettings provider } } -public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, IMessageHeadersFactory, IResponseProducer, IResponseConsumer, IMessageBusBulkProducer +public abstract class MessageBusBase : IDisposable, IAsyncDisposable, + IMasterMessageBus, + IMessageScopeFactory, + IMessageHeadersFactory, + IResponseProducer, + IResponseConsumer, + ITransportProducer, + ITransportBulkProducer { private readonly ILogger _logger; private CancellationTokenSource _cancellationTokenSource = new(); @@ -34,14 +42,7 @@ public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMes public virtual MessageBusSettings Settings { get; } - public virtual IMessageSerializer Serializer - { - get - { - _serializer ??= GetSerializer(); - return _serializer; - } - } + public virtual IMessageSerializer Serializer => _serializer ??= GetSerializer(); public IMessageTypeResolver MessageTypeResolver { get; } @@ -53,7 +54,7 @@ public virtual IMessageSerializer Serializer protected ProducerByMessageTypeCache ProducerSettingsByMessageType { get; private set; } protected IPendingRequestStore PendingRequestStore { get; set; } - protected PendingRequestManager PendingRequestManager { get; set; } + protected IPendingRequestManager PendingRequestManager { get; set; } public CancellationToken CancellationToken => _cancellationTokenSource.Token; @@ -99,11 +100,14 @@ protected MessageBusBase(MessageBusSettings settings) _headerService = new MessageHeaderService(LoggerFactory.CreateLogger(), Settings, MessageTypeResolver); - RuntimeTypeCache = new RuntimeTypeCache(); + RuntimeTypeCache = settings.ServiceProvider.GetRequiredService(); MessageBusTarget = new MessageBusProxy(this, Settings.ServiceProvider); - CurrentTimeProvider = settings.ServiceProvider.GetRequiredService(); + CurrentTimeProvider = settings.ServiceProvider.GetRequiredService(); + + PendingRequestManager = settings.ServiceProvider.GetRequiredService(); + PendingRequestStore = PendingRequestManager.Store; } protected void AddInit(Task task) @@ -114,7 +118,11 @@ protected void AddInit(Task task) _initTask = prevInitTask?.ContinueWith(_ => task, CancellationToken) ?? task; } } - + + /// + /// Awaits (if any) bus intialization (e.g. topology provisining) before we can produce message into the bus. + /// + /// protected async Task EnsureInitFinished() { var initTask = _initTask; @@ -169,15 +177,6 @@ protected void OnBuildProvider() protected virtual void Build() { ProducerSettingsByMessageType = new ProducerByMessageTypeCache(_logger, BuildProducerByBaseMessageType(), RuntimeTypeCache); - - BuildPendingRequestStore(); - } - - protected virtual void BuildPendingRequestStore() - { - PendingRequestStore = new InMemoryPendingRequestStore(); - PendingRequestManager = new PendingRequestManager(PendingRequestStore, () => CurrentTimeProvider.CurrentTime, TimeSpan.FromSeconds(1), LoggerFactory); - PendingRequestManager.Start(); } private Dictionary BuildProducerByBaseMessageType() @@ -360,12 +359,6 @@ protected async virtual ValueTask DisposeAsyncCore() _cancellationTokenSource.Dispose(); _cancellationTokenSource = null; } - - if (PendingRequestManager != null) - { - PendingRequestManager.Dispose(); - PendingRequestManager = null; - } } protected virtual Task CreateConsumers() @@ -391,8 +384,6 @@ protected async virtual Task DestroyConsumers() public ICurrentTimeProvider CurrentTimeProvider { get; protected set; } - public virtual int? MaxMessagesPerTransaction => null; - protected ProducerSettings GetProducerSettings(Type messageType) { var producerSettings = ProducerSettingsByMessageType[messageType]; @@ -412,80 +403,137 @@ protected virtual string GetDefaultPath(Type messageType, ProducerSettings produ _logger.LogDebug("Applying default path {Path} for message type {MessageType}", path, messageType); return path; - } - - protected async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) - { - var envelope = new BulkMessageEnvelope(message, messageType, messageHeaders); - var result = await ProduceToTransportBulk([envelope], path, targetBus, cancellationToken); - if (result.Exception != null) - { - if (result.Exception is ProducerMessageBusException) - { - throw (result.Exception); - } + } + + public abstract Task ProduceToTransport( + object message, + Type messageType, + string path, + IDictionary messageHeaders, + IMessageBusTarget targetBus, + CancellationToken cancellationToken); + + protected void OnProduceToTransport(object message, + Type messageType, + string path, + IDictionary messageHeaders) + => _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, messageType, path); + + public virtual int? MaxMessagesPerTransaction => null; - throw new ProducerMessageBusException($"Producing message {message} of type {messageType?.Name} to path {path} resulted in error: {result.Exception.Message}", result.Exception); - } + public async virtual Task> ProduceToTransportBulk( + IReadOnlyCollection envelopes, + string path, + IMessageBusTarget targetBus, + CancellationToken cancellationToken) + where T : BulkMessageEnvelope + { + var dispatched = new List(); + try + { + foreach (var envelope in envelopes) + { + await ProduceToTransport(envelope.Message, envelope.MessageType, path, envelope.Headers, targetBus, cancellationToken) + .ConfigureAwait(false); + + dispatched.Add(envelope); + } + return new(dispatched, null); + } + catch (Exception ex) + { + return new(dispatched, ex); + } } - protected abstract Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) where T : BulkMessageEnvelope; - - public virtual Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) + public async virtual Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) { if (message == null) throw new ArgumentNullException(nameof(message)); - AssertActive(); + AssertActive(); + await EnsureInitFinished(); // check if the cancellation was already requested - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - var messageType = message.GetType(); - var producerSettings = GetProducerSettings(messageType); - - path ??= GetDefaultPath(producerSettings.MessageType, producerSettings); - - var messageHeaders = CreateHeaders(); - if (messageHeaders != null) - { - _headerService.AddMessageHeaders(messageHeaders, headers, message, producerSettings); - } - - var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; - - var producerInterceptors = RuntimeTypeCache.ProducerInterceptorType.ResolveAll(serviceProvider, messageType); - var publishInterceptors = RuntimeTypeCache.PublishInterceptorType.ResolveAll(serviceProvider, messageType); - if (producerInterceptors != null || publishInterceptors != null) - { - var context = new PublishContext - { - Path = path, - CancellationToken = cancellationToken, - Headers = messageHeaders, - Bus = new MessageBusProxy(this, serviceProvider), - ProducerSettings = producerSettings - }; - - var pipeline = new PublishInterceptorPipeline(this, message, producerSettings, targetBus, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); - return pipeline.Next(); - } - - return PublishInternal(message, path, messageHeaders, cancellationToken, producerSettings, targetBus); - } - - protected internal virtual Task PublishInternal(object message, string path, IDictionary messageHeaders, CancellationToken cancellationToken, ProducerSettings producerSettings, IMessageBusTarget targetBus) - { - _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, producerSettings.MessageType, path); - return ProduceToTransport(message, producerSettings.MessageType, path, messageHeaders, targetBus, cancellationToken); - } - + cancellationToken.ThrowIfCancellationRequested(); + + var messageType = message.GetType(); + + // check if the message type passed is in reality a collection of messages + var collectionInfo = RuntimeTypeCache.GetCollectionTypeInfo(messageType); + if (collectionInfo != null) + { + messageType = collectionInfo.ItemType; + } + + var producerSettings = GetProducerSettings(messageType); + path ??= GetDefaultPath(producerSettings.MessageType, producerSettings); + + if (collectionInfo != null) + { + // produce multiple messages to transport + var messages = collectionInfo + .ToCollection(message) + .Select(m => new BulkMessageEnvelope(m, messageType, GetMessageHeaders(m, headers, producerSettings))) + .ToList(); + + var result = await ProduceToTransportBulk(messages, path, targetBus, cancellationToken); + + if (result.Exception != null) + { + if (result.Exception is ProducerMessageBusException) + { + // We want to pass the same exception to the sender as it happened in the handler/consumer + ExceptionDispatchInfo.Capture(result.Exception).Throw(); + } + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, result.Exception), result.Exception); + } + return; + } + + var messageHeaders = GetMessageHeaders(message, headers, producerSettings); + + // Producer interceptors do not work on collections (batch publish) + var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; + + var producerInterceptors = RuntimeTypeCache.ProducerInterceptorType.ResolveAll(serviceProvider, messageType); + var publishInterceptors = RuntimeTypeCache.PublishInterceptorType.ResolveAll(serviceProvider, messageType); + if (producerInterceptors != null || publishInterceptors != null) + { + var context = new PublishContext + { + Path = path, + CancellationToken = cancellationToken, + Headers = messageHeaders, + Bus = new MessageBusProxy(this, serviceProvider), + ProducerSettings = producerSettings + }; + + var pipeline = new PublishInterceptorPipeline(this, RuntimeTypeCache, message, producerSettings, targetBus, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); + await pipeline.Next(); + return; + } + + // produce a single message to transport + await ProduceToTransport(message, messageType, path, messageHeaders, targetBus, cancellationToken); + } + + protected static string GetProducerErrorMessage(string path, object message, Type messageType, Exception ex) + => $"Producing message {message} of type {messageType?.Name} to path {path} resulted in error: {ex.Message}"; + /// /// Create an instance of message headers. /// /// - public virtual IDictionary CreateHeaders() => new Dictionary(10); + public virtual IDictionary CreateHeaders() => new Dictionary(10); + + private IDictionary GetMessageHeaders(object message, IDictionary headers, ProducerSettings producerSettings) + { + var messageHeaders = CreateHeaders(); + if (messageHeaders != null) + { + _headerService.AddMessageHeaders(messageHeaders, headers, message, producerSettings); + } + return messageHeaders; + } protected virtual TimeSpan GetDefaultRequestTimeout(Type requestType, ProducerSettings producerSettings) { @@ -496,69 +544,71 @@ protected virtual TimeSpan GetDefaultRequestTimeout(Type requestType, ProducerSe return timeout; } - public virtual Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) + public virtual async Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) { if (request == null) throw new ArgumentNullException(nameof(request)); AssertActive(); AssertRequestResponseConfigured(); + await EnsureInitFinished(); // check if the cancellation was already requested - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + cancellationToken.ThrowIfCancellationRequested(); var requestType = request.GetType(); - var responseType = typeof(TResponse); - var producerSettings = GetProducerSettings(requestType); - - path ??= GetDefaultPath(requestType, producerSettings); - timeout ??= GetDefaultRequestTimeout(requestType, producerSettings); - - var created = CurrentTimeProvider.CurrentTime; - var expires = created.Add(timeout.Value); - - // generate the request guid - var requestId = GenerateRequestId(); - - var requestHeaders = CreateHeaders(); - if (requestHeaders != null) - { - _headerService.AddMessageHeaders(requestHeaders, headers, request, producerSettings); - if (requestId != null) - { - requestHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); - } - requestHeaders.SetHeader(ReqRespMessageHeaders.Expires, expires); - } - - var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; - - var producerInterceptors = RuntimeTypeCache.ProducerInterceptorType.ResolveAll(serviceProvider, requestType); - var sendInterceptors = RuntimeTypeCache.SendInterceptorType.ResolveAll(serviceProvider, (requestType, responseType)); - if (producerInterceptors != null || sendInterceptors != null) - { - var context = new SendContext - { - Path = path, - CancellationToken = cancellationToken, - Headers = requestHeaders, - Bus = new MessageBusProxy(this, serviceProvider), - ProducerSettings = producerSettings, - Created = created, - Expires = expires, - RequestId = requestId, - }; - - var pipeline = new SendInterceptorPipeline(this, request, producerSettings, targetBus, context, producerInterceptors: producerInterceptors, sendInterceptors: sendInterceptors); - return pipeline.Next(); - } - - return SendInternal(request, path, requestType, responseType, producerSettings, created, expires, requestId, requestHeaders, targetBus, cancellationToken); - } - + var responseType = typeof(TResponse); + + var producerSettings = GetProducerSettings(requestType); + + path ??= GetDefaultPath(requestType, producerSettings); + timeout ??= GetDefaultRequestTimeout(requestType, producerSettings); + + var created = CurrentTimeProvider.CurrentTime; + var expires = created.Add(timeout.Value); + + // generate the request guid + var requestId = GenerateRequestId(); + + var requestHeaders = CreateHeaders(); + if (requestHeaders != null) + { + _headerService.AddMessageHeaders(requestHeaders, headers, request, producerSettings); + if (requestId != null) + { + requestHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); + } + requestHeaders.SetHeader(ReqRespMessageHeaders.Expires, expires); + } + + var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; + + var producerInterceptors = RuntimeTypeCache.ProducerInterceptorType.ResolveAll(serviceProvider, requestType); + var sendInterceptors = RuntimeTypeCache.SendInterceptorType.ResolveAll(serviceProvider, (requestType, responseType)); + if (producerInterceptors != null || sendInterceptors != null) + { + var context = new SendContext + { + Path = path, + CancellationToken = cancellationToken, + Headers = requestHeaders, + Bus = new MessageBusProxy(this, serviceProvider), + ProducerSettings = producerSettings, + Created = created, + Expires = expires, + RequestId = requestId, + }; + + var pipeline = new SendInterceptorPipeline(this, request, producerSettings, targetBus, context, producerInterceptors: producerInterceptors, sendInterceptors: sendInterceptors); + return await pipeline.Next(); + } + + return await SendInternal(request, path, requestType, responseType, producerSettings, created, expires, requestId, requestHeaders, targetBus, cancellationToken); + } + protected async internal virtual Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { + if (request == null) throw new ArgumentNullException(nameof(request)); + if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); + // record the request state var requestState = new PendingRequestState(requestId, request, requestType, responseType, created, expires, cancellationToken); PendingRequestStore.Add(requestState); @@ -570,8 +620,15 @@ protected async internal virtual Task SendInternal SendInternal requestHeaders, string path, ProducerSettings producerSettings, IMessageBusTarget targetBus) - { - if (request == null) throw new ArgumentNullException(nameof(request)); - if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); - - if (requestHeaders != null) - { - requestHeaders.SetHeader(ReqRespMessageHeaders.ReplyTo, Settings.RequestResponse.Path); - _headerService.AddMessageTypeHeader(request, requestHeaders); - } - - return ProduceToTransport(request, producerSettings.MessageType, path, requestHeaders, targetBus); - } - - public virtual Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker) + public virtual Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker, CancellationToken cancellationToken) { if (requestHeaders == null) throw new ArgumentNullException(nameof(requestHeaders)); if (consumerInvoker == null) throw new ArgumentNullException(nameof(consumerInvoker)); @@ -622,8 +665,8 @@ public virtual Task ProduceResponse(string requestId, object request, IReadOnlyD } _headerService.AddMessageTypeHeader(response, responseHeaders); - - return ProduceToTransport(response, responseType, (string)replyTo, responseHeaders, null); + + return ProduceToTransport(response, responseType, (string)replyTo, responseHeaders, null, cancellationToken); } /// @@ -646,19 +689,6 @@ public virtual Task OnResponseArrived(byte[] responsePayload, string responseException = new RequestHandlerFaultedMessageBusException(errorMessage); } - return OnResponseArrived(responsePayload, path, requestId, responseException); - } - - /// - /// Should be invoked by the concrete bus implementation whenever there is a message arrived on the reply to topic name. - /// - /// - /// - /// - /// - /// - public virtual Task OnResponseArrived(byte[] responsePayload, string path, string requestId, Exception responseException, object response = null) - { var requestState = PendingRequestStore.GetById(requestId); if (requestState == null) { @@ -689,7 +719,9 @@ public virtual Task OnResponseArrived(byte[] responsePayload, string try { // deserialize the response message - response = responsePayload != null ? Serializer.Deserialize(requestState.ResponseType, responsePayload) : response; + var response = responsePayload != null + ? Serializer.Deserialize(requestState.ResponseType, responsePayload) + : null; // resolve the response requestState.TaskCompletionSource.TrySetResult(response); @@ -730,7 +762,4 @@ public virtual IMessageScope CreateMessageScope(ConsumerSettings consumerSetting } public virtual Task ProvisionTopology() => Task.CompletedTask; - - Task> IMessageBusBulkProducer.ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) - => ProduceToTransportBulk(envelopes, path, targetBus, cancellationToken); } diff --git a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs index 7ca83079..4b0f459d 100644 --- a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs +++ b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs @@ -2,8 +2,6 @@ abstract internal class ProducerInterceptorPipeline where TContext : ProducerContext { - protected readonly MessageBusBase _bus; - protected readonly object _message; protected readonly ProducerSettings _producerSettings; protected readonly IMessageBusTarget _targetBus; @@ -16,17 +14,15 @@ abstract internal class ProducerInterceptorPipeline where TContext : P protected bool _targetVisited; - protected ProducerInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, TContext context, IEnumerable producerInterceptors) + protected ProducerInterceptorPipeline(RuntimeTypeCache runtimeTypeCache, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, TContext context, IEnumerable producerInterceptors) { - _bus = bus; - _message = message; _producerSettings = producerSettings; _targetBus = targetBus; _context = context; _producerInterceptors = producerInterceptors; - _producerInterceptorFunc = bus.RuntimeTypeCache.ProducerInterceptorType[message.GetType()]; + _producerInterceptorFunc = runtimeTypeCache.ProducerInterceptorType[message.GetType()]; _producerInterceptorsVisited = producerInterceptors is null; _producerInterceptorsEnumerator = _producerInterceptors?.GetEnumerator(); } diff --git a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs index c1d51856..f79cf76c 100644 --- a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs +++ b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs @@ -2,14 +2,16 @@ internal class PublishInterceptorPipeline : ProducerInterceptorPipeline { + private readonly ITransportProducer _bus; private readonly Func, IProducerContext, Task> _publishInterceptorFunc; private IEnumerator _publishInterceptorsEnumerator; private bool _publishInterceptorsVisited = false; - public PublishInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, PublishContext context, IEnumerable producerInterceptors, IEnumerable publishInterceptors) - : base(bus, message, producerSettings, targetBus, context, producerInterceptors) + public PublishInterceptorPipeline(ITransportProducer bus, RuntimeTypeCache runtimeTypeCache, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, PublishContext context, IEnumerable producerInterceptors, IEnumerable publishInterceptors) + : base(runtimeTypeCache, message, producerSettings, targetBus, context, producerInterceptors) { - _publishInterceptorFunc = bus.RuntimeTypeCache.PublishInterceptorType[message.GetType()]; + _bus = bus; + _publishInterceptorFunc = runtimeTypeCache.PublishInterceptorType[message.GetType()]; _publishInterceptorsVisited = publishInterceptors is null; _publishInterceptorsEnumerator = publishInterceptors?.GetEnumerator(); } @@ -40,7 +42,7 @@ public async Task Next() if (!_targetVisited) { _targetVisited = true; - await _bus.PublishInternal(_message, _context.Path, _context.Headers, _context.CancellationToken, _producerSettings, _targetBus); + await _bus.ProduceToTransport(_message, _message.GetType(), _context.Path, _context.Headers, _targetBus, _context.CancellationToken); return null; } diff --git a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs index 06549c01..9e167cfb 100644 --- a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs +++ b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs @@ -2,13 +2,15 @@ internal class SendInterceptorPipeline : ProducerInterceptorPipeline { + private readonly MessageBusBase _bus; private readonly Func _sendInterceptorFunc; private IEnumerator _sendInterceptorsEnumerator; private bool _sendInterceptorsVisited = false; public SendInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, SendContext context, IEnumerable producerInterceptors, IEnumerable sendInterceptors) - : base(bus, message, producerSettings, targetBus, context, producerInterceptors) + : base(bus.RuntimeTypeCache, message, producerSettings, targetBus, context, producerInterceptors) { + _bus = bus; _sendInterceptorFunc = bus.RuntimeTypeCache.SendInterceptorType[(message.GetType(), typeof(TResponse))]; _sendInterceptorsVisited = sendInterceptors is null; _sendInterceptorsEnumerator = sendInterceptors?.GetEnumerator(); diff --git a/src/SlimMessageBus.Host/RequestResponse/IPendingRequestManager.cs b/src/SlimMessageBus.Host/RequestResponse/IPendingRequestManager.cs new file mode 100644 index 00000000..5f3327f3 --- /dev/null +++ b/src/SlimMessageBus.Host/RequestResponse/IPendingRequestManager.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host; + +public interface IPendingRequestManager +{ + IPendingRequestStore Store { get; } + void CleanPendingRequests(); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/RequestResponse/InMemoryPendingRequestStore.cs b/src/SlimMessageBus.Host/RequestResponse/InMemoryPendingRequestStore.cs index 9d302eb5..345297e2 100644 --- a/src/SlimMessageBus.Host/RequestResponse/InMemoryPendingRequestStore.cs +++ b/src/SlimMessageBus.Host/RequestResponse/InMemoryPendingRequestStore.cs @@ -7,7 +7,7 @@ namespace SlimMessageBus.Host; public class InMemoryPendingRequestStore : IPendingRequestStore { private readonly object _itemsLock = new(); - private readonly Dictionary _items = new(); + private readonly Dictionary _items = []; #region Implementation of IPendingRequestsStore diff --git a/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs b/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs index 542f7af5..f6ee7774 100644 --- a/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs +++ b/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs @@ -3,34 +3,29 @@ /// /// Manages the pending requests - ensure requests which exceeded the allotted timeout period are removed. /// -public class PendingRequestManager : IDisposable +public class PendingRequestManager : IPendingRequestManager, IDisposable { private readonly ILogger _logger; + private readonly TimeSpan _timerInterval; private readonly Timer _timer; private readonly object _timerSync = new(); - private readonly TimeSpan _timerInterval; - private readonly Func _timeProvider; + private readonly ICurrentTimeProvider _timeProvider; private readonly Action _onRequestTimeout; private bool _cleanInProgress; public IPendingRequestStore Store { get; } - public PendingRequestManager(IPendingRequestStore store, Func timeProvider, TimeSpan interval, ILoggerFactory loggerFactory, Action onRequestTimeout = null) + public PendingRequestManager(IPendingRequestStore store, ICurrentTimeProvider timeProvider, ILoggerFactory loggerFactory, TimeSpan? interval = null, Action onRequestTimeout = null) { _logger = loggerFactory.CreateLogger(); Store = store; _onRequestTimeout = onRequestTimeout; _timeProvider = timeProvider; - _timerInterval = interval; - _timer = new Timer(state => TimerCallback(), null, Timeout.Infinite, Timeout.Infinite); - } - - public void Start() - { - _timer.Change(TimeSpan.Zero, _timerInterval); + _timerInterval = interval ?? TimeSpan.FromSeconds(3); + _timer = new Timer(state => TimerCallback(), null, _timerInterval, _timerInterval); } #region IDisposable @@ -78,7 +73,7 @@ private void TimerCallback() /// public virtual void CleanPendingRequests() { - var now = _timeProvider(); + var now = _timeProvider.CurrentTime; var requestsToCancel = Store.FindAllToCancel(now); foreach (var requestState in requestsToCancel) @@ -96,4 +91,4 @@ public virtual void CleanPendingRequests() } Store.RemoveAll(requestsToCancel.Select(x => x.Id)); } -} +} diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index 97de35bb..5e61c809 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -121,11 +121,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig Common.NuGet.Properties.xml = Common.NuGet.Properties.xml Common.Properties.xml = Common.Properties.xml - ..\CONTRIBUTING.md = ..\CONTRIBUTING.md - ..\build\do_build.ps1 = ..\build\do_build.ps1 - ..\build\do_package.ps1 = ..\build\do_package.ps1 - ..\build\do_test.ps1 = ..\build\do_test.ps1 - ..\build\do_test_ci.ps1 = ..\build\do_test_ci.ps1 Host.Plugin.Properties.xml = Host.Plugin.Properties.xml ..\README.md = ..\README.md ..\build\tasks.ps1 = ..\build\tasks.ps1 @@ -141,7 +136,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Seriali EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{CBE53E71-7F48-415C-BD43-B812EC207BC6}" ProjectSection(SolutionItems) = preProject + ..\CONTRIBUTING.md = ..\CONTRIBUTING.md ..\docs\intro.md = ..\docs\intro.md + ..\docs\NuGet.md = ..\docs\NuGet.md ..\docs\provider_azure_eventhubs.md = ..\docs\provider_azure_eventhubs.md ..\docs\provider_azure_servicebus.md = ..\docs\provider_azure_servicebus.md ..\docs\provider_hybrid.md = ..\docs\provider_hybrid.md @@ -278,6 +275,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Nats-SingleNode", "Nats-Sin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.AspNetCore.Test", "Tests\SlimMessageBus.Host.AspNetCore.Test\SlimMessageBus.Host.AspNetCore.Test.csproj", "{9FCBF788-1F0C-43E2-909D-1F96B2685F38}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Artifacts", "Artifacts", "{0F4AD1B7-157D-4ABC-A379-68BF207F2FC3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -883,6 +882,7 @@ Global {7D5E4522-8D44-471B-AB01-70FC65948B36} = {A5B15524-93B8-4CCE-AC6D-A22984498BA0} {81D3FE99-67E6-40AC-A893-AAC4002A30B1} = {A5B15524-93B8-4CCE-AC6D-A22984498BA0} {A6E0EF17-5D09-481A-90F9-16FC24E12AA8} = {D7DCD96A-C3B8-4D22-8216-0E8ADD30C03F} + {CBE53E71-7F48-415C-BD43-B812EC207BC6} = {0F4AD1B7-157D-4ABC-A379-68BF207F2FC3} {5F0638BD-2967-4257-A73D-80C45F40CE4F} = {9291D340-B4FA-44A3-8060-C14743FB1712} {D7DCD96A-C3B8-4D22-8216-0E8ADD30C03F} = {75BDDBB5-8DB8-4893-BD89-8FFC6C42244D} {59F88FB5-6D19-4520-87E8-227B3539BBB3} = {A5B15524-93B8-4CCE-AC6D-A22984498BA0} @@ -922,6 +922,8 @@ Global {8507237C-68C3-46AD-B7DA-800791C6FDDB} = {9F005B5C-A856-4351-8C0C-47A8B785C637} {DB624D5F-CB7C-4E16-B1E2-3B368FCB5A46} = {9F005B5C-A856-4351-8C0C-47A8B785C637} {AD05234E-A925-44C0-977E-FEAC2A75B98C} = {9F005B5C-A856-4351-8C0C-47A8B785C637} + {137BFD32-CD0A-47CA-8884-209CD49DEE8C} = {0F4AD1B7-157D-4ABC-A379-68BF207F2FC3} + {1A71BB05-58ED-4B27-B4A4-A03D9E608C1C} = {0F4AD1B7-157D-4ABC-A379-68BF207F2FC3} {969AAB37-AEFC-40F9-9F89-B4B5E45E13C9} = {D3D6FD9A-968A-45BB-86C7-4527C72A057E} {CDF578D6-FE85-4A44-A99A-32490F047FDA} = {9F005B5C-A856-4351-8C0C-47A8B785C637} {57290E47-603D-46D0-BF13-AC1D6481380A} = {9291D340-B4FA-44A3-8060-C14743FB1712} diff --git a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs index 2cd56421..d23f7c74 100644 --- a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs @@ -16,9 +16,9 @@ namespace SlimMessageBus.Host.AzureEventHub.Test; /// Inside the GitHub Actions pipeline, the Azure Event Hub infrastructure is shared, and so if tests are run in parallel they might affect each other (flaky tests). /// [Trait("Category", "Integration")] -public class EventHubMessageBusIt(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) +public class EventHubMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { - private const int NumberOfMessages = 77; + private const int NumberOfMessages = 100; protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { @@ -51,8 +51,10 @@ protected override void SetupServices(ServiceCollection services, IConfiguration public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); - [Fact] - public async Task BasicPubSub() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSub(bool bulkProduce) { // arrange var hubName = "test-ping"; @@ -84,10 +86,17 @@ public async Task BasicPubSub() .Range(0, NumberOfMessages) .Select(i => new PingMessage { Counter = i, Timestamp = DateTime.UtcNow }) .ToList(); - - foreach (var m in messages) + + if (bulkProduce) { - await messageBus.Publish(m); + await messageBus.Publish(messages); + } + else + { + foreach (var m in messages) + { + await messageBus.Publish(m); + } } stopwatch.Stop(); diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/GlobalUsings.cs index d84cd0bc..caba35da 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/GlobalUsings.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/GlobalUsings.cs @@ -1,6 +1,10 @@ -global using Xunit; -global using Xunit.Abstractions; -global using FluentAssertions; +global using FluentAssertions; + +global using Microsoft.Extensions.Logging.Abstractions; + global using Moq; -global using Microsoft.Extensions.Logging; + global using SecretStore; + +global using Xunit; +global using Xunit.Abstractions; diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs index 8b8d487f..538a7964 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs @@ -21,9 +21,10 @@ namespace SlimMessageBus.Host.AzureServiceBus.Test; /// Inside the GitHub Actions pipeline, the Azure Service Bus infrastructure is shared, and this tests attempts to isolate itself by using unique queue/topic names. /// [Trait("Category", "Integration")] -public class ServiceBusMessageBusIt(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) +public class ServiceBusMessageBusIt(ITestOutputHelper output) + : BaseIntegrationTest(output) { - private const int NumberOfMessages = 77; + private const int NumberOfMessages = 100; protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { @@ -65,8 +66,10 @@ private static void MessageModifierWithSession(PingMessage message, ServiceBusMe sbMessage.SessionId = $"DecimalDigit_{message.Counter / 10 % 10:00}"; } - [Fact] - public async Task BasicPubSubOnTopic() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSubOnTopic(bool bulkProduce) { var subscribers = 2; var topic = TopicName(); @@ -85,11 +88,13 @@ public async Task BasicPubSubOnTopic() })); }); - await BasicPubSub(subscribers); + await BasicPubSub(subscribers, bulkProduce: bulkProduce); } - [Fact] - public async Task BasicPubSubOnQueue() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSubOnQueue(bool bulkProduce) { var queue = QueueName(); @@ -104,7 +109,7 @@ public async Task BasicPubSubOnQueue() .Instances(20)); }); - await BasicPubSub(1); + await BasicPubSub(1, bulkProduce: bulkProduce); } [Fact] @@ -134,7 +139,7 @@ public class TestData public IReadOnlyCollection ConsumedMessages { get; set; } } - private async Task BasicPubSub(int expectedMessageCopies, Action additionalAssertion = null) + private async Task BasicPubSub(int expectedMessageCopies, Action additionalAssertion = null, bool bulkProduce = false) { // arrange var testMetric = ServiceProvider.GetRequiredService(); @@ -152,10 +157,17 @@ private async Task BasicPubSub(int expectedMessageCopies, Action addit .Select(i => i % 2 == 0 ? new PingMessage { Counter = i } : new PingDerivedMessage { Counter = i }) .ToList(); - foreach (var producedMessage in producedMessages) + if (bulkProduce) + { + await messageBus.Publish(producedMessages); + } + else { - // Send them in order - await messageBus.Publish(producedMessage); + foreach (var producedMessage in producedMessages) + { + // Send them in order + await messageBus.Publish(producedMessage); + } } stopwatch.Stop(); @@ -277,8 +289,10 @@ private async Task BasicReqResp() responses.All(x => x.Item1.Message == x.Item2.Message).Should().BeTrue(); } - [Fact] - public async Task FIFOUsingSessionsOnQueue() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task FIFOUsingSessionsOnQueue(bool bulkProduce) { var queue = QueueName(); @@ -293,7 +307,7 @@ public async Task FIFOUsingSessionsOnQueue() .Instances(1) .EnableSession(x => x.MaxConcurrentSessions(10).SessionIdleTimeout(TimeSpan.FromSeconds(5)))); }); - await BasicPubSub(1, CheckMessagesWithinSameSessionAreInOrder); + await BasicPubSub(1, CheckMessagesWithinSameSessionAreInOrder, bulkProduce: bulkProduce); } private static void CheckMessagesWithinSameSessionAreInOrder(TestData testData) @@ -308,8 +322,10 @@ private static void CheckMessagesWithinSameSessionAreInOrder(TestData testData) } } - [Fact] - public async Task FIFOUsingSessionsOnTopic() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task FIFOUsingSessionsOnTopic(bool bulkProduce) { var queue = QueueName(); @@ -325,7 +341,7 @@ public async Task FIFOUsingSessionsOnTopic() .EnableSession(x => x.MaxConcurrentSessions(10).SessionIdleTimeout(TimeSpan.FromSeconds(5)))); }); - await BasicPubSub(1, CheckMessagesWithinSameSessionAreInOrder); + await BasicPubSub(1, CheckMessagesWithinSameSessionAreInOrder, bulkProduce: bulkProduce); } private static string QueueName([CallerMemberName] string testName = null) diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs index 5f50e183..c4b4cf32 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs @@ -6,6 +6,7 @@ using Azure.Messaging.ServiceBus; using SlimMessageBus.Host; +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Interceptor; using SlimMessageBus.Host.Serialization; @@ -25,6 +26,8 @@ public ServiceBusMessageBusTests() serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); serviceProviderMock.Setup(x => x.GetService(typeof(IEnumerable))).Returns(Array.Empty()); serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + serviceProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); BusBuilder.WithDependencyResolver(serviceProviderMock.Object); diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs index a9bd4975..cc0cd751 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs @@ -7,7 +7,7 @@ namespace SlimMessageBus.Host.Integration.Test; using SlimMessageBus.Host.Memory; [Trait("Category", "Integration")] -public class HybridTests : BaseIntegrationTest +public class HybridTests(ITestOutputHelper output) : BaseIntegrationTest(output) { public enum SerializerType { @@ -17,10 +17,6 @@ public enum SerializerType public record RunOptions(SerializerType SerializerType, Action ServicesBuilder); - public HybridTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - } - protected RunOptions Options { get; set; } protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs index 0777772c..58ff8628 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageBusCurrent/MessageBusCurrentTests.cs @@ -6,9 +6,9 @@ /// /// This test verifies that the MessageBus.Current accessor works correctly and looks up in the current message scope. /// -/// +/// [Trait("Category", "Integration")] -public class MessageBusCurrentTests(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) +public class MessageBusCurrentTests(ITestOutputHelper output) : BaseIntegrationTest(output) { protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs index 40261fe9..bf2dd439 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/MessageScopeAccessor/MessageScopeAccessorTests.cs @@ -7,10 +7,9 @@ /// /// This test verifies that the correctly looks up the for the current message scope. /// -/// +/// [Trait("Category", "Integration")] -public class MessageScopeAccessorTests(ITestOutputHelper testOutputHelper) - : BaseIntegrationTest(testOutputHelper) +public class MessageScopeAccessorTests(ITestOutputHelper output) : BaseIntegrationTest(output) { protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs index e8a61a40..5566d40d 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForConsumersTest.cs @@ -32,6 +32,7 @@ public KafkaPartitionConsumerForConsumersTest() massageBusMock.ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(SomeMessageConsumer))).Returns(_consumer); massageBusMock.ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(ILoggerFactory))).Returns(_loggerFactory); massageBusMock.ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); + massageBusMock.ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); var headerSerializer = new StringValueSerializer(); diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs index ec010812..83c32571 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs @@ -23,11 +23,10 @@ namespace SlimMessageBus.Host.Kafka.Test; /// [Trait("Category", "Integration")] [Trait("Transport", "Kafka")] -public class KafkaMessageBusIt(ITestOutputHelper testOutputHelper) - : BaseIntegrationTest(testOutputHelper) +public class KafkaMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { private const int NumberOfMessages = 300; - private readonly static TimeSpan DelayTimeSpan = TimeSpan.FromSeconds(5); + private static readonly TimeSpan DelayTimeSpan = TimeSpan.FromSeconds(5); protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { @@ -64,9 +63,10 @@ protected override void SetupServices(ServiceCollection services, IConfiguration public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); [Theory] - [InlineData(300, 100, 120)] - [InlineData(300, 120, 100)] - public async Task BasicPubSub(int numberOfMessages, int delayProducerAt, int delayConsumerAt) + [InlineData(300, 100, 120, false)] + [InlineData(300, 120, 100, false)] + [InlineData(300, 100, 120, true)] + public async Task BasicPubSub(int numberOfMessages, int delayProducerAt, int delayConsumerAt, bool bulkProduce) { // arrange AddBusConfiguration(mbb => @@ -128,17 +128,25 @@ public async Task BasicPubSub(int numberOfMessages, int delayProducerAt, int del .Select(i => new PingMessage(DateTime.UtcNow, i)) .ToList(); - var index = 0; - foreach (var m in messages) + if (bulkProduce) { - if (index == delayProducerAt) + await messageBus.Publish(messages); + } + else + { + // Publish messages one by one (to simulate a delay between messages + var index = 0; + foreach (var m in messages) { - // We want to force the Partition EOF event to be triggered by Kafka - Logger.LogInformation("Waiting some time before publish to force Partition EOF event (MessageIndex: {MessageIndex})", index); - await Task.Delay(DelayTimeSpan); + if (index == delayProducerAt) + { + // We want to force the Partition EOF event to be triggered by Kafka + Logger.LogInformation("Waiting some time before publish to force Partition EOF event (MessageIndex: {MessageIndex})", index); + await Task.Delay(DelayTimeSpan); + } + await messageBus.Publish(m); + index++; } - await messageBus.Publish(m); - index++; } stopwatch.Stop(); diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs index 278267f7..0382512b 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusTest.cs @@ -1,4 +1,6 @@ -namespace SlimMessageBus.Host.Kafka.Test; +namespace SlimMessageBus.Host.Kafka.Test; + +using SlimMessageBus.Host.Collections; public class KafkaMessageBusTest : IDisposable { @@ -18,6 +20,9 @@ public KafkaMessageBusTest() serviceProviderMock.Setup(x => x.GetService(typeof(ILogger))).CallBase(); serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + serviceProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Array.CreateInstance(t.GetGenericArguments()[0], 0)); + serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + serviceProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); MbSettings = new MessageBusSettings { diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs index d100af1f..bd02dda1 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/MessageBusMock.cs @@ -1,5 +1,6 @@ namespace SlimMessageBus.Host.Kafka.Test; +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Serialization; using SlimMessageBus.Host.Test.Common; @@ -26,6 +27,9 @@ public MessageBusMock() ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(SerializerMock.Object); ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(CurrentTimeProvider); + ServiceProviderMock.ProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Array.CreateInstance(t.GetGenericArguments()[0], 0)); + ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + ServiceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); BusSettings = new MessageBusSettings { diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs index 057da73b..fbcdc410 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusIt.cs @@ -11,7 +11,7 @@ using SlimMessageBus.Host.Test.Common.IntegrationTest; [Trait("Category", "Integration")] -public class MemoryMessageBusIt(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) +public class MemoryMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { private const int NumberOfMessages = 1023; diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index 2640acb2..7a9378ae 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -3,10 +3,12 @@ namespace SlimMessageBus.Host.Memory.Test; using System.Text; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using SlimMessageBus.Host; +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Consumer; using SlimMessageBus.Host.Interceptor; using SlimMessageBus.Host.Serialization; @@ -35,8 +37,11 @@ public MemoryMessageBusTests() _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(_messageSerializerMock.Object); _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); - _serviceProviderMock.ProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Enumerable.Empty()); _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + _serviceProviderMock.ProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Enumerable.Empty()); + _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IEnumerable))).Returns(Array.Empty()); + _serviceProviderMock.ProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); _messageSerializerMock .Setup(x => x.Serialize(It.IsAny(), It.IsAny())) @@ -177,13 +182,12 @@ public async Task When_Publish_Given_PerMessageScopeEnabled_Then_TheScopeIsCreat _serviceProviderMock.ScopeFactoryMock.Verify(x => x.CreateScope(), Times.Once); _serviceProviderMock.ScopeFactoryMock.VerifyNoOtherCalls(); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IServiceScopeFactory)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IServiceScopeFactory)), Times.Once); + VerifyCommonServiceProvider(); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); scopeProviderMock.Should().NotBeNull(); @@ -224,16 +228,13 @@ public async Task When_Publish_Given_PerMessageScopeDisabled_Then_TheScopeIsNotC await _subject.Value.ProducePublish(m); // assert - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IServiceProvider)), Times.Never); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(SomeMessageAConsumer)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + VerifyCommonServiceProvider(); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumerMock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); @@ -306,11 +307,19 @@ public async Task When_ProducePublish_Given_PerMessageScopeDisabledOrEnabled_And currentServiceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); currentServiceProviderMock.ProviderMock.VerifyNoOtherCalls(); + VerifyCommonServiceProvider(); + + _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); + } + + private void VerifyCommonServiceProvider() + { _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); - _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(RuntimeTypeCache)), Times.Once); + _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IPendingRequestManager)), Times.Once); } [Fact] @@ -343,9 +352,9 @@ public async Task When_Publish_Given_TwoConsumersOnSameTopic_Then_BothAreInvoked _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + + VerifyCommonServiceProvider(); + _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumer1Mock.VerifySet(x => x.Context = It.IsAny(), Times.Once); @@ -386,7 +395,6 @@ public async Task When_Send_Given_AConsumersAndHandlerOnSameTopic_Then_BothAreIn response.Id.Should().Be(m.Id); // current scope is not changed - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IServiceScopeFactory)), Times.Never); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(SomeRequestConsumer)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(SomeRequestHandler)), Times.Once); @@ -394,9 +402,7 @@ public async Task When_Send_Given_AConsumersAndHandlerOnSameTopic_Then_BothAreIn _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); - _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + VerifyCommonServiceProvider(); _serviceProviderMock.ProviderMock.VerifyNoOtherCalls(); consumer2Mock.Verify(x => x.OnHandle(m, It.IsAny()), Times.Once); diff --git a/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs index df90624b..47798ef8 100644 --- a/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs @@ -5,14 +5,10 @@ namespace SlimMessageBus.Host.Mqtt.Test; [Trait("Category", "Integration")] [Trait("Transport", "Mqtt")] -public class MqttMessageBusIt : BaseIntegrationTest +public class MqttMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { private const int NumberOfMessages = 77; - public MqttMessageBusIt(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - } - protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) { services.AddSlimMessageBus(mbb => @@ -41,8 +37,10 @@ protected override void SetupServices(ServiceCollection services, IConfiguration public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); - [Fact] - public async Task BasicPubSubOnTopic() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSubOnTopic(bool bulkProduce) { var concurrency = 2; var topic = "test-ping"; @@ -54,10 +52,10 @@ public async Task BasicPubSubOnTopic() .Consume(x => x.Topic(topic).Instances(concurrency)); }); - await BasicPubSub(1); + await BasicPubSub(1, bulkProduce: bulkProduce); } - private async Task BasicPubSub(int expectedMessageCopies) + private async Task BasicPubSub(int expectedMessageCopies, bool bulkProduce) { // arrange var messageBus = MessageBus; @@ -78,9 +76,16 @@ private async Task BasicPubSub(int expectedMessageCopies) .Select(i => new PingMessage(i, Guid.NewGuid())) .ToList(); - var messageTasks = producedMessages.Select(m => messageBus.Publish(m)); - // wait until all messages are sent - await Task.WhenAll(messageTasks); + if (bulkProduce) + { + await messageBus.Publish(producedMessages); + } + else + { + var messageTasks = producedMessages.Select(m => messageBus.Publish(m)); + // wait until all messages are sent + await Task.WhenAll(messageTasks); + } stopwatch.Stop(); Logger.LogInformation("Published {MessageCount} messages in {Duration}", producedMessages.Count, stopwatch.Elapsed); diff --git a/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs index 87341802..42588d99 100644 --- a/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Nats.Test/NatsMessageBusIt.cs @@ -9,7 +9,7 @@ namespace SlimMessageBus.Host.Nats.Test; [Trait("Category", "Integration")] [Trait("Transport", "Nats")] -public class NatsMessageBusIt(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) +public class NatsMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { private const int NumberOfMessages = 100; @@ -35,8 +35,10 @@ protected override void SetupServices(ServiceCollection services, IConfiguration public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); - [Fact] - public async Task BasicPubSubOnTopic() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSubOnTopic(bool bulkProduce) { var concurrency = 2; var topic = "test-ping"; @@ -48,10 +50,10 @@ public async Task BasicPubSubOnTopic() .Consume(x => x.Topic(topic).Instances(concurrency)); }); - await BasicPubSub(1); + await BasicPubSub(1, bulkProduce); } - private async Task BasicPubSub(int expectedMessageCopies) + private async Task BasicPubSub(int expectedMessageCopies, bool bulkProduce) { // arrange var messageBus = MessageBus; @@ -71,10 +73,17 @@ private async Task BasicPubSub(int expectedMessageCopies) .Range(0, NumberOfMessages) .Select(i => new PingMessage(i, Guid.NewGuid())) .ToList(); - - var messageTasks = producedMessages.Select(m => messageBus.Publish(m)); - // wait until all messages are sent - await Task.WhenAll(messageTasks); + + if (bulkProduce) + { + await messageBus.Publish(producedMessages); + } + else + { + var messageTasks = producedMessages.Select(m => messageBus.Publish(m)); + // wait until all messages are sent + await Task.WhenAll(messageTasks); + } stopwatch.Stop(); Logger.LogInformation("Published {MessageCount} messages in {Duration}", producedMessages.Count, stopwatch.Elapsed); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs index 98728288..835025fd 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/BaseOutboxIntegrationTest.cs @@ -2,7 +2,7 @@ using SlimMessageBus.Host.Outbox.Sql.DbContext.Test.DataAccess; -public abstract class BaseOutboxIntegrationTest(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) +public abstract class BaseOutboxIntegrationTest(ITestOutputHelper output) : BaseIntegrationTest(output) { protected async Task PerformDbOperation(Func action) { diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs index 4b5f3caa..628ee8cf 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxBenchmarkTests.cs @@ -18,11 +18,11 @@ /// and then measure times when outbox is enabled. /// This should help asses the overhead of outbox and to baseline future outbox improvements. /// -/// +/// [Trait("Category", "Integration")] // for benchmarks [Trait("Transport", "Outbox")] [Collection(CustomerContext.Schema)] -public class OutboxBenchmarkTests(ITestOutputHelper testOutputHelper) : BaseOutboxIntegrationTest(testOutputHelper) +public class OutboxBenchmarkTests(ITestOutputHelper output) : BaseOutboxIntegrationTest(output) { private bool _useOutbox; private BusType _testParamBusType; diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs index fa6578d8..8d67cb92 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs @@ -13,7 +13,7 @@ [Trait("Category", "Integration")] [Trait("Transport", "Outbox")] [Collection(CustomerContext.Schema)] -public class OutboxTests(ITestOutputHelper testOutputHelper) : BaseOutboxIntegrationTest(testOutputHelper) +public class OutboxTests(ITestOutputHelper output) : BaseOutboxIntegrationTest(output) { private bool _testParamUseHybridBus; private TransactionType _testParamTransactionType; @@ -214,6 +214,7 @@ public async Task Given_PublishExternalEventInTransaction_When_ExceptionThrownDu _testParamUseHybridBus = false; _testParamTransactionType = TransactionType.TransactionScope; _testParamBusType = busType; + _testParamIdGenerationMode = mode; await PrepareDatabase(); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs index 515e9a10..f26de7e2 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Test/Services/OutboxSendingTaskTests.cs @@ -8,7 +8,7 @@ public class DispatchBatchTests { private readonly ILoggerFactory _loggerFactory; private readonly Mock, Guid>> _outboxRepositoryMock; - private readonly Mock _producerMock; + private readonly Mock _producerMock; private readonly Mock _messageBusTargetMock; private readonly OutboxSettings _outboxSettings; private readonly IServiceProvider _serviceProvider; @@ -17,7 +17,7 @@ public class DispatchBatchTests public DispatchBatchTests() { _outboxRepositoryMock = new Mock, Guid>>(); - _producerMock = new Mock(); + _producerMock = new Mock(); _messageBusTargetMock = new Mock(); _outboxSettings = new OutboxSettings { MaxDeliveryAttempts = 5 }; _serviceProvider = Mock.Of(); @@ -92,7 +92,7 @@ public class ProcessMessagesTests private readonly Mock _mockCompositeMessageBus; private readonly Mock _mockMessageBusTarget; private readonly Mock _mockMasterMessageBus; - private readonly Mock _mockMessageBusBulkProducer; + private readonly Mock _mockMessageBusBulkProducer; private readonly OutboxSettings _outboxSettings; private readonly OutboxSendingTask, Guid> _sut; @@ -102,7 +102,7 @@ public ProcessMessagesTests() _mockCompositeMessageBus = new Mock(); _mockMessageBusTarget = new Mock(); _mockMasterMessageBus = new Mock(); - _mockMessageBusBulkProducer = _mockMasterMessageBus.As(); + _mockMessageBusBulkProducer = _mockMasterMessageBus.As(); _outboxSettings = new OutboxSettings { diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs index 105d84f6..d66f7801 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs @@ -12,8 +12,7 @@ namespace SlimMessageBus.Host.RabbitMQ.Test.IntegrationTests; [Trait("Category", "Integration")] [Trait("Transport", "RabbitMQ")] -public class RabbitMqMessageBusIt(ITestOutputHelper testOutputHelper) - : BaseIntegrationTest(testOutputHelper) +public class RabbitMqMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { private const int NumberOfMessages = 144; diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/GlobalUsings.cs index 1478215a..7aea699a 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/GlobalUsings.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/GlobalUsings.cs @@ -1,6 +1,7 @@ global using FluentAssertions; global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Abstractions; global using Moq; diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs index f0f2e8c6..300181bd 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs @@ -12,8 +12,7 @@ namespace SlimMessageBus.Host.Redis.Test; [Trait("Category", "Integration")] [Trait("Transport", "Redis")] -public class RedisMessageBusIt(ITestOutputHelper testOutputHelper) - : BaseIntegrationTest(testOutputHelper) +public class RedisMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) { private const int NumberOfMessages = 77; @@ -45,8 +44,10 @@ protected override void SetupServices(ServiceCollection services, IConfiguration public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); - [Fact] - public async Task BasicPubSubOnTopic() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSubOnTopic(bool bulkProduce) { var concurrency = 2; var consumers = 2; @@ -68,11 +69,13 @@ public async Task BasicPubSubOnTopic() })); }); - await BasicPubSub(consumers); + await BasicPubSub(consumers, bulkProduce); } - [Fact] - public async Task BasicPubSubOnQueue() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicPubSubOnQueue(bool bulkProduce) { var concurrency = 2; var consumers = 2; @@ -94,10 +97,10 @@ public async Task BasicPubSubOnQueue() })); }); - await BasicPubSub(consumers); + await BasicPubSub(consumers, bulkProduce); } - private async Task BasicPubSub(int expectedMessageCopies) + private async Task BasicPubSub(int expectedMessageCopies, bool bulkProduce) { // arrange var messageBus = MessageBus; @@ -119,20 +122,28 @@ private async Task BasicPubSub(int expectedMessageCopies) .Range(0, NumberOfMessages) .Select(i => new PingMessage(i, Guid.NewGuid())) .ToList(); - - var messageTasks = producedMessages.Select(m => messageBus.Publish(m)); - // wait until all messages are sent - await Task.WhenAll(messageTasks).ConfigureAwait(false); + + + if (bulkProduce) + { + await messageBus.Publish(producedMessages); + } + else + { + var messageTasks = producedMessages.Select(m => messageBus.Publish(m)); + // wait until all messages are sent + await Task.WhenAll(messageTasks).ConfigureAwait(false); + } stopwatch.Stop(); - Logger.LogInformation("Published {0} messages in {1}", producedMessages.Count, stopwatch.Elapsed); + Logger.LogInformation("Published {MessageCount} messages in {Duration}", producedMessages.Count, stopwatch.Elapsed); // consume stopwatch.Restart(); await consumedMessages.WaitUntilArriving(expectedCount: expectedMessageCopies * producedMessages.Count); stopwatch.Stop(); - Logger.LogInformation("Consumed {0} messages in {1}", consumedMessages, stopwatch.Elapsed); + Logger.LogInformation("Consumed {MessageCount} messages in {Duration}", consumedMessages, stopwatch.Elapsed); // assert @@ -222,13 +233,13 @@ private async Task BasicReqResp() var responseTasks = requests.Select(async req => { var resp = await messageBus.Send(req).ConfigureAwait(false); - Logger.LogDebug("Received response for index {0:000}", req.Index); + Logger.LogDebug("Received response for index {ResponseIndex:000}", req.Index); responses.Add((req, resp)); }); await Task.WhenAll(responseTasks).ConfigureAwait(false); stopwatch.Stop(); - Logger.LogInformation("Published and received {0} messages in {1}", responses.Count, stopwatch.Elapsed); + Logger.LogInformation("Published and received {MessageCount} messages in {Duration}", responses.Count, stopwatch.Elapsed); // assert @@ -242,18 +253,14 @@ private record PingMessage(int Counter, Guid Value); private class PingConsumer(ILogger logger, TestEventCollector messages) : IConsumer, IConsumerWithContext { - private readonly ILogger _logger = logger; - private readonly TestEventCollector _messages = messages; - public IConsumerContext Context { get; set; } #region Implementation of IConsumer public Task OnHandle(PingMessage message, CancellationToken cancellationToken) { - _messages.Add(message); - - _logger.LogInformation("Got message {0} on topic {1}.", message.Counter, Context.Path); + messages.Add(message); + logger.LogInformation("Got message {MessageCounter} on topic {Topic}.", message.Counter, Context.Path); return Task.CompletedTask; } @@ -266,18 +273,12 @@ private class EchoResponse { public string Message { get; set; } - #region Overrides of Object - public override string ToString() => $"EchoResponse(Message={Message})"; - - #endregion } private class EchoRequestHandler : IRequestHandler { - public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) - { - return Task.FromResult(new EchoResponse { Message = request.Message }); - } + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) + => Task.FromResult(new EchoResponse { Message = request.Message }); } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs index beced739..fd9c9e1c 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs @@ -5,6 +5,8 @@ namespace SlimMessageBus.Host.Redis.Test; using Newtonsoft.Json; +using SlimMessageBus.Host.Collections; +using SlimMessageBus.Host.Interceptor; using SlimMessageBus.Host.Serialization; using StackExchange.Redis; @@ -26,7 +28,11 @@ public RedisMessageBusTest() _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(_messageSerializerMock.Object); _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + _serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); _serviceProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns(Enumerable.Empty()); + _serviceProviderMock.Setup(x => x.GetService(typeof(IEnumerable))).Returns(Array.Empty()); + _serviceProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); + _settings.ServiceProvider = _serviceProviderMock.Object; diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.Test.Common/GlobalUsings.cs index d84cd0bc..e7957f36 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/GlobalUsings.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/GlobalUsings.cs @@ -1,6 +1,15 @@ -global using Xunit; -global using Xunit.Abstractions; -global using FluentAssertions; -global using Moq; +global using System.Diagnostics; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; + +global using Moq; + global using SecretStore; + +global using Serilog; +global using Serilog.Extensions.Logging; + +global using Xunit; +global using Xunit.Abstractions; diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs index b024b5a3..88db4f39 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs @@ -1,18 +1,5 @@ namespace SlimMessageBus.Host.Test.Common.IntegrationTest; -using System.Diagnostics; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -using Serilog; -using Serilog.Extensions.Logging; - -using SlimMessageBus.Host; - -using Xunit; - /// /// Base integration test setup that: /// - uses MS Dependency Injection @@ -32,7 +19,7 @@ public abstract class BaseIntegrationTest : IAsyncLifetime protected IConfigurationRoot Configuration { get; } protected ServiceProvider ServiceProvider => _serviceProvider.Value; - protected BaseIntegrationTest(ITestOutputHelper testOutputHelper) + protected BaseIntegrationTest(ITestOutputHelper output) { // Creating a `LoggerProviderCollection` lets Serilog optionally write // events through other dynamically-added MEL ILoggerProviders. @@ -42,7 +29,7 @@ protected BaseIntegrationTest(ITestOutputHelper testOutputHelper) Log.Logger = new LoggerConfiguration() //.WriteTo.Providers(providers) - .WriteTo.TestOutput(testOutputHelper, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}") + .WriteTo.TestOutput(output, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}") .ReadFrom.Configuration(Configuration) .CreateLogger(); diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/XunitLogger.cs b/src/Tests/SlimMessageBus.Host.Test.Common/XunitLogger.cs index 5091506b..205ee41b 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/XunitLogger.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/XunitLogger.cs @@ -1,6 +1,6 @@ namespace SlimMessageBus.Host.Test.Common; -public class XunitLogger : ILogger, IDisposable +public class XunitLogger : Microsoft.Extensions.Logging.ILogger, IDisposable { private readonly ITestOutputHelper output; private readonly string categoryName; @@ -33,9 +33,7 @@ public void Dispose() } } -public class XunitLogger : XunitLogger, ILogger +public class XunitLogger(ILoggerFactory loggerFactory) + : XunitLogger(((XunitLoggerFactory)loggerFactory).Output, typeof(T).Name), ILogger { - public XunitLogger(ILoggerFactory loggerFactory) : base(((XunitLoggerFactory)loggerFactory).Output, typeof(T).Name) - { - } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs b/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs index 40729f8b..5f334b83 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs @@ -10,7 +10,8 @@ public void AddProvider(ILoggerProvider provider) { } - public ILogger CreateLogger(string categoryName) => new XunitLogger(_output, categoryName); + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) + => new XunitLogger(_output, categoryName); public void Dispose() { diff --git a/src/Tests/SlimMessageBus.Host.Test/Collections/ProducerByMessageTypeCacheTests.cs b/src/Tests/SlimMessageBus.Host.Test/Collections/ProducerByMessageTypeCacheTests.cs index d28bcf68..ee6cee28 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Collections/ProducerByMessageTypeCacheTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Collections/ProducerByMessageTypeCacheTests.cs @@ -6,18 +6,32 @@ public class ProducerByMessageTypeCacheTests { - public static IEnumerable Data => new List - { - new object[] { typeof(Message), typeof(Message) }, - new object[] { typeof(AMessage), typeof(AMessage) }, - new object[] { typeof(BMessage), typeof(Message)}, - new object[] { typeof(B1Message), typeof(Message) }, - new object[] { typeof(B2Message), typeof(B2Message) }, - new object[] { typeof(ISomeMessage), typeof(ISomeMessage) }, - new object[] { typeof(SomeMessage), typeof(ISomeMessage) }, - new object[] { typeof(object), null }, - new object[] { typeof(int), null }, - }; + public static IEnumerable Data => + [ + [typeof(Message), typeof(Message)], + + [typeof(AMessage), typeof(AMessage)], + + [typeof(BMessage), typeof(Message)], + [typeof(B1Message), typeof(Message)], + + [typeof(B2Message), typeof(B2Message)], + + [typeof(ISomeMessage), typeof(ISomeMessage)], + [typeof(SomeMessage), typeof(ISomeMessage)], + + [typeof(object), null], + [typeof(int), null], + + [typeof(IEnumerable), typeof(Message)], + [typeof(Message[]), typeof(Message)], + + [typeof(IEnumerable), typeof(Message)], + [typeof(BMessage[]), typeof(Message)], + + [typeof(IEnumerable), typeof(Message)] , + [typeof(B1Message[]), typeof(Message)], + ]; [Theory] [MemberData(nameof(Data))] @@ -25,10 +39,10 @@ public void Given_ComplexMessageTypeHierarchy_When_GetProducer_Then_ResolvesProp { // arrange var mbb = MessageBusBuilder.Create(); - mbb.Produce(x => x.DefaultTopic("t1")); - mbb.Produce(x => x.DefaultTopic("t2")); - mbb.Produce(x => x.DefaultTopic("t3")); - mbb.Produce(x => x.DefaultTopic("t4")); + mbb.Produce(x => x.DefaultPath("t1")); + mbb.Produce(x => x.DefaultPath("t2")); + mbb.Produce(x => x.DefaultPath("t3")); + mbb.Produce(x => x.DefaultPath("t4")); var producerByBaseMessageType = mbb.Settings.Producers.ToDictionary(x => x.MessageType); @@ -49,20 +63,6 @@ public void Given_ComplexMessageTypeHierarchy_When_GetProducer_Then_ResolvesProp { subject[messageType].Should().BeNull(); } - - subject[typeof(Message)].Should().BeSameAs(producerByBaseMessageType[typeof(Message)]); - - subject[typeof(AMessage)].Should().BeSameAs(producerByBaseMessageType[typeof(AMessage)]); - subject[typeof(BMessage)].Should().BeSameAs(producerByBaseMessageType[typeof(Message)]); - - subject[typeof(B1Message)].Should().BeSameAs(producerByBaseMessageType[typeof(Message)]); - subject[typeof(B2Message)].Should().BeSameAs(producerByBaseMessageType[typeof(B2Message)]); - - subject[typeof(ISomeMessage)].Should().BeSameAs(producerByBaseMessageType[typeof(ISomeMessage)]); - subject[typeof(SomeMessage)].Should().BeSameAs(producerByBaseMessageType[typeof(ISomeMessage)]); - - subject[typeof(object)].Should().BeNull(); - subject[typeof(int)].Should().BeNull(); } internal record Message; diff --git a/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs b/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs index 5b975bf4..d412d632 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs @@ -82,4 +82,41 @@ public void When_GetClosedGenericType(Type openGenericType, Type genericParamete // assert result.Should().Be(expectedResult); } + + public static TheoryData Data => new() + { + { (new SomeMessage[] { new() }).Concat([new SomeMessage()]), true }, + + { new List { new(), new() }, true }, + + { new SomeMessage[] { new(), new() }, true}, + + { new HashSet { new(), new() }, true }, + + { new object(), false }, + }; + + [Theory] + [MemberData(nameof(Data))] + public void Given_ObjectThatIsCollection_When_Then(object collection, bool isCollection) + { + // arrange + + // actr + var collectionInfo = _subject.GetCollectionTypeInfo(collection.GetType()); + + // assert + if (isCollection) + { + collectionInfo.Should().NotBeNull(); + collectionInfo.ItemType.Should().Be(typeof(SomeMessage)); + var col = collectionInfo.ToCollection(collection); + col.Should().NotBeNull(); + col.Should().BeSameAs((IEnumerable)collection); + } + else + { + collectionInfo.Should().BeNull(); + } + } } diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs index 93cf7edf..1affedae 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConsumerInstanceMessageProcessorTest.cs @@ -29,7 +29,7 @@ public async Task When_ProcessMessage_ProcessesAsyncDisposableMessage_Then_Messa object MessageProvider(Type messageType, byte[] payload) => mockMessage.Object; - var p = new MessageProcessor(new[] { _handlerSettings }, _busMock.Bus, MessageProvider, "path", responseProducer: _busMock.Bus); + var p = new MessageProcessor([_handlerSettings], _busMock.Bus, MessageProvider, "path", responseProducer: _busMock.Bus); _busMock.SerializerMock.Setup(x => x.Deserialize(typeof(SomeRequest), It.IsAny())).Returns(mockMessage.Object); @@ -145,7 +145,8 @@ public async Task When_ProcessMessage_Given_FailedRequest_Then_ErrorResponseIsSe It.IsAny>(), null, ex, - It.IsAny())); + It.IsAny(), + It.IsAny())); } [Fact] @@ -185,7 +186,8 @@ private void VerifyProduceResponseNeverCalled() It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny()), Times.Never); + It.IsAny(), + It.IsAny()), Times.Never); } [Fact] @@ -273,7 +275,7 @@ public async Task When_ProcessMessage_Given_RequestArrived_Then_RequestHandlerIn .Returns(new[] { requestHandlerInterceptor.Object }); _busMock.BusMock - .Setup(x => x.ProduceResponse(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ProduceResponse(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); _messageProviderMock.Setup(x => x(request.GetType(), requestPayload)).Returns(request); @@ -320,7 +322,7 @@ public async Task When_ProcessMessage_Given_ArrivedRequestWithoutResponse_Then_R .Returns(new[] { requestHandlerInterceptor.Object }); _busMock.BusMock - .Setup(x => x.ProduceResponse(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ProduceResponse(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var consumerSettings = new HandlerBuilder(_busMock.Bus.Settings).Topic(_topic).WithHandler>().ConsumerSettings; diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs index 60c1f7c6..46862dc1 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs @@ -2,6 +2,7 @@ namespace SlimMessageBus.Host.Test; using Microsoft.Extensions.DependencyInjection; +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Interceptor; using SlimMessageBus.Host.Serialization; @@ -26,13 +27,18 @@ public MessageBusMock() ChildDependencyResolverMocks = []; + var currentTimeProviderMock = new Mock(); + currentTimeProviderMock.SetupGet(x => x.CurrentTime).Returns(() => CurrentTime); + void SetupDependencyResolver(Mock mock) where T : class, IServiceProvider { mock.Setup(x => x.GetService(typeof(IConsumer))).Returns(ConsumerMock.Object); mock.Setup(x => x.GetService(typeof(IRequestHandler))).Returns(HandlerMock.Object); mock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); mock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>) && t.GetGenericArguments().Length == 1 && t.GetGenericArguments()[0].IsGenericType && InterceptorTypes.Contains(t.GetGenericArguments()[0].GetGenericTypeDefinition())))) - .Returns(Enumerable.Empty()); + .Returns(Enumerable.Empty()); + mock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + mock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), currentTimeProviderMock.Object, NullLoggerFactory.Instance)); } ServiceProviderMock = new Mock(); diff --git a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs index 30140f62..6a3350aa 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs @@ -5,6 +5,7 @@ namespace SlimMessageBus.Host.Test.Hybrid; using Newtonsoft.Json; using SlimMessageBus.Host; +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Hybrid; using SlimMessageBus.Host.Test.Common; @@ -40,6 +41,8 @@ public HybridMessageBusTest() _serviceProviderMock.Setup(x => x.GetService(typeof(ILoggerFactory))).Returns(_loggerFactoryMock.Object); _serviceProviderMock.Setup(x => x.GetService(typeof(IEnumerable))).Returns(Array.Empty()); _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + _serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + _serviceProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); _loggerFactoryMock.Setup(x => x.CreateLogger(It.IsAny())).Returns(_loggerMock.Object); diff --git a/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs b/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs index 1db90b8c..b7f84861 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs @@ -1,5 +1,6 @@ -namespace SlimMessageBus.Host.Test; - +namespace SlimMessageBus.Host.Test; + +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Interceptor; public class PublishInterceptorPipelineTests @@ -46,10 +47,10 @@ public async Task When_Next_Then_InterceptorIsCalledAndTargetIsCalled(bool produ }; _busMock.BusMock - .Setup(x => x.PublishInternal(message, context.Path, context.Headers, context.CancellationToken, producerSettings, _busMock.Bus.MessageBusTarget)) + .Setup(x => x.ProduceToTransport(message, message.GetType(), context.Path, context.Headers, _busMock.Bus.MessageBusTarget, context.CancellationToken)) .Returns(() => Task.FromResult(null)); - var subject = new PublishInterceptorPipeline(_busMock.Bus, message, producerSettings, _busMock.Bus.MessageBusTarget, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); + var subject = new PublishInterceptorPipeline(_busMock.Bus, new RuntimeTypeCache(), message, producerSettings, _busMock.Bus.MessageBusTarget, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); // act var result = await subject.Next(); @@ -69,6 +70,6 @@ public async Task When_Next_Then_InterceptorIsCalledAndTargetIsCalled(bool produ } publishInterceptorMock.VerifyNoOtherCalls(); - _busMock.BusMock.Verify(x => x.PublishInternal(message, context.Path, context.Headers, context.CancellationToken, producerSettings, _busMock.Bus.MessageBusTarget), Times.Once); + _busMock.BusMock.Verify(x => x.ProduceToTransport(message, message.GetType(), context.Path, context.Headers, _busMock.Bus.MessageBusTarget, context.CancellationToken), Times.Once); } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs index 57c27fbb..2984ce66 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs @@ -1,7 +1,6 @@ namespace SlimMessageBus.Host.Test; -using Moq.Protected; - +using SlimMessageBus.Host.Collections; using SlimMessageBus.Host.Test.Common; public class MessageBusBaseTests : IDisposable @@ -33,8 +32,10 @@ public MessageBusBaseTests() _serviceProviderMock = new Mock(); _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageSerializer))).Returns(new JsonMessageSerializer()); _serviceProviderMock.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(new AssemblyQualifiedNameMessageTypeResolver()); - _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + _serviceProviderMock.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(() => currentTimeProviderMock.Object); _serviceProviderMock.Setup(x => x.GetService(It.Is(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))).Returns((Type t) => Array.CreateInstance(t.GetGenericArguments()[0], 0)); + _serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + _serviceProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), currentTimeProviderMock.Object, NullLoggerFactory.Instance)); BusBuilder = MessageBusBuilder.Create() .Produce(x => @@ -494,11 +495,9 @@ public async Task When_Publish_Given_InterceptorsInDI_Then_InterceptorInfluenceI _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Never); _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Never); - _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); - _serviceProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); - _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageSerializer)), Times.Between(0, 1, Moq.Range.Inclusive)); - _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); - _serviceProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + + VerifyCommonServiceProvider(); + _serviceProviderMock.VerifyNoOtherCalls(); if (producerInterceptorCallsNext != null) @@ -594,11 +593,9 @@ public async Task When_Send_Given_InterceptorsInDI_Then_InterceptorInfluenceIfTh _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable>)), Times.Once); - _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); - _serviceProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); - _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageSerializer)), Times.Between(0, 1, Moq.Range.Inclusive)); - _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); - _serviceProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + + VerifyCommonServiceProvider(); + _serviceProviderMock.VerifyNoOtherCalls(); if (producerInterceptorCallsNext != null) @@ -618,6 +615,17 @@ public async Task When_Send_Given_InterceptorsInDI_Then_InterceptorInfluenceIfTh sendInterceptorMock.VerifyNoOtherCalls(); } + private void VerifyCommonServiceProvider() + { + _serviceProviderMock.Verify(x => x.GetService(typeof(IEnumerable)), Times.Between(0, 2, Moq.Range.Inclusive)); + _serviceProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); + _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageSerializer)), Times.Between(0, 1, Moq.Range.Inclusive)); + _serviceProviderMock.Verify(x => x.GetService(typeof(IMessageTypeResolver)), Times.Once); + _serviceProviderMock.Verify(x => x.GetService(typeof(ICurrentTimeProvider)), Times.Once); + _serviceProviderMock.Verify(x => x.GetService(typeof(RuntimeTypeCache)), Times.Once); + _serviceProviderMock.Verify(x => x.GetService(typeof(IPendingRequestManager)), Times.Once); + } + [Fact] public async Task When_Start_Given_ConcurrentCalls_Then_ItOnlyStartsConsumersOnce() { @@ -676,13 +684,17 @@ public async Task When_Given_NoReplyToHeader_DoNothing() object value; var mockRequestHeaders = new Mock>(); - mockRequestHeaders.Setup(x => x.TryGetValue(ReqRespMessageHeaders.ReplyTo, out value)).Returns(false).Verifiable(Times.Once); + mockRequestHeaders + .Setup(x => x.TryGetValue(ReqRespMessageHeaders.ReplyTo, out value)) + .Returns(false).Verifiable(Times.Once); var mockMessageTypeResolver = new Mock(); var mockServiceProvider = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IMessageTypeResolver))).Returns(mockMessageTypeResolver.Object); mockServiceProvider.Setup(x => x.GetService(typeof(ICurrentTimeProvider))).Returns(new CurrentTimeProvider()); + mockServiceProvider.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); + mockServiceProvider.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); var mockMessageTypeConsumerInvokerSettings = new Mock(); mockMessageTypeConsumerInvokerSettings.SetupGet(x => x.ParentSettings).Returns(() => new ConsumerSettings() { ResponseType = response.GetType() }); @@ -690,20 +702,11 @@ public async Task When_Given_NoReplyToHeader_DoNothing() var settings = new MessageBusSettings { ServiceProvider = mockServiceProvider.Object }; var mockMessageBus = new Mock(settings) { CallBase = true }; - mockMessageBus.Protected().Setup>>( - "ProduceToTransportBulk", - [typeof(BulkMessageEnvelope)], - false, - ItExpr.IsAny>(), - ItExpr.IsAny(), - ItExpr.IsAny(), - ItExpr.IsAny()) - .Verifiable(Times.Never); var target = mockMessageBus.Object; // act - await target.ProduceResponse(requestId, request, mockRequestHeaders.Object, response, null, mockMessageTypeConsumerInvokerSettings.Object); + await target.ProduceResponse(requestId, request, mockRequestHeaders.Object, response, null, mockMessageTypeConsumerInvokerSettings.Object, It.IsAny()); // assert mockRequestHeaders.VerifyAll(); diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs index e2696828..20f63294 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs @@ -1,5 +1,5 @@ -namespace SlimMessageBus.Host.Test; - +namespace SlimMessageBus.Host.Test; + public class MessageBusTested : MessageBusBase { internal int _startedCount; @@ -35,48 +35,31 @@ protected internal override Task OnStop() Interlocked.Increment(ref _stoppedCount); return base.OnStop(); } - - protected override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) - { - await EnsureInitFinished(); - - var dispatched = new List(envelopes.Count); - try - { - foreach (var envelope in envelopes) - { - var messageType = envelope.Message.GetType(); - - OnProduced(messageType, path, envelope.Message); - - if (messageType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IRequest<>))) - { - var messagePayload = Serializer.Serialize(envelope.MessageType, envelope.Message); - var req = Serializer.Deserialize(messageType, messagePayload); - - var resp = OnReply(messageType, path, req); - if (resp == null) - { - continue; - } - - envelope.Headers.TryGetHeader(ReqRespMessageHeaders.ReplyTo, out string replyTo); - envelope.Headers.TryGetHeader(ReqRespMessageHeaders.RequestId, out string requestId); - - var responseHeaders = CreateHeaders(); - responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); - - var responsePayload = Serializer.Serialize(resp.GetType(), resp); - await OnResponseArrived(responsePayload, replyTo, (IReadOnlyDictionary)responseHeaders); - } - } - } - catch (Exception ex) - { - return new(dispatched, ex); - } - - return new(dispatched, null); + + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + OnProduced(messageType, path, message); + + if (messageType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IRequest<>))) + { + var messagePayload = Serializer.Serialize(messageType, message); + var req = Serializer.Deserialize(messageType, messagePayload); + + var resp = OnReply(messageType, path, req); + if (resp == null) + { + return; + } + + messageHeaders.TryGetHeader(ReqRespMessageHeaders.ReplyTo, out string replyTo); + messageHeaders.TryGetHeader(ReqRespMessageHeaders.RequestId, out string requestId); + + var responseHeaders = CreateHeaders(); + responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); + + var responsePayload = Serializer.Serialize(resp.GetType(), resp); + await OnResponseArrived(responsePayload, replyTo, (IReadOnlyDictionary)responseHeaders); + } } #endregion diff --git a/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestManagerTest.cs b/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestManagerTest.cs index 0510fb8f..1442469e 100644 --- a/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestManagerTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestManagerTest.cs @@ -1,7 +1,5 @@ namespace SlimMessageBus.Host.Test; -using Microsoft.Extensions.Logging.Abstractions; - public class PendingRequestManagerTest : IDisposable { private readonly PendingRequestManager _subject; @@ -19,8 +17,11 @@ public PendingRequestManagerTest() _store = new Mock(); _timeoutCallback = new Mock>(); + + var currentTimeProviderMock = new Mock(); + currentTimeProviderMock.Setup(x => x.CurrentTime).Returns(() => _timeNow); - _subject = new PendingRequestManager(_store.Object, () => _timeNow, _cleanInterval, NullLoggerFactory.Instance, _timeoutCallback.Object); + _subject = new PendingRequestManager(_store.Object, currentTimeProviderMock.Object, NullLoggerFactory.Instance, _cleanInterval, _timeoutCallback.Object); } [Fact] From 4cbf73f8fc696f31ef69b31e69a3288b88ba9a72 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sat, 9 Nov 2024 23:58:59 +0100 Subject: [PATCH 05/21] Amazon.SQS transport Signed-off-by: Tomasz Maruszak --- .github/workflows/build.yml | 6 +- README.md | 8 +- build/tasks.ps1 | 3 +- docs/NuGet.md | 1 + docs/README.md | 9 +- docs/intro.md | 20 +- docs/intro.t.md | 18 + docs/provider_amazon_sqs.md | 286 +++++++++++++++ docs/provider_amazon_sqs.t.md | 270 +++++++++++++++ docs/provider_memory.md | 2 +- docs/provider_memory.t.md | 2 +- .../Sample.Simple.ConsoleApp/Program.cs | 28 +- .../Sample.Simple.ConsoleApp.csproj | 1 + .../Sample.Simple.ConsoleApp/appsettings.json | 4 + .../ClientFactory/ISqsClientProvider.cs | 9 + .../StaticCredentialsSqsClientProvider.cs | 45 +++ .../TemporaryCredentialsSqsClientProvider.cs | 102 ++++++ .../Config/Delegates.cs | 19 + .../Config/SqsConsumerBuilderExtensions.cs | 69 ++++ .../Config/SqsProducerBuilderExtensions.cs | 130 +++++++ .../Config/SqsProducerFifoBuilder.cs | 34 ++ .../Config/SqsProperties.cs | 18 + .../SqsRequestResponseBuilderExtensions.cs | 17 + .../Consumer/ISqsConsumerErrorHandler.cs | 5 + .../Consumer/SqsBaseConsumer.cs | 145 ++++++++ .../Consumer/SqsConsumerContextExtensions.cs | 20 ++ .../Consumer/SqsQueueConsumer.cs | 16 + .../GlobalUsings.cs | 10 + .../Headers/DefaultSqsHeaderSerializer.cs | 32 ++ .../Headers/ISqsHeaderSerializer.cs | 7 + .../MessageBusBuilderExtensions.cs | 20 ++ .../SlimMessageBus.Host.AmazonSQS.csproj | 23 ++ .../SqsMessageBus.cs | 238 +++++++++++++ .../SqsMessageBusSettings.cs | 67 ++++ .../SqsTopologyService.cs | 160 +++++++++ .../SqsTopologySettings.cs | 34 ++ .../EhPartitionConsumerForConsumers.cs | 7 +- .../EhPartitionConsumerForResponses.cs | 10 +- .../EventHubMessageBus.cs | 6 +- .../SlimMessageBus.Host.AzureEventHub.csproj | 5 +- .../ServiceBusConsumerContextExtensions.cs | 4 +- .../ServiceBusMessageBus.cs | 10 +- .../ServiceBusTopologyService.cs | 2 +- ...SlimMessageBus.Host.AzureServiceBus.csproj | 8 +- .../Settings/HasProviderExtensions.cs | 16 +- .../Settings/ProviderExtensionProperty.cs | 9 + .../KafkaPartitionConsumerForResponses.cs | 16 +- .../KafkaMessageBus.cs | 9 +- .../MqttMessageBus.cs | 5 +- .../NatsMessageBus.cs | 8 +- .../Services/OutboxSendingTask.cs | 3 +- .../Consumers/RabbitMqConsumer.cs | 12 +- .../Consumers/RabbitMqResponseConsumer.cs | 12 +- .../RabbitMqMessageBus.cs | 9 +- .../RedisMessageBus.cs | 4 +- .../JsonMessageSerializer.cs | 32 +- .../SerializationBuilderExtensions.cs | 3 + .../JsonMessageSerializer.cs | 18 +- .../SerializationBuilderExtensions.cs | 3 + .../IMessageSerializer.cs | 17 +- src/SlimMessageBus.Host.Sql/SqlMessageBus.cs | 2 +- .../Collections/AsyncTaskList.cs | 58 ++++ .../Consumer/MessageProcessors/Delegates.cs | 3 +- .../MessageProcessors/IResponseConsumer.cs | 6 - .../ResponseMessageProcessor.cs | 109 +++++- .../Helpers/CompatMethods.cs | 29 +- src/SlimMessageBus.Host/MessageBusBase.cs | 143 ++------ .../RequestResponse/PendingRequestManager.cs | 8 +- src/SlimMessageBus.sln | 23 ++ .../GlobalUsings.cs | 12 + .../Headers/DefaultSqsHeaderSerializerTest.cs | 32 ++ .../SlimMessageBus.Host.AmazonSQS.Test.csproj | 21 ++ .../SqsMessageBusIt.cs | 327 ++++++++++++++++++ .../appsettings.json | 15 + .../ServiceBusMessageBusIt.cs | 4 +- .../KafkaPartitionConsumerForResponsesTest.cs | 36 +- .../JsonMessageSerializerTests.cs | 21 +- .../SerializationBuilderExtensionsTest.cs | 1 + .../JsonMessageSerializerTests.cs | 18 +- .../SerializationBuilderExtensionsTest.cs | 1 + .../IntegrationTest/BaseIntegrationTest.cs | 5 +- .../Collections/AsyncTaskListTests.cs | 36 ++ .../MessageBusTested.cs | 29 +- 83 files changed, 2790 insertions(+), 255 deletions(-) create mode 100644 docs/provider_amazon_sqs.md create mode 100644 docs/provider_amazon_sqs.t.md create mode 100644 src/SlimMessageBus.Host.AmazonSQS/ClientFactory/ISqsClientProvider.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/ClientFactory/StaticCredentialsSqsClientProvider.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/ClientFactory/TemporaryCredentialsSqsClientProvider.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Config/Delegates.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Config/SqsConsumerBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerFifoBuilder.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Config/SqsProperties.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Config/SqsRequestResponseBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsConsumerContextExtensions.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsQueueConsumer.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Headers/DefaultSqsHeaderSerializer.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/Headers/ISqsHeaderSerializer.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/MessageBusBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj create mode 100644 src/SlimMessageBus.Host.AmazonSQS/SqsMessageBus.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/SqsMessageBusSettings.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/SqsTopologyService.cs create mode 100644 src/SlimMessageBus.Host.AmazonSQS/SqsTopologySettings.cs create mode 100644 src/SlimMessageBus.Host.Configuration/Settings/ProviderExtensionProperty.cs create mode 100644 src/SlimMessageBus.Host/Collections/AsyncTaskList.cs delete mode 100644 src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseConsumer.cs create mode 100644 src/Tests/SlimMessageBus.Host.AmazonSQS.Test/GlobalUsings.cs create mode 100644 src/Tests/SlimMessageBus.Host.AmazonSQS.Test/Headers/DefaultSqsHeaderSerializerTest.cs create mode 100644 src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SlimMessageBus.Host.AmazonSQS.Test.csproj create mode 100644 src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SqsMessageBusIt.cs create mode 100644 src/Tests/SlimMessageBus.Host.AmazonSQS.Test/appsettings.json create mode 100644 src/Tests/SlimMessageBus.Host.Test/Collections/AsyncTaskListTests.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 424ad509..a4502db1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,7 +102,7 @@ jobs: --verbosity normal \ --logger "trx;LogFilePrefix=Integration" \ --collect:"XPlat Code Coverage;Format=opencover" \ - --filter "Category=Integration&Transport=Outbox" + --filter "Category=Integration" working-directory: ./src env: # Connects to the Azure cloud @@ -111,6 +111,10 @@ jobs: azure_eventhub_connectionstring: ${{ secrets.azure_eventhub_connectionstring }} azure_storagecontainer_connectionstring: ${{ secrets.azure_storagecontainer_connectionstring }} + # Connects to AWS cloud + amazon_access_key: ${{ secrets.amazon_access_key }} + amazon_secret_access_key: ${{ secrets.amazon_secret_access_key }} + _kafka_brokers: ${{ secrets.kafka_brokers }} _kafka_username: ${{ secrets.kafka_username }} _kafka_password: ${{ secrets.kafka_password }} diff --git a/README.md b/README.md index 0afa8dac..a5d018f5 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zarusz_SlimMessageBus&metric=vulnerabilities)](https://sonarcloud.io/summary/overall?id=zarusz_SlimMessageBus) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zarusz_SlimMessageBus&metric=alert_status)](https://sonarcloud.io/summary/overall?id=zarusz_SlimMessageBus) -> The v2 release is available (see [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/Host.Transport-2.0.0)). > The v3 release is [under construction](https://github.com/zarusz/SlimMessageBus/tree/release/v3). +> The v2 release is available (see [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/Host.Transport-2.0.0)). - [Key elements of SlimMessageBus](#key-elements-of-slimmessagebus) - [Docs](#docs) @@ -47,6 +47,7 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i - [Introduction](docs/intro.md) - Providers: + - [Amazon SQS/SNS](docs/provider_amazon_sqs.md) - [Apache Kafka](docs/provider_kafka.md) - [Azure EventHubs](docs/provider_azure_eventhubs.md) - [Azure ServiceBus](docs/provider_azure_servicebus.md) @@ -69,11 +70,12 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SlimMessageBus` | The core API for SlimMessageBus | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.svg)](https://www.nuget.org/packages/SlimMessageBus) | | **Transport providers** | | | +| `.Host.AmazonSQS` | Transport provider for Amazon SQS / SNS | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AmazonSQS.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AmazonSQS) | | `.Host.AzureEventHub` | Transport provider for Azure Event Hubs | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AzureEventHub.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AzureEventHub) | | `.Host.AzureServiceBus` | Transport provider for Azure Service Bus | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AzureServiceBus.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AzureServiceBus) | | `.Host.Kafka` | Transport provider for Apache Kafka | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Kafka.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Kafka) | -| `.Host.Memory` | Transport provider implementation for in-process (in memory) message passing (no messaging infrastructure required) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Memory.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Memory) | | `.Host.MQTT` | Transport provider for MQTT | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.MQTT.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.MQTT) | +| `.Host.Memory` | Transport provider implementation for in-process (in memory) message passing (no messaging infrastructure required) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Memory.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Memory) | | `.Host.NATS` | Transport provider for [NATS](https://nats.io/) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.NATS.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.NATS) | | `.Host.RabbitMQ` | Transport provider for RabbitMQ | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.RabbitMQ.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.RabbitMQ) | | `.Host.Redis` | Transport provider for Redis | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Redis.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Redis) | @@ -186,7 +188,7 @@ services.AddSlimMessageBus(mbb => // Scan assembly for consumers, handlers, interceptors, and register into MSDI .AddServicesFromAssemblyContaining() - //.AddServicesFromAssembly(Assembly.GetExecutingAssembly()); + //.AddServicesFromAssembly(Assembly.GetExecutingAssembly()) // Add JSON serializer .AddJsonSerializer(); // requires SlimMessageBus.Host.Serialization.Json or SlimMessageBus.Host.Serialization.SystemTextJson package diff --git a/build/tasks.ps1 b/build/tasks.ps1 index 8eb62a4c..09afe10c 100644 --- a/build/tasks.ps1 +++ b/build/tasks.ps1 @@ -35,7 +35,8 @@ $projects = @( "SlimMessageBus.Host.Sql", "SlimMessageBus.Host.Sql.Common", "SlimMessageBus.Host.Nats", - + "SlimMessageBus.Host.AmazonSQS", + "SlimMessageBus.Host.FluentValidation", "SlimMessageBus.Host.Outbox", diff --git a/docs/NuGet.md b/docs/NuGet.md index df5f1c9f..23b474ef 100644 --- a/docs/NuGet.md +++ b/docs/NuGet.md @@ -6,6 +6,7 @@ SlimMessageBus additionally provides request-response implementation over messag Transports: +- Amazon SQS/SNS - Apache Kafka - Azure Event Hub - Azure Service Bus diff --git a/docs/README.md b/docs/README.md index dbbeca35..088bf118 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,14 +2,15 @@ - [Introduction](intro.md) - Providers + - [Amazon SQS/SNS](provider_amazon_sqs.md) - [Apache Kafka](provider_kafka.md) - - [Azure Event Hubs](provider_azure_eventhubs.md) - - [Azure Service Bus](provider_azure_servicebus.md) + - [Azure EventHubs](provider_azure_eventhubs.md) + - [Azure ServiceBus](provider_azure_servicebus.md) - [Hybrid](provider_hybrid.md) - [MQTT](provider_mqtt.md) - [Memory](provider_memory.md) - - [RabbitMq](provider_rabbitmq.md) + - [NATS](provider_nats.md) + - [RabbitMQ](provider_rabbitmq.md) - [Redis](provider_redis.md) - [SQL](provider_sql.md) - - [NATS](provider_nats.md) - [Serialization Plugins](serialization.md) diff --git a/docs/intro.md b/docs/intro.md index e415e8e6..909c2f5a 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -41,6 +41,8 @@ - [Logging](#logging) - [Debugging](#debugging) - [Provider specific functionality](#provider-specific-functionality) +- [Topology Provisioning](#topology-provisioning) + - [Triggering Topology Provisioning](#triggering-topology-provisioning) ## Configuration @@ -1142,4 +1144,20 @@ Providers: - [Memory](provider_memory.md) - [RabbitMQ](provider_rabbitmq.md) - [Redis](provider_redis.md) -- [SQL](provider_sql.md) \ No newline at end of file +- [SQL](provider_sql.md) + +## Topology Provisioning + +Most of the transport providers support the automatic creation of the needed messaging topology (queues, topics, subscriptions, etc). + +### Triggering Topology Provisioning + +Topology provisioning occurs when the bus is first created (e.g., at application startup). If the underlying topology changes (e.g., queues or topics are manually deleted), you may need to re-trigger provisioning programmatically: + +```csharp +ITopologyControl ctrl = // injected + +await ctrl.ProvisionTopology(); +``` + +This allows to recreate missing elements in the infrastructure without restarting the whole application. \ No newline at end of file diff --git a/docs/intro.t.md b/docs/intro.t.md index 31a7bf0c..f5512cba 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -41,6 +41,8 @@ - [Logging](#logging) - [Debugging](#debugging) - [Provider specific functionality](#provider-specific-functionality) +- [Topology Provisioning](#topology-provisioning) + - [Triggering Topology Provisioning](#triggering-topology-provisioning) ## Configuration @@ -1129,3 +1131,19 @@ Providers: - [RabbitMQ](provider_rabbitmq.md) - [Redis](provider_redis.md) - [SQL](provider_sql.md) + +## Topology Provisioning + +Most of the transport providers support the automatic creation of the needed messaging topology (queues, topics, subscriptions, etc). + +### Triggering Topology Provisioning + +Topology provisioning occurs when the bus is first created (e.g., at application startup). If the underlying topology changes (e.g., queues or topics are manually deleted), you may need to re-trigger provisioning programmatically: + +```csharp +ITopologyControl ctrl = // injected + +await ctrl.ProvisionTopology(); +``` + +This allows to recreate missing elements in the infrastructure without restarting the whole application. diff --git a/docs/provider_amazon_sqs.md b/docs/provider_amazon_sqs.md new file mode 100644 index 00000000..205f1a6a --- /dev/null +++ b/docs/provider_amazon_sqs.md @@ -0,0 +1,286 @@ +# Amazon SQS Provider for SlimMessageBus + +Before diving into this provider documentation, please make sure to read the [Introduction](intro.md). + +### Table of Contents + +- [Configuration](#configuration) +- [Amazon SNS](#amazon-sns) +- [Producing Messages](#producing-messages) +- [Consuming Messages](#consuming-messages) + - [Consumer Context](#consumer-context) +- [Transport-Specific Settings](#transport-specific-settings) +- [Headers](#headers) +- [Request-Response Configuration](#request-response-configuration) + - [Producing Request Messages](#producing-request-messages) + - [Handling Request Messages](#handling-request-messages) +- [Topology Provisioning](#topology-provisioning) + +## Configuration + +To configure Amazon SQS as your transport provider, you need to specify the AWS region and choose an authentication method: + +- **Static Credentials**: [Learn more](https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html) +- **Temporary Credentials**: [Learn more](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS) + +```csharp +using SlimMessageBus.Host.AmazonSQS; +``` + +```cs +services.AddSlimMessageBus((mbb) => +{ + mbb.WithProviderAmazonSQS(cfg => + { + cfg.UseRegion(Amazon.RegionEndpoint.EUCentral1); + + // Use static credentials: https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html + cfg.UseCredentials(accessKey, secretAccessKey); + + // Use temporary credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS + //cfg.UseTemporaryCredentials(roleArn, roleSessionName); + + AdditionalSqsSetup(cfg); + }); +}); +``` + +For an example configuration, check out this file: [`SqsMessageBusSettings`](../src/SlimMessageBus.Host.AmazonSQS/SqsMessageBusSettings.cs). The settings allow you to customize the SQS client object and control topology provisioning for advanced scenarios. + +## Amazon SNS + +Support for Amazon SNS (Simple Notification Service) will be added soon to this transport plugin. + +## Producing Messages + +To produce a `TMessage` to an Amazon SQS queue or an SNS topic: + +```csharp +// Send TMessage to Amazon SQS queue +mbb.Produce(x => x.UseQueue()); + +// OR + +// Send TMessage to Amazon SNS topic +mbb.Produce(x => x.UseTopic()); +``` + +This configuration sends `TMessage` to the specified Amazon SQS queue or SNS topic. You can then produce messages like this: + +```csharp +TMessage msg; + +// Send msg to "some-queue" +await bus.Publish(msg, "some-queue"); + +// OR + +// Send msg to "some-topic" +await bus.Publish(msg, "some-topic"); +``` + +The second parameter (`path`) specifies the name of the queue or topic to use. + +If you have a default queue or topic configured for a message type: + +```csharp +mbb.Produce(x => x.DefaultQueue("some-queue")); + +// OR + +mbb.Produce(x => x.DefaultTopic("some-topic")); +``` + +You can simply call `bus.Publish(msg)` without providing the second parameter: + +```csharp +// Send msg to the default queue "some-queue" (or "some-topic") +bus.Publish(msg); +``` + +Note that if no explicit configuration is provided, the system assumes the message will be sent to a topic (equivalent to using `UseTopic()`). + +## Consuming Messages + +To consume messages of type `TMessage` by `TConsumer` from an Amazon SNS topic named `some-topic`: + +```csharp +mbb.Consume(x => x + .Queue("some-topic") + //.WithConsumer()); +``` + +To consume messages from an Amazon SQS queue named `some-queue`: + +```csharp +mbb.Consume(x => x + .Queue("some-queue") + //.WithConsumer()); +``` + +### Consumer Context + +The consumer can implement the `IConsumerWithContext` interface to access native Amazon SQS messages: + +```csharp +public class PingConsumer : IConsumer, IConsumerWithContext +{ + public IConsumerContext Context { get; set; } + + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) + { + // Access the native Amazon SQS message: + var transportMessage = Context.GetTransportMessage(); // Amazon.SQS.Model.Message type + } +} +``` + +This can be helpful to extract properties like `MessageId` or `Attributes` from the native SQS message. + +## Transport-Specific Settings + +Producer and consumer configurations have additional settings like: + +- **EnableFifo** +- **MaxMessageCount** + +For a producer: + +```csharp +mbb.Produce(x => x + .EnableFifo(f => f + .DeduplicationId((m, h) => (m.Counter + 1000).ToString()) + .GroupId((m, h) => m.Counter % 2 == 0 ? "even" : "odd") + ) +); +``` + +For a consumer: + +```csharp +mbb.Consume(x => x + .WithConsumer() + .Queue("some-queue") + .MaxMessageCount(10) + .Instances(1)); +``` + +These default values can also be set at the message bus level using `SqsMessageBusSettings`: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.MaxMessageCount = 10; +}); +``` + +Settings at the consumer level take priority over the global defaults. + +## Headers + +Amazon SQS has specific requirements for message headers, detailed in [this guide](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes). + +Headers in SlimMessageBus (SMB) can be any type (`object`). To convert them, SMB uses a header serializer, like the default implementation: [DefaultSqsHeaderSerializer](../src/SlimMessageBus.Host.AmazonSQS/Headers/DefaultSqsHeaderSerializer.cs). + +By default, string values are attempted to be converted to `Guid`, `bool`, and `DateTime`. + +You can override the serializer through `SqsMessageBusSettings`. + +## Request-Response Configuration + +### Producing Request Messages + +When sending a request, you need to specify whether it should be delivered to a topic or queue (see [Producing Messages](#producing-messages)). + +To receive responses on a queue: + +```csharp +mbb.ExpectRequestResponses(x => +{ + x.ReplyToQueue("test-echo-queue-resp"); + x.DefaultTimeout(TimeSpan.FromSeconds(60)); +}); +``` + +Or to receive responses on a topic: + +```csharp +mbb.ExpectRequestResponses(x => +{ + x.ReplyToTopic("test-echo-resp"); + x.DefaultTimeout(TimeSpan.FromSeconds(60)); +}); +``` + +Each service instance should have a dedicated queue or topic to ensure the response arrives back at the correct instance. This ensures the internal task `Task` of `bus.Send(TRequest)` resumes correctly. + +### Handling Request Messages + +For services handling requests, you must configure the request consumption settings for a specific queue (or topic): + +Example for queue: + +```csharp +mbb.Handle(x => x + .Queue(queue) + .WithHandler()); +``` + +Example for topic: + +```csharp +mbb.Handle(x => x + .Topic(topic) + .SubscriptionName("handler") + .WithHandler()); +``` + +Ensure that if a request is sent to a queue, the consumer is also listening on that queue (and similarly for topics). Mixing queues and topics for requests and responses is not supported. + +## Topology Provisioning + +Amazon SQS can automatically create any queues declared in your SMB configuration when needed. This process occurs when the SMB instance starts, and only for queues that do not yet exist. If a queue already exists, it will not be modified. + +> **Note**: Automatic topology creation is enabled by default. + +To disable automatic topology provisioning: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.TopologyProvisioning.Enabled = false; +}); +``` + +You can also customize how queues are created by modifying the `CreateQueueOptions`: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.TopologyProvisioning = new() + { + CreateQueueOptions = (options) => + { + // Customize queue options here + } + }; +}); +``` + +You can control which services create queues: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.TopologyProvisioning = new() + { + Enabled = true, + CanProducerCreateQueue = true, // Only producers can create queues + CanConsumerCreateQueue = false, // Consumers cannot create queues + }; +}); +``` + +> By default, both flags are enabled (`true`). + +This flexibility allows you to define ownership of queues/topics clearly—e.g., producers handle queue creation while consumers manage subscriptions. \ No newline at end of file diff --git a/docs/provider_amazon_sqs.t.md b/docs/provider_amazon_sqs.t.md new file mode 100644 index 00000000..d440caa5 --- /dev/null +++ b/docs/provider_amazon_sqs.t.md @@ -0,0 +1,270 @@ +# Amazon SQS Provider for SlimMessageBus + +Before diving into this provider documentation, please make sure to read the [Introduction](intro.md). + +### Table of Contents + +- [Configuration](#configuration) +- [Amazon SNS](#amazon-sns) +- [Producing Messages](#producing-messages) +- [Consuming Messages](#consuming-messages) + - [Consumer Context](#consumer-context) +- [Transport-Specific Settings](#transport-specific-settings) +- [Headers](#headers) +- [Request-Response Configuration](#request-response-configuration) + - [Producing Request Messages](#producing-request-messages) + - [Handling Request Messages](#handling-request-messages) +- [Topology Provisioning](#topology-provisioning) + +## Configuration + +To configure Amazon SQS as your transport provider, you need to specify the AWS region and choose an authentication method: + +- **Static Credentials**: [Learn more](https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html) +- **Temporary Credentials**: [Learn more](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS) + +```csharp +using SlimMessageBus.Host.AmazonSQS; +``` + +@[:cs](../src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SqsMessageBusIt.cs,ExampleSetup) + +For an example configuration, check out this file: [`SqsMessageBusSettings`](../src/SlimMessageBus.Host.AmazonSQS/SqsMessageBusSettings.cs). The settings allow you to customize the SQS client object and control topology provisioning for advanced scenarios. + +## Amazon SNS + +Support for Amazon SNS (Simple Notification Service) will be added soon to this transport plugin. + +## Producing Messages + +To produce a `TMessage` to an Amazon SQS queue or an SNS topic: + +```csharp +// Send TMessage to Amazon SQS queue +mbb.Produce(x => x.UseQueue()); + +// OR + +// Send TMessage to Amazon SNS topic +mbb.Produce(x => x.UseTopic()); +``` + +This configuration sends `TMessage` to the specified Amazon SQS queue or SNS topic. You can then produce messages like this: + +```csharp +TMessage msg; + +// Send msg to "some-queue" +await bus.Publish(msg, "some-queue"); + +// OR + +// Send msg to "some-topic" +await bus.Publish(msg, "some-topic"); +``` + +The second parameter (`path`) specifies the name of the queue or topic to use. + +If you have a default queue or topic configured for a message type: + +```csharp +mbb.Produce(x => x.DefaultQueue("some-queue")); + +// OR + +mbb.Produce(x => x.DefaultTopic("some-topic")); +``` + +You can simply call `bus.Publish(msg)` without providing the second parameter: + +```csharp +// Send msg to the default queue "some-queue" (or "some-topic") +bus.Publish(msg); +``` + +Note that if no explicit configuration is provided, the system assumes the message will be sent to a topic (equivalent to using `UseTopic()`). + +## Consuming Messages + +To consume messages of type `TMessage` by `TConsumer` from an Amazon SNS topic named `some-topic`: + +```csharp +mbb.Consume(x => x + .Queue("some-topic") + //.WithConsumer()); +``` + +To consume messages from an Amazon SQS queue named `some-queue`: + +```csharp +mbb.Consume(x => x + .Queue("some-queue") + //.WithConsumer()); +``` + +### Consumer Context + +The consumer can implement the `IConsumerWithContext` interface to access native Amazon SQS messages: + +```csharp +public class PingConsumer : IConsumer, IConsumerWithContext +{ + public IConsumerContext Context { get; set; } + + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) + { + // Access the native Amazon SQS message: + var transportMessage = Context.GetTransportMessage(); // Amazon.SQS.Model.Message type + } +} +``` + +This can be helpful to extract properties like `MessageId` or `Attributes` from the native SQS message. + +## Transport-Specific Settings + +Producer and consumer configurations have additional settings like: + +- **EnableFifo** +- **MaxMessageCount** + +For a producer: + +```csharp +mbb.Produce(x => x + .EnableFifo(f => f + .DeduplicationId((m, h) => (m.Counter + 1000).ToString()) + .GroupId((m, h) => m.Counter % 2 == 0 ? "even" : "odd") + ) +); +``` + +For a consumer: + +```csharp +mbb.Consume(x => x + .WithConsumer() + .Queue("some-queue") + .MaxMessageCount(10) + .Instances(1)); +``` + +These default values can also be set at the message bus level using `SqsMessageBusSettings`: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.MaxMessageCount = 10; +}); +``` + +Settings at the consumer level take priority over the global defaults. + +## Headers + +Amazon SQS has specific requirements for message headers, detailed in [this guide](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes). + +Headers in SlimMessageBus (SMB) can be any type (`object`). To convert them, SMB uses a header serializer, like the default implementation: [DefaultSqsHeaderSerializer](../src/SlimMessageBus.Host.AmazonSQS/Headers/DefaultSqsHeaderSerializer.cs). + +By default, string values are attempted to be converted to `Guid`, `bool`, and `DateTime`. + +You can override the serializer through `SqsMessageBusSettings`. + +## Request-Response Configuration + +### Producing Request Messages + +When sending a request, you need to specify whether it should be delivered to a topic or queue (see [Producing Messages](#producing-messages)). + +To receive responses on a queue: + +```csharp +mbb.ExpectRequestResponses(x => +{ + x.ReplyToQueue("test-echo-queue-resp"); + x.DefaultTimeout(TimeSpan.FromSeconds(60)); +}); +``` + +Or to receive responses on a topic: + +```csharp +mbb.ExpectRequestResponses(x => +{ + x.ReplyToTopic("test-echo-resp"); + x.DefaultTimeout(TimeSpan.FromSeconds(60)); +}); +``` + +Each service instance should have a dedicated queue or topic to ensure the response arrives back at the correct instance. This ensures the internal task `Task` of `bus.Send(TRequest)` resumes correctly. + +### Handling Request Messages + +For services handling requests, you must configure the request consumption settings for a specific queue (or topic): + +Example for queue: + +```csharp +mbb.Handle(x => x + .Queue(queue) + .WithHandler()); +``` + +Example for topic: + +```csharp +mbb.Handle(x => x + .Topic(topic) + .SubscriptionName("handler") + .WithHandler()); +``` + +Ensure that if a request is sent to a queue, the consumer is also listening on that queue (and similarly for topics). Mixing queues and topics for requests and responses is not supported. + +## Topology Provisioning + +Amazon SQS can automatically create any queues declared in your SMB configuration when needed. This process occurs when the SMB instance starts, and only for queues that do not yet exist. If a queue already exists, it will not be modified. + +> **Note**: Automatic topology creation is enabled by default. + +To disable automatic topology provisioning: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.TopologyProvisioning.Enabled = false; +}); +``` + +You can also customize how queues are created by modifying the `CreateQueueOptions`: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.TopologyProvisioning = new() + { + CreateQueueOptions = (options) => + { + // Customize queue options here + } + }; +}); +``` + +You can control which services create queues: + +```csharp +mbb.WithProviderAmazonSQS(cfg => +{ + cfg.TopologyProvisioning = new() + { + Enabled = true, + CanProducerCreateQueue = true, // Only producers can create queues + CanConsumerCreateQueue = false, // Consumers cannot create queues + }; +}); +``` + +> By default, both flags are enabled (`true`). + +This flexibility allows you to define ownership of queues/topics clearly—e.g., producers handle queue creation while consumers manage subscriptions. diff --git a/docs/provider_memory.md b/docs/provider_memory.md index ac9777c8..df12b4df 100644 --- a/docs/provider_memory.md +++ b/docs/provider_memory.md @@ -203,7 +203,7 @@ services.AddSlimMessageBus(mbb => When the `.Publish()` is invoked in the non-blocking mode: -- the consumers will be executed in another async task (in the background), +- the consumers will be executed in another async task (in thMemoryMessageBusIte background), - that task cancellation token will be bound to the message bus lifecycle (consumers are stopped, the bus is disposed or application shuts down), - the order of message delivery to consumer will match the order of publish, - however, when number of concurrent consumer instances > 0 (`.Instances(N)`) then up to N messages will be processed concurrently (having impact on ordering) diff --git a/docs/provider_memory.t.md b/docs/provider_memory.t.md index 347d678e..e557b61a 100644 --- a/docs/provider_memory.t.md +++ b/docs/provider_memory.t.md @@ -191,7 +191,7 @@ To use non-blocking publish use the `EnableBlockingPublish` property setting: When the `.Publish()` is invoked in the non-blocking mode: -- the consumers will be executed in another async task (in the background), +- the consumers will be executed in another async task (in thMemoryMessageBusIte background), - that task cancellation token will be bound to the message bus lifecycle (consumers are stopped, the bus is disposed or application shuts down), - the order of message delivery to consumer will match the order of publish, - however, when number of concurrent consumer instances > 0 (`.Instances(N)`) then up to N messages will be processed concurrently (having impact on ordering) diff --git a/src/Samples/Sample.Simple.ConsoleApp/Program.cs b/src/Samples/Sample.Simple.ConsoleApp/Program.cs index 08552f5f..2f571e5f 100644 --- a/src/Samples/Sample.Simple.ConsoleApp/Program.cs +++ b/src/Samples/Sample.Simple.ConsoleApp/Program.cs @@ -14,6 +14,7 @@ using SlimMessageBus; using SlimMessageBus.Host; +using SlimMessageBus.Host.AmazonSQS; using SlimMessageBus.Host.AzureEventHub; using SlimMessageBus.Host.AzureServiceBus; using SlimMessageBus.Host.Kafka; @@ -28,6 +29,7 @@ enum Provider AzureEventHub, Redis, Memory, + AmazonSQS } /// @@ -166,10 +168,9 @@ private static void ConfigureMessageBus(MessageBusBuilder mbb, IConfiguration co //.WithConsumer(nameof(AddCommandConsumer.OnHandle)) //.WithConsumer((consumer, message, name) => consumer.OnHandle(message, name)) .KafkaGroup(consumerGroup) // for Apache Kafka - .EventHubGroup(consumerGroup) // for Azure Event Hub - // for Azure Service Bus - .SubscriptionName(consumerGroup) - .SubscriptionSqlFilter("2=2") + .EventHubGroup(consumerGroup) // for Azure Event Hub + .SubscriptionName(consumerGroup) // for Azure Service Bus + .SubscriptionSqlFilter("2=2") // for Azure Service Bus .CreateTopicOptions((options) => { options.RequiresDuplicateDetection = true; @@ -242,7 +243,7 @@ private static void ConfigureMessageBus(MessageBusBuilder mbb, IConfiguration co builder.WithProviderServiceBus(cfg => { cfg.ConnectionString = serviceBusConnectionString; - cfg.TopologyProvisioning = new ServiceBusTopologySettings() + cfg.TopologyProvisioning = new() { Enabled = true, CreateQueueOptions = (options) => @@ -319,14 +320,25 @@ void AddSsl(ClientConfig c) break; case Provider.Redis: - // Ensure your Kafka broker is running - var redisConnectionString = Secrets.Service.PopulateSecrets(configuration["Redis:ConnectionString"]); - + var redisConnectionString = Secrets.Service.PopulateSecrets(configuration["Redis:ConnectionString"]); + // Or use Redis as provider builder.WithProviderRedis(cfg => { cfg.ConnectionString = redisConnectionString; }); + break; + + case Provider.AmazonSQS: + var accessKey = Secrets.Service.PopulateSecrets(configuration["Amazon:AccessKey"]); + var secretAccess = Secrets.Service.PopulateSecrets(configuration["Amazon:SecretAccess"]); + + // Or use Amazon SQS as provider + builder.WithProviderAmazonSQS(cfg => + { + cfg.UseRegion(Amazon.RegionEndpoint.EUCentral1); + cfg.UseCredentials(accessKey, secretAccess); + }); break; } }); diff --git a/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj b/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj index bf1a5f0e..27646aa4 100644 --- a/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj +++ b/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Samples/Sample.Simple.ConsoleApp/appsettings.json b/src/Samples/Sample.Simple.ConsoleApp/appsettings.json index dcce6a12..da90185f 100644 --- a/src/Samples/Sample.Simple.ConsoleApp/appsettings.json +++ b/src/Samples/Sample.Simple.ConsoleApp/appsettings.json @@ -22,5 +22,9 @@ }, "Redis": { "ConnectionString": "{{redis_connectionstring}}" + }, + "Amazon": { + "AccessKey": "{{amazon_access_key}}", + "SecretAccessKey": "{{amazon_secret_access_key}}" } } diff --git a/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/ISqsClientProvider.cs b/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/ISqsClientProvider.cs new file mode 100644 index 00000000..73b4da01 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/ISqsClientProvider.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public interface ISqsClientProvider +{ + AmazonSQSClient Client { get; } + Task EnsureClientAuthenticated(); +} + + diff --git a/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/StaticCredentialsSqsClientProvider.cs b/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/StaticCredentialsSqsClientProvider.cs new file mode 100644 index 00000000..5aa259ce --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/StaticCredentialsSqsClientProvider.cs @@ -0,0 +1,45 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public class StaticCredentialsSqsClientProvider : ISqsClientProvider, IDisposable +{ + private bool _disposedValue; + + private readonly AmazonSQSClient _client; + + public StaticCredentialsSqsClientProvider(AmazonSQSConfig sqsConfig, AWSCredentials credentials) + => _client = new AmazonSQSClient(credentials, sqsConfig); + + #region ISqsClientProvider + + public AmazonSQSClient Client => _client; + + public Task EnsureClientAuthenticated() => Task.CompletedTask; + + #endregion + + #region Dispose Pattern + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _client?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion +} + + diff --git a/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/TemporaryCredentialsSqsClientProvider.cs b/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/TemporaryCredentialsSqsClientProvider.cs new file mode 100644 index 00000000..04890d90 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/ClientFactory/TemporaryCredentialsSqsClientProvider.cs @@ -0,0 +1,102 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +using Amazon.SecurityToken; +using Amazon.SecurityToken.Model; + +public class TemporaryCredentialsSqsClientProvider : ISqsClientProvider, IDisposable +{ + private bool _disposedValue; + + private readonly AmazonSQSConfig _sqsConfig; + private readonly string _roleArn; + private readonly string _roleSessionName; + + private readonly AmazonSecurityTokenServiceClient _stsClient; + private readonly Timer _timer; + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + + private AmazonSQSClient _client; + private DateTime _clientCredentialsExpiry; + + public TemporaryCredentialsSqsClientProvider(AmazonSQSConfig sqsConfig, string roleArn, string roleSessionName) + { + _stsClient = new AmazonSecurityTokenServiceClient(); + _sqsConfig = sqsConfig; + _roleArn = roleArn; + _roleSessionName = roleSessionName; + _timer = new Timer(state => _ = EnsureClientAuthenticated(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + } + + #region ISqsClientProvider + + public AmazonSQSClient Client => _client; + + public async Task EnsureClientAuthenticated() + { + if (_client == null || DateTime.UtcNow >= _clientCredentialsExpiry) + { + await _semaphoreSlim.WaitAsync(); + try + { + var oldClient = _client; + (_client, _clientCredentialsExpiry) = await RefreshCredentialsAsync(); + oldClient?.Dispose(); + } + finally + { + _semaphoreSlim.Release(); + } + } + } + + #endregion + + private async Task<(AmazonSQSClient Client, DateTime ClientExpiry)> RefreshCredentialsAsync() + { + var assumeRoleRequest = new AssumeRoleRequest + { + RoleArn = _roleArn, + RoleSessionName = _roleSessionName + }; + + var assumeRoleResponse = await _stsClient.AssumeRoleAsync(assumeRoleRequest); + + var temporaryCredentials = new SessionAWSCredentials( + assumeRoleResponse.Credentials.AccessKeyId, + assumeRoleResponse.Credentials.SecretAccessKey, + assumeRoleResponse.Credentials.SessionToken + ); + + var clientCredentialsExpiry = assumeRoleResponse.Credentials.Expiration.AddMinutes(-5); // Renew 5 mins before expiry + + var client = new AmazonSQSClient(temporaryCredentials, _sqsConfig); + return (client, clientCredentialsExpiry); + } + + #region Dispose Pattern + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _client?.Dispose(); + _stsClient?.Dispose(); + _timer?.Dispose(); + } + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion +} + + diff --git a/src/SlimMessageBus.Host.AmazonSQS/Config/Delegates.cs b/src/SlimMessageBus.Host.AmazonSQS/Config/Delegates.cs new file mode 100644 index 00000000..76abe51e --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Config/Delegates.cs @@ -0,0 +1,19 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +/// +/// Allows to convert an message and headers into a Message Group Id (used for FIFO queues to group messages together and ensure order of processing). +/// +/// +/// +/// +/// +public delegate string MessageGroupIdProvider(T message, IDictionary headers); + +/// +/// Allows to convert an message and headers into a Message Deduplication Id (Amazon SQS performs deduplication within a 5-minute window). +/// +/// +/// +/// +/// +public delegate string MessageDeduplicationIdProvider(T message, IDictionary headers); diff --git a/src/SlimMessageBus.Host.AmazonSQS/Config/SqsConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsConsumerBuilderExtensions.cs new file mode 100644 index 00000000..427a19ff --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsConsumerBuilderExtensions.cs @@ -0,0 +1,69 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public static class SqsConsumerBuilderExtensions +{ + public static TConsumerBuilder Queue(this TConsumerBuilder consumerBuilder, string queue) + where TConsumerBuilder : AbstractConsumerBuilder + { + if (consumerBuilder is null) throw new ArgumentNullException(nameof(consumerBuilder)); + if (queue is null) throw new ArgumentNullException(nameof(queue)); + + consumerBuilder.ConsumerSettings.PathKind = PathKind.Queue; + consumerBuilder.ConsumerSettings.Path = queue; + return consumerBuilder; + } + + /// + /// Specifies the visibility timeout for the message. Default is 30 seconds. + /// for more information. + /// + /// + /// + /// + /// + /// + /// + public static ConsumerBuilder VisibilityTimeout(this ConsumerBuilder consumerBuilder, int visibilityTimeoutSeconds) + { + if (consumerBuilder is null) throw new ArgumentNullException(nameof(consumerBuilder)); + if (visibilityTimeoutSeconds <= 0) throw new ArgumentOutOfRangeException(nameof(visibilityTimeoutSeconds)); + + SqsProperties.VisibilityTimeout.Set(consumerBuilder.Settings, visibilityTimeoutSeconds); + return consumerBuilder; + } + + /// + /// Specifies the maximum number of messages to receive in a single poll. Default is 1, maximum is 10. + /// for more information. + /// + /// + /// + /// + /// + /// + /// + public static ConsumerBuilder MaxMessages(this ConsumerBuilder consumerBuilder, int maxMessages) + { + if (consumerBuilder is null) throw new ArgumentNullException(nameof(consumerBuilder)); + if (maxMessages <= 0 || maxMessages > 10) throw new ArgumentOutOfRangeException(nameof(maxMessages)); + + SqsProperties.MaxMessages.Set(consumerBuilder.Settings, maxMessages); + return consumerBuilder; + } + + /// + /// Specifies the message attribute names to fetch. Default is "All". + /// + /// + /// + /// + /// + /// + public static ConsumerBuilder FetchMessageAttributes(this ConsumerBuilder consumerBuilder, params string[] messageAttributeNames) + { + if (consumerBuilder is null) throw new ArgumentNullException(nameof(consumerBuilder)); + + SqsProperties.MessageAttributes.Set(consumerBuilder.Settings, messageAttributeNames); + return consumerBuilder; + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerBuilderExtensions.cs b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerBuilderExtensions.cs new file mode 100644 index 00000000..7ea2107a --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerBuilderExtensions.cs @@ -0,0 +1,130 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public static class SqsProducerBuilderExtensions +{ + public static ProducerBuilder DefaultQueue(this ProducerBuilder producerBuilder, string queue) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + if (queue is null) throw new ArgumentNullException(nameof(queue)); + + producerBuilder.ToQueue(); + return producerBuilder.DefaultPath(queue); + } + + /// + /// The path parameter name in should be treated as a SNS topic name + /// + /// + /// + /// + public static ProducerBuilder ToTopic(this ProducerBuilder producerBuilder) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + + producerBuilder.Settings.PathKind = PathKind.Topic; + return producerBuilder; + } + + /// + /// The path parameter name in should be treated as a SQS queue name + /// + /// + /// + /// + public static ProducerBuilder ToQueue(this ProducerBuilder producerBuilder) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + + producerBuilder.Settings.PathKind = PathKind.Queue; + return producerBuilder; + } + + /// + /// Enables FIFO support for the queue when it will be provisioned. + /// + /// + /// + /// + /// + public static ProducerBuilder EnableFifo(this ProducerBuilder producerBuilder, Action> builder = null) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + + SqsProperties.EnableFifo.Set(producerBuilder.Settings, true); + + builder?.Invoke(new SqsProducerFifoBuilder(producerBuilder.Settings)); + + return producerBuilder; + } + + /// + /// Sets the tags for the queue when it will be provisioned. + /// + /// + /// + /// + /// + /// + public static ProducerBuilder Tags(this ProducerBuilder producerBuilder, Dictionary tags) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + if (tags is null) throw new ArgumentNullException(nameof(tags)); + + SqsProperties.Tags.Set(producerBuilder.Settings, tags); + + return producerBuilder; + } + + /// + /// Sets the attributes for the queue when it will be provisioned. See the available names in . + /// + /// + /// + /// + /// + /// + public static ProducerBuilder Attributes(this ProducerBuilder producerBuilder, Dictionary attributes) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + if (attributes is null) throw new ArgumentNullException(nameof(attributes)); + + SqsProperties.Attributes.Set(producerBuilder.Settings, attributes); + + return producerBuilder; + } + + /// + /// Sets the for the queue when it will be provisioned. + /// + /// + /// + /// + /// + /// + public static ProducerBuilder VisibilityTimeout(this ProducerBuilder producerBuilder, int visibilityTimeoutSeconds) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + + SqsProperties.VisibilityTimeout.Set(producerBuilder.Settings, visibilityTimeoutSeconds); + + return producerBuilder; + } + + /// + /// Sets the for the queue when it will be provisioned. + /// + /// + /// + /// + /// + /// + public static ProducerBuilder Policy(this ProducerBuilder producerBuilder, string policy) + { + if (producerBuilder is null) throw new ArgumentNullException(nameof(producerBuilder)); + if (policy is null) throw new ArgumentNullException(nameof(policy)); + + SqsProperties.Policy.Set(producerBuilder.Settings, policy); + + return producerBuilder; + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerFifoBuilder.cs b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerFifoBuilder.cs new file mode 100644 index 00000000..ae6f40f4 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProducerFifoBuilder.cs @@ -0,0 +1,34 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public class SqsProducerFifoBuilder(ProducerSettings producerSettings) +{ + /// + /// Used for FIFO queues to provide a message group id in order to group messages together and ensure order of processing. + /// + /// + /// + /// + /// + public SqsProducerFifoBuilder GroupId(MessageGroupIdProvider provider) + { + if (provider is null) throw new ArgumentNullException(nameof(provider)); + + SqsProperties.MessageGroupId.Set(producerSettings, (message, headers) => provider((T)message, headers)); + return this; + } + + /// + /// Used to specfiy a message deduplication id for the message. This is used to prevent duplicate messages from being sent (Amazon SQS performs deduplication within a 5-minute window). + /// + /// + /// + /// + /// + public SqsProducerFifoBuilder DeduplicationId(MessageDeduplicationIdProvider provider) + { + if (provider is null) throw new ArgumentNullException(nameof(provider)); + + SqsProperties.MessageDeduplicationId.Set(producerSettings, (message, headers) => provider((T)message, headers)); + return this; + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProperties.cs b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProperties.cs new file mode 100644 index 00000000..58a1273a --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsProperties.cs @@ -0,0 +1,18 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +static internal class SqsProperties +{ + // producer + static readonly internal ProviderExtensionProperty EnableFifo = new("Sqs_EnableFifo"); + static readonly internal ProviderExtensionProperty> MessageGroupId = new("Sqs_MessageGroupId"); + static readonly internal ProviderExtensionProperty> MessageDeduplicationId = new("Sqs_MessageDeduplicationId"); + static readonly internal ProviderExtensionProperty> Tags = new("Sqs_Tags"); + static readonly internal ProviderExtensionProperty> Attributes = new("Sqs_Attributes"); + static readonly internal ProviderExtensionProperty Policy = new("Sqs_Policy"); + + // consumer + static readonly internal ProviderExtensionProperty MaxMessages = new("Sqs_MaxMessages"); + static readonly internal ProviderExtensionProperty VisibilityTimeout = new("Sqs_VisibilityTimeout"); + static readonly internal ProviderExtensionProperty WaitTimeSeconds = new("Sqs_WaitTimeSeconds"); + static readonly internal ProviderExtensionProperty MessageAttributes = new("Sqs_MessageAttributes"); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Config/SqsRequestResponseBuilderExtensions.cs b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsRequestResponseBuilderExtensions.cs new file mode 100644 index 00000000..3f6fd71a --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Config/SqsRequestResponseBuilderExtensions.cs @@ -0,0 +1,17 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public static class SqsRequestResponseBuilderExtensions +{ + public static RequestResponseBuilder ReplyToQueue(this RequestResponseBuilder builder, string queue, Action builderConfig = null) + { + if (builder is null) throw new ArgumentNullException(nameof(builder)); + if (queue is null) throw new ArgumentNullException(nameof(queue)); + + builder.Settings.Path = queue; + builder.Settings.PathKind = PathKind.Queue; + + builderConfig?.Invoke(builder); + + return builder; + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs new file mode 100644 index 00000000..6172cfb5 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs @@ -0,0 +1,5 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public interface ISqsConsumerErrorHandler : IConsumerErrorHandler +{ +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs new file mode 100644 index 00000000..95818eb7 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs @@ -0,0 +1,145 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public abstract class SqsBaseConsumer : AbstractConsumer +{ + private readonly ISqsClientProvider _clientProvider; + + // consumer settings + private readonly int _maxMessages; + private readonly int _visibilityTimeout; + private readonly List _messageAttributeNames; + + private Task _task; + + public SqsMessageBus MessageBus { get; } + protected IMessageProcessor MessageProcessor { get; } + protected string Path { get; } + protected ISqsHeaderSerializer HeaderSerializer { get; } + + protected SqsBaseConsumer( + SqsMessageBus messageBus, + ISqsClientProvider clientProvider, + string path, + IMessageProcessor messageProcessor, + IEnumerable consumerSettings, + ILogger logger) + : base(logger ?? throw new ArgumentNullException(nameof(logger))) + { + MessageBus = messageBus ?? throw new ArgumentNullException(nameof(messageBus)); + _clientProvider = clientProvider ?? throw new ArgumentNullException(nameof(clientProvider)); + Path = path ?? throw new ArgumentNullException(nameof(path)); + MessageProcessor = messageProcessor ?? throw new ArgumentNullException(nameof(messageProcessor)); + HeaderSerializer = messageBus.HeaderSerializer; + T GetSingleValue(Func selector, string settingName, T defaultValue = default) + { + var set = consumerSettings.Select(x => selector(x)).Where(x => x is not null && !x.Equals(defaultValue)).ToHashSet(); + if (set.Count > 1) + { + throw new ConfigurationMessageBusException($"All declared consumers across the same queue {path} must have the same {settingName} settings."); + } + return set.FirstOrDefault() ?? defaultValue; + } + + _maxMessages = GetSingleValue(x => x.GetOrDefault(SqsProperties.MaxMessages), nameof(SqsConsumerBuilderExtensions.MaxMessages)) ?? messageBus.ProviderSettings.MaxMessageCount; + _visibilityTimeout = GetSingleValue(x => x.GetOrDefault(SqsProperties.VisibilityTimeout), nameof(SqsConsumerBuilderExtensions.VisibilityTimeout)) ?? 30; + _messageAttributeNames = new List(GetSingleValue(x => x.GetOrDefault(SqsProperties.MessageAttributes), nameof(SqsConsumerBuilderExtensions.FetchMessageAttributes)) ?? ["All"]); + } + + private async Task> ReceiveMessagesByUrl(string queueUrl) + { + var messageResponse = await _clientProvider.Client.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrl, + MessageAttributeNames = _messageAttributeNames, + MaxNumberOfMessages = _maxMessages, + VisibilityTimeout = _visibilityTimeout, + // For information about long polling, see + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html + // Setting WaitTimeSeconds to non-zero enables long polling. + WaitTimeSeconds = 5 + }, CancellationToken); + + return messageResponse.Messages; + } + + private async Task DeleteMessageBatchByUrl(string queueUrl, IReadOnlyCollection messages) + { + var deleteRequest = new DeleteMessageBatchRequest + { + QueueUrl = queueUrl, + Entries = new List(messages.Count) + }; + foreach (var message in messages) + { + deleteRequest.Entries.Add(new DeleteMessageBatchRequestEntry + { + Id = message.MessageId, + ReceiptHandle = message.ReceiptHandle + }); + } + + var deleteResponse = await _clientProvider.Client.DeleteMessageBatchAsync(deleteRequest, CancellationToken); + + // ToDo: capture failed messages + return deleteResponse.Failed.Count > 0; + } + + protected override Task OnStart() + { + Logger.LogInformation("Starting consumer for Queue: {Queue}", Path); + _task = Run(); + return Task.CompletedTask; + } + + protected override async Task OnStop() + { + Logger.LogInformation("Stopping consumer for Queue: {Queue}", Path); + await _task.ConfigureAwait(false); + _task = null; + } + + protected async Task Run() + { + var queueUrl = MessageBus.GetQueueUrlOrException(Path); + + var messagesToDelete = new List(_maxMessages); + + while (!CancellationToken.IsCancellationRequested) + { + try + { + var messages = await ReceiveMessagesByUrl(queueUrl).ConfigureAwait(false); + foreach (var message in messages) + { + var messageHeaders = message + .MessageAttributes + .ToDictionary(x => x.Key, x => HeaderSerializer.Deserialize(x.Key, x.Value)); + + var r = await MessageProcessor.ProcessMessage(message, messageHeaders, cancellationToken: CancellationToken).ConfigureAwait(false); + if (r.Exception != null) + { + Logger.LogError(r.Exception, "Message processing error - Queue: {Queue}, MessageId: {MessageId}", Path, message.MessageId); + // ToDo: DLQ handling + break; + } + messagesToDelete.Add(message); + } + + if (messagesToDelete.Count > 0) + { + await DeleteMessageBatchByUrl(queueUrl, messagesToDelete).ConfigureAwait(false); + messagesToDelete.Clear(); + } + } + catch (TaskCanceledException) + { + // ignore, need to finish + } + catch (Exception ex) + { + Logger.LogError(ex, "Error while processing messages - Queue: {Queue}", Path); + await Task.Delay(2000, CancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsConsumerContextExtensions.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsConsumerContextExtensions.cs new file mode 100644 index 00000000..9587ff9b --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsConsumerContextExtensions.cs @@ -0,0 +1,20 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public static class SqsConsumerContextExtensions +{ + private const string MessageKey = "Sqs_Message"; + + public static Message GetTransportMessage(this IConsumerContext context) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + + return context.GetPropertyOrDefault(MessageKey); + } + + internal static void SetTransportMessage(this ConsumerContext context, Message message) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + + context.Properties[MessageKey] = message; + } +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsQueueConsumer.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsQueueConsumer.cs new file mode 100644 index 00000000..16a35d38 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsQueueConsumer.cs @@ -0,0 +1,16 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public class SqsQueueConsumer( + SqsMessageBus messageBus, + string path, + ISqsClientProvider clientProvider, + IMessageProcessor messageProcessor, + IEnumerable consumerSettings) + : SqsBaseConsumer(messageBus, + clientProvider, + path, + messageProcessor, + consumerSettings, + messageBus.LoggerFactory.CreateLogger()) +{ +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs b/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs new file mode 100644 index 00000000..ea8ffa7a --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs @@ -0,0 +1,10 @@ +global using Amazon; +global using Amazon.Runtime; +global using Amazon.SQS; +global using Amazon.SQS.Model; + +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; + +global using SlimMessageBus.Host.Serialization; diff --git a/src/SlimMessageBus.Host.AmazonSQS/Headers/DefaultSqsHeaderSerializer.cs b/src/SlimMessageBus.Host.AmazonSQS/Headers/DefaultSqsHeaderSerializer.cs new file mode 100644 index 00000000..a6a5f6f4 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Headers/DefaultSqsHeaderSerializer.cs @@ -0,0 +1,32 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public class DefaultSqsHeaderSerializer(bool detectStringType = true) : ISqsHeaderSerializer +{ + const string DataTypeNumber = "Number"; + const string DataTypeString = "String"; + + public MessageAttributeValue Serialize(string key, object value) => value switch + { + // See more https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes + var x when x is long || x is int || x is short || x is byte => new MessageAttributeValue + { + DataType = DataTypeNumber, + StringValue = value.ToString() + }, + _ => new MessageAttributeValue + { + DataType = DataTypeString, + StringValue = value?.ToString() + } + }; + + public object Deserialize(string key, MessageAttributeValue value) => value.DataType switch + { + DataTypeNumber when long.TryParse(value.StringValue, out var longValue) => longValue, + DataTypeString when detectStringType && key != ReqRespMessageHeaders.RequestId && Guid.TryParse(value.StringValue, out var guid) => guid, + DataTypeString when detectStringType && bool.TryParse(value.StringValue, out var b) => b, + DataTypeString when detectStringType && DateTime.TryParse(value.StringValue, out var dt) => dt, + DataTypeString => value.StringValue, + _ => null + }; +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Headers/ISqsHeaderSerializer.cs b/src/SlimMessageBus.Host.AmazonSQS/Headers/ISqsHeaderSerializer.cs new file mode 100644 index 00000000..026f3106 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/Headers/ISqsHeaderSerializer.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public interface ISqsHeaderSerializer +{ + MessageAttributeValue Serialize(string key, object value); + object Deserialize(string key, MessageAttributeValue value); +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/MessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.AmazonSQS/MessageBusBuilderExtensions.cs new file mode 100644 index 00000000..73ccaccd --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/MessageBusBuilderExtensions.cs @@ -0,0 +1,20 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public static class MessageBusBuilderExtensions +{ + public static MessageBusBuilder WithProviderAmazonSQS(this MessageBusBuilder mbb, Action configure) + { + if (mbb is null) throw new ArgumentNullException(nameof(mbb)); + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + var providerSettings = new SqsMessageBusSettings(); + configure(providerSettings); + + mbb.PostConfigurationActions.Add((services) => + { + services.TryAddSingleton(providerSettings.ClientProviderFactory); + }); + + return mbb.WithProvider(settings => new SqsMessageBus(settings, providerSettings)); + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj b/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj new file mode 100644 index 00000000..6c0f4335 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj @@ -0,0 +1,23 @@ + + + + + + Amazon SQS provider for SlimMessageBus + See https://github.com/zarusz/SlimMessageBus/releases + Amazon AWS SQS provider SlimMessageBus MessageBus bus facade messaging + icon.png + latest + + + + + + + + + + + + + diff --git a/src/SlimMessageBus.Host.AmazonSQS/SqsMessageBus.cs b/src/SlimMessageBus.Host.AmazonSQS/SqsMessageBus.cs new file mode 100644 index 00000000..6ff977df --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/SqsMessageBus.cs @@ -0,0 +1,238 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +using SlimMessageBus.Host.Serialization; + +public class SqsMessageBus : MessageBusBase +{ + private readonly ILogger _logger; + private readonly ISqsClientProvider _clientProvider; + private readonly Dictionary _queueUrlByPath = []; + private readonly IMessageSerializer _messageSerializer; + + public ISqsHeaderSerializer HeaderSerializer { get; } + + public SqsMessageBus(MessageBusSettings settings, SqsMessageBusSettings providerSettings) + : base(settings, providerSettings) + { + _logger = LoggerFactory.CreateLogger(); + _clientProvider = settings.ServiceProvider.GetRequiredService(); + _messageSerializer = Serializer as IMessageSerializer + ?? throw new ConfigurationMessageBusException($"Serializer for Amazon SQS must be able to serialize into a string (it needs to implement {nameof(IMessageSerializer)})"); + HeaderSerializer = providerSettings.SqsHeaderSerializer; + OnBuildProvider(); + } + + protected override void Build() + { + base.Build(); + InitTaskList.Add(InitAsync, CancellationToken); + } + + protected override async Task CreateConsumers() + { + await base.CreateConsumers(); + + void AddConsumerFrom(string path, PathKind pathKind, IMessageProcessor messageProcessor, IEnumerable consumerSettings) + { + if (pathKind == PathKind.Queue) + { + _logger.LogInformation("Creating consumer for Queue: {Queue}", path); + var consumer = new SqsQueueConsumer(this, path, _clientProvider, messageProcessor, consumerSettings); + AddConsumer(consumer); + } + } + + static void InitConsumerContext(Message m, ConsumerContext ctx) => ctx.SetTransportMessage(m); + + object MessageProvider(Type messageType, Message transportMessage) => _messageSerializer.Deserialize(messageType, transportMessage.Body); + + foreach (var ((path, pathKind), consumerSettings) in Settings.Consumers + .GroupBy(x => (x.Path, x.PathKind)) + .ToDictionary(x => x.Key, x => x.ToList())) + { + var messageProcessor = new MessageProcessor( + consumerSettings, + this, + messageProvider: MessageProvider, + path: path, + responseProducer: this, + consumerContextInitializer: InitConsumerContext, + consumerErrorHandlerOpenGenericType: typeof(ISqsConsumerErrorHandler<>)); + + AddConsumerFrom(path, pathKind, messageProcessor, consumerSettings); + } + + if (Settings.RequestResponse != null) + { + var messageProcessor = new ResponseMessageProcessor( + LoggerFactory, + Settings.RequestResponse, + messageProvider: MessageProvider, + PendingRequestStore, + CurrentTimeProvider); + + AddConsumerFrom( + Settings.RequestResponse.Path, + Settings.RequestResponse.PathKind, + messageProcessor, + [Settings.RequestResponse]); + } + } + + /// + /// Performs initialization that has to happen before the first message produce happens. + /// + /// + private async Task InitAsync() + { + try + { + _logger.LogInformation("Ensuring client is authenticate"); + // Ensure the client finished the first authentication + await _clientProvider.EnsureClientAuthenticated(); + + // Provision the topology if enabled + if (ProviderSettings.TopologyProvisioning?.Enabled ?? false) + { + _logger.LogInformation("Provisioning topology"); + await ProvisionTopology(); + } + + // Read the Queue/Topic URLs for the producers + _logger.LogInformation("Populating queue URLs"); + await PopulatePathToUrlMappings(); + } + catch (Exception ex) + { + _logger.LogError(ex, "SQS Transport initialization failed: {ErrorMessage}", ex.Message); + } + } + + public override async Task ProvisionTopology() + { + await base.ProvisionTopology(); + + var provisioningService = new SqsTopologyService(LoggerFactory.CreateLogger(), Settings, ProviderSettings, _clientProvider); + await provisioningService.ProvisionTopology(); // provisioning happens asynchronously + } + + private async Task PopulatePathToUrlMappings() + { + var queuePaths = Settings.Producers.Where(x => x.PathKind == PathKind.Queue).Select(x => x.DefaultPath) + .Concat(Settings.Consumers.Where(x => x.PathKind == PathKind.Queue).Select(x => x.Path)) + .Concat(Settings.RequestResponse?.PathKind == PathKind.Queue ? [Settings.RequestResponse.Path] : []) + .ToHashSet(); + + foreach (var queuePath in queuePaths) + { + try + { + _logger.LogDebug("Populating URL for queue {QueueName}", queuePath); + var queueResponse = await _clientProvider.Client.GetQueueUrlAsync(queuePath, CancellationToken); + _queueUrlByPath[queuePath] = queueResponse.QueueUrl; + } + catch (QueueDoesNotExistException ex) + { + _logger.LogError(ex, "Queue {QueueName} does not exist, ensure that it either exists or topology provisioning is enabled", queuePath); + } + } + } + internal string GetQueueUrlOrException(string path) + { + if (_queueUrlByPath.TryGetValue(path, out var queueUrl)) + { + return queueUrl; + } + throw new ProducerMessageBusException($"Queue {path} has unknown URL at this point. Ensure the queue exists in Amazon SQS."); + } + + public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + OnProduceToTransport(message, messageType, path, messageHeaders); + + var queueUrl = GetQueueUrlOrException(path); + try + { + var (payload, attributes, deduplicationId, groupId) = GetTransportMessage(message, messageType, messageHeaders); + + await _clientProvider.Client.SendMessageAsync(new SendMessageRequest(queueUrl, payload) + { + MessageAttributes = attributes, + MessageDeduplicationId = deduplicationId, + MessageGroupId = groupId + }, cancellationToken); + } + catch (Exception ex) when (ex is not ProducerMessageBusException && ex is not TaskCanceledException) + { + throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); + } + } + + // Chunk if exceeds 10 messages and payload size (Amazon SQS limits) + private const int MaxMessagesInBatch = 10; + + public override async Task> ProduceToTransportBulk(IReadOnlyCollection envelopes, string path, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + var dispatched = new List(envelopes.Count); + try + { + var queueUrl = GetQueueUrlOrException(path); + + var entries = new List(MaxMessagesInBatch); + + var envelopeChunks = envelopes.Chunk(MaxMessagesInBatch); + foreach (var envelopeChunk in envelopeChunks) + { + foreach (var envelope in envelopeChunk) + { + var (payload, attributes, deduplicationId, groupId) = GetTransportMessage(envelope.Message, envelope.MessageType, envelope.Headers); + + entries.Add(new SendMessageBatchRequestEntry(Guid.NewGuid().ToString(), payload) + { + MessageAttributes = attributes, + MessageDeduplicationId = deduplicationId, + MessageGroupId = groupId + }); + } + + await _clientProvider.Client.SendMessageBatchAsync(new SendMessageBatchRequest(queueUrl, entries), cancellationToken); + + entries.Clear(); + + dispatched.AddRange(envelopeChunk); + } + + return new(dispatched, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Producing message batch to path {Path} resulted in error {Error}", path, ex.Message); + return new(dispatched, ex); + } + } + + private (string Payload, Dictionary Attributes, string DeduplicationId, string GroupId) GetTransportMessage(object message, Type messageType, IDictionary messageHeaders) + { + var producerSettings = GetProducerSettings(messageType); + + var messageDeduplicationIdProvider = producerSettings.GetOrDefault(SqsProperties.MessageDeduplicationId, null); + var deduplicationId = messageDeduplicationIdProvider?.Invoke(message, messageHeaders); + + var messageGroupIdProvider = producerSettings.GetOrDefault(SqsProperties.MessageGroupId, null); + var groupId = messageGroupIdProvider?.Invoke(message, messageHeaders); + + Dictionary messageAttributes = null; + if (messageHeaders != null) + { + messageAttributes = []; + foreach (var header in messageHeaders) + { + var headerValue = HeaderSerializer.Serialize(header.Key, header.Value); + messageAttributes.Add(header.Key, headerValue); + } + } + + var messagePayload = _messageSerializer.Serialize(messageType, message); + return (messagePayload, messageAttributes, deduplicationId, groupId); + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/SqsMessageBusSettings.cs b/src/SlimMessageBus.Host.AmazonSQS/SqsMessageBusSettings.cs new file mode 100644 index 00000000..8783e48e --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/SqsMessageBusSettings.cs @@ -0,0 +1,67 @@ +namespace SlimMessageBus.Host.AmazonSQS; +public class SqsMessageBusSettings +{ + /// + /// The factory method to create the client provider which is used to manage the . + /// + public Func ClientProviderFactory { get; set; } + + /// + /// The configuration for the SQS client. + /// + public AmazonSQSConfig SqsClientConfig { get; set; } = new(); + + /// + /// Serializer used to serialize SQS message header values. + /// By default the is used. + /// + public ISqsHeaderSerializer SqsHeaderSerializer { get; set; } = new DefaultSqsHeaderSerializer(); + + /// + /// Settings for auto creation of queues if they don't exist. + /// + public SqsTopologySettings TopologyProvisioning { get; set; } = new(); + + /// + /// Connect to AWS using long term credentials. + /// See https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html + /// + /// + /// + /// + public SqsMessageBusSettings UseCredentials(string accessKey, string secretKey) + { + ClientProviderFactory = (svp) => new StaticCredentialsSqsClientProvider(SqsClientConfig, new BasicAWSCredentials(accessKey, secretKey)); + return this; + } + + /// + /// Connect to AWS using temporary credentials (recommended) + /// See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS + /// + /// + /// + /// + public SqsMessageBusSettings UseTemporaryCredentials(string roleArn, string roleSessionName) + { + ClientProviderFactory = (svp) => new TemporaryCredentialsSqsClientProvider(SqsClientConfig, roleArn, roleSessionName); + return this; + } + + /// + /// Sets the region for the SQS client. + /// + /// + /// + public SqsMessageBusSettings UseRegion(RegionEndpoint region) + { + SqsClientConfig.RegionEndpoint = region; + return this; + } + + /// + /// Maximum message count to be recieved by the consumer in one batch (1-10). Default is 10. + /// + public int MaxMessageCount { get; set; } = 10; + +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/SqsTopologyService.cs b/src/SlimMessageBus.Host.AmazonSQS/SqsTopologyService.cs new file mode 100644 index 00000000..b71b4654 --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/SqsTopologyService.cs @@ -0,0 +1,160 @@ +namespace SlimMessageBus.Host.AmazonSQS; +public class SqsTopologyService +{ + private readonly ILogger _logger; + private readonly MessageBusSettings _settings; + private readonly SqsMessageBusSettings _providerSettings; + private readonly ISqsClientProvider _clientProvider; + + public SqsTopologyService( + ILogger logger, + MessageBusSettings settings, + SqsMessageBusSettings providerSettings, + ISqsClientProvider clientProvider) + { + _logger = logger; + _settings = settings; + _providerSettings = providerSettings; + _clientProvider = clientProvider; + } + + public Task ProvisionTopology() => _providerSettings.TopologyProvisioning.OnProvisionTopology(_clientProvider.Client, DoProvisionTopology); + + private async Task CreateQueue(string path, + bool fifo, + int? visibilityTimeout, + string policy, + Dictionary attributes, + Dictionary tags) + { + try + { + try + { + var queueUrl = await _clientProvider.Client.GetQueueUrlAsync(path); + if (queueUrl != null) + { + return; + } + } + catch (QueueDoesNotExistException) + { + // proceed to create the queue + } + + var createQueueRequest = new CreateQueueRequest + { + QueueName = path, + Attributes = [] + }; + + if (fifo) + { + createQueueRequest.Attributes.Add(QueueAttributeName.FifoQueue, "true"); + } + + if (visibilityTimeout != null) + { + createQueueRequest.Attributes.Add(QueueAttributeName.VisibilityTimeout, visibilityTimeout.ToString()); + } + + if (policy != null) + { + createQueueRequest.Attributes.Add(QueueAttributeName.Policy, policy); + } + + if (attributes.Count > 0) + { + createQueueRequest.Attributes = attributes; + } + + if (tags.Count > 0) + { + createQueueRequest.Tags = tags; + } + + _providerSettings.TopologyProvisioning.CreateQueueOptions?.Invoke(createQueueRequest); + + try + { + var createQueueResponse = await _clientProvider.Client.CreateQueueAsync(createQueueRequest); + _logger.LogInformation("Created queue {QueueName} with URL {QueueUrl}", path, createQueueResponse.QueueUrl); + } + catch (QueueNameExistsException) + { + _logger.LogInformation("Queue {QueueName} already exists", path); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating queue {QueueName}", path); + } + } + + private async Task DoProvisionTopology() + { + try + { + _logger.LogInformation("Topology provisioning started..."); + + if (_providerSettings.TopologyProvisioning.CanConsumerCreateQueue) + { + var consumersSettingsByPath = _settings.Consumers + .OfType() + .Concat([_settings.RequestResponse]) + .Where(x => x != null) + .GroupBy(x => (x.Path, x.PathKind)) + .ToDictionary(x => x.Key, x => x.ToList()); + + foreach (var ((path, pathKind), consumerSettingsList) in consumersSettingsByPath) + { + if (pathKind == PathKind.Queue) + { + await CreateQueue( + path: path, + fifo: consumerSettingsList.Any(cs => cs.GetOrDefault(SqsProperties.EnableFifo, _settings, false)), + visibilityTimeout: consumerSettingsList.Select(cs => cs.GetOrDefault(SqsProperties.VisibilityTimeout, _settings, null)).FirstOrDefault(x => x != null), + policy: consumerSettingsList.Select(cs => cs.GetOrDefault(SqsProperties.Policy, _settings, null)).FirstOrDefault(x => x != null), + attributes: [], + tags: []); + } + } + } + + if (_providerSettings.TopologyProvisioning.CanProducerCreateQueue) + { + foreach (var producer in _settings.Producers) + { + var attributes = producer.GetOrDefault(SqsProperties.Attributes, []) + .Concat(_settings.GetOrDefault(SqsProperties.Attributes, [])) + .GroupBy(x => x.Key, x => x.Value) + .ToDictionary(x => x.Key, x => x.First()); + + var tags = producer.GetOrDefault(SqsProperties.Tags, []) + .Concat(_settings.GetOrDefault(SqsProperties.Tags, [])) + .GroupBy(x => x.Key, x => x.Value) + .ToDictionary(x => x.Key, x => x.First()); + + if (producer.PathKind == PathKind.Queue) + { + await CreateQueue( + path: producer.DefaultPath, + fifo: producer.GetOrDefault(SqsProperties.EnableFifo, _settings, false), + visibilityTimeout: producer.GetOrDefault(SqsProperties.VisibilityTimeout, _settings, null), + policy: producer.GetOrDefault(SqsProperties.Policy, _settings, null), + attributes: attributes, + tags: tags); + } + } + } + } + catch (Exception e) + { + _logger.LogError(e, "Could not provision Amazon SQS topology"); + } + finally + { + _logger.LogInformation("Topology provisioning finished"); + } + } +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/SqsTopologySettings.cs b/src/SlimMessageBus.Host.AmazonSQS/SqsTopologySettings.cs new file mode 100644 index 00000000..d4dc9aab --- /dev/null +++ b/src/SlimMessageBus.Host.AmazonSQS/SqsTopologySettings.cs @@ -0,0 +1,34 @@ +namespace SlimMessageBus.Host.AmazonSQS; + +public class SqsTopologySettings +{ + /// + /// Indicates whether topology provisioning is enabled. Default is true. + /// + public bool Enabled { get; set; } = true; + /// + /// A filter that allows (or not) for declared producers to provision needed queues. True by default. + /// + public bool CanProducerCreateQueue { get; set; } = true; + /// + /// A filter that allows (or not) for declared consumers to provision needed queues. True by default. + /// + public bool CanConsumerCreateQueue { get; set; } = true; + /// + /// Default configuration to be applied when a topic needs to be created (). + /// + public Action CreateQueueOptions { get; set; } + + /// + /// Interceptor that allows to intercept the topology provisioning process. + /// + public SqsTopologyInterceptor OnProvisionTopology { get; set; } = (client, provision) => provision(); +} + +/// +/// Interceptor that allows to intercept the topology provisioning process and to apply custom logic before and after the provisioning process. +/// +/// The SQS client +/// Delegate allowing to perform topology provisioning +/// +public delegate Task SqsTopologyInterceptor(AmazonSQSClient client, Func provision); diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForConsumers.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForConsumers.cs index 58d23550..7182ac6c 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForConsumers.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForConsumers.cs @@ -7,7 +7,7 @@ public class EhPartitionConsumerForConsumers : EhPartitionConsumer { private readonly IEnumerable _consumerSettings; - public EhPartitionConsumerForConsumers(EventHubMessageBus messageBus, IEnumerable consumerSettings, GroupPathPartitionId pathGroupPartition) + public EhPartitionConsumerForConsumers(EventHubMessageBus messageBus, IEnumerable consumerSettings, GroupPathPartitionId pathGroupPartition, MessageProvider messageProvider) : base(messageBus, pathGroupPartition) { _consumerSettings = consumerSettings ?? throw new ArgumentNullException(nameof(consumerSettings)); @@ -16,7 +16,7 @@ public EhPartitionConsumerForConsumers(EventHubMessageBus messageBus, IEnumerabl MessageProcessor = new MessageProcessor( _consumerSettings, MessageBus, - messageProvider: GetMessageFromTransportMessage, + messageProvider: messageProvider, path: GroupPathPartition.ToString(), responseProducer: MessageBus, consumerContextInitializer: InitializeConsumerContext, @@ -30,9 +30,6 @@ protected ICheckpointTrigger CreateCheckpointTrigger() return f.Create(_consumerSettings); } - private object GetMessageFromTransportMessage(Type messageType, EventData e) - => MessageBus.Serializer.Deserialize(messageType, e.Body.ToArray()); - private static void InitializeConsumerContext(EventData nativeMessage, ConsumerContext consumerContext) => consumerContext.SetTransportMessage(nativeMessage); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForResponses.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForResponses.cs index 061fcc44..17439275 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForResponses.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumerForResponses.cs @@ -9,12 +9,18 @@ namespace SlimMessageBus.Host.AzureEventHub; /// public class EhPartitionConsumerForResponses : EhPartitionConsumer { - public EhPartitionConsumerForResponses(EventHubMessageBus messageBus, RequestResponseSettings requestResponseSettings, GroupPathPartitionId pathGroupPartition) + public EhPartitionConsumerForResponses( + EventHubMessageBus messageBus, + RequestResponseSettings requestResponseSettings, + GroupPathPartitionId pathGroupPartition, + MessageProvider messageProvider, + IPendingRequestStore pendingRequestStore, + ICurrentTimeProvider currentTimeProvider) : base(messageBus, pathGroupPartition) { if (requestResponseSettings == null) throw new ArgumentNullException(nameof(requestResponseSettings)); - MessageProcessor = new ResponseMessageProcessor(MessageBus.LoggerFactory, requestResponseSettings, MessageBus, messagePayloadProvider: eventData => eventData.EventBody.ToArray()); + MessageProcessor = new ResponseMessageProcessor(MessageBus.LoggerFactory, requestResponseSettings, messageProvider, pendingRequestStore, currentTimeProvider); CheckpointTrigger = new CheckpointTrigger(requestResponseSettings, MessageBus.LoggerFactory); } } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs index d8a79cbe..3aae9ded 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs @@ -55,17 +55,19 @@ protected override async Task CreateConsumers() { await base.CreateConsumers(); + object MessageProvider(Type messageType, EventData transportMessage) => Serializer.Deserialize(messageType, transportMessage.Body.ToArray()); + foreach (var (groupPath, consumerSettings) in Settings.Consumers.GroupBy(x => new GroupPath(path: x.Path, group: x.GetGroup())).ToDictionary(x => x.Key, x => x.ToList())) { _logger.LogInformation("Creating consumer for Path: {Path}, Group: {Group}", groupPath.Path, groupPath.Group); - AddConsumer(new EhGroupConsumer(this, groupPath, groupPathPartition => new EhPartitionConsumerForConsumers(this, consumerSettings, groupPathPartition))); + AddConsumer(new EhGroupConsumer(this, groupPath, groupPathPartition => new EhPartitionConsumerForConsumers(this, consumerSettings, groupPathPartition, MessageProvider))); } if (Settings.RequestResponse != null) { var pathGroup = new GroupPath(Settings.RequestResponse.Path, Settings.RequestResponse.GetGroup()); _logger.LogInformation("Creating response consumer for Path: {Path}, Group: {Group}", pathGroup.Path, pathGroup.Group); - AddConsumer(new EhGroupConsumer(this, pathGroup, groupPathPartition => new EhPartitionConsumerForResponses(this, Settings.RequestResponse, groupPathPartition))); + AddConsumer(new EhGroupConsumer(this, pathGroup, groupPathPartition => new EhPartitionConsumerForResponses(this, Settings.RequestResponse, groupPathPartition, MessageProvider, PendingRequestStore, CurrentTimeProvider))); } } diff --git a/src/SlimMessageBus.Host.AzureEventHub/SlimMessageBus.Host.AzureEventHub.csproj b/src/SlimMessageBus.Host.AzureEventHub/SlimMessageBus.Host.AzureEventHub.csproj index efdd6a9c..2ce27850 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/SlimMessageBus.Host.AzureEventHub.csproj +++ b/src/SlimMessageBus.Host.AzureEventHub/SlimMessageBus.Host.AzureEventHub.csproj @@ -3,9 +3,9 @@ - Azure Event Hubs provider SlimMessageBus MessageBus bus facade messaging + Ak,m Event Hubs provider for SlimMessageBus See https://github.com/zarusz/SlimMessageBus/releases - Azure Event Hubs provider for SlimMessageBus + Azure Event Hubs provider SlimMessageBus MessageBus bus facade messaging icon.png latest @@ -16,7 +16,6 @@ - diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/ServiceBusConsumerContextExtensions.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/ServiceBusConsumerContextExtensions.cs index ac500c35..a3aefb29 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/ServiceBusConsumerContextExtensions.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/ServiceBusConsumerContextExtensions.cs @@ -1,7 +1,5 @@ namespace SlimMessageBus.Host.AzureServiceBus; -using Azure.Messaging.ServiceBus; - public static class ServiceBusConsumerContextExtensions { private const string MessageKey = "ServiceBus_Message"; @@ -13,7 +11,7 @@ public static ServiceBusReceivedMessage GetTransportMessage(this IConsumerContex return context.GetPropertyOrDefault(MessageKey); } - public static void SetTransportMessage(this ConsumerContext context, ServiceBusReceivedMessage message) + internal static void SetTransportMessage(this ConsumerContext context, ServiceBusReceivedMessage message) { if (context is null) throw new ArgumentNullException(nameof(context)); diff --git a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs index 156efc4e..cd87ed56 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs @@ -59,7 +59,7 @@ protected override void Build() if (ProviderSettings.TopologyProvisioning?.Enabled ?? false) { - AddInit(ProvisionTopology()); + InitTaskList.Add(ProvisionTopology, CancellationToken); } _client = ProviderSettings.ClientFactory(); @@ -86,6 +86,7 @@ void AddConsumerFrom(TopicSubscriptionParams topicSubscription, IMessageProcesso } static void InitConsumerContext(ServiceBusReceivedMessage m, ConsumerContext ctx) => ctx.SetTransportMessage(m); + object MessageProvider(Type messageType, ServiceBusReceivedMessage m) => Serializer.Deserialize(messageType, m.Body.ToArray()); foreach (var ((path, subscriptionName), consumerSettings) in Settings.Consumers .GroupBy(x => (x.Path, SubscriptionName: x.GetSubscriptionName(ProviderSettings))) @@ -95,7 +96,7 @@ void AddConsumerFrom(TopicSubscriptionParams topicSubscription, IMessageProcesso var messageProcessor = new MessageProcessor( consumerSettings, this, - messageProvider: (messageType, m) => Serializer.Deserialize(messageType, m.Body.ToArray()), + messageProvider: MessageProvider, path: path.ToString(), responseProducer: this, consumerContextInitializer: InitConsumerContext, @@ -110,8 +111,9 @@ void AddConsumerFrom(TopicSubscriptionParams topicSubscription, IMessageProcesso var messageProcessor = new ResponseMessageProcessor( LoggerFactory, Settings.RequestResponse, - responseConsumer: this, - messagePayloadProvider: m => m.Body.ToArray()); + MessageProvider, + PendingRequestStore, + CurrentTimeProvider); AddConsumerFrom(topicSubscription, messageProcessor, [Settings.RequestResponse]); } diff --git a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs index 0be23396..f687fd0c 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs @@ -164,7 +164,7 @@ protected async Task DoProvisionTopology() var topologyProvisioning = _providerSettings.TopologyProvisioning; var consumersSettingsByPath = _settings.Consumers.OfType() - .Concat(new[] { _settings.RequestResponse }) + .Concat([_settings.RequestResponse]) .Where(x => x != null) .GroupBy(x => (x.Path, x.PathKind)) .ToDictionary(x => x.Key, x => x.ToList()); diff --git a/src/SlimMessageBus.Host.AzureServiceBus/SlimMessageBus.Host.AzureServiceBus.csproj b/src/SlimMessageBus.Host.AzureServiceBus/SlimMessageBus.Host.AzureServiceBus.csproj index 114d18e0..4652b894 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/SlimMessageBus.Host.AzureServiceBus.csproj +++ b/src/SlimMessageBus.Host.AzureServiceBus/SlimMessageBus.Host.AzureServiceBus.csproj @@ -3,11 +3,11 @@ - latest Azure Service Bus provider for SlimMessageBus + See https://github.com/zarusz/SlimMessageBus/releases Azure Service Bus provider SlimMessageBus MessageBus bus facade messaging client icon.png - + latest @@ -19,9 +19,7 @@ - - <_Parameter1>SlimMessageBus.Host.AzureServiceBus.Test - + diff --git a/src/SlimMessageBus.Host.Configuration/Settings/HasProviderExtensions.cs b/src/SlimMessageBus.Host.Configuration/Settings/HasProviderExtensions.cs index e961a7a1..8da226bc 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/HasProviderExtensions.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/HasProviderExtensions.cs @@ -18,6 +18,9 @@ public T GetOrCreate(string key, Func factoryMethod) return typedValue; } + public T GetOrCreate(ProviderExtensionProperty property, Func factoryMethod) + => GetOrCreate(property.Key, factoryMethod); + public T GetOrDefault(string key, T defaultValue = default) { if (Properties.TryGetValue(key, out var value)) @@ -27,6 +30,9 @@ public T GetOrDefault(string key, T defaultValue = default) return defaultValue; } + public T GetOrDefault(ProviderExtensionProperty property, T defaultValue = default) + => GetOrDefault(property.Key, defaultValue); + public T GetOrDefault(string key, MessageBusSettings messageBusSettings, T defaultValue = default) { if (Properties.TryGetValue(key, out var value) @@ -36,7 +42,10 @@ public T GetOrDefault(string key, MessageBusSettings messageBusSettings, T de return (T)value; } return defaultValue; - } + } + + public T GetOrDefault(ProviderExtensionProperty property, MessageBusSettings messageBusSettings, T defaultValue = default) + => GetOrDefault(property.Key, messageBusSettings, defaultValue); public T GetOrDefault(string key, HasProviderExtensions parentSettings, T defaultValue = default) { @@ -46,5 +55,8 @@ public T GetOrDefault(string key, HasProviderExtensions parentSettings, T def return (T)value; } return defaultValue; - } + } + + public T GetOrDefault(ProviderExtensionProperty property, HasProviderExtensions parentSettings, T defaultValue = default) + => GetOrDefault(property.Key, parentSettings, defaultValue); } diff --git a/src/SlimMessageBus.Host.Configuration/Settings/ProviderExtensionProperty.cs b/src/SlimMessageBus.Host.Configuration/Settings/ProviderExtensionProperty.cs new file mode 100644 index 00000000..c82a4326 --- /dev/null +++ b/src/SlimMessageBus.Host.Configuration/Settings/ProviderExtensionProperty.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host; + +public class ProviderExtensionProperty(string key) +{ + public string Key { get; } = key; + + public void Set(HasProviderExtensions settings, T value) + => settings.Properties[Key] = value; +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumerForResponses.cs b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumerForResponses.cs index 3b3cdfad..f4052905 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumerForResponses.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumerForResponses.cs @@ -8,7 +8,16 @@ namespace SlimMessageBus.Host.Kafka; /// public class KafkaPartitionConsumerForResponses : KafkaPartitionConsumer { - public KafkaPartitionConsumerForResponses(ILoggerFactory loggerFactory, RequestResponseSettings requestResponseSettings, string group, TopicPartition topicPartition, IKafkaCommitController commitController, IResponseConsumer responseConsumer, IMessageSerializer headerSerializer) + public KafkaPartitionConsumerForResponses( + ILoggerFactory loggerFactory, + RequestResponseSettings requestResponseSettings, + string group, + TopicPartition topicPartition, + IKafkaCommitController commitController, + MessageProvider messageProvider, + IPendingRequestStore pendingRequestStore, + ICurrentTimeProvider currentTimeProvider, + IMessageSerializer headerSerializer) : base( loggerFactory, [requestResponseSettings], @@ -19,8 +28,9 @@ public KafkaPartitionConsumerForResponses(ILoggerFactory loggerFactory, RequestR messageProcessor: new ResponseMessageProcessor( loggerFactory, requestResponseSettings, - responseConsumer, - messagePayloadProvider: m => m.Message.Value)) + messageProvider, + pendingRequestStore, + currentTimeProvider)) { } } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs index a50c977e..69ce65ea 100644 --- a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs +++ b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs @@ -67,7 +67,11 @@ void AddGroupConsumer(string group, IReadOnlyCollection topics, Func new KafkaPartitionConsumerForResponses(LoggerFactory, Settings.RequestResponse, Settings.RequestResponse.GetGroup(), tp, cc, this, HeaderSerializer); + object MessageProvider(Type messageType, ConsumeResult transportMessage) + => Serializer.Deserialize(messageType, transportMessage.Message.Value); + + IKafkaPartitionConsumer ResponseProcessorFactory(TopicPartition tp, IKafkaCommitController cc) + => new KafkaPartitionConsumerForResponses(LoggerFactory, Settings.RequestResponse, Settings.RequestResponse.GetGroup(), tp, cc, MessageProvider, PendingRequestStore, CurrentTimeProvider, HeaderSerializer); foreach (var consumersByGroup in Settings.Consumers.GroupBy(x => x.GetGroup())) { @@ -75,7 +79,8 @@ void AddGroupConsumer(string group, IReadOnlyCollection topics, Func x.Path).ToDictionary(x => x.Key, x => x.ToArray()); var topics = consumersByTopic.Keys.ToList(); - IKafkaPartitionConsumer ConsumerProcessorFactory(TopicPartition tp, IKafkaCommitController cc) => new KafkaPartitionConsumerForConsumers(LoggerFactory, consumersByTopic[tp.Topic], group, tp, cc, HeaderSerializer, this); + IKafkaPartitionConsumer ConsumerProcessorFactory(TopicPartition tp, IKafkaCommitController cc) + => new KafkaPartitionConsumerForConsumers(LoggerFactory, consumersByTopic[tp.Topic], group, tp, cc, HeaderSerializer, this); var processorFactory = ConsumerProcessorFactory; diff --git a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs index a876e219..24cc1cb8 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs @@ -77,8 +77,9 @@ void AddTopicConsumer(string topic, IMessageProcessor me var processor = new ResponseMessageProcessor( LoggerFactory, Settings.RequestResponse, - responseConsumer: this, - messagePayloadProvider: m => m.PayloadSegment.Array); + messageProvider: MessageProvider, + PendingRequestStore, + CurrentTimeProvider); AddTopicConsumer(Settings.RequestResponse.Path, processor); } diff --git a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs index 79af07d8..3ea6254a 100644 --- a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs +++ b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs @@ -23,7 +23,7 @@ public NatsMessageBus(MessageBusSettings settings, NatsMessageBusSettings provid protected override void Build() { base.Build(); - AddInit(CreateConnectionAsync()); + InitTaskList.Add(CreateConnectionAsync, CancellationToken); } private Task CreateConnectionAsync() @@ -56,12 +56,14 @@ protected override async Task CreateConsumers() await base.CreateConsumers(); + object MessageProvider(Type messageType, NatsMsg transportMessage) => Serializer.Deserialize(messageType, transportMessage.Data); + foreach (var (subject, consumerSettings) in Settings.Consumers.GroupBy(x => x.Path).ToDictionary(x => x.Key, x => x.ToList())) { var processor = new MessageProcessor>( consumerSettings, messageBus: this, - messageProvider: (type, message) => Serializer.Deserialize(type, message.Data), + messageProvider: MessageProvider, subject, this, consumerErrorHandlerOpenGenericType: typeof(INatsConsumerErrorHandler<>)); @@ -71,7 +73,7 @@ protected override async Task CreateConsumers() if (Settings.RequestResponse != null) { - var processor = new ResponseMessageProcessor>(LoggerFactory, Settings.RequestResponse, this, message => message.Data); + var processor = new ResponseMessageProcessor>(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider); AddSubjectConsumer(Settings.RequestResponse.Path, processor); } } diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs index 7698a37a..160cf0aa 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxSendingTask.cs @@ -270,7 +270,7 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxM { var busName = busGroup.Key; var bus = GetBus(compositeMessageBus, messageBusTarget, busName); - if (bus == null || bus is not ITransportBulkProducer bulkProducer) + if (bus is not ITransportBulkProducer bulkProducer) { foreach (var outboxMessage in busGroup) { @@ -285,7 +285,6 @@ async internal Task SendMessages(IServiceProvider serviceProvider, IOutboxM abortedIds.Add(outboxMessage.Id); } - continue; } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs index 2040822c..52ec4eb4 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs @@ -11,7 +11,15 @@ public class RabbitMqConsumer : AbstractRabbitMqConsumer protected override RabbitMqMessageAcknowledgementMode AcknowledgementMode => _acknowledgementMode; - public RabbitMqConsumer(ILoggerFactory loggerFactory, IRabbitMqChannel channel, string queueName, IList consumers, IMessageSerializer serializer, MessageBusBase messageBus, IHeaderValueConverter headerValueConverter) + public RabbitMqConsumer( + ILoggerFactory loggerFactory, + IRabbitMqChannel channel, + string queueName, + IList consumers, + IMessageSerializer serializer, + MessageBusBase messageBus, + MessageProvider messageProvider, + IHeaderValueConverter headerValueConverter) : base(loggerFactory.CreateLogger(), channel, queueName, headerValueConverter) { _acknowledgementMode = consumers.Select(x => x.GetOrDefault(RabbitMqProperties.MessageAcknowledgementMode, messageBus.Settings)).FirstOrDefault(x => x != null) @@ -21,7 +29,7 @@ public RabbitMqConsumer(ILoggerFactory loggerFactory, IRabbitMqChannel channel, messageBus, path: queueName, responseProducer: messageBus, - messageProvider: (messageType, m) => serializer.Deserialize(messageType, m.Body.ToArray()), + messageProvider: messageProvider, consumerContextInitializer: InitializeConsumerContext, consumerErrorHandlerOpenGenericType: typeof(IRabbitMqConsumerErrorHandler<>)); } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs index a40bc40e..5a8366e4 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs @@ -6,10 +6,18 @@ public class RabbitMqResponseConsumer : AbstractRabbitMqConsumer protected override RabbitMqMessageAcknowledgementMode AcknowledgementMode => RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade; - public RabbitMqResponseConsumer(ILoggerFactory loggerFactory, IRabbitMqChannel channel, string queueName, RequestResponseSettings requestResponseSettings, MessageBusBase messageBus, IHeaderValueConverter headerValueConverter) + public RabbitMqResponseConsumer( + ILoggerFactory loggerFactory, + IRabbitMqChannel channel, + string queueName, + RequestResponseSettings requestResponseSettings, + MessageProvider messageProvider, + IPendingRequestStore pendingRequestStore, + ICurrentTimeProvider currentTimeProvider, + IHeaderValueConverter headerValueConverter) : base(loggerFactory.CreateLogger(), channel, queueName, headerValueConverter) { - _messageProcessor = new ResponseMessageProcessor(loggerFactory, requestResponseSettings, messageBus, m => m.Body.ToArray()); + _messageProcessor = new ResponseMessageProcessor(loggerFactory, requestResponseSettings, messageProvider, pendingRequestStore, currentTimeProvider); } protected override async Task OnMessageReceived(Dictionary messageHeaders, BasicDeliverEventArgs transportMessage) diff --git a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs index e8207191..278d408a 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs @@ -28,13 +28,15 @@ protected override void Build() { base.Build(); - AddInit(CreateConnection()); + InitTaskList.Add(CreateConnection, CancellationToken); } protected override async Task CreateConsumers() { await base.CreateConsumers(); + object MessageProvider(Type messageType, BasicDeliverEventArgs transportMessage) => Serializer.Deserialize(messageType, transportMessage.Body.ToArray()); + foreach (var (queueName, consumers) in Settings.Consumers.GroupBy(x => x.GetQueueName()).ToDictionary(x => x.Key, x => x.ToList())) { AddConsumer(new RabbitMqConsumer(LoggerFactory, @@ -43,6 +45,7 @@ protected override async Task CreateConsumers() consumers, Serializer, messageBus: this, + MessageProvider, ProviderSettings.HeaderValueConverter)); } @@ -52,7 +55,9 @@ protected override async Task CreateConsumers() channel: this, queueName: Settings.RequestResponse.GetQueueName(), Settings.RequestResponse, - this, + MessageProvider, + PendingRequestStore, + CurrentTimeProvider, ProviderSettings.HeaderValueConverter)); } } diff --git a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs index 29ba4efd..eef314c3 100644 --- a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs +++ b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs @@ -141,11 +141,11 @@ void AddTopicConsumer(string topic, ISubscriber subscriber, IMessageProcessor(LoggerFactory, Settings.RequestResponse, this, messagePayloadProvider: m => m.Payload)); + AddTopicConsumer(Settings.RequestResponse.Path, subscriber, new ResponseMessageProcessor(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider)); } else { - queues.Add((Settings.RequestResponse.Path, new ResponseMessageProcessor(LoggerFactory, Settings.RequestResponse, this, messagePayloadProvider: m => m.Payload))); + queues.Add((Settings.RequestResponse.Path, new ResponseMessageProcessor(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider))); } } diff --git a/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs b/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs index 2aa0356e..c3998f70 100644 --- a/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs +++ b/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs @@ -7,7 +7,7 @@ namespace SlimMessageBus.Host.Serialization.Json; using Newtonsoft.Json; -public class JsonMessageSerializer : IMessageSerializer +public class JsonMessageSerializer : IMessageSerializer, IMessageSerializer { private readonly ILogger _logger; private readonly Encoding _encoding; @@ -31,9 +31,7 @@ public byte[] Serialize(Type t, object message) { var jsonPayload = JsonConvert.SerializeObject(message, t, _serializerSettings); _logger.LogDebug("Type {MessageType} serialized from {Message} to JSON {MessageJson}", t, message, jsonPayload); - - var payload = _encoding.GetBytes(jsonPayload); - return payload; + return _encoding.GetBytes(jsonPayload); } public object Deserialize(Type t, byte[] payload) @@ -42,16 +40,32 @@ public object Deserialize(Type t, byte[] payload) try { jsonPayload = _encoding.GetString(payload); - var message = JsonConvert.DeserializeObject(jsonPayload, t, _serializerSettings); - _logger.LogDebug("Type {MessageType} deserialized from JSON {MessageJson} to {Message}", t, jsonPayload, message); - return message; + return Deserialize(t, jsonPayload); } catch (Exception e) { _logger.LogError(e, "Type {MessageType} could not been deserialized, payload: {MessagePayload}, JSON: {MessageJson}", t, _logger.IsEnabled(LogLevel.Debug) ? Convert.ToBase64String(payload) : "(...)", jsonPayload); throw; } - } - + } + + #endregion + + #region Implementation of IMessageSerializer + + string IMessageSerializer.Serialize(Type t, object message) + { + var payload = JsonConvert.SerializeObject(message, t, _serializerSettings); + _logger.LogDebug("Type {MessageType} serialized from {Message} to JSON {MessageJson}", t, message, payload); + return payload; + } + + public object Deserialize(Type t, string payload) + { + var message = JsonConvert.DeserializeObject(payload, t, _serializerSettings); + _logger.LogDebug("Type {MessageType} deserialized from JSON {MessageJson} to {Message}", t, payload, message); + return message; + } + #endregion } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Serialization.Json/SerializationBuilderExtensions.cs b/src/SlimMessageBus.Host.Serialization.Json/SerializationBuilderExtensions.cs index 4f6c0bad..83502385 100644 --- a/src/SlimMessageBus.Host.Serialization.Json/SerializationBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.Serialization.Json/SerializationBuilderExtensions.cs @@ -24,7 +24,10 @@ public static TBuilder AddJsonSerializer(this TBuilder builder, Encodi { builder.RegisterSerializer(services => { + // Add the implementation services.TryAddSingleton(svp => new JsonMessageSerializer(jsonSerializerSettings ?? svp.GetService(), encoding, svp.GetRequiredService>())); + // Add the serializer as IMessageSerializer + services.TryAddSingleton(svp => svp.GetRequiredService() as IMessageSerializer); }); return builder; } diff --git a/src/SlimMessageBus.Host.Serialization.SystemTextJson/JsonMessageSerializer.cs b/src/SlimMessageBus.Host.Serialization.SystemTextJson/JsonMessageSerializer.cs index e929909f..fab9cff9 100644 --- a/src/SlimMessageBus.Host.Serialization.SystemTextJson/JsonMessageSerializer.cs +++ b/src/SlimMessageBus.Host.Serialization.SystemTextJson/JsonMessageSerializer.cs @@ -6,7 +6,7 @@ /// /// Implementation of using . /// -public class JsonMessageSerializer : IMessageSerializer +public class JsonMessageSerializer : IMessageSerializer, IMessageSerializer { /// /// options for the JSON serializer. By default adds converter. @@ -30,9 +30,23 @@ public virtual JsonSerializerOptions CreateDefaultOptions() return options; } + #region Implementation of IMessageSerializer + public byte[] Serialize(Type t, object message) => JsonSerializer.SerializeToUtf8Bytes(message, t, Options); public object Deserialize(Type t, byte[] payload) => - JsonSerializer.Deserialize(payload, t, Options)!; + JsonSerializer.Deserialize(payload, t, Options)!; + + #endregion + + #region Implementation of IMessageSerializer + + string IMessageSerializer.Serialize(Type t, object message) + => JsonSerializer.Serialize(message, t, Options); + + object IMessageSerializer.Deserialize(Type t, string payload) + => JsonSerializer.Deserialize(payload, t, Options)!; + + #endregion } diff --git a/src/SlimMessageBus.Host.Serialization.SystemTextJson/SerializationBuilderExtensions.cs b/src/SlimMessageBus.Host.Serialization.SystemTextJson/SerializationBuilderExtensions.cs index e9af7d27..e8725b31 100644 --- a/src/SlimMessageBus.Host.Serialization.SystemTextJson/SerializationBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.Serialization.SystemTextJson/SerializationBuilderExtensions.cs @@ -19,7 +19,10 @@ public static TBuilder AddJsonSerializer(this TBuilder builder, JsonSe { builder.RegisterSerializer(services => { + // Add the implementation services.TryAddSingleton(svp => new JsonMessageSerializer(options ?? svp.GetService())); + // Add the serializer as IMessageSerializer + services.TryAddSingleton(svp => svp.GetRequiredService() as IMessageSerializer); }); return builder; } diff --git a/src/SlimMessageBus.Host.Serialization/IMessageSerializer.cs b/src/SlimMessageBus.Host.Serialization/IMessageSerializer.cs index 191719f4..060ee3c5 100644 --- a/src/SlimMessageBus.Host.Serialization/IMessageSerializer.cs +++ b/src/SlimMessageBus.Host.Serialization/IMessageSerializer.cs @@ -1,7 +1,18 @@ namespace SlimMessageBus.Host.Serialization; -public interface IMessageSerializer +/// +/// Serializer for messages into byte[]. +/// +public interface IMessageSerializer : IMessageSerializer { - byte[] Serialize(Type t, object message); - object Deserialize(Type t, byte[] payload); +} + +/// +/// Serializer for messages into the given payload type (byte[] etc). +/// +/// +public interface IMessageSerializer +{ + TPayload Serialize(Type t, object message); + object Deserialize(Type t, TPayload payload); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs b/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs index 305bd7a8..ecb5a577 100644 --- a/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs +++ b/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs @@ -11,7 +11,7 @@ protected override void Build() { base.Build(); - AddInit(ProvisionTopology()); + InitTaskList.Add(ProvisionTopology, CancellationToken); } public override async Task ProvisionTopology() diff --git a/src/SlimMessageBus.Host/Collections/AsyncTaskList.cs b/src/SlimMessageBus.Host/Collections/AsyncTaskList.cs new file mode 100644 index 00000000..41b70d8a --- /dev/null +++ b/src/SlimMessageBus.Host/Collections/AsyncTaskList.cs @@ -0,0 +1,58 @@ +namespace SlimMessageBus.Host.Collections; + +public interface IAsyncTaskList +{ + void Add(Func taskFactory, CancellationToken cancellationToken); + Task EnsureAllFinished(); +} + +/// +/// Tracks a list of Tasks that have to be awaited before we can proceed. +/// +public class AsyncTaskList : IAsyncTaskList +{ + private readonly object _currentTaskLock = new(); + private Task _currentTask = null; + + public void Add(Func taskFactory, CancellationToken cancellationToken) + { + static async Task AddNext(Task prevTask, Func taskFactory) + { + await prevTask; + await taskFactory(); + } + + lock (_currentTaskLock) + { + var prevTask = _currentTask; + _currentTask = prevTask != null + ? AddNext(prevTask, taskFactory) + : taskFactory(); + } + } + + + /// + /// Awaits (if any) bus intialization tasks (e.g. topology provisining) before we can produce message into the bus (or consume messages). + /// + /// + public async Task EnsureAllFinished() + { + var initTask = _currentTask; + if (initTask != null) + { + await initTask.ConfigureAwait(false); + + lock (_currentTaskLock) + { + // Clear if await finished and the current task chain was the one we awaited + if (ReferenceEquals(_currentTask, initTask)) + { + _currentTask = null; + } + } + } + } + + +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/Delegates.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/Delegates.cs index dfe47783..c6975bb3 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/Delegates.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/Delegates.cs @@ -4,9 +4,10 @@ /// Provides the message payload (binary) from the transport message. /// /// +/// /// /// -public delegate byte[] MessagePayloadProvider(T transportMessage); +public delegate TPayload MessagePayloadProvider(T transportMessage); /// /// Initializes the consumer context from the specified transport message. diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseConsumer.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseConsumer.cs deleted file mode 100644 index eace101f..00000000 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IResponseConsumer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SlimMessageBus.Host; - -public interface IResponseConsumer -{ - Task OnResponseArrived(byte[] responsePayload, string path, IReadOnlyDictionary responseHeaders); -} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs index e5f8fb5d..10242fc2 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs @@ -1,44 +1,121 @@ namespace SlimMessageBus.Host; +public abstract class ResponseMessageProcessor +{ +} + /// /// The implementation that processes the responses arriving to the bus. /// -/// -public class ResponseMessageProcessor : IMessageProcessor +/// +public class ResponseMessageProcessor : ResponseMessageProcessor, IMessageProcessor { - private readonly ILogger> _logger; + private readonly ILogger _logger; private readonly RequestResponseSettings _requestResponseSettings; - private readonly IResponseConsumer _responseConsumer; private readonly IReadOnlyCollection _consumerSettings; - private readonly MessagePayloadProvider _messagePayloadProvider; + private readonly MessageProvider _messageProvider; + private readonly IPendingRequestStore _pendingRequestStore; + private readonly ICurrentTimeProvider _currentTimeProvider; - public ResponseMessageProcessor(ILoggerFactory loggerFactory, RequestResponseSettings requestResponseSettings, IResponseConsumer responseConsumer, MessagePayloadProvider messagePayloadProvider) + public ResponseMessageProcessor(ILoggerFactory loggerFactory, + RequestResponseSettings requestResponseSettings, + MessageProvider messageProvider, + IPendingRequestStore pendingRequestStore, + ICurrentTimeProvider currentTimeProvider) { if (loggerFactory is null) throw new ArgumentNullException(nameof(loggerFactory)); - _logger = loggerFactory.CreateLogger>(); + _logger = loggerFactory.CreateLogger(); _requestResponseSettings = requestResponseSettings ?? throw new ArgumentNullException(nameof(requestResponseSettings)); - _responseConsumer = responseConsumer ?? throw new ArgumentNullException(nameof(responseConsumer)); - _consumerSettings = new List { _requestResponseSettings }; - _messagePayloadProvider = messagePayloadProvider ?? throw new ArgumentNullException(nameof(messagePayloadProvider)); + _consumerSettings = [_requestResponseSettings]; + _messageProvider = messageProvider ?? throw new ArgumentNullException(nameof(messageProvider)); + _pendingRequestStore = pendingRequestStore; + _currentTimeProvider = currentTimeProvider; } public IReadOnlyCollection ConsumerSettings => _consumerSettings; - public async Task ProcessMessage(TMessage transportMessage, IReadOnlyDictionary messageHeaders, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) + public Task ProcessMessage(TTransportMessage transportMessage, IReadOnlyDictionary messageHeaders, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) { + Exception ex; try { - var messagePayload = _messagePayloadProvider(transportMessage); - var exception = await _responseConsumer.OnResponseArrived(messagePayload, _requestResponseSettings.Path, messageHeaders); - return new(exception, _requestResponseSettings, null); + ex = OnResponseArrived(transportMessage, _requestResponseSettings.Path, messageHeaders); } catch (Exception e) { _logger.LogError(e, "Error occurred while consuming response message, {Message}", transportMessage); - // We can only continue and process all messages in the lease - return new(e, _requestResponseSettings, null); + ex = e; } + return Task.FromResult(new ProcessMessageResult(ex, _requestResponseSettings, null)); } + + /// + /// Should be invoked by the concrete bus implementation whenever there is a message arrived on the reply to topic. + /// + /// The response message + /// + /// The response message headers + /// + private Exception OnResponseArrived(TTransportMessage transportMessage, string path, IReadOnlyDictionary responseHeaders) + { + if (!responseHeaders.TryGetHeader(ReqRespMessageHeaders.RequestId, out string requestId)) + { + return new ConsumerMessageBusException($"The response message arriving on path {path} did not have the {ReqRespMessageHeaders.RequestId} header. Unable to math the response with the request. This likely indicates a misconfiguration."); + } + + var requestState = _pendingRequestStore.GetById(requestId); + if (requestState == null) + { + _logger.LogDebug("The response message for request id {RequestId} arriving on path {Path} will be disregarded. Either the request had already expired, had been cancelled or it was already handled (this response message is a duplicate).", requestId, path); + // ToDo: add and API hook to these kind of situation + return null; + } + + try + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + var tookTimespan = _currentTimeProvider.CurrentTime.Subtract(requestState.Created); + _logger.LogDebug("Response arrived for {Request} on path {Path} (time: {RequestTime} ms)", requestState, path, tookTimespan); + } + + if (responseHeaders.TryGetHeader(ReqRespMessageHeaders.Error, out string errorMessage)) + { + // error response arrived + + var responseException = new RequestHandlerFaultedMessageBusException(errorMessage); + _logger.LogDebug(responseException, "Response arrived for {Request} on path {Path} with error: {ResponseError}", requestState, path, responseException.Message); + requestState.TaskCompletionSource.TrySetException(responseException); + } + else + { + // response arrived + try + { + // deserialize the response message + var response = transportMessage != null + ? _messageProvider(requestState.ResponseType, transportMessage) + : null; + + // resolve the response + requestState.TaskCompletionSource.TrySetResult(response); + } + catch (Exception e) + { + _logger.LogDebug(e, "Could not deserialize the response message for {Request} arriving on path {Path}", requestState, path); + requestState.TaskCompletionSource.TrySetException(e); + } + } + } + finally + { + // remove the request from the queue + _pendingRequestStore.Remove(requestId); + } + + return null; + } + } diff --git a/src/SlimMessageBus.Host/Helpers/CompatMethods.cs b/src/SlimMessageBus.Host/Helpers/CompatMethods.cs index 3407478f..145d8865 100644 --- a/src/SlimMessageBus.Host/Helpers/CompatMethods.cs +++ b/src/SlimMessageBus.Host/Helpers/CompatMethods.cs @@ -23,7 +23,34 @@ public static bool TryAdd(this IDictionary dict, K key, V value) return false; } - public static HashSet ToHashSet(this IEnumerable items) => new HashSet(items); + public static HashSet ToHashSet(this IEnumerable items) => new(items); + +#if NETSTANDARD2_0 + + public static IEnumerable> Chunk(this IEnumerable items, int size) + { + var chunk = new List(size); + + foreach (var item in items) + { + if (chunk.Count < size) + { + chunk.Add(item); + } + else + { + yield return chunk; + chunk = new List(size); + } + } + + if (chunk.Count > 0) + { + yield return chunk; + } + } + +#endif } public static class TimeSpanExtensions diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index d023c2ff..e5e99498 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -5,23 +5,18 @@ namespace SlimMessageBus.Host; using SlimMessageBus.Host.Consumer; using SlimMessageBus.Host.Services; - -public abstract class MessageBusBase : MessageBusBase where TProviderSettings : class + +public abstract class MessageBusBase(MessageBusSettings settings, TProviderSettings providerSettings) : MessageBusBase(settings) + where TProviderSettings : class { - public TProviderSettings ProviderSettings { get; } - - protected MessageBusBase(MessageBusSettings settings, TProviderSettings providerSettings) : base(settings) - { - ProviderSettings = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); - } -} + public TProviderSettings ProviderSettings { get; } = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); +} public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, IMessageHeadersFactory, IResponseProducer, - IResponseConsumer, ITransportProducer, ITransportBulkProducer { @@ -30,6 +25,7 @@ public abstract class MessageBusBase : IDisposable, IAsyncDisposable, private IMessageSerializer _serializer; private readonly MessageHeaderService _headerService; private readonly List _consumers = []; + public ILoggerFactory LoggerFactory { get; protected set; } /// /// Special market reference that signifies a dummy producer settings for response types. @@ -38,8 +34,6 @@ public abstract class MessageBusBase : IDisposable, IAsyncDisposable, public RuntimeTypeCache RuntimeTypeCache { get; } - public ILoggerFactory LoggerFactory { get; } - public virtual MessageBusSettings Settings { get; } public virtual IMessageSerializer Serializer => _serializer ??= GetSerializer(); @@ -64,9 +58,14 @@ public abstract class MessageBusBase : IDisposable, IAsyncDisposable, protected bool IsDisposed { get; private set; } #endregion - - private readonly object _initTaskLock = new(); - private Task _initTask = null; + + /// + /// Maintains a list of tasks that should be completed before the bus can produce the first message or start consumers. + /// Add async things like + /// - connection creations here to the underlying transport client + /// - provision topology + /// + protected readonly AsyncTaskList InitTaskList = new(); #region Start & Stop @@ -110,36 +109,6 @@ protected MessageBusBase(MessageBusSettings settings) PendingRequestStore = PendingRequestManager.Store; } - protected void AddInit(Task task) - { - lock (_initTaskLock) - { - var prevInitTask = _initTask; - _initTask = prevInitTask?.ContinueWith(_ => task, CancellationToken) ?? task; - } - } - - /// - /// Awaits (if any) bus intialization (e.g. topology provisining) before we can produce message into the bus. - /// - /// - protected async Task EnsureInitFinished() - { - var initTask = _initTask; - if (initTask != null) - { - await initTask.ConfigureAwait(false); - - lock (_initTaskLock) - { - if (ReferenceEquals(_initTask, initTask)) - { - _initTask = null; - } - } - } - } - protected virtual IMessageSerializer GetSerializer() => Settings.GetSerializer(Settings.ServiceProvider); protected virtual IMessageBusSettingsValidationService ValidationService { get => new DefaultMessageBusSettingsValidationService(Settings); } @@ -154,7 +123,7 @@ protected void OnBuildProvider() Build(); // Notify the bus has been created - before any message can be produced - AddInit(OnBusLifecycle(MessageBusLifecycleEventType.Created)); + InitTaskList.Add(() => OnBusLifecycle(MessageBusLifecycleEventType.Created), CancellationToken); // Auto start consumers if enabled if (Settings.AutoStartConsumers) @@ -222,7 +191,7 @@ public async Task Start() try { - await EnsureInitFinished(); + await InitTaskList.EnsureAllFinished(); _logger.LogInformation("Starting consumers for {BusName} bus...", Name); await OnBusLifecycle(MessageBusLifecycleEventType.Starting).ConfigureAwait(false); @@ -261,7 +230,7 @@ public async Task Stop() try { - await EnsureInitFinished(); + await InitTaskList.EnsureAllFinished(); _logger.LogInformation("Stopping consumers for {BusName} bus...", Name); await OnBusLifecycle(MessageBusLifecycleEventType.Stopping).ConfigureAwait(false); @@ -450,7 +419,7 @@ public async virtual Task ProducePublish(object message, string path = null, IDi { if (message == null) throw new ArgumentNullException(nameof(message)); AssertActive(); - await EnsureInitFinished(); + await InitTaskList.EnsureAllFinished(); // check if the cancellation was already requested cancellationToken.ThrowIfCancellationRequested(); @@ -548,8 +517,8 @@ public virtual async Task ProduceSend(object request, stri { if (request == null) throw new ArgumentNullException(nameof(request)); AssertActive(); - AssertRequestResponseConfigured(); - await EnsureInitFinished(); + AssertRequestResponseConfigured(); + await InitTaskList.EnsureAllFinished(); // check if the cancellation was already requested cancellationToken.ThrowIfCancellationRequested(); @@ -669,78 +638,6 @@ public virtual Task ProduceResponse(string requestId, object request, IReadOnlyD return ProduceToTransport(response, responseType, (string)replyTo, responseHeaders, null, cancellationToken); } - /// - /// Should be invoked by the concrete bus implementation whenever there is a message arrived on the reply to topic. - /// - /// - /// - /// - public virtual Task OnResponseArrived(byte[] responsePayload, string path, IReadOnlyDictionary responseHeaders) - { - if (!responseHeaders.TryGetHeader(ReqRespMessageHeaders.RequestId, out string requestId)) - { - _logger.LogError("The response message arriving on path {Path} did not have the {HeaderName} header. Unable to math the response with the request. This likely indicates a misconfiguration.", path, ReqRespMessageHeaders.RequestId); - return Task.FromResult(null); - } - - Exception responseException = null; - if (responseHeaders.TryGetHeader(ReqRespMessageHeaders.Error, out string errorMessage)) - { - responseException = new RequestHandlerFaultedMessageBusException(errorMessage); - } - - var requestState = PendingRequestStore.GetById(requestId); - if (requestState == null) - { - _logger.LogDebug("The response message for request id {RequestId} arriving on path {Path} will be disregarded. Either the request had already expired, had been cancelled or it was already handled (this response message is a duplicate).", requestId, path); - - // ToDo: add and API hook to these kind of situation - return Task.FromResult(null); - } - - try - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - var tookTimespan = CurrentTimeProvider.CurrentTime.Subtract(requestState.Created); - _logger.LogDebug("Response arrived for {Request} on path {Path} (time: {RequestTime} ms)", requestState, path, tookTimespan); - } - - if (responseException != null) - { - // error response arrived - _logger.LogDebug(responseException, "Response arrived for {Request} on path {Path} with error: {ResponseError}", requestState, path, responseException.Message); - - requestState.TaskCompletionSource.TrySetException(responseException); - } - else - { - // response arrived - try - { - // deserialize the response message - var response = responsePayload != null - ? Serializer.Deserialize(requestState.ResponseType, responsePayload) - : null; - - // resolve the response - requestState.TaskCompletionSource.TrySetResult(response); - } - catch (Exception e) - { - _logger.LogDebug(e, "Could not deserialize the response message for {Request} arriving on path {Path}", requestState, path); - requestState.TaskCompletionSource.TrySetException(e); - } - } - } - finally - { - // remove the request from the queue - PendingRequestStore.Remove(requestId); - } - return Task.FromResult(null); - } - /// /// Generates unique request IDs /// diff --git a/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs b/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs index f6ee7774..a4731927 100644 --- a/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs +++ b/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs @@ -7,7 +7,6 @@ public class PendingRequestManager : IPendingRequestManager, IDisposable { private readonly ILogger _logger; - private readonly TimeSpan _timerInterval; private readonly Timer _timer; private readonly object _timerSync = new(); @@ -23,9 +22,10 @@ public PendingRequestManager(IPendingRequestStore store, ICurrentTimeProvider ti Store = store; _onRequestTimeout = onRequestTimeout; - _timeProvider = timeProvider; - _timerInterval = interval ?? TimeSpan.FromSeconds(3); - _timer = new Timer(state => TimerCallback(), null, _timerInterval, _timerInterval); + _timeProvider = timeProvider; + + var timerInterval = interval ?? TimeSpan.FromSeconds(3); + _timer = new Timer(state => TimerCallback(), null, timerInterval, timerInterval); } #region IDisposable diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index 3cb22665..f96a2a9c 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -139,6 +139,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{CBE53E71-7 ..\CONTRIBUTING.md = ..\CONTRIBUTING.md ..\docs\intro.md = ..\docs\intro.md ..\docs\NuGet.md = ..\docs\NuGet.md + ..\docs\provider_amazon_sqs.md = ..\docs\provider_amazon_sqs.md ..\docs\provider_azure_eventhubs.md = ..\docs\provider_azure_eventhubs.md ..\docs\provider_azure_servicebus.md = ..\docs\provider_azure_servicebus.md ..\docs\provider_hybrid.md = ..\docs\provider_hybrid.md @@ -278,6 +279,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.AspNetC EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Artifacts", "Artifacts", "{0F4AD1B7-157D-4ABC-A379-68BF207F2FC3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.AmazonSQS", "SlimMessageBus.Host.AmazonSQS\SlimMessageBus.Host.AmazonSQS.csproj", "{4DF4BC7C-5EE3-4310-BC40-054C1494444E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.AmazonSQS.Test", "Tests\SlimMessageBus.Host.AmazonSQS.Test\SlimMessageBus.Host.AmazonSQS.Test.csproj", "{9255A33D-9697-4E69-9418-AD31656FF8AC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -846,6 +851,22 @@ Global {9FCBF788-1F0C-43E2-909D-1F96B2685F38}.Release|Any CPU.Build.0 = Release|Any CPU {9FCBF788-1F0C-43E2-909D-1F96B2685F38}.Release|x86.ActiveCfg = Release|Any CPU {9FCBF788-1F0C-43E2-909D-1F96B2685F38}.Release|x86.Build.0 = Release|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Debug|x86.Build.0 = Debug|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Release|Any CPU.Build.0 = Release|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Release|x86.ActiveCfg = Release|Any CPU + {4DF4BC7C-5EE3-4310-BC40-054C1494444E}.Release|x86.Build.0 = Release|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Debug|x86.ActiveCfg = Debug|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Debug|x86.Build.0 = Debug|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|Any CPU.Build.0 = Release|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|x86.ActiveCfg = Release|Any CPU + {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -932,6 +953,8 @@ Global {46C40625-D1AC-4EA1-9562-4F1837D417CE} = {A5B15524-93B8-4CCE-AC6D-A22984498BA0} {5250E48D-36C7-4214-8D7E-5924A9E337C6} = {59F88FB5-6D19-4520-87E8-227B3539BBB3} {9FCBF788-1F0C-43E2-909D-1F96B2685F38} = {9F005B5C-A856-4351-8C0C-47A8B785C637} + {4DF4BC7C-5EE3-4310-BC40-054C1494444E} = {9291D340-B4FA-44A3-8060-C14743FB1712} + {9255A33D-9697-4E69-9418-AD31656FF8AC} = {9F005B5C-A856-4351-8C0C-47A8B785C637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {435A0D65-610C-4B84-B1AA-2C7FBE72DB80} diff --git a/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/GlobalUsings.cs new file mode 100644 index 00000000..b9eb4462 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/GlobalUsings.cs @@ -0,0 +1,12 @@ +global using FluentAssertions; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; + +global using SecretStore; + +global using SlimMessageBus.Host.Test.Common.IntegrationTest; + +global using Xunit; +global using Xunit.Abstractions; diff --git a/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/Headers/DefaultSqsHeaderSerializerTest.cs b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/Headers/DefaultSqsHeaderSerializerTest.cs new file mode 100644 index 00000000..daf379a3 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/Headers/DefaultSqsHeaderSerializerTest.cs @@ -0,0 +1,32 @@ +namespace SlimMessageBus.Host.AmazonSQS.Test; + +public class DefaultSqsHeaderSerializerTest +{ + public static readonly TheoryData Data = new() + { + { null, null }, + { 10, 10 }, + { false, false }, + { true, true }, + { "string", "string" }, + { DateTime.Now.Date, DateTime.Now.Date }, + { Guid.Parse("{529194F3-AEAA-497D-A495-C84DD67C2DDA}"), Guid.Parse("{529194F3-AEAA-497D-A495-C84DD67C2DDA}") }, + { Guid.Empty, Guid.Empty }, + }; + + [Theory] + [MemberData(nameof(Data))] + public void When_Serialize_Given_VariousValueTypes_Then_RestoresTheValue(object value, object expectedValue) + { + // arrange + var serializer = new DefaultSqsHeaderSerializer(); + var key = "key"; + + // act + var result = serializer.Serialize(key, value); + var resultValue = serializer.Deserialize(key, result); + + // assert + resultValue.Should().Be(expectedValue); + } +} diff --git a/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SlimMessageBus.Host.AmazonSQS.Test.csproj b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SlimMessageBus.Host.AmazonSQS.Test.csproj new file mode 100644 index 00000000..87b829ac --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SlimMessageBus.Host.AmazonSQS.Test.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + Always + + + PreserveNewest + + + + diff --git a/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SqsMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SqsMessageBusIt.cs new file mode 100644 index 00000000..090b79f0 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/SqsMessageBusIt.cs @@ -0,0 +1,327 @@ +namespace SlimMessageBus.Host.AmazonSQS.Test; + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +using Amazon.SQS.Model; + +using Microsoft.Extensions.Logging; + +using SlimMessageBus.Host.Serialization.SystemTextJson; + +/// +/// Runs the integration tests for the . +/// Notice that this test needs to run against a real Amazon SQS infrastructure. +/// Inside the GitHub Actions pipeline, the Amazon SQS infrastructure is shared, and this tests attempts to isolate itself by using unique queue names. +/// +[Trait("Category", "Integration")] +[Trait("Transport", "AmazonSQS")] +public class SqsMessageBusIt(ITestOutputHelper output) : BaseIntegrationTest(output) +{ + private const int NumberOfMessages = 100; + private const string QueueNamePrefix = "SMB"; + private const string CreatedDateTag = "CreatedDate"; + + protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) + { + var today = DateTime.UtcNow.Date.ToString("o"); + + services.AddSingleton>(); + + services.AddSlimMessageBus((mbb) => + { + mbb.AddServicesFromAssemblyContaining(); + mbb.AddJsonSerializer(); + ApplyBusConfiguration(mbb); + }); + + void AdditionalSqsSetup(SqsMessageBusSettings cfg) + { + cfg.TopologyProvisioning.CreateQueueOptions = opts => + { + // Tag the queue with the creation date + opts.Tags.Add(CreatedDateTag, today); + }; + cfg.TopologyProvisioning.OnProvisionTopology = async (client, provision) => + { + // Remove all older test queues (SQS does not support queue auto deletion) + var r = await client.ListQueuesAsync(QueueNamePrefix); + foreach (var queueUrl in r.QueueUrls) + { + var tagsResponse = await client.ListQueueTagsAsync(new ListQueueTagsRequest { QueueUrl = queueUrl }); + if (!tagsResponse.Tags.TryGetValue(CreatedDateTag, out var createdDateTag) || createdDateTag != today) + { + await client.DeleteQueueAsync(queueUrl); + } + } + await provision(); + }; + } + + var accessKey = Secrets.Service.PopulateSecrets(configuration["Amazon:AccessKey"]); + var secretAccessKey = Secrets.Service.PopulateSecrets(configuration["Amazon:SecretAccessKey"]); + + var roleArn = Secrets.Service.PopulateSecrets(configuration["Amazon:RoleArn"]); + var roleSessionName = Secrets.Service.PopulateSecrets(configuration["Amazon:RoleSessionName"]); + + // doc:fragment:ExampleSetup + services.AddSlimMessageBus((mbb) => + { + mbb.WithProviderAmazonSQS(cfg => + { + cfg.UseRegion(Amazon.RegionEndpoint.EUCentral1); + + // Use static credentials: https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html + cfg.UseCredentials(accessKey, secretAccessKey); + + // Use temporary credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS + //cfg.UseTemporaryCredentials(roleArn, roleSessionName); + + AdditionalSqsSetup(cfg); + }); + }); + // doc:fragment:ExampleSetup + } + + public IMessageBus MessageBus => ServiceProvider.GetRequiredService(); + + [Theory] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(true, false)] + public async Task BasicQueue(bool fifo, bool bulkProduce) + { + var queue = string.Concat(QueueName(), fifo ? ".fifo" : string.Empty); + AddBusConfiguration(mbb => + { + mbb + .Produce(x => + { + x.DefaultQueue(queue); + if (fifo) + { + x.EnableFifo(f => f + .DeduplicationId((m, h) => (m.Counter + 1000).ToString()) + .GroupId((m, h) => m.Counter % 2 == 0 ? "even" : "odd") + ); + } + }) + .Consume(x => x + .Queue(queue) + .WithConsumer() + .WithConsumer() + .Instances(20)); + }); + + await BasicProducerConsumer(1, bulkProduce: bulkProduce); + } + + public class TestData + { + public List ProducedMessages { get; set; } + public IReadOnlyCollection ConsumedMessages { get; set; } + } + + private async Task BasicProducerConsumer(int expectedMessageCopies, Action additionalAssertion = null, bool bulkProduce = false) + { + // arrange + var testMetric = ServiceProvider.GetRequiredService(); + var consumedMessages = ServiceProvider.GetRequiredService>(); + + var messageBus = MessageBus; + + // act + + // publish + var stopwatch = Stopwatch.StartNew(); + + var producedMessages = Enumerable + .Range(0, NumberOfMessages) + .Select(i => i % 2 == 0 ? new PingMessage(i) : new PingDerivedMessage(i)) + .ToList(); + + if (bulkProduce) + { + await messageBus.Publish(producedMessages); + } + else + { + foreach (var producedMessage in producedMessages) + { + // Send them in order + await messageBus.Publish(producedMessage); + } + } + + stopwatch.Stop(); + Logger.LogInformation("Published {Count} messages in {Elapsed}", producedMessages.Count, stopwatch.Elapsed); + + // consume + stopwatch.Restart(); + + await consumedMessages.WaitUntilArriving(newMessagesTimeout: 5); + + stopwatch.Stop(); + + // assert + + // ensure number of instances of consumers created matches + var expectedConsumedCount = producedMessages.Count + producedMessages.OfType().Count(); + testMetric.CreatedConsumerCount.Should().Be(expectedConsumedCount * expectedMessageCopies); + consumedMessages.Count.Should().Be(expectedConsumedCount * expectedMessageCopies); + + // ... the content should match + foreach (var producedMessage in producedMessages) + { + var messageCopies = consumedMessages.Snapshot() + .Count(x => + x.Message.Counter == producedMessage.Counter + && x.Message.Value == producedMessage.Value + /*&& x.MessageId == GetMessageId(x.Message)*/); + messageCopies.Should().Be((producedMessage is PingDerivedMessage ? 2 : 1) * expectedMessageCopies); + } + + additionalAssertion?.Invoke(new TestData { ProducedMessages = producedMessages, ConsumedMessages = consumedMessages.Snapshot() }); + } + + [Fact] + public async Task BasicReqRespOnQueue() + { + var queue = QueueName(); + var responseQueue = $"{queue}-resp"; + + AddBusConfiguration(mbb => + { + mbb.Produce(x => + { + x.DefaultQueue(queue); + }) + .Handle(x => x.Queue(queue) + .WithHandler() + .Instances(20)) + .ExpectRequestResponses(x => + { + x.ReplyToQueue(responseQueue); + x.DefaultTimeout(TimeSpan.FromSeconds(60)); + }); + }); + await BasicReqResp(); + } + + private async Task BasicReqResp() + { + // arrange + var messageBus = MessageBus; + + // act + + // publish + var stopwatch = Stopwatch.StartNew(); + + var requests = Enumerable + .Range(0, NumberOfMessages) + .Select(i => new EchoRequest { Index = i, Message = $"Echo {i}" }) + .ToList(); + + var responses = new ConcurrentBag>(); + var responseTasks = requests.Select(async req => + { + var resp = await messageBus.Send(req).ConfigureAwait(false); + responses.Add(Tuple.Create(req, resp)); + }); + await Task.WhenAll(responseTasks).ConfigureAwait(false); + + stopwatch.Stop(); + Logger.LogInformation("Published and received {Count} messages in {Elapsed}", responses.Count, stopwatch.Elapsed); + + // assert + + // all messages got back + responses.Count.Should().Be(NumberOfMessages); + responses.All(x => x.Item1.Message == x.Item2.Message).Should().BeTrue(); + } + + private static string QueueName([CallerMemberName] string testName = null) + => $"{QueueNamePrefix}_{DateTimeOffset.UtcNow.Ticks}_{testName}"; +} + +public record TestEvent(PingMessage Message); + +public record PingMessage(int Counter) +{ + public Guid Value { get; set; } = Guid.NewGuid(); + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} + +public record PingDerivedMessage(int Counter) : PingMessage(Counter); + +public class PingConsumer : IConsumer, IConsumerWithContext +{ + private readonly ILogger _logger; + private readonly TestEventCollector _messages; + + public PingConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) + { + _logger = logger; + _messages = messages; + testMetric.OnCreatedConsumer(); + } + + public IConsumerContext Context { get; set; } + + public Task OnHandle(PingMessage message, CancellationToken cancellationToken) + { + var transportMessage = Context.GetTransportMessage(); + + _messages.Add(new(message)); + + _logger.LogInformation("Got message {Counter:000} on path {Path} message id {MessageId}.", message.Counter, Context.Path, transportMessage.MessageId); + return Task.CompletedTask; + } +} + +public class PingDerivedConsumer : IConsumer, IConsumerWithContext +{ + private readonly ILogger _logger; + private readonly TestEventCollector _messages; + + public PingDerivedConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) + { + _logger = logger; + _messages = messages; + testMetric.OnCreatedConsumer(); + } + + public IConsumerContext Context { get; set; } + + public Task OnHandle(PingDerivedMessage message, CancellationToken cancellationToken) + { + var transportMessage = Context.GetTransportMessage(); + + _messages.Add(new(message)); + + _logger.LogInformation("Got message {Counter:000} on path {Path} message id {MessageId}.", message.Counter, Context.Path, transportMessage.MessageId); + return Task.CompletedTask; + } +} + +public record EchoRequest : IRequest +{ + public int Index { get; set; } + public string Message { get; set; } +} + +public record EchoResponse(string Message); + +public class EchoRequestHandler : IRequestHandler +{ + public EchoRequestHandler(TestMetric testMetric) + { + testMetric.OnCreatedConsumer(); + } + + public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) + => Task.FromResult(new EchoResponse(request.Message)); +} \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/appsettings.json new file mode 100644 index 00000000..f8ba05d9 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.AmazonSQS.Test/appsettings.json @@ -0,0 +1,15 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, + "Amazon": { + "AccessKey": "{{amazon_access_key}}", + "SecretAccessKey": "{{amazon_secret_access_key}}" + } +} diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs index 562aec11..db9d3e56 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs @@ -476,7 +476,5 @@ public EchoRequestHandler(TestMetric testMetric) } public Task OnHandle(EchoRequest request, CancellationToken cancellationToken) - { - return Task.FromResult(new EchoResponse(request.Message)); - } + => Task.FromResult(new EchoResponse(request.Message)); } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForResponsesTest.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForResponsesTest.cs index 53ec6a22..8139f91b 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForResponsesTest.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaPartitionConsumerForResponsesTest.cs @@ -10,7 +10,9 @@ public class KafkaPartitionConsumerForResponsesTest : IDisposable private readonly TopicPartition _topicPartition; private readonly Mock _commitControllerMock = new(); private readonly Mock _checkpointTrigger = new(); - private KafkaPartitionConsumerForResponses _subject; + private KafkaPartitionConsumerForResponses _subject; + private readonly Mock> _messageProvider = new(); + private readonly Mock _pendingRequestStore = new(); public KafkaPartitionConsumerForResponsesTest() { @@ -30,7 +32,15 @@ public KafkaPartitionConsumerForResponsesTest() } }; - _subject = new KafkaPartitionConsumerForResponses(_messageBusMock.Bus.LoggerFactory, requestResponseSettings, requestResponseSettings.GetGroup(), _topicPartition, _commitControllerMock.Object, _messageBusMock.Bus, _messageBusMock.SerializerMock.Object) + _subject = new KafkaPartitionConsumerForResponses(_messageBusMock.Bus.LoggerFactory, + requestResponseSettings, + requestResponseSettings.GetGroup(), + _topicPartition, + _commitControllerMock.Object, + _messageProvider.Object, + _pendingRequestStore.Object, + _messageBusMock.CurrentTimeProvider, + new DefaultKafkaHeaderSerializer()) { CheckpointTrigger = _checkpointTrigger.Object }; @@ -75,14 +85,26 @@ public void When_OnPartitionAssigned_Then_ShouldResetTrigger() public async Task When_OnMessage_Given_SuccessMessage_ThenOnResponseArrived() { // arrange - var message = GetSomeMessage(); - _subject.OnPartitionAssigned(message.TopicPartition); + var requestId = "1"; + var request = new SomeMessage(); + var responseTransportMessage = GetSomeMessage(); + responseTransportMessage.Message.Headers.Add(ReqRespMessageHeaders.RequestId, Encoding.UTF8.GetBytes(requestId)); + var response = new SomeMessage(); + + _subject.OnPartitionAssigned(responseTransportMessage.TopicPartition); + + var pendingRequestState = new PendingRequestState(requestId, request, request.GetType(), response.GetType(), DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddHours(1), default); + _pendingRequestStore.Setup(x => x.GetById(requestId)).Returns(pendingRequestState); + + _messageProvider.Setup(x => x(response.GetType(), responseTransportMessage)).Returns(response); // act - await _subject.OnMessage(message); + await _subject.OnMessage(responseTransportMessage); - // assert - _messageBusMock.BusMock.Verify(x => x.OnResponseArrived(message.Message.Value, message.Topic, It.Is>(x => x.ContainsKey("test-header"))), Times.Once); + // assert + pendingRequestState.TaskCompletionSource.Task.IsCompleted.Should().BeTrue(); + var result = await pendingRequestState.TaskCompletionSource.Task; + result.Should().Be(response); } [Fact] diff --git a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs index f19dea13..0a237e38 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs @@ -15,9 +15,13 @@ public class JsonMessageSerializerTests [Guid.Empty, "00000000-0000-0000-0000-000000000000"], ]; + public JsonMessageSerializerTests() + { + } + [Theory] [MemberData(nameof(Data))] - public void When_SerializeAndDeserialize_Given_TypeObject_Then_TriesToInferPrimitiveTypes(object value, object expectedValue) + public void When_SerializeAndDeserialize_Given_TypeObjectAndBytesPayload_Then_TriesToInferPrimitiveTypes(object value, object expectedValue) { // arrange var subject = new JsonMessageSerializer(); @@ -29,4 +33,19 @@ public void When_SerializeAndDeserialize_Given_TypeObject_Then_TriesToInferPrimi // assert deserializedValue.Should().Be(expectedValue); } + + [Theory] + [MemberData(nameof(Data))] + public void When_SerializeAndDeserialize_Given_TypeObjectAndStringPayload_Then_TriesToInferPrimitiveTypes(object value, object expectedValue) + { + // arrange + var subject = new JsonMessageSerializer() as IMessageSerializer; + + // act + var json = subject.Serialize(typeof(object), value); + var deserializedValue = subject.Deserialize(typeof(object), json); + + // assert + deserializedValue.Should().Be(expectedValue); + } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SerializationBuilderExtensionsTest.cs b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SerializationBuilderExtensionsTest.cs index c7305d61..a855004b 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SerializationBuilderExtensionsTest.cs +++ b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SerializationBuilderExtensionsTest.cs @@ -16,6 +16,7 @@ public void When_AddJsonSerializer_Given_Builder_Then_ServicesRegistered() builder.PostConfigurationActions.ToList().ForEach(action => action(services)); services.Should().ContainSingle(x => x.ServiceType == typeof(IMessageSerializer)); + services.Should().ContainSingle(x => x.ServiceType == typeof(IMessageSerializer)); services.Should().ContainSingle(x => x.ServiceType == typeof(JsonMessageSerializer)); } } diff --git a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs index 1f7fd888..b4af80c1 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs @@ -1,6 +1,5 @@ namespace SlimMessageBus.Host.Serialization.SystemTextJson.Test; - public class JsonMessageSerializerTests { public static IEnumerable Data => @@ -16,7 +15,7 @@ public class JsonMessageSerializerTests [Theory] [MemberData(nameof(Data))] - public void When_SerializeAndDeserialize_Given_TypeObject_Then_TriesToInferPrimitiveTypes(object value, object expectedValue) + public void When_SerializeAndDeserialize_Given_TypeObjectAndBytesPayload_Then_TriesToInferPrimitiveTypes(object value, object expectedValue) { // arrange var subject = new JsonMessageSerializer(); @@ -29,6 +28,21 @@ public void When_SerializeAndDeserialize_Given_TypeObject_Then_TriesToInferPrimi deserializedValue.Should().Be(expectedValue); } + [Theory] + [MemberData(nameof(Data))] + public void When_SerializeAndDeserialize_Given_TypeObjectAndStringPayload_Then_TriesToInferPrimitiveTypes(object value, object expectedValue) + { + // arrange + var subject = new JsonMessageSerializer() as IMessageSerializer; + + // act + var json = subject.Serialize(typeof(object), value); + var deserializedValue = subject.Deserialize(typeof(object), json); + + // assert + deserializedValue.Should().Be(expectedValue); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SerializationBuilderExtensionsTest.cs b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SerializationBuilderExtensionsTest.cs index 97a217f7..2863f76c 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SerializationBuilderExtensionsTest.cs +++ b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SerializationBuilderExtensionsTest.cs @@ -16,6 +16,7 @@ public void When_AddJsonSerializer_Given_Builder_Then_ServicesRegistered() builder.PostConfigurationActions.ToList().ForEach(action => action(services)); services.Should().ContainSingle(x => x.ServiceType == typeof(IMessageSerializer)); + services.Should().ContainSingle(x => x.ServiceType == typeof(IMessageSerializer)); services.Should().ContainSingle(x => x.ServiceType == typeof(JsonMessageSerializer)); } } diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs index 88db4f39..9d5d1577 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs @@ -74,10 +74,7 @@ protected async Task EnsureConsumersStarted() while (!consumerControl.IsStarted && timeout.ElapsedMilliseconds < 5000) await Task.Delay(100); } - public Task InitializeAsync() - { - return Task.CompletedTask; - } + public Task InitializeAsync() => Task.CompletedTask; async Task IAsyncLifetime.DisposeAsync() { diff --git a/src/Tests/SlimMessageBus.Host.Test/Collections/AsyncTaskListTests.cs b/src/Tests/SlimMessageBus.Host.Test/Collections/AsyncTaskListTests.cs new file mode 100644 index 00000000..4a15d096 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Test/Collections/AsyncTaskListTests.cs @@ -0,0 +1,36 @@ +namespace SlimMessageBus.Host.Test.Collections; + +using System.Collections.Concurrent; + +using SlimMessageBus.Host.Collections; + +public class AsyncTaskListTests +{ + [Fact] + public async Task Given_TaskAdded_When_EnsureAllFinished_Then_TaskIsCompleted() + { + // arrange + + var numberList = new ConcurrentQueue(); + + async Task RunTask(int n) + { + await Task.Delay(100); + numberList.Enqueue(n); + } + + var subject = new AsyncTaskList(); + subject.Add(() => RunTask(1), default); + subject.Add(() => RunTask(2), default); + + // act + await subject.EnsureAllFinished(); + + // assert + numberList.Should().HaveCount(2); + numberList.TryDequeue(out var n1).Should().BeTrue(); + n1.Should().Be(1); + numberList.TryDequeue(out var n2).Should().BeTrue(); + n2.Should().Be(2); + } +} diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs index 20f63294..771866b8 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs @@ -3,7 +3,9 @@ public class MessageBusTested : MessageBusBase { internal int _startedCount; - internal int _stoppedCount; + internal int _stoppedCount; + + public IMessageProcessor RequestResponseMessageProcessor { get; private set; } public MessageBusTested(MessageBusSettings settings, ICurrentTimeProvider currentTimeProvider) : base(settings) @@ -12,8 +14,19 @@ public MessageBusTested(MessageBusSettings settings, ICurrentTimeProvider curren OnReply = (type, payload, req) => null; CurrentTimeProvider = currentTimeProvider; - OnBuildProvider(); + OnBuildProvider(); } + + protected override async Task CreateConsumers() + { + await base.CreateConsumers(); + + if (Settings.RequestResponse != null) + { + RequestResponseMessageProcessor = new ResponseMessageProcessor(LoggerFactory, Settings.RequestResponse, (mt, m) => m, PendingRequestStore, CurrentTimeProvider); + AddConsumer(new MessageBusTestedConsumer(NullLogger.Instance)); + } + } public ProducerSettings Public_GetProducerSettings(Type messageType) => GetProducerSettings(messageType); @@ -54,11 +67,10 @@ public override async Task ProduceToTransport(object message, Type messageType, messageHeaders.TryGetHeader(ReqRespMessageHeaders.ReplyTo, out string replyTo); messageHeaders.TryGetHeader(ReqRespMessageHeaders.RequestId, out string requestId); - var responseHeaders = CreateHeaders(); + var responseHeaders = CreateHeaders() as Dictionary; responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); - var responsePayload = Serializer.Serialize(resp.GetType(), resp); - await OnResponseArrived(responsePayload, replyTo, (IReadOnlyDictionary)responseHeaders); + await RequestResponseMessageProcessor.ProcessMessage(resp, responseHeaders, null, null, cancellationToken); } } @@ -67,5 +79,12 @@ public override async Task ProduceToTransport(object message, Type messageType, public void TriggerPendingRequestCleanup() { PendingRequestManager.CleanPendingRequests(); + } + + public class MessageBusTestedConsumer(ILogger logger) : AbstractConsumer(logger) + { + protected override Task OnStart() => Task.CompletedTask; + + protected override Task OnStop() => Task.CompletedTask; } } From 2de6b64731c287cdb129667e3708e228be340e30 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Thu, 28 Nov 2024 23:54:51 +0100 Subject: [PATCH 06/21] Bump the version Signed-off-by: Tomasz Maruszak --- src/Host.Plugin.Properties.xml | 2 +- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index 6a16e12a..d8a8d921 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc12 + 3.0.0-rc900 \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index 64248c7a..36463945 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc12 + 3.0.0-rc900 diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index 1b44979d..97abd9cc 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc12 + 3.0.0-rc900 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index a7ac5286..16df001a 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc12 + 3.0.0-rc900 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index ed798b6d..c8a0c17f 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc12 + 3.0.0-rc900 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. From a7d2ac1e8be68a89a8cb8a221d88ee0fb2712395 Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Wed, 11 Dec 2024 11:09:19 +0800 Subject: [PATCH 07/21] #349 Azure Service Bus correlation filters Signed-off-by: Richard Pringle --- docs/provider_azure_servicebus.md | 8 ++- .../AsbAbstractConsumerSettingsExtensions.cs | 6 +- .../Config/AsbConsumerBuilderExtensions.cs | 64 +++++++++++++++++++ .../Config/SubscriptionCorrelationRule.cs | 14 ++++ .../Config/SubscriptionRule.cs | 6 ++ .../Config/SubscriptionSqlFilter.cs | 5 +- .../ServiceBusTopologyService.cs | 45 ++++++++++++- .../ServiceBusTopologyServiceTests.cs | 39 +++++++++++ 8 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionCorrelationRule.cs create mode 100644 src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionRule.cs diff --git a/docs/provider_azure_servicebus.md b/docs/provider_azure_servicebus.md index 9593bc83..4f8d55ac 100644 --- a/docs/provider_azure_servicebus.md +++ b/docs/provider_azure_servicebus.md @@ -217,7 +217,7 @@ mbb.Consume(x => x .MaxAutoLockRenewalDuration(TimeSpan.FromMinutes(7)) .SubQueue(SubQueue.DeadLetter) .PrefetchCount(10) - .SubscriptionSqlFilter("1=1") // ASB subscription SQL filters can also be created - see topology creation section + .SubscriptionSqlFilter("1=1") // ASB subscription filters can also be created - see topology creation section .Instances(1)); ``` @@ -342,7 +342,9 @@ When there is a need to get ahold of the `SessionId` for the message processed, > Since 1.19.0 ASB transport provider can automatically create the required ASB queue/topic/subscription/rule that have been declared as part of the SMB configuration. -The provisioning happens as soon as the SMB instance is created and prior any consumers start processing messages. The creation happens only when a particular topic/queue/subscription/rule does not exist. If it exist the SMB will not alter it. +The provisioning happens as soon as the SMB instance is created and prior to consumers starting to process messages. The creation happens only when a particular topic/queue/subscription/rule does not exist, and will not be modified if it already exists. + +SMB supports both [SQL](https://learn.microsoft.com/en-us/azure/service-bus-messaging/topic-filters#sql-filters) and [Correlation](https://learn.microsoft.com/en-us/azure/service-bus-messaging/topic-filters#correlation-filters) filters through the `SubscriptionSqlFilter` and `SubscriptionCorrelationFilter` consumer builders. > In order for the ASB provisioning to work, the Azure Service Bus connection string has to use a key with the `Manage` scope permission. @@ -409,7 +411,7 @@ mbb.Consume(x => x .Topic("some-topic") .WithConsumer() .SubscriptionName("some-service") - .SubscriptionSqlFilter("1=1") // this will create a rule with SQL filter + .SubscriptionCorrelationFilter(correlationId: "some-id") // this will create a rule to filter messages with a CorrelationId of 'some-id' .CreateTopicOptions((options) => { options.RequiresDuplicateDetection = false; diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbAbstractConsumerSettingsExtensions.cs b/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbAbstractConsumerSettingsExtensions.cs index eeaab5e1..5ab28840 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbAbstractConsumerSettingsExtensions.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbAbstractConsumerSettingsExtensions.cs @@ -55,12 +55,12 @@ static internal void SetMaxConcurrentSessions(this AbstractConsumerSettings cons static internal int? GetMaxConcurrentSessions(this AbstractConsumerSettings consumerSettings) => consumerSettings.GetOrDefault(AsbProperties.MaxConcurrentSessionsKey); - static internal IDictionary GetRules(this AbstractConsumerSettings consumerSettings, bool createIfNotExists = false) + static internal IDictionary GetRules(this AbstractConsumerSettings consumerSettings, bool createIfNotExists = false) { - var filterByName = consumerSettings.GetOrDefault>(AsbProperties.RulesKey); + var filterByName = consumerSettings.GetOrDefault>(AsbProperties.RulesKey); if (filterByName == null && createIfNotExists) { - filterByName = new Dictionary(); + filterByName = new Dictionary(); consumerSettings.Properties[AsbProperties.RulesKey] = filterByName; } return filterByName; diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbConsumerBuilderExtensions.cs index dca52ffa..217d3b1c 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbConsumerBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Config/AsbConsumerBuilderExtensions.cs @@ -160,6 +160,70 @@ public static TConsumerBuilder SubscriptionSqlFilter(this TCon return builder; } + /// + /// Adds a named correlation filter to the subscription (Azure Service Bus). Setting relevant only if topology provisioning enabled. + /// + /// + /// + /// The name of the filter + /// Value to be applied as the 'CorrelationId' filter. + /// Value to be applied as the 'MessageId' filter. + /// Value to be applied as the 'To' filter. + /// Value to be applied as the 'ReplyTo' filter. + /// Value to be applied as the 'Subject' filter. + /// Value to be applied as the 'SessionId' filter. + /// Value to be applied as the 'ReplyToSessionId' filter. + /// Value to be applied as the 'ContentType' filter. + /// Filters to be applied to application specific properties. + /// + /// + public static TConsumerBuilder SubscriptionCorrelationFilter( + this TConsumerBuilder builder, + string ruleName = "default", + string correlationId = "", + string messageId = "", + string to = "", + string replyTo = "", + string subject = "", + string sessionId = "", + string replyToSessionId = "", + string contentType = "", + IDictionary applicationProperties = null) + where TConsumerBuilder : IAbstractConsumerBuilder + { + if (builder is null) throw new ArgumentNullException(nameof(builder)); + + if (string.IsNullOrWhiteSpace(correlationId) + && string.IsNullOrWhiteSpace(messageId) + && string.IsNullOrWhiteSpace(to) + && string.IsNullOrWhiteSpace(replyTo) + && string.IsNullOrWhiteSpace(subject) + && string.IsNullOrWhiteSpace(sessionId) + && string.IsNullOrWhiteSpace(replyToSessionId) + && string.IsNullOrWhiteSpace(contentType) + && (applicationProperties == null || applicationProperties?.Count == 0)) + { + throw new ArgumentException("At least one property must contain a value to use as a filter"); + } + + var filterByName = builder.ConsumerSettings.GetRules(createIfNotExists: true); + filterByName[ruleName] = new SubscriptionCorrelationRule + { + Name = ruleName, + CorrelationId = correlationId, + MessageId = messageId, + To = to, + ReplyTo = replyTo, + Subject = subject, + SessionId = sessionId, + ReplyToSessionId = replyToSessionId, + ContentType = contentType, + ApplicationProperties = applicationProperties + }; + + return builder; + } + /// /// when the ASB queue does not exist and needs to be created /// diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionCorrelationRule.cs b/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionCorrelationRule.cs new file mode 100644 index 00000000..6acfb898 --- /dev/null +++ b/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionCorrelationRule.cs @@ -0,0 +1,14 @@ +namespace SlimMessageBus.Host.AzureServiceBus; + +public record SubscriptionCorrelationRule : SubscriptionRule +{ + public string CorrelationId { get; set; } + public string MessageId { get; set; } + public string To { get; set; } + public string ReplyTo { get; set; } + public string Subject { get; set; } + public string SessionId { get; set; } + public string ReplyToSessionId { get; set; } + public string ContentType { get; set; } + public IDictionary ApplicationProperties { get; set; } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionRule.cs b/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionRule.cs new file mode 100644 index 00000000..1b1d1256 --- /dev/null +++ b/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionRule.cs @@ -0,0 +1,6 @@ +namespace SlimMessageBus.Host.AzureServiceBus; + +public abstract record SubscriptionRule +{ + public string Name { get; set; } +} diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionSqlFilter.cs b/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionSqlFilter.cs index e7f3be22..0055a9c7 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionSqlFilter.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Config/SubscriptionSqlFilter.cs @@ -1,8 +1,7 @@ namespace SlimMessageBus.Host.AzureServiceBus; -public record SubscriptionSqlRule +public record SubscriptionSqlRule : SubscriptionRule { - public string Name { get; set; } public string SqlFilter { get; set; } public string SqlAction { get; set; } -} \ No newline at end of file +} diff --git a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs index 71adafcf..d28fc98c 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusTopologyService.cs @@ -331,10 +331,51 @@ IReadOnlyCollection MergeFilters(string path, string subscrip throw new ConfigurationMessageBusException($"All rules across the same path/subscription {path}/{subscriptionName} must have unique names (Duplicate: '{name}')."); } + RuleFilter filter; + RuleAction action = null; + switch (rule) + { + case SubscriptionSqlRule sqlRule: + filter = new SqlRuleFilter(sqlRule.SqlFilter); + if (!string.IsNullOrWhiteSpace(sqlRule.SqlAction)) + { + action = new SqlRuleAction(sqlRule.SqlAction); + } + + break; + + case SubscriptionCorrelationRule correlationRule: + var correlationRuleFilter = new CorrelationRuleFilter + { + CorrelationId = correlationRule.CorrelationId, + MessageId = correlationRule.MessageId, + To = correlationRule.To, + ReplyTo = correlationRule.ReplyTo, + Subject = correlationRule.Subject, + SessionId = correlationRule.SessionId, + ReplyToSessionId = correlationRule.ReplyToSessionId, + ContentType = correlationRule.ContentType, + }; + + if (correlationRule.ApplicationProperties != null) + { + foreach (var (key, value) in correlationRule.ApplicationProperties) + { + correlationRuleFilter.ApplicationProperties.Add(key, value); + } + } + + filter = correlationRuleFilter; + break; + + default: + throw new NotSupportedException($"Filter of type '{rule.GetType()}' is not supported"); + } + var createRuleOptions = new CreateRuleOptions(name) { - Filter = new SqlRuleFilter(rule.SqlFilter), - Action = !string.IsNullOrWhiteSpace(rule.SqlAction) ? new SqlRuleAction(rule.SqlAction) : null + Filter = filter, + Action = action }; _providerSettings.TopologyProvisioning?.CreateSubscriptionFilterOptions?.Invoke(createRuleOptions); diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs index 06fcfcda..5e1def1e 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusTopologyServiceTests.cs @@ -344,6 +344,34 @@ public async Task When_FilterConfigurationDiffersWithServer_And_CannotReplaceSub _mockAdminClient.Verify(x => x.UpdateRuleAsync(_topicName, _subscriptionName, It.IsAny(), It.IsAny()), Times.Never); } + [Fact] + public async Task When_FilterTypeConfigurationDiffersWithServer_And_CannotReplaceSubscriptionFilters_Then_DoNotUpdateRule() + { + // arrange + const string ruleName = "rule-name"; + + var applicationProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Sample", "Value"} + }; + + _defaultConsumerBuilder + .SubscriptionCorrelationFilter(ruleName, applicationProperties: applicationProperties); + + ProviderBusSettings.TopologyProvisioning.CanConsumerReplaceSubscriptionFilters = false; + + _mockAdminClient.Setup(x => x.TopicExistsAsync(_topicName, It.IsAny())).Returns(ResponseTask(true)); + _mockAdminClient.Setup(x => x.SubscriptionExistsAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(ResponseTask(true)); + _mockAdminClient.Setup(x => x.GetRulesAsync(_topicName, _subscriptionName, It.IsAny())).Returns(AsyncPage(ServiceBusModelFactory.RuleProperties(ruleName, new SqlRuleFilter("1 = 1")))); + _mockAdminClient.Setup(x => x.UpdateRuleAsync(_topicName, _subscriptionName, It.IsAny(), It.IsAny())).Verifiable(); + + // act + await _target.ProvisionTopology(); + + // assert + _mockAdminClient.Verify(x => x.UpdateRuleAsync(_topicName, _subscriptionName, It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public async Task When_FilterConfigurationDiffersWithServer_And_CanValidateSubscriptionFiltersOnly_Then_DoNotChangeAnyRules() { @@ -384,6 +412,17 @@ public async Task When_FilterConfigurationDiffersWithServer_And_CanValidateSubsc _mockAdminClient.Verify(x => x.UpdateRuleAsync(_topicName, _subscriptionName, It.IsAny(), It.IsAny()), Times.Never); _mockAdminClient.Verify(x => x.DeleteRuleAsync(_topicName, _subscriptionName, It.IsAny(), It.IsAny()), Times.Never); } + + [Fact] + public void When_SubscriptionCorrelationFilter_ContainsNoConfiguration_ThrowException() + { + // act + var act = () => _defaultConsumerBuilder + .SubscriptionCorrelationFilter("rule-name"); + + // assert + act.Should().Throw(); + } } private static Task> ResponseTask(T value) From 2e5662df3cdd068186b5503fca78fe33564e9d17 Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Sun, 29 Dec 2024 15:46:01 +0800 Subject: [PATCH 08/21] #347 Refactor IConsumerErrorHandler to return a 'retry' response instead of supplying a 'retry' delegate Signed-off-by: Richard Pringle --- docs/intro.md | 51 +- docs/intro.t.md | 35 +- .../Consumer/ISqsConsumerErrorHandler.cs | 6 +- .../GlobalUsings.cs | 2 +- .../Consumer/IEventHubConsumerErrorHandler.cs | 6 +- .../GlobalUsings.cs | 12 +- .../IServiceBusConsumerErrorHandler.cs | 6 +- .../GlobalUsings.cs | 10 +- .../Consumer/IKafkaConsumerErrorHandler.cs | 6 +- src/SlimMessageBus.Host.Kafka/GlobalUsings.cs | 4 +- .../Consumers/IMemoryConsumerErrorHandler.cs | 6 +- .../GlobalUsings.cs | 3 +- src/SlimMessageBus.Host.Mqtt/GlobalUsings.cs | 3 +- .../IMqttConsumerErrorHandler.cs | 6 +- src/SlimMessageBus.Host.Nats/GlobalUsings.cs | 5 +- .../INatsConsumerErrorHandler.cs | 6 +- .../IRabbitMqConsumerErrorHandler.cs | 6 +- .../GlobalUsings.cs | 4 +- .../Consumers/IRedisConsumerErrorHandler.cs | 6 +- src/SlimMessageBus.Host.Redis/GlobalUsings.cs | 3 +- .../Collections/RuntimeTypeCache.cs | 4 +- .../ErrorHandling/ConsumerErrorHandler.cs | 13 + .../ConsumerErrorHandlerResult.cs | 37 +- .../ErrorHandling/IConsumerErrorHandler.cs | 18 +- .../MessageProcessors/MessageHandler.cs | 63 +- src/SlimMessageBus.Host/MessageBusBase.cs | 926 +++++++++--------- src/SlimMessageBus.sln | 12 +- src/SlimMessageBus/IMessageBus.cs | 6 +- .../MemoryMessageBusTests.cs | 4 +- .../IntegrationTests/RabbitMqMessageBusIt.cs | 3 +- .../Consumer/MessageHandlerTest.cs | 72 +- 31 files changed, 754 insertions(+), 590 deletions(-) create mode 100644 src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs diff --git a/docs/intro.md b/docs/intro.md index 909c2f5a..872ce976 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1059,28 +1059,41 @@ public class LoggingConsumerInterceptor : IConsumerInterceptor](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs) enables the definition of custom error handling for specific message types: +Message processing by consumers or handlers may result in exceptions. The [IConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs) provides a standard way to integrate custom error handling logic across different transports. ```cs public interface IConsumerErrorHandler { /// - /// Executed when the message consumer (or handler) errors out. This interface allows to intercept and handle the exception. - /// Use the consumer context to get ahold of transport specific options to proceed (acknowledge/reject message). + /// + /// Executed when the message consumer (or handler) errors out. The interface allows for interception of + /// exceptions to manipulate the processing pipeline (success/fail/retry). + /// + /// + /// The consumer context is available to apply transport specific operations (acknowledge/reject/dead letter/etc). + /// + /// + /// If message execution is to be re-attempted, any delays/jitter should be applied before the method returns. + /// /// /// The message that failed to process. - /// Performs another message processing try. The return value is relevant if the consumer was a request handler (it will be its response value). Ensure to pass the return value to the result of the error handler. /// The consumer context for the message processing pipeline. /// Exception that occurred during message processing. + /// The number of times the message has been attempted to be processed. /// The error handling result. - Task OnHandleError(T message, Func> retry, IConsumerContext consumerContext, Exception exception); + Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); } ``` -> The `retry()` parameter allows the message processing pipeline, including consumer interceptors, to retry processing when transient errors occur and retries are desired. +The returned `ConsumerErrorHandlerResult` object is used to override the execution for the remainder of the execution pipeline. +| Result | Description | +|---------|-------------| +| Failure | The message failed to be processed and should be returned to the queue | +| Success | The pipeline must treat the message as having been processed successfully | +| SuccessWithResponse | The pipeline to treat the messagage as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | +| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | + +[^1]: `Retry` will recreate the message scope on every atttempt if `PerMessageScopeEnabled` has been enabled. To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: @@ -1106,6 +1119,26 @@ Transport plugins provide specialized error handling interfaces. Examples includ This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. +Sample retry with exponential back-off: +```cs +public class RetryHandler : ConsumerErrorHandler +{ + private static readonly Random _random = new(); + + public override async Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) + { + if (attempts < 3) + { + var delay = (attempts * 1000) + (_random.Next(1000) - 500); + await Task.Delay(delay, consumerContext.CancellationToken); + return Retry(); + } + + return Failure(); + } +} +``` + ## Logging SlimMessageBus uses [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions): diff --git a/docs/intro.t.md b/docs/intro.t.md index f5512cba..e998bb39 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -1059,14 +1059,19 @@ public class LoggingConsumerInterceptor : IConsumerInterceptor](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs) enables the definition of custom error handling for specific message types: +Message processing by consumers or handlers may result in exceptions. The [IConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs) provides a standard way to integrate custom error handling logic across different transports. @[:cs](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs,Interface) -> The `retry()` parameter allows the message processing pipeline, including consumer interceptors, to retry processing when transient errors occur and retries are desired. +The returned `ConsumerErrorHandlerResult` object is used to override the execution for the remainder of the execution pipeline. +| Result | Description | +|---------|-------------| +| Failure | The message failed to be processed and should be returned to the queue | +| Success | The pipeline must treat the message as having been processed successfully | +| SuccessWithResponse | The pipeline to treat the messagage as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | +| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | + +[^1]: `Retry` will recreate the message scope on every atttempt if `PerMessageScopeEnabled` has been enabled. To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: @@ -1092,6 +1097,26 @@ Transport plugins provide specialized error handling interfaces. Examples includ This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. +Sample retry with exponential back-off: +```cs +public class RetryHandler : ConsumerErrorHandler +{ + private static readonly Random _random = new(); + + public override async Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) + { + if (attempts < 3) + { + var delay = (attempts * 1000) + (_random.Next(1000) - 500); + await Task.Delay(delay, consumerContext.CancellationToken); + return Retry(); + } + + return Failure(); + } +} +``` + ## Logging SlimMessageBus uses [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions): diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs index 6172cfb5..c4221549 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.AmazonSQS; -public interface ISqsConsumerErrorHandler : IConsumerErrorHandler -{ -} +public interface ISqsConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class SqsConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs b/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs index ea8ffa7a..cf7639ba 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/GlobalUsings.cs @@ -7,4 +7,4 @@ global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Logging; -global using SlimMessageBus.Host.Serialization; +global using SlimMessageBus.Host.Consumer.ErrorHandling; diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs index 89c8c322..335f02bb 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.AzureEventHub; -public interface IEventHubConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IEventHubConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class EventHubConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureEventHub/GlobalUsings.cs b/src/SlimMessageBus.Host.AzureEventHub/GlobalUsings.cs index a6282365..b55064f5 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/GlobalUsings.cs @@ -1,9 +1,9 @@ -global using Microsoft.Extensions.Logging; +global using Azure.Messaging.EventHubs; +global using Azure.Messaging.EventHubs.Producer; +global using Azure.Storage.Blobs; + +global using Microsoft.Extensions.Logging; -global using SlimMessageBus.Host; global using SlimMessageBus.Host.Collections; +global using SlimMessageBus.Host.Consumer.ErrorHandling; global using SlimMessageBus.Host.Services; - -global using Azure.Messaging.EventHubs; -global using Azure.Messaging.EventHubs.Producer; -global using Azure.Storage.Blobs; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs index 4d1832ee..a8ad8166 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.AzureServiceBus; -public interface IServiceBusConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IServiceBusConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class ServiceBusConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureServiceBus/GlobalUsings.cs b/src/SlimMessageBus.Host.AzureServiceBus/GlobalUsings.cs index bbed1600..f7c6beaa 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/GlobalUsings.cs @@ -1,8 +1,8 @@ -global using Microsoft.Extensions.Logging; +global using Azure.Messaging.ServiceBus; +global using Azure.Messaging.ServiceBus.Administration; + +global using Microsoft.Extensions.Logging; -global using SlimMessageBus.Host; global using SlimMessageBus.Host.Collections; +global using SlimMessageBus.Host.Consumer.ErrorHandling; global using SlimMessageBus.Host.Services; - -global using Azure.Messaging.ServiceBus; -global using Azure.Messaging.ServiceBus.Administration; diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs index 7e980d1e..a7f391bf 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.Kafka; -public interface IKafkaConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IKafkaConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class KafkaConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/GlobalUsings.cs b/src/SlimMessageBus.Host.Kafka/GlobalUsings.cs index 652c8bea..b5028412 100644 --- a/src/SlimMessageBus.Host.Kafka/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Kafka/GlobalUsings.cs @@ -2,7 +2,7 @@ global using Microsoft.Extensions.Logging; -global using SlimMessageBus.Host; -global using SlimMessageBus.Host.Serialization; global using SlimMessageBus.Host.Collections; +global using SlimMessageBus.Host.Consumer.ErrorHandling; +global using SlimMessageBus.Host.Serialization; global using SlimMessageBus.Host.Services; diff --git a/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs index 6dd77086..f235ab2c 100644 --- a/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.Memory; -public interface IMemoryConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IMemoryConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class MemoryConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Memory/GlobalUsings.cs b/src/SlimMessageBus.Host.Memory/GlobalUsings.cs index ff869fd3..756dc51e 100644 --- a/src/SlimMessageBus.Host.Memory/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Memory/GlobalUsings.cs @@ -1,3 +1,4 @@ global using Microsoft.Extensions.Logging; -global using SlimMessageBus.Host.Serialization; \ No newline at end of file +global using SlimMessageBus.Host.Consumer.ErrorHandling; +global using SlimMessageBus.Host.Serialization; diff --git a/src/SlimMessageBus.Host.Mqtt/GlobalUsings.cs b/src/SlimMessageBus.Host.Mqtt/GlobalUsings.cs index dac948d8..3d32ac81 100644 --- a/src/SlimMessageBus.Host.Mqtt/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Mqtt/GlobalUsings.cs @@ -4,4 +4,5 @@ global using MQTTnet.Client; global using MQTTnet.Extensions.ManagedClient; -global using SlimMessageBus.Host.Services; \ No newline at end of file +global using SlimMessageBus.Host.Consumer.ErrorHandling; +global using SlimMessageBus.Host.Services; diff --git a/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs index 7a47506e..9bef2034 100644 --- a/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.Mqtt; -public interface IMqttConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IMqttConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class MqttConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Nats/GlobalUsings.cs b/src/SlimMessageBus.Host.Nats/GlobalUsings.cs index b5c4039b..2c8e9b12 100644 --- a/src/SlimMessageBus.Host.Nats/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Nats/GlobalUsings.cs @@ -1,5 +1,6 @@ global using Microsoft.Extensions.Logging; -global using SlimMessageBus.Host.Services; +global using NATS.Client.Core; -global using NATS.Client.Core; \ No newline at end of file +global using SlimMessageBus.Host.Consumer.ErrorHandling; +global using SlimMessageBus.Host.Services; diff --git a/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs index e2725a0a..8867a961 100644 --- a/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.Nats; -public interface INatsConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface INatsConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class NatsConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs index b22819b9..2a48eab3 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.RabbitMQ; -public interface IRabbitMqConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IRabbitMqConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class RabbitMqConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/GlobalUsings.cs b/src/SlimMessageBus.Host.RabbitMQ/GlobalUsings.cs index ed6111b3..d4ef3ccb 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/GlobalUsings.cs @@ -3,5 +3,5 @@ global using RabbitMQ.Client; global using RabbitMQ.Client.Events; -global using SlimMessageBus.Host.Serialization; -global using SlimMessageBus.Host.Services; \ No newline at end of file +global using SlimMessageBus.Host.Consumer.ErrorHandling; +global using SlimMessageBus.Host.Services; diff --git a/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs index a52070b7..d041b78b 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs @@ -1,5 +1,5 @@ namespace SlimMessageBus.Host.Redis; -public interface IRedisConsumerErrorHandler : IConsumerErrorHandler -{ -} \ No newline at end of file +public interface IRedisConsumerErrorHandler : IConsumerErrorHandler; + +public abstract class RedisConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Redis/GlobalUsings.cs b/src/SlimMessageBus.Host.Redis/GlobalUsings.cs index f0ff2e4a..47b60afa 100644 --- a/src/SlimMessageBus.Host.Redis/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Redis/GlobalUsings.cs @@ -1,7 +1,8 @@ global using Microsoft.Extensions.Logging; global using SlimMessageBus.Host.Collections; +global using SlimMessageBus.Host.Consumer.ErrorHandling; global using SlimMessageBus.Host.Serialization; global using SlimMessageBus.Host.Services; -global using StackExchange.Redis; \ No newline at end of file +global using StackExchange.Redis; diff --git a/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs b/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs index 76c8813a..1b525653 100644 --- a/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/RuntimeTypeCache.cs @@ -16,7 +16,7 @@ public class RuntimeTypeCache : IRuntimeTypeCache public IGenericTypeCache>, IConsumerContext, Task>> ConsumerInterceptorType { get; } public IGenericTypeCache2> HandlerInterceptorType { get; } - public IGenericTypeCache>, IConsumerContext, Exception, Task>> ConsumerErrorHandlerType { get; } + public IGenericTypeCache>> ConsumerErrorHandlerType { get; } public RuntimeTypeCache() { @@ -78,7 +78,7 @@ public RuntimeTypeCache() typeof(IRequestHandlerInterceptor<,>), nameof(IRequestHandlerInterceptor.OnHandle)); - ConsumerErrorHandlerType = new GenericTypeCache>, IConsumerContext, Exception, Task>>( + ConsumerErrorHandlerType = new GenericTypeCache>>( typeof(IConsumerErrorHandler<>), nameof(IConsumerErrorHandler.OnHandleError)); } diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs new file mode 100644 index 00000000..94d584c7 --- /dev/null +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs @@ -0,0 +1,13 @@ +namespace SlimMessageBus.Host.Consumer.ErrorHandling; + +public abstract class ConsumerErrorHandler : BaseConsumerErrorHandler, IConsumerErrorHandler +{ + public abstract Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); +} + +public abstract class BaseConsumerErrorHandler +{ + public static ConsumerErrorHandlerResult Failure() => ConsumerErrorHandlerResult.Failure; + public static ConsumerErrorHandlerResult Retry() => ConsumerErrorHandlerResult.Retry; + public static ConsumerErrorHandlerResult Success(object response = null) => response == null ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.SuccessWithResponse(response); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs index a80fed9a..7b914fee 100644 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs @@ -2,23 +2,42 @@ public record ConsumerErrorHandlerResult { - private static readonly object NoResponse = new(); + private static readonly object _noResponse = new(); - public bool Handled { get; private set; } + private ConsumerErrorHandlerResult(ConsumerErrorHandlerResultEnum result, object response = null) + { + Result = result; + Response = response ?? _noResponse; + } + + public ConsumerErrorHandlerResultEnum Result { get; private set; } public object Response { get; private set; } - public bool HasResponse => !ReferenceEquals(Response, NoResponse); + public bool HasResponse => !ReferenceEquals(Response, _noResponse); /// - /// The error handler was not able to handle the exception. + /// The message should be placed back into the queue. /// - public static readonly ConsumerErrorHandlerResult Failure = new() { Handled = false, Response = NoResponse }; + public static readonly ConsumerErrorHandlerResult Failure = new(ConsumerErrorHandlerResultEnum.Fail); + /// - /// The error handler was able to handle the exception. + /// The message processor should evaluate the message as having been processed successfully. /// - public static readonly ConsumerErrorHandlerResult Success = new() { Handled = true, Response = NoResponse }; + public static readonly ConsumerErrorHandlerResult Success = new(ConsumerErrorHandlerResultEnum.Success); /// - /// The error handler was able to handle the exception, and has a fallback response for the or . + /// The message processor should evaluate the message as having been processed successfully and use the specified fallback response for the or . /// - public static ConsumerErrorHandlerResult SuccessWithResponse(object response) => new() { Handled = true, Response = response }; + public static ConsumerErrorHandlerResult SuccessWithResponse(object response) => new(ConsumerErrorHandlerResultEnum.Success, response); + + /// + /// Retry processing the message without placing it back in the queue. + /// + public static readonly ConsumerErrorHandlerResult Retry = new(ConsumerErrorHandlerResultEnum.Retry); +} + +public enum ConsumerErrorHandlerResultEnum +{ + Fail, + Retry, + Success } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs index 557ed44d..5c40030d 100644 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs @@ -4,14 +4,22 @@ public interface IConsumerErrorHandler { /// - /// Executed when the message consumer (or handler) errors out. This interface allows to intercept and handle the exception. - /// Use the consumer context to get ahold of transport specific options to proceed (acknowledge/reject message). + /// + /// Executed when the message consumer (or handler) errors out. The interface allows for interception of + /// exceptions to manipulate the processing pipeline (success/fail/retry). + /// + /// + /// The consumer context is available to apply transport specific operations (acknowledge/reject/dead letter/etc). + /// + /// + /// If message execution is to be re-attempted, any delays/jitter should be applied before the method returns. + /// /// /// The message that failed to process. - /// Performs another message processing try. The return value is relevant if the consumer was a request handler (it will be its response value). Ensure to pass the return value to the result of the error handler. /// The consumer context for the message processing pipeline. /// Exception that occurred during message processing. + /// The number of times the message has been attempted to be processed. /// The error handling result. - Task OnHandleError(T message, Func> retry, IConsumerContext consumerContext, Exception exception); + Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); } -// doc:fragment:Interface +// doc:fragment:Interface \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index 40d594f0..21717605 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -65,63 +65,61 @@ public MessageHandler( var hasResponse = consumerInvoker.ParentSettings.ConsumerMode == ConsumerMode.RequestResponse; var responseType = hasResponse ? consumerInvoker.ParentSettings.ResponseType ?? typeof(Void) : null; - object response = null; - Exception responseException = null; string requestId = null; - if (hasResponse && messageHeaders != null) { messageHeaders.TryGetHeader(ReqRespMessageHeaders.RequestId, out requestId); } - await using (var messageScope = _messageScopeFactory.CreateMessageScope(consumerInvoker.ParentSettings, message, consumerContextProperties, currentServiceProvider)) + DateTimeOffset? messageExpires = null; + if (messageHeaders != null && messageHeaders.TryGetHeader(ReqRespMessageHeaders.Expires, out DateTimeOffset? expires) && expires != null) { - if (messageHeaders != null && messageHeaders.TryGetHeader(ReqRespMessageHeaders.Expires, out DateTimeOffset? expires) && expires != null) - { - // Verify if the request/message is already expired - var currentTime = _currentTimeProvider.CurrentTime; - if (currentTime > expires.Value) - { - // ToDo: Call interceptor + messageExpires = expires; + } - // Do not process the expired message - return (ResponseForExpiredRequest, null, requestId); - } + var attempts = 0; + var consumerType = consumerInvoker.ConsumerType; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + await using var messageScope = _messageScopeFactory.CreateMessageScope(consumerInvoker.ParentSettings, message, consumerContextProperties, currentServiceProvider); + if (messageExpires != null && messageExpires < _currentTimeProvider.CurrentTime) + { + // ToDo: Call interceptor + // Do not process the expired message + return (ResponseForExpiredRequest, null, requestId); } var messageBusTarget = new MessageBusProxy(MessageBus, messageScope.ServiceProvider); - - Type consumerType = null; object consumerInstance = null; - try { - consumerType = consumerInvoker.ConsumerType; consumerInstance = messageScope.ServiceProvider.GetService(consumerType) ?? throw new ConfigurationMessageBusException($"Could not resolve consumer/handler type {consumerType} from the DI container. Please check that the configured type {consumerType} is registered within the DI container."); var consumerContext = CreateConsumerContext(messageHeaders, consumerInvoker, transportMessage, consumerInstance, messageBusTarget, consumerContextProperties, cancellationToken); try { - response = await DoHandleInternal(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext).ConfigureAwait(false); + var response = await DoHandleInternal(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext).ConfigureAwait(false); + return (response, null, requestId); } catch (Exception ex) { - // Give the consumer error handler a chance to take action - var handleErrorResult = await DoHandleError(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext, ex).ConfigureAwait(false); - if (!handleErrorResult.Handled) + attempts++; + var handleErrorResult = await DoHandleError(message, messageType, messageScope, consumerContext, ex, attempts, cancellationToken).ConfigureAwait(false); + if (handleErrorResult.Result != ConsumerErrorHandlerResultEnum.Retry) { - responseException = ex; - } - if (handleErrorResult.HasResponse) - { - response = handleErrorResult.Response; + var exception = handleErrorResult.Result != ConsumerErrorHandlerResultEnum.Success ? ex : null; + var response = handleErrorResult.HasResponse ? handleErrorResult.Response : null; + return (response, exception, requestId); } } } catch (Exception e) { - responseException = e; + return (null, e, requestId); } finally { @@ -132,8 +130,6 @@ public MessageHandler( } } } - - return (response, responseException, requestId); } private async Task DoHandleInternal(object message, IMessageTypeConsumerInvokerSettings consumerInvoker, Type messageType, bool hasResponse, Type responseType, IMessageScope messageScope, IConsumerContext consumerContext) @@ -150,7 +146,7 @@ private async Task DoHandleInternal(object message, IMessageTypeConsumer return await ExecuteConsumer(message, consumerContext, consumerInvoker, responseType).ConfigureAwait(false); } - private async Task DoHandleError(object message, IMessageTypeConsumerInvokerSettings consumerInvoker, Type messageType, bool hasResponse, Type responseType, IMessageScope messageScope, IConsumerContext consumerContext, Exception ex) + private async Task DoHandleError(object message, Type messageType, IMessageScope messageScope, IConsumerContext consumerContext, Exception ex, int attempts, CancellationToken cancellationToken) { var errorHandlerResult = ConsumerErrorHandlerResult.Failure; @@ -166,11 +162,8 @@ private async Task DoHandleError(object message, IMe { _logger.LogDebug(ex, "Consumer error handler of type {ConsumerErrorHandlerType} will be used to handle the exception during processing of message of type {MessageType}", consumerErrorHandler.GetType(), messageType); - // Give a chance to the consumer error handler to take action - Task retry() => DoHandleInternal(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext); - var consumerErrorHandlerMethod = RuntimeTypeCache.ConsumerErrorHandlerType[messageType]; - errorHandlerResult = await consumerErrorHandlerMethod(consumerErrorHandler, message, retry, consumerContext, ex).ConfigureAwait(false); + errorHandlerResult = await consumerErrorHandlerMethod(consumerErrorHandler, message, consumerContext, ex, attempts).ConfigureAwait(false); } return errorHandlerResult; diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index e5e99498..02384e3a 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -1,17 +1,17 @@ -namespace SlimMessageBus.Host; - -using System.Globalization; +namespace SlimMessageBus.Host; + +using System.Globalization; using System.Runtime.ExceptionServices; -using SlimMessageBus.Host.Consumer; -using SlimMessageBus.Host.Services; +using SlimMessageBus.Host.Consumer; +using SlimMessageBus.Host.Services; public abstract class MessageBusBase(MessageBusSettings settings, TProviderSettings providerSettings) : MessageBusBase(settings) - where TProviderSettings : class -{ - public TProviderSettings ProviderSettings { get; } = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); + where TProviderSettings : class +{ + public TProviderSettings ProviderSettings { get; } = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); } - + public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, @@ -19,359 +19,359 @@ public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IResponseProducer, ITransportProducer, ITransportBulkProducer -{ - private readonly ILogger _logger; - private CancellationTokenSource _cancellationTokenSource = new(); - private IMessageSerializer _serializer; - private readonly MessageHeaderService _headerService; +{ + private readonly ILogger _logger; + private CancellationTokenSource _cancellationTokenSource = new(); + private IMessageSerializer _serializer; + private readonly MessageHeaderService _headerService; private readonly List _consumers = []; public ILoggerFactory LoggerFactory { get; protected set; } - - /// - /// Special market reference that signifies a dummy producer settings for response types. - /// - protected static readonly ProducerSettings MarkerProducerSettingsForResponses = new(); - - public RuntimeTypeCache RuntimeTypeCache { get; } - - public virtual MessageBusSettings Settings { get; } - - public virtual IMessageSerializer Serializer => _serializer ??= GetSerializer(); - - public IMessageTypeResolver MessageTypeResolver { get; } - - /// - /// Default that corresponds to the root DI container, and pointing at self as the bus target. - /// - public virtual IMessageBusTarget MessageBusTarget { get; } - - protected ProducerByMessageTypeCache ProducerSettingsByMessageType { get; private set; } - - protected IPendingRequestStore PendingRequestStore { get; set; } - protected IPendingRequestManager PendingRequestManager { get; set; } - - public CancellationToken CancellationToken => _cancellationTokenSource.Token; - - #region Disposing - - protected bool IsDisposing { get; private set; } - protected bool IsDisposed { get; private set; } - - #endregion + + /// + /// Special market reference that signifies a dummy producer settings for response types. + /// + protected static readonly ProducerSettings MarkerProducerSettingsForResponses = new(); + + public RuntimeTypeCache RuntimeTypeCache { get; } + + public virtual MessageBusSettings Settings { get; } + + public virtual IMessageSerializer Serializer => _serializer ??= GetSerializer(); + + public IMessageTypeResolver MessageTypeResolver { get; } + + /// + /// Default that corresponds to the root DI container, and pointing at self as the bus target. + /// + public virtual IMessageBusTarget MessageBusTarget { get; } + + protected ProducerByMessageTypeCache ProducerSettingsByMessageType { get; private set; } + + protected IPendingRequestStore PendingRequestStore { get; set; } + protected IPendingRequestManager PendingRequestManager { get; set; } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + #region Disposing + + protected bool IsDisposing { get; private set; } + protected bool IsDisposed { get; private set; } + + #endregion /// /// Maintains a list of tasks that should be completed before the bus can produce the first message or start consumers. /// Add async things like /// - connection creations here to the underlying transport client /// - provision topology - /// - protected readonly AsyncTaskList InitTaskList = new(); - - #region Start & Stop - - private readonly object _startLock = new(); - - public bool IsStarted { get; private set; } - - protected bool IsStarting { get; private set; } - protected bool IsStopping { get; private set; } - - #endregion - - public virtual string Name => Settings.Name ?? "Main"; - - public IReadOnlyCollection Consumers => _consumers; - - protected MessageBusBase(MessageBusSettings settings) - { - Settings = settings ?? throw new ArgumentNullException(nameof(settings)); - - if (settings.ServiceProvider is null) throw new ConfigurationMessageBusException($"The bus {Name} has no {nameof(settings.ServiceProvider)} configured"); - - // Try to resolve from DI. If not available, suppress logging by using the NullLoggerFactory - LoggerFactory = settings.ServiceProvider.GetService() ?? NullLoggerFactory.Instance; - - _logger = LoggerFactory.CreateLogger(); - - var messageTypeResolverType = settings.MessageTypeResolverType ?? typeof(IMessageTypeResolver); - MessageTypeResolver = (IMessageTypeResolver)settings.ServiceProvider.GetService(messageTypeResolverType) - ?? throw new ConfigurationMessageBusException($"The bus {Name} could not resolve the required type {messageTypeResolverType.Name} from {nameof(Settings.ServiceProvider)}"); - - _headerService = new MessageHeaderService(LoggerFactory.CreateLogger(), Settings, MessageTypeResolver); - - RuntimeTypeCache = settings.ServiceProvider.GetRequiredService(); - + /// + protected readonly AsyncTaskList InitTaskList = new(); + + #region Start & Stop + + private readonly object _startLock = new(); + + public bool IsStarted { get; private set; } + + protected bool IsStarting { get; private set; } + protected bool IsStopping { get; private set; } + + #endregion + + public virtual string Name => Settings.Name ?? "Main"; + + public IReadOnlyCollection Consumers => _consumers; + + protected MessageBusBase(MessageBusSettings settings) + { + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + + if (settings.ServiceProvider is null) throw new ConfigurationMessageBusException($"The bus {Name} has no {nameof(settings.ServiceProvider)} configured"); + + // Try to resolve from DI. If not available, suppress logging by using the NullLoggerFactory + LoggerFactory = settings.ServiceProvider.GetService() ?? NullLoggerFactory.Instance; + + _logger = LoggerFactory.CreateLogger(); + + var messageTypeResolverType = settings.MessageTypeResolverType ?? typeof(IMessageTypeResolver); + MessageTypeResolver = (IMessageTypeResolver)settings.ServiceProvider.GetService(messageTypeResolverType) + ?? throw new ConfigurationMessageBusException($"The bus {Name} could not resolve the required type {messageTypeResolverType.Name} from {nameof(Settings.ServiceProvider)}"); + + _headerService = new MessageHeaderService(LoggerFactory.CreateLogger(), Settings, MessageTypeResolver); + + RuntimeTypeCache = settings.ServiceProvider.GetRequiredService(); + MessageBusTarget = new MessageBusProxy(this, Settings.ServiceProvider); CurrentTimeProvider = settings.ServiceProvider.GetRequiredService(); - PendingRequestManager = settings.ServiceProvider.GetRequiredService(); - PendingRequestStore = PendingRequestManager.Store; - } - - protected virtual IMessageSerializer GetSerializer() => Settings.GetSerializer(Settings.ServiceProvider); - - protected virtual IMessageBusSettingsValidationService ValidationService { get => new DefaultMessageBusSettingsValidationService(Settings); } - - /// - /// Called by the provider to initialize the bus. - /// - protected void OnBuildProvider() - { - ValidationService.AssertSettings(); - - Build(); - - // Notify the bus has been created - before any message can be produced - InitTaskList.Add(() => OnBusLifecycle(MessageBusLifecycleEventType.Created), CancellationToken); - - // Auto start consumers if enabled - if (Settings.AutoStartConsumers) - { - // Fire and forget start - _ = Task.Run(async () => - { - try - { - await Start().ConfigureAwait(false); - } - catch (Exception e) - { - _logger.LogError(e, "Could not auto start consumers"); - } - }); - } - } - - protected virtual void Build() - { - ProducerSettingsByMessageType = new ProducerByMessageTypeCache(_logger, BuildProducerByBaseMessageType(), RuntimeTypeCache); - } - - private Dictionary BuildProducerByBaseMessageType() - { - var producerByBaseMessageType = Settings.Producers.ToDictionary(producerSettings => producerSettings.MessageType); - - foreach (var consumerSettings in Settings.Consumers.Where(x => x.ResponseType != null)) - { - // A response type can be used across different requests hence TryAdd - producerByBaseMessageType.TryAdd(consumerSettings.ResponseType, MarkerProducerSettingsForResponses); - } - return producerByBaseMessageType; - } - - private IEnumerable _lifecycleInterceptors; - - private async Task OnBusLifecycle(MessageBusLifecycleEventType eventType) - { - _lifecycleInterceptors ??= Settings.ServiceProvider?.GetServices(); - if (_lifecycleInterceptors != null) - { - foreach (var i in _lifecycleInterceptors) - { - var task = i.OnBusLifecycle(eventType, MessageBusTarget); - if (task != null) - { - await task; - } - } - } - } - - public async Task Start() - { - lock (_startLock) - { - if (IsStarting || IsStarted) - { - return; - } - IsStarting = true; - } - - try - { - await InitTaskList.EnsureAllFinished(); - - _logger.LogInformation("Starting consumers for {BusName} bus...", Name); - await OnBusLifecycle(MessageBusLifecycleEventType.Starting).ConfigureAwait(false); - - await CreateConsumers(); - await OnStart().ConfigureAwait(false); - await Task.WhenAll(_consumers.Select(x => x.Start())).ConfigureAwait(false); - - await OnBusLifecycle(MessageBusLifecycleEventType.Started).ConfigureAwait(false); - _logger.LogInformation("Started consumers for {BusName} bus", Name); - - lock (_startLock) - { - IsStarted = true; - } - } - finally - { - lock (_startLock) - { - IsStarting = false; - } - } - } - - public async Task Stop() - { - lock (_startLock) - { - if (IsStopping || !IsStarted) - { - return; - } - IsStopping = true; - } - - try - { - await InitTaskList.EnsureAllFinished(); - - _logger.LogInformation("Stopping consumers for {BusName} bus...", Name); - await OnBusLifecycle(MessageBusLifecycleEventType.Stopping).ConfigureAwait(false); - - await Task.WhenAll(_consumers.Select(x => x.Stop())).ConfigureAwait(false); - await OnStop().ConfigureAwait(false); - await DestroyConsumers().ConfigureAwait(false); - - await OnBusLifecycle(MessageBusLifecycleEventType.Stopped).ConfigureAwait(false); - _logger.LogInformation("Stopped consumers for {BusName} bus", Name); - - lock (_startLock) - { - IsStarted = false; - } - } - finally - { - lock (_startLock) - { - IsStopping = false; - } - } - } - - protected internal virtual Task OnStart() => Task.CompletedTask; - protected internal virtual Task OnStop() => Task.CompletedTask; - - protected void AssertActive() - { - if (IsDisposed) - { - throw new MessageBusException("The message bus is disposed at this time"); - } - } - - protected virtual void AssertRequestResponseConfigured() - { - if (Settings.RequestResponse == null) - { - throw new SendMessageBusException("An attempt to send request when request/response communication was not configured for the message bus. Ensure you configure the bus properly before the application starts."); - } - } - - #region Implementation of IDisposable and IAsyncDisposable - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - DisposeAsyncInternal().ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - - public async ValueTask DisposeAsync() - { - await DisposeAsyncInternal().ConfigureAwait(false); - GC.SuppressFinalize(this); - } - - private async ValueTask DisposeAsyncInternal() - { - if (!IsDisposed && !IsDisposing) - { - IsDisposing = true; - try - { - await DisposeAsyncCore().ConfigureAwait(false); - } - finally - { - IsDisposed = true; - IsDisposing = false; - } - } - } - - /// - /// Stops the consumers and disposes of internal bus objects. - /// - /// - protected async virtual ValueTask DisposeAsyncCore() - { - await Stop().ConfigureAwait(false); - - if (_cancellationTokenSource != null) + PendingRequestManager = settings.ServiceProvider.GetRequiredService(); + PendingRequestStore = PendingRequestManager.Store; + } + + protected virtual IMessageSerializer GetSerializer() => Settings.GetSerializer(Settings.ServiceProvider); + + protected virtual IMessageBusSettingsValidationService ValidationService { get => new DefaultMessageBusSettingsValidationService(Settings); } + + /// + /// Called by the provider to initialize the bus. + /// + protected void OnBuildProvider() + { + ValidationService.AssertSettings(); + + Build(); + + // Notify the bus has been created - before any message can be produced + InitTaskList.Add(() => OnBusLifecycle(MessageBusLifecycleEventType.Created), CancellationToken); + + // Auto start consumers if enabled + if (Settings.AutoStartConsumers) + { + // Fire and forget start + _ = Task.Run(async () => + { + try + { + await Start().ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogError(e, "Could not auto start consumers"); + } + }); + } + } + + protected virtual void Build() + { + ProducerSettingsByMessageType = new ProducerByMessageTypeCache(_logger, BuildProducerByBaseMessageType(), RuntimeTypeCache); + } + + private Dictionary BuildProducerByBaseMessageType() + { + var producerByBaseMessageType = Settings.Producers.ToDictionary(producerSettings => producerSettings.MessageType); + + foreach (var consumerSettings in Settings.Consumers.Where(x => x.ResponseType != null)) + { + // A response type can be used across different requests hence TryAdd + producerByBaseMessageType.TryAdd(consumerSettings.ResponseType, MarkerProducerSettingsForResponses); + } + return producerByBaseMessageType; + } + + private IEnumerable _lifecycleInterceptors; + + private async Task OnBusLifecycle(MessageBusLifecycleEventType eventType) + { + _lifecycleInterceptors ??= Settings.ServiceProvider?.GetServices(); + if (_lifecycleInterceptors != null) + { + foreach (var i in _lifecycleInterceptors) + { + var task = i.OnBusLifecycle(eventType, MessageBusTarget); + if (task != null) + { + await task; + } + } + } + } + + public async Task Start() + { + lock (_startLock) + { + if (IsStarting || IsStarted) + { + return; + } + IsStarting = true; + } + + try + { + await InitTaskList.EnsureAllFinished(); + + _logger.LogInformation("Starting consumers for {BusName} bus...", Name); + await OnBusLifecycle(MessageBusLifecycleEventType.Starting).ConfigureAwait(false); + + await CreateConsumers(); + await OnStart().ConfigureAwait(false); + await Task.WhenAll(_consumers.Select(x => x.Start())).ConfigureAwait(false); + + await OnBusLifecycle(MessageBusLifecycleEventType.Started).ConfigureAwait(false); + _logger.LogInformation("Started consumers for {BusName} bus", Name); + + lock (_startLock) + { + IsStarted = true; + } + } + finally + { + lock (_startLock) + { + IsStarting = false; + } + } + } + + public async Task Stop() + { + lock (_startLock) + { + if (IsStopping || !IsStarted) + { + return; + } + IsStopping = true; + } + + try + { + await InitTaskList.EnsureAllFinished(); + + _logger.LogInformation("Stopping consumers for {BusName} bus...", Name); + await OnBusLifecycle(MessageBusLifecycleEventType.Stopping).ConfigureAwait(false); + + await Task.WhenAll(_consumers.Select(x => x.Stop())).ConfigureAwait(false); + await OnStop().ConfigureAwait(false); + await DestroyConsumers().ConfigureAwait(false); + + await OnBusLifecycle(MessageBusLifecycleEventType.Stopped).ConfigureAwait(false); + _logger.LogInformation("Stopped consumers for {BusName} bus", Name); + + lock (_startLock) + { + IsStarted = false; + } + } + finally + { + lock (_startLock) + { + IsStopping = false; + } + } + } + + protected internal virtual Task OnStart() => Task.CompletedTask; + protected internal virtual Task OnStop() => Task.CompletedTask; + + protected void AssertActive() + { + if (IsDisposed) + { + throw new MessageBusException("The message bus is disposed at this time"); + } + } + + protected virtual void AssertRequestResponseConfigured() + { + if (Settings.RequestResponse == null) + { + throw new SendMessageBusException("An attempt to send request when request/response communication was not configured for the message bus. Ensure you configure the bus properly before the application starts."); + } + } + + #region Implementation of IDisposable and IAsyncDisposable + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposeAsyncInternal().ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncInternal().ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + private async ValueTask DisposeAsyncInternal() + { + if (!IsDisposed && !IsDisposing) + { + IsDisposing = true; + try + { + await DisposeAsyncCore().ConfigureAwait(false); + } + finally + { + IsDisposed = true; + IsDisposing = false; + } + } + } + + /// + /// Stops the consumers and disposes of internal bus objects. + /// + /// + protected async virtual ValueTask DisposeAsyncCore() + { + await Stop().ConfigureAwait(false); + + if (_cancellationTokenSource != null) { await _cancellationTokenSource.CancelAsync(); - _cancellationTokenSource.Dispose(); - _cancellationTokenSource = null; - } - } - - protected virtual Task CreateConsumers() - { - _logger.LogInformation("Creating consumers for {BusName} bus...", Name); - return Task.CompletedTask; - } - - protected async virtual Task DestroyConsumers() - { - _logger.LogInformation("Destroying consumers for {BusName} bus...", Name); - - foreach (var consumer in _consumers) - { - await consumer.DisposeSilently("Consumer", _logger).ConfigureAwait(false); - } - _consumers.Clear(); - } - - #endregion - - protected void AddConsumer(AbstractConsumer consumer) => _consumers.Add(consumer); - - public ICurrentTimeProvider CurrentTimeProvider { get; protected set; } - - protected ProducerSettings GetProducerSettings(Type messageType) - { - var producerSettings = ProducerSettingsByMessageType[messageType]; - if (producerSettings == null && !ReferenceEquals(producerSettings, MarkerProducerSettingsForResponses)) - { - throw new ProducerMessageBusException($"Message of type {messageType} was not registered as a supported produce message. Please check your MessageBus configuration and include this type or one of its base types."); - } - return producerSettings; - } - - protected virtual string GetDefaultPath(Type messageType, ProducerSettings producerSettings) - { - if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); - - var path = producerSettings.DefaultPath - ?? throw new ProducerMessageBusException($"An attempt to produce message of type {messageType} without specifying path, but there was no default path configured. Double check your configuration."); - - _logger.LogDebug("Applying default path {Path} for message type {MessageType}", path, messageType); - return path; + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + } + + protected virtual Task CreateConsumers() + { + _logger.LogInformation("Creating consumers for {BusName} bus...", Name); + return Task.CompletedTask; + } + + protected async virtual Task DestroyConsumers() + { + _logger.LogInformation("Destroying consumers for {BusName} bus...", Name); + + foreach (var consumer in _consumers) + { + await consumer.DisposeSilently("Consumer", _logger).ConfigureAwait(false); + } + _consumers.Clear(); + } + + #endregion + + protected void AddConsumer(AbstractConsumer consumer) => _consumers.Add(consumer); + + public ICurrentTimeProvider CurrentTimeProvider { get; protected set; } + + protected ProducerSettings GetProducerSettings(Type messageType) + { + var producerSettings = ProducerSettingsByMessageType[messageType]; + if (producerSettings == null && !ReferenceEquals(producerSettings, MarkerProducerSettingsForResponses)) + { + throw new ProducerMessageBusException($"Message of type {messageType} was not registered as a supported produce message. Please check your MessageBus configuration and include this type or one of its base types."); + } + return producerSettings; + } + + protected virtual string GetDefaultPath(Type messageType, ProducerSettings producerSettings) + { + if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); + + var path = producerSettings.DefaultPath + ?? throw new ProducerMessageBusException($"An attempt to produce message of type {messageType} without specifying path, but there was no default path configured. Double check your configuration."); + + _logger.LogDebug("Applying default path {Path} for message type {MessageType}", path, messageType); + return path; } public abstract Task ProduceToTransport( @@ -386,10 +386,10 @@ protected void OnProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders) - => _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, messageType, path); + => _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, messageType, path); + + public virtual int? MaxMessagesPerTransaction => null; - public virtual int? MaxMessagesPerTransaction => null; - public async virtual Task> ProduceToTransportBulk( IReadOnlyCollection envelopes, string path, @@ -407,21 +407,21 @@ await ProduceToTransport(envelope.Message, envelope.MessageType, path, envelope. dispatched.Add(envelope); } - return new(dispatched, null); + return new(dispatched, null); } catch (Exception ex) { return new(dispatched, ex); } - } - - public async virtual Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) - { - if (message == null) throw new ArgumentNullException(nameof(message)); + } + + public async virtual Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) + { + if (message == null) throw new ArgumentNullException(nameof(message)); AssertActive(); - await InitTaskList.EnsureAllFinished(); - - // check if the cancellation was already requested + await InitTaskList.EnsureAllFinished(); + + // check if the cancellation was already requested cancellationToken.ThrowIfCancellationRequested(); var messageType = message.GetType(); @@ -488,10 +488,10 @@ public async virtual Task ProducePublish(object message, string path = null, IDi protected static string GetProducerErrorMessage(string path, object message, Type messageType, Exception ex) => $"Producing message {message} of type {messageType?.Name} to path {path} resulted in error: {ex.Message}"; - /// - /// Create an instance of message headers. - /// - /// + /// + /// Create an instance of message headers. + /// + /// public virtual IDictionary CreateHeaders() => new Dictionary(10); private IDictionary GetMessageHeaders(object message, IDictionary headers, ProducerSettings producerSettings) @@ -503,27 +503,27 @@ private IDictionary GetMessageHeaders(object message, IDictionar } return messageHeaders; } - - protected virtual TimeSpan GetDefaultRequestTimeout(Type requestType, ProducerSettings producerSettings) - { - if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); - - var timeout = producerSettings.Timeout ?? Settings.RequestResponse.Timeout; - _logger.LogDebug("Applying default timeout {MessageTimeout} for message type {MessageType}", timeout, requestType); - return timeout; - } - - public virtual async Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) - { - if (request == null) throw new ArgumentNullException(nameof(request)); - AssertActive(); + + protected virtual TimeSpan GetDefaultRequestTimeout(Type requestType, ProducerSettings producerSettings) + { + if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); + + var timeout = producerSettings.Timeout ?? Settings.RequestResponse.Timeout; + _logger.LogDebug("Applying default timeout {MessageTimeout} for message type {MessageType}", timeout, requestType); + return timeout; + } + + public virtual async Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + AssertActive(); AssertRequestResponseConfigured(); - await InitTaskList.EnsureAllFinished(); - - // check if the cancellation was already requested - cancellationToken.ThrowIfCancellationRequested(); - - var requestType = request.GetType(); + await InitTaskList.EnsureAllFinished(); + + // check if the cancellation was already requested + cancellationToken.ThrowIfCancellationRequested(); + + var requestType = request.GetType(); var responseType = typeof(TResponse); var producerSettings = GetProducerSettings(requestType); @@ -573,22 +573,22 @@ public virtual async Task ProduceSend(object request, stri return await SendInternal(request, path, requestType, responseType, producerSettings, created, expires, requestId, requestHeaders, targetBus, cancellationToken); } - protected async internal virtual Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) - { - if (request == null) throw new ArgumentNullException(nameof(request)); - if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); - - // record the request state - var requestState = new PendingRequestState(requestId, request, requestType, responseType, created, expires, cancellationToken); - PendingRequestStore.Add(requestState); - - if (_logger.IsEnabled(LogLevel.Trace)) - { - _logger.LogTrace("Added to PendingRequests, total is {RequestCount}", PendingRequestStore.GetCount()); - } - - try - { + protected async internal virtual Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); + + // record the request state + var requestState = new PendingRequestState(requestId, request, requestType, responseType, created, expires, cancellationToken); + PendingRequestStore.Add(requestState); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("Added to PendingRequests, total is {RequestCount}", PendingRequestStore.GetCount()); + } + + try + { _logger.LogDebug("Sending request message {MessageType} to path {Path} with reply to {ReplyTo}", requestState, path, Settings.RequestResponse.Path); if (requestHeaders != null) @@ -597,66 +597,66 @@ protected async internal virtual Task SendInternal to Task - var responseUntyped = await requestState.TaskCompletionSource.Task.ConfigureAwait(false); - return (TResponseMessage)responseUntyped; - } - - public virtual Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker, CancellationToken cancellationToken) - { - if (requestHeaders == null) throw new ArgumentNullException(nameof(requestHeaders)); - if (consumerInvoker == null) throw new ArgumentNullException(nameof(consumerInvoker)); - - var responseType = consumerInvoker.ParentSettings.ResponseType; - if (!requestHeaders.TryGetHeader(ReqRespMessageHeaders.ReplyTo, out object replyTo)) - { - _logger.LogDebug($$"""Skipping sending response {Response} of type {MessageType} as the header {{ReqRespMessageHeaders.ReplyTo}} is missing for RequestId: {RequestId}""", response, responseType, requestId); - return Task.CompletedTask; - } - - _logger.LogDebug("Sending the response {Response} of type {MessageType} for RequestId: {RequestId}...", response, responseType, requestId); - - var responseHeaders = CreateHeaders(); - responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); - if (responseException != null) - { - responseHeaders.SetHeader(ReqRespMessageHeaders.Error, responseException.Message); - } - - _headerService.AddMessageTypeHeader(response, responseHeaders); - - return ProduceToTransport(response, responseType, (string)replyTo, responseHeaders, null, cancellationToken); - } - - /// - /// Generates unique request IDs - /// - /// - protected virtual string GenerateRequestId() => Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - - public virtual bool IsMessageScopeEnabled(ConsumerSettings consumerSettings, IDictionary consumerContextProperties) - => consumerSettings.IsMessageScopeEnabled ?? Settings.IsMessageScopeEnabled ?? true; - - public virtual IMessageScope CreateMessageScope(ConsumerSettings consumerSettings, object message, IDictionary consumerContextProperties, IServiceProvider currentServiceProvider) - { - var createMessageScope = IsMessageScopeEnabled(consumerSettings, consumerContextProperties); - - if (createMessageScope) - { - _logger.LogDebug("Creating message scope for {Message} of type {MessageType}", message, message.GetType()); - } - return new MessageScopeWrapper(currentServiceProvider ?? Settings.ServiceProvider, createMessageScope); - } - - public virtual Task ProvisionTopology() => Task.CompletedTask; -} + await ProduceToTransport(request, producerSettings.MessageType, path, requestHeaders, targetBus, cancellationToken); + } + catch (Exception e) + { + _logger.LogDebug(e, "Publishing of request message failed"); + // remove from registry + PendingRequestStore.Remove(requestId); + throw; + } + + // convert Task to Task + var responseUntyped = await requestState.TaskCompletionSource.Task.ConfigureAwait(false); + return (TResponseMessage)responseUntyped; + } + + public virtual Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker, CancellationToken cancellationToken) + { + if (requestHeaders == null) throw new ArgumentNullException(nameof(requestHeaders)); + if (consumerInvoker == null) throw new ArgumentNullException(nameof(consumerInvoker)); + + var responseType = consumerInvoker.ParentSettings.ResponseType; + if (!requestHeaders.TryGetHeader(ReqRespMessageHeaders.ReplyTo, out object replyTo)) + { + _logger.LogDebug($$"""Skipping sending response {Response} of type {MessageType} as the header {{ReqRespMessageHeaders.ReplyTo}} is missing for RequestId: {RequestId}""", response, responseType, requestId); + return Task.CompletedTask; + } + + _logger.LogDebug("Sending the response {Response} of type {MessageType} for RequestId: {RequestId}...", response, responseType, requestId); + + var responseHeaders = CreateHeaders(); + responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); + if (responseException != null) + { + responseHeaders.SetHeader(ReqRespMessageHeaders.Error, responseException.Message); + } + + _headerService.AddMessageTypeHeader(response, responseHeaders); + + return ProduceToTransport(response, responseType, (string)replyTo, responseHeaders, null, cancellationToken); + } + + /// + /// Generates unique request IDs + /// + /// + protected virtual string GenerateRequestId() => Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + + public virtual bool IsMessageScopeEnabled(ConsumerSettings consumerSettings, IDictionary consumerContextProperties) + => consumerSettings.IsMessageScopeEnabled ?? Settings.IsMessageScopeEnabled ?? true; + + public virtual IMessageScope CreateMessageScope(ConsumerSettings consumerSettings, object message, IDictionary consumerContextProperties, IServiceProvider currentServiceProvider) + { + var createMessageScope = IsMessageScopeEnabled(consumerSettings, consumerContextProperties); + + if (createMessageScope) + { + _logger.LogDebug("Creating message scope for {Message} of type {MessageType}", message, message.GetType()); + } + return new MessageScopeWrapper(currentServiceProvider ?? Settings.ServiceProvider, createMessageScope); + } + + public virtual Task ProvisionTopology() => Task.CompletedTask; +} diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index f96a2a9c..39ff2b16 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -137,16 +137,18 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{CBE53E71-7F48-415C-BD43-B812EC207BC6}" ProjectSection(SolutionItems) = preProject ..\CONTRIBUTING.md = ..\CONTRIBUTING.md - ..\docs\intro.md = ..\docs\intro.md + ..\docs\intro.t.md = ..\docs\intro.t.md ..\docs\NuGet.md = ..\docs\NuGet.md - ..\docs\provider_amazon_sqs.md = ..\docs\provider_amazon_sqs.md + ..\docs\plugin_asyncapi.t.md = ..\docs\plugin_asyncapi.t.md + ..\docs\provider_amazon_sqs.t.md = ..\docs\provider_amazon_sqs.t.md ..\docs\provider_azure_eventhubs.md = ..\docs\provider_azure_eventhubs.md ..\docs\provider_azure_servicebus.md = ..\docs\provider_azure_servicebus.md ..\docs\provider_hybrid.md = ..\docs\provider_hybrid.md - ..\docs\provider_kafka.md = ..\docs\provider_kafka.md - ..\docs\provider_memory.md = ..\docs\provider_memory.md + ..\docs\provider_kafka.t.md = ..\docs\provider_kafka.t.md + ..\docs\provider_memory.t.md = ..\docs\provider_memory.t.md ..\docs\provider_mqtt.md = ..\docs\provider_mqtt.md - ..\docs\provider_nats.md = ..\docs\provider_nats.md + ..\docs\provider_nats.t.md = ..\docs\provider_nats.t.md + ..\docs\plugin_outbox.t.md = ..\docs\plugin_outbox.t.md ..\docs\provider_redis.md = ..\docs\provider_redis.md ..\docs\README.md = ..\docs\README.md ..\README.md = ..\README.md diff --git a/src/SlimMessageBus/IMessageBus.cs b/src/SlimMessageBus/IMessageBus.cs index 4e0e7ae2..28ab8635 100644 --- a/src/SlimMessageBus/IMessageBus.cs +++ b/src/SlimMessageBus/IMessageBus.cs @@ -1,5 +1,5 @@ -namespace SlimMessageBus; +namespace SlimMessageBus; -public interface IMessageBus : IRequestResponseBus, IPublishBus -{ +public interface IMessageBus : IRequestResponseBus, IPublishBus +{ } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index 7a9378ae..1e934b8b 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -429,7 +429,7 @@ public async Task When_Publish_Given_AConsumersThatThrowsException_Then_Exceptio var consumerErrorHandlerMock = new Mock>(); consumerErrorHandlerMock - .Setup(x => x.OnHandleError(It.IsAny(), It.IsAny>>(), It.IsAny(), It.IsAny())) + .Setup(x => x.OnHandleError(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => errorHandlerHandlesError ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.Failure); _serviceProviderMock.ProviderMock @@ -477,7 +477,7 @@ public async Task When_Send_Given_AHandlerThatThrowsException_Then_ExceptionIsBu var consumerErrorHandlerMock = new Mock>(); consumerErrorHandlerMock - .Setup(x => x.OnHandleError(It.IsAny(), It.IsAny>>(), It.IsAny(), It.IsAny())) + .Setup(x => x.OnHandleError(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => errorHandlerHandlesError ? ConsumerErrorHandlerResult.SuccessWithResponse(null) : ConsumerErrorHandlerResult.Failure); _serviceProviderMock.ProviderMock diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs index 5a70273d..3c884a16 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs @@ -371,7 +371,7 @@ public static Task SimulateFakeException(int counter) /// public class CustomRabbitMqConsumerErrorHandler : IRabbitMqConsumerErrorHandler { - public Task OnHandleError(T message, Func> next, IConsumerContext consumerContext, Exception exception) + public Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) { // Check if this is consumer context for RabbitMQ var isRabbitMqContext = consumerContext.GetTransportMessage() != null; @@ -388,6 +388,7 @@ public Task OnHandleError(T message, Func>(); consumerErrorHandlerMock - .Setup(x => x.OnHandleError(someMessage, It.IsAny>>(), It.IsAny(), someException)) + .Setup(x => x.OnHandleError(someMessage, It.IsAny(), someException, It.IsAny())) .ReturnsAsync(() => errorHandlerWasAbleToHandle ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.Failure); if (errorHandlerRegistered) @@ -134,7 +134,6 @@ public async Task When_DoHandle_Given_ConsumerThatThrowsExceptionAndErrorHandler var result = await subject.DoHandle(someMessage, messageHeaders, consumerInvoker: consumerInvokerMock.Object, currentServiceProvider: busMock.ServiceProviderMock.Object); // assert - result.RequestId.Should().BeNull(); result.Response.Should().BeNull(); @@ -156,7 +155,7 @@ public async Task When_DoHandle_Given_ConsumerThatThrowsExceptionAndErrorHandler { consumerErrorHandlerMock .Verify( - x => x.OnHandleError(someMessage, It.IsAny>>(), It.IsAny(), someException), + x => x.OnHandleError(someMessage, It.IsAny(), someException, It.IsAny()), Times.Once()); consumerErrorHandlerMock @@ -164,6 +163,73 @@ public async Task When_DoHandle_Given_ConsumerThatThrowsExceptionAndErrorHandler } } + [Fact] + public async Task When_DoHandle_Given_ConsumerThatThrowsExceptionAndErrorHandlerRegisteredAndRequestsARetry_Then_RetryInvocation() + { + // arrange + var someMessage = new SomeMessage(); + var someException = fixture.Create(); + var messageHeaders = fixture.Create>(); + + var consumerMock = new Mock>(); + + var consumerErrorHandlerMock = new Mock>(); + + consumerErrorHandlerMock + .Setup(x => x.OnHandleError(someMessage, It.IsAny(), someException, It.IsAny())) + .ReturnsAsync(() => ConsumerErrorHandlerResult.Retry); + + busMock.ServiceProviderMock + .Setup(x => x.GetService(typeof(IConsumerErrorHandler))) + .Returns(consumerErrorHandlerMock.Object); + + busMock.ServiceProviderMock + .Setup(x => x.GetService(typeof(IConsumer))) + .Returns(consumerMock.Object); + + consumerMethodMock + .SetupSequence(x => x(consumerMock.Object, someMessage, It.IsAny(), It.IsAny())) + .ThrowsAsync(someException) + .Returns(Task.CompletedTask); + + consumerInvokerMock + .SetupGet(x => x.ParentSettings) + .Returns(new ConsumerSettings()); + + consumerInvokerMock + .SetupGet(x => x.ConsumerType) + .Returns(typeof(IConsumer)); + + messageScopeMock + .SetupGet(x => x.ServiceProvider) + .Returns(busMock.ServiceProviderMock.Object); + + // act + var result = await subject.DoHandle(someMessage, messageHeaders, consumerInvoker: consumerInvokerMock.Object, currentServiceProvider: busMock.ServiceProviderMock.Object); + + // assert + result.RequestId.Should().BeNull(); + result.Response.Should().BeNull(); + result.ResponseException.Should().BeNull(); + + messageScopeFactoryMock + .Verify(x => x.CreateMessageScope(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()), Times.Exactly(2)); + + consumerMethodMock + .Verify(x => x(consumerMock.Object, someMessage, It.IsAny(), It.IsAny()), Times.Exactly(2)); + + consumerMethodMock + .VerifyNoOtherCalls(); + + consumerErrorHandlerMock + .Verify( + x => x.OnHandleError(someMessage, It.IsAny(), someException, It.IsAny()), + Times.Once()); + + consumerErrorHandlerMock + .VerifyNoOtherCalls(); + } + [Fact] public async Task When_ExecuteConsumer_Given_Handler_Then_ReturnsResponse() { From 2e39563cd80b69b677f41dfcbe3600caca1546f0 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sun, 29 Dec 2024 13:14:35 +0100 Subject: [PATCH 09/21] Prepare release Signed-off-by: Tomasz Maruszak --- docs/intro.md | 19 +++++++++++-------- docs/intro.t.md | 19 +++++++++++-------- src/Host.Plugin.Properties.xml | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index 872ce976..6fa2acf1 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1086,14 +1086,15 @@ public interface IConsumerErrorHandler ``` The returned `ConsumerErrorHandlerResult` object is used to override the execution for the remainder of the execution pipeline. -| Result | Description | -|---------|-------------| -| Failure | The message failed to be processed and should be returned to the queue | -| Success | The pipeline must treat the message as having been processed successfully | -| SuccessWithResponse | The pipeline to treat the messagage as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | -| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | -[^1]: `Retry` will recreate the message scope on every atttempt if `PerMessageScopeEnabled` has been enabled. +| Result | Description | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Failure | The message failed to be processed and should be returned to the queue | +| Success | The pipeline must treat the message as having been processed successfully | +| SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | +| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | + +[^1]: `Retry` will recreate the message scope on every attempt if `PerMessageScopeEnabled` has been enabled. To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: @@ -1114,12 +1115,14 @@ Transport plugins provide specialized error handling interfaces. Examples includ - [INatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) - [IServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) - [IEventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) +- [ISqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) > The message processing pipeline will always attempt to use the transport-specific error handler (e.g., `IMemoryConsumerErrorHandler`) first. If unavailable, it will then look for the generic error handler (`IConsumerErrorHandler`). This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. -Sample retry with exponential back-off: +Sample retry with exponential back-off (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): + ```cs public class RetryHandler : ConsumerErrorHandler { diff --git a/docs/intro.t.md b/docs/intro.t.md index e998bb39..0aec5bb9 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -1064,14 +1064,15 @@ Message processing by consumers or handlers may result in exceptions. The [ICons @[:cs](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs,Interface) The returned `ConsumerErrorHandlerResult` object is used to override the execution for the remainder of the execution pipeline. -| Result | Description | -|---------|-------------| -| Failure | The message failed to be processed and should be returned to the queue | -| Success | The pipeline must treat the message as having been processed successfully | -| SuccessWithResponse | The pipeline to treat the messagage as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | -| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | -[^1]: `Retry` will recreate the message scope on every atttempt if `PerMessageScopeEnabled` has been enabled. +| Result | Description | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Failure | The message failed to be processed and should be returned to the queue | +| Success | The pipeline must treat the message as having been processed successfully | +| SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | +| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | + +[^1]: `Retry` will recreate the message scope on every attempt if `PerMessageScopeEnabled` has been enabled. To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: @@ -1092,12 +1093,14 @@ Transport plugins provide specialized error handling interfaces. Examples includ - [INatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) - [IServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) - [IEventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) +- [ISqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) > The message processing pipeline will always attempt to use the transport-specific error handler (e.g., `IMemoryConsumerErrorHandler`) first. If unavailable, it will then look for the generic error handler (`IConsumerErrorHandler`). This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. -Sample retry with exponential back-off: +Sample retry with exponential back-off (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): + ```cs public class RetryHandler : ConsumerErrorHandler { diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index d8a8d921..ea2f607f 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc900 + 3.0.0-rc901 \ No newline at end of file From 8f78ce5049928aa98e6221ebed82f70403dc4e0a Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Mon, 30 Dec 2024 00:03:54 +0800 Subject: [PATCH 10/21] #347 Documentation Signed-off-by: Richard Pringle --- docs/intro.md | 4 +--- docs/intro.t.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index 6fa2acf1..0746dfc1 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1092,9 +1092,7 @@ The returned `ConsumerErrorHandlerResult` object is used to override the executi | Failure | The message failed to be processed and should be returned to the queue | | Success | The pipeline must treat the message as having been processed successfully | | SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | -| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | - -[^1]: `Retry` will recreate the message scope on every attempt if `PerMessageScopeEnabled` has been enabled. +| Retry | Re-create and execute the pipeline (including the message scope when using [per-message DI container scopes](#per-message-di-container-scope)) | To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: diff --git a/docs/intro.t.md b/docs/intro.t.md index 0aec5bb9..7e1e4f79 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -1070,9 +1070,7 @@ The returned `ConsumerErrorHandlerResult` object is used to override the executi | Failure | The message failed to be processed and should be returned to the queue | | Success | The pipeline must treat the message as having been processed successfully | | SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | -| Retry | Execute the pipeline again (any delay/jitter should be applied before returning from method)[^1] | - -[^1]: `Retry` will recreate the message scope on every attempt if `PerMessageScopeEnabled` has been enabled. +| Retry | Re-create and execute the pipeline (including the message scope when using [per-message DI container scopes](#per-message-di-container-scope)) | To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: From fd2e9e37e7483594c824739e2d1ab252aa643128 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Wed, 1 Jan 2025 10:05:48 +0100 Subject: [PATCH 11/21] Include release/* branches in PR targets Signed-off-by: Tomasz Maruszak --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4502db1..da23228a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: push: branches: ["master", "release/*", "feature/*"] pull_request_target: - branches: ["master", "devops/*"] + branches: ["master", "release/*", "devops/*"] workflow_dispatch: permissions: From 15834b6f86740b1019b785c06ce74e774a89e121 Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Tue, 31 Dec 2024 22:50:47 +0800 Subject: [PATCH 12/21] #356 Abandon/dead letter message Signed-off-by: Richard Pringle --- docs/intro.md | 48 +++++++++- docs/intro.t.md | 48 +++++++++- .../Consumer/ISqsConsumerErrorHandler.cs | 2 +- .../Consumer/SqsBaseConsumer.cs | 5 + .../Consumer/EhPartitionConsumer.cs | 5 + .../Consumer/IEventHubConsumerErrorHandler.cs | 2 +- .../Consumer/AsbBaseConsumer.cs | 54 +++++++---- .../IServiceBusConsumerErrorHandler.cs | 2 +- .../Consumer/IKafkaConsumerErrorHandler.cs | 2 +- .../Consumer/KafkaPartitionConsumer.cs | 5 + .../Consumers/IMemoryConsumerErrorHandler.cs | 2 +- .../MemoryMessageBus.cs | 5 + .../IMqttConsumerErrorHandler.cs | 2 +- .../INatsConsumerErrorHandler.cs | 2 +- .../RabbitMqConsumerBuilderExtensions.cs | 16 +++- .../Config/RabbitMqHasProviderExtensions.cs | 2 +- .../RabbitMqMessageBusBuilderExtensions.cs | 4 +- .../Config/RabbitMqProperties.cs | 4 +- .../IRabbitMqConsumerErrorHandler.cs | 2 +- ...RabbitMqAutoAcknowledgeMessageProcessor.cs | 50 +++++++--- .../Consumers/RabbitMqConsumer.cs | 4 +- .../Consumers/RabbitMqResponseConsumer.cs | 24 +++-- ...itMqMessageBusSettingsValidationService.cs | 3 +- .../Consumers/IRedisConsumerErrorHandler.cs | 2 +- .../Consumers/RedisListCheckerConsumer.cs | 5 + .../Consumers/RedisTopicConsumer.cs | 5 + .../ErrorHandling/ConsumerErrorHandler.cs | 7 +- .../ConsumerErrorHandlerResult.cs | 27 +++--- .../ConcurrentMessageProcessorDecorator.cs | 8 +- .../MessageProcessors/IMessageHandler.cs | 2 +- .../MessageProcessors/IMessageProcessor.cs | 3 +- .../MessageProcessors/MessageHandler.cs | 18 ++-- .../MessageProcessors/MessageProcessor.cs | 9 +- .../MessageProcessors/ProcessResult.cs | 9 ++ .../ResponseMessageProcessor.cs | 9 +- .../ServiceBusMessageBusIt.cs | 91 +++++++++++++++++++ .../ConcurrentMessageProcessorQueueTests.cs | 2 +- .../Consumers/MessageProcessorQueueTests.cs | 2 +- ...tMqAutoAcknowledgeMessageProcessorTests.cs | 37 +++++--- .../IntegrationTest/BaseIntegrationTest.cs | 13 +++ ...ConcurrentMessageProcessorDecoratorTest.cs | 6 +- 41 files changed, 428 insertions(+), 120 deletions(-) create mode 100644 src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs diff --git a/docs/intro.md b/docs/intro.md index 0746dfc1..984d8886 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1089,10 +1089,11 @@ The returned `ConsumerErrorHandlerResult` object is used to override the executi | Result | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Abandon | The message should be sent to the dead letter queue/exchange. **Not supported by all transports.** | | Failure | The message failed to be processed and should be returned to the queue | | Success | The pipeline must treat the message as having been processed successfully | | SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | -| Retry | Re-create and execute the pipeline (including the message scope when using [per-message DI container scopes](#per-message-di-container-scope)) | +| Retry | Re-create and execute the pipeline (including the message scope when using [per-message DI container scopes](#per-message-di-container-scope)) | To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: @@ -1119,8 +1120,35 @@ Transport plugins provide specialized error handling interfaces. Examples includ This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. -Sample retry with exponential back-off (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): + +### Abandon +#### Azure Service Bus +The Azure Service Bus transport has full support for abandoning messages to the dead letter queue. + +#### RabbitMQ +Abandon will issue a `Nack` with `requeue: false`. + +#### Other transports +No other transports currently support `Abandon` and calling `Abandon` will result in `NotSupportedException` being thrown. + +### Failure +#### RabbitMQ +While RabbitMQ supports dead letter exchanges, SMB's default implementation is not to requeue messages on `Failure`. If requeuing is required, it can be enabled by setting `RequeueOnFailure()` when configuring a consumer/handler. + +Please be aware that as RabbitMQ does not have a maximum delivery count and enabling requeue may result in an infinite message loop. When `RequeueOnFailure()` has been set, it is the developer's responsibility to configure an appropriate `IConsumerErrorHandler` that will `Abandon` all non-transient exceptions. +```cs +.Handle(x => x + .Queue("echo-request-handler") + .ExchangeBinding("test-echo") + .DeadLetterExchange("echo-request-handler-dlq") + // requeue a message on failure + .RequeueOnFailure() + .WithHandler()) +``` + +### Example usage +Retry with exponential back-off and short-curcuit dead letter on non-transient exceptions (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): ```cs public class RetryHandler : ConsumerErrorHandler { @@ -1128,6 +1156,11 @@ public class RetryHandler : ConsumerErrorHandler public override async Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) { + if (!IsTranientException(exception)) + { + return Abandon(); + } + if (attempts < 3) { var delay = (attempts * 1000) + (_random.Next(1000) - 500); @@ -1137,9 +1170,18 @@ public class RetryHandler : ConsumerErrorHandler return Failure(); } + + private static bool IsTransientException(Exception exception) + { + while (exception is not SqlException && exception.InnerException != null) + { + exception = exception.InnerException; + } + + return exception is SqlException { Number: -2 or 1205 }; // Timeout or deadlock + } } ``` - ## Logging SlimMessageBus uses [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions): diff --git a/docs/intro.t.md b/docs/intro.t.md index 7e1e4f79..6d64a390 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -1067,10 +1067,11 @@ The returned `ConsumerErrorHandlerResult` object is used to override the executi | Result | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Abandon | The message should be sent to the dead letter queue/exchange. **Not supported by all transports.** | | Failure | The message failed to be processed and should be returned to the queue | | Success | The pipeline must treat the message as having been processed successfully | | SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | -| Retry | Re-create and execute the pipeline (including the message scope when using [per-message DI container scopes](#per-message-di-container-scope)) | +| Retry | Re-create and execute the pipeline (including the message scope when using [per-message DI container scopes](#per-message-di-container-scope)) | To enable SMB to recognize the error handler, it must be registered within the Microsoft Dependency Injection (MSDI) framework: @@ -1097,8 +1098,35 @@ Transport plugins provide specialized error handling interfaces. Examples includ This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. -Sample retry with exponential back-off (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): + +### Abandon +#### Azure Service Bus +The Azure Service Bus transport has full support for abandoning messages to the dead letter queue. + +#### RabbitMQ +Abandon will issue a `Nack` with `requeue: false`. + +#### Other transports +No other transports currently support `Abandon` and calling `Abandon` will result in `NotSupportedException` being thrown. + +### Failure +#### RabbitMQ +While RabbitMQ supports dead letter exchanges, SMB's default implementation is not to requeue messages on `Failure`. If requeuing is required, it can be enabled by setting `RequeueOnFailure()` when configuring a consumer/handler. + +Please be aware that as RabbitMQ does not have a maximum delivery count and enabling requeue may result in an infinite message loop. When `RequeueOnFailure()` has been set, it is the developer's responsibility to configure an appropriate `IConsumerErrorHandler` that will `Abandon` all non-transient exceptions. +```cs +.Handle(x => x + .Queue("echo-request-handler") + .ExchangeBinding("test-echo") + .DeadLetterExchange("echo-request-handler-dlq") + // requeue a message on failure + .RequeueOnFailure() + .WithHandler()) +``` + +### Example usage +Retry with exponential back-off and short-curcuit dead letter on non-transient exceptions (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): ```cs public class RetryHandler : ConsumerErrorHandler { @@ -1106,6 +1134,11 @@ public class RetryHandler : ConsumerErrorHandler public override async Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) { + if (!IsTranientException(exception)) + { + return Abandon(); + } + if (attempts < 3) { var delay = (attempts * 1000) + (_random.Next(1000) - 500); @@ -1115,9 +1148,18 @@ public class RetryHandler : ConsumerErrorHandler return Failure(); } + + private static bool IsTransientException(Exception exception) + { + while (exception is not SqlException && exception.InnerException != null) + { + exception = exception.InnerException; + } + + return exception is SqlException { Number: -2 or 1205 }; // Timeout or deadlock + } } ``` - ## Logging SlimMessageBus uses [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions): diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs index c4221549..d7cb3e57 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface ISqsConsumerErrorHandler : IConsumerErrorHandler; -public abstract class SqsConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class SqsConsumerErrorHandler : ConsumerErrorHandler, ISqsConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs index 95818eb7..b5fec9ad 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs @@ -116,6 +116,11 @@ protected async Task Run() .ToDictionary(x => x.Key, x => HeaderSerializer.Deserialize(x.Key, x.Value)); var r = await MessageProcessor.ProcessMessage(message, messageHeaders, cancellationToken: CancellationToken).ConfigureAwait(false); + if (r.Result == ProcessResult.Abandon) + { + throw new NotSupportedException("Transport does not support abandoning messages"); + } + if (r.Exception != null) { Logger.LogError(r.Exception, "Message processing error - Queue: {Queue}, MessageId: {MessageId}", Path, message.MessageId); diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs index f84f7a79..853b12da 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs @@ -42,6 +42,11 @@ public async Task ProcessEventAsync(ProcessEventArgs args) var headers = GetHeadersFromTransportMessage(args.Data); var r = await MessageProcessor.ProcessMessage(args.Data, headers, cancellationToken: args.CancellationToken).ConfigureAwait(false); + if (r.Result == ProcessResult.Abandon) + { + throw new NotSupportedException("Transport does not support abandoning messages"); + } + if (r.Exception != null) { // Note: The OnMessageFaulted was called at this point by the MessageProcessor. diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs index 335f02bb..978bcbea 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IEventHubConsumerErrorHandler : IConsumerErrorHandler; -public abstract class EventHubConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class EventHubConsumerErrorHandler : ConsumerErrorHandler, IEventHubConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs index 9154d6d8..daba0eb8 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs @@ -137,18 +137,23 @@ private Task ServiceBusSessionProcessor_SessionClosingAsync(ProcessSessionEventA } private Task ServiceBusSessionProcessor_ProcessMessageAsync(ProcessSessionMessageEventArgs args) - => ProcessMessageAsyncInternal(args.Message, args.CompleteMessageAsync, args.AbandonMessageAsync, args.CancellationToken); + => ProcessMessageAsyncInternal(args.Message, args.CompleteMessageAsync, args.AbandonMessageAsync, args.DeadLetterMessageAsync, args.CancellationToken); private Task ServiceBusSessionProcessor_ProcessErrorAsync(ProcessErrorEventArgs args) => ProcessErrorAsyncInternal(args.Exception, args.ErrorSource); protected Task ServiceBusProcessor_ProcessMessagesAsync(ProcessMessageEventArgs args) - => ProcessMessageAsyncInternal(args.Message, args.CompleteMessageAsync, args.AbandonMessageAsync, args.CancellationToken); + => ProcessMessageAsyncInternal(args.Message, args.CompleteMessageAsync, args.AbandonMessageAsync, args.DeadLetterMessageAsync, args.CancellationToken); protected Task ServiceBusProcessor_ProcessErrorAsync(ProcessErrorEventArgs args) => ProcessErrorAsyncInternal(args.Exception, args.ErrorSource); - protected async Task ProcessMessageAsyncInternal(ServiceBusReceivedMessage message, Func completeMessage, Func, CancellationToken, Task> abandonMessage, CancellationToken token) + protected async Task ProcessMessageAsyncInternal( + ServiceBusReceivedMessage message, + Func completeMessage, + Func, CancellationToken, Task> abandonMessage, + Func deadLetterMessage, + CancellationToken token) { // Process the message. Logger.LogDebug("Received message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); @@ -160,29 +165,38 @@ protected async Task ProcessMessageAsyncInternal(ServiceBusReceivedMessage messa // to avoid unnecessary exceptions. Logger.LogDebug("Abandon message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); await abandonMessage(message, null, token).ConfigureAwait(false); - return; } var r = await MessageProcessor.ProcessMessage(message, message.ApplicationProperties, cancellationToken: token).ConfigureAwait(false); - if (r.Exception != null) + switch (r.Result) { - Logger.LogError(r.Exception, "Abandon message (exception occurred while processing) - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); - - var messageProperties = new Dictionary - { - // Set the exception message - ["SMB.Exception"] = r.Exception.Message - }; - await abandonMessage(message, messageProperties, token).ConfigureAwait(false); - - return; + case ProcessResult.Success: + // Complete the message so that it is not received again. + // This can be done only if the subscriptionClient is created in ReceiveMode.PeekLock mode (which is the default). + Logger.LogDebug("Complete message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); + await completeMessage(message, token).ConfigureAwait(false); + return; + + case ProcessResult.Abandon: + Logger.LogError(r.Exception, "Dead letter message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); + await deadLetterMessage(message, r.Exception?.GetType().Name ?? string.Empty, r.Exception?.Message ?? string.Empty, token).ConfigureAwait(false); + return; + + case ProcessResult.Fail: + var messageProperties = new Dictionary(); + { + // Set the exception message + messageProperties.Add("SMB.Exception", r.Exception.Message); + } + + Logger.LogError(r.Exception, "Abandon message (exception occurred while processing) - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); + await abandonMessage(message, messageProperties, token).ConfigureAwait(false); + return; + + default: + throw new NotImplementedException(); } - - // Complete the message so that it is not received again. - // This can be done only if the subscriptionClient is created in ReceiveMode.PeekLock mode (which is the default). - Logger.LogDebug("Complete message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); - await completeMessage(message, token).ConfigureAwait(false); } protected Task ProcessErrorAsyncInternal(Exception exception, ServiceBusErrorSource errorSource) diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs index a8ad8166..3e963ae2 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IServiceBusConsumerErrorHandler : IConsumerErrorHandler; -public abstract class ServiceBusConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class ServiceBusConsumerErrorHandler : ConsumerErrorHandler, IServiceBusConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs index a7f391bf..684ad008 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IKafkaConsumerErrorHandler : IConsumerErrorHandler; -public abstract class KafkaConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class KafkaConsumerErrorHandler : ConsumerErrorHandler, IKafkaConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs index d4b73d02..0474acab 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs @@ -115,6 +115,11 @@ public async Task OnMessage(ConsumeResult message) } var r = await _messageProcessor.ProcessMessage(message, messageHeaders, cancellationToken: _cancellationTokenSource.Token).ConfigureAwait(false); + if (r.Result == ProcessResult.Abandon) + { + throw new NotSupportedException("Transport does not support abandoning messages"); + } + if (r.Exception != null) { // The IKafkaConsumerErrorHandler and OnMessageFaulted was called at this point by the MessageProcessor. diff --git a/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs index f235ab2c..9f02e618 100644 --- a/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IMemoryConsumerErrorHandler : IConsumerErrorHandler; -public abstract class MemoryConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class MemoryConsumerErrorHandler : ConsumerErrorHandler, IMemoryConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs index 20b36e1a..ed5d7e74 100644 --- a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs +++ b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs @@ -158,6 +158,11 @@ private async Task ProduceInternal(object me var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; // Execute the message processor in synchronous manner var r = await messageProcessor.ProcessMessage(transportMessage, messageHeadersReadOnly, currentServiceProvider: serviceProvider, cancellationToken: cancellationToken); + if (r.Result == ProcessResult.Abandon) + { + throw new NotSupportedException("Transport does not support abandoning messages"); + } + if (r.Exception != null) { // We want to pass the same exception to the sender as it happened in the handler/consumer diff --git a/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs index 9bef2034..6fcf2bba 100644 --- a/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Mqtt/IMqttConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IMqttConsumerErrorHandler : IConsumerErrorHandler; -public abstract class MqttConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class MqttConsumerErrorHandler : ConsumerErrorHandler, IMqttConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs index 8867a961..ad1f8833 100644 --- a/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface INatsConsumerErrorHandler : IConsumerErrorHandler; -public abstract class NatsConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class NatsConsumerErrorHandler : ConsumerErrorHandler, INatsConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs index baec5ffd..e7b368fd 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs @@ -118,5 +118,19 @@ public static TConsumerBuilder AcknowledgementMode(this TConsu builder.ConsumerSettings.Properties[RabbitMqProperties.MessageAcknowledgementMode] = mode; return builder; } -} + /// + /// Requeues a message on failure. Abandoned messages will not be requeued. + /// This may lead to an infinite loop if the reason for the failure is not transient. Responsibility lies with the developer to ensure that this flow does not occur. + /// + /// + /// + /// + /// + public static TConsumerBuilder RequeueOnFailure(this TConsumerBuilder builder, bool state = true) + where TConsumerBuilder : AbstractConsumerBuilder + { + builder.ConsumerSettings.Properties[RabbitMqProperties.ReqeueOnFailure] = state; + return builder; + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqHasProviderExtensions.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqHasProviderExtensions.cs index 85e8c34e..dc2f6048 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqHasProviderExtensions.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqHasProviderExtensions.cs @@ -14,6 +14,6 @@ public static string GetQueueName(this AbstractConsumerSettings c) public static string GetBindingRoutingKey(this AbstractConsumerSettings c, HasProviderExtensions settings = null) => c.GetOrDefault(RabbitMqProperties.BindingRoutingKey, settings, null); - public static string GetExchageType(this ProducerSettings p, HasProviderExtensions settings = null) + public static string GetExchangeType(this ProducerSettings p, HasProviderExtensions settings = null) => p.GetOrDefault(RabbitMqProperties.ExchangeType, settings, null); } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqMessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqMessageBusBuilderExtensions.cs index 3a4aa7f4..8af865a8 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqMessageBusBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqMessageBusBuilderExtensions.cs @@ -14,8 +14,8 @@ public static MessageBusBuilder WithProviderRabbitMQ(this MessageBusBuilder mbb, if (mbb == null) throw new ArgumentNullException(nameof(mbb)); if (configure == null) throw new ArgumentNullException(nameof(configure)); - var providerSettings = mbb.Settings.GetOrCreate(RabbitMqProperties.ProvderSettings, () => new RabbitMqMessageBusSettings()); - + var providerSettings = mbb.Settings.GetOrCreate(RabbitMqProperties.ProviderSettings, () => new RabbitMqMessageBusSettings()); + configure(providerSettings); return mbb.WithProvider(settings => new RabbitMqMessageBus(settings, providerSettings)); diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs index ed74d444..1e32d821 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs @@ -28,7 +28,9 @@ static internal class RabbitMqProperties public static readonly string Message = $"RabbitMQ_{nameof(Message)}"; - public static readonly string ProvderSettings = $"RabbitMQ_{nameof(ProvderSettings)}"; + public static readonly string ProviderSettings = $"RabbitMQ_{nameof(ProviderSettings)}"; public static readonly string MessageAcknowledgementMode = $"RabbitMQ_{nameof(MessageAcknowledgementMode)}"; + + public static readonly string ReqeueOnFailure = $"RabbitMQ_{nameof(ReqeueOnFailure)}"; } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs index 2a48eab3..4bac0cde 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IRabbitMqConsumerErrorHandler : IConsumerErrorHandler; -public abstract class RabbitMqConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class RabbitMqConsumerErrorHandler : ConsumerErrorHandler, IRabbitMqConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs index 9cad553e..439aef92 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs @@ -7,17 +7,33 @@ /// /// /// -internal sealed class RabbitMqAutoAcknowledgeMessageProcessor(IMessageProcessor target, - ILogger logger, - RabbitMqMessageAcknowledgementMode acknowledgementMode, - IRabbitMqConsumer consumer) - : IMessageProcessor, IDisposable +internal sealed class RabbitMqAutoAcknowledgeMessageProcessor : IMessageProcessor, IDisposable { - public IReadOnlyCollection ConsumerSettings => target.ConsumerSettings; + private readonly IMessageProcessor _target; + private readonly ILogger _logger; + private readonly RabbitMqMessageAcknowledgementMode _acknowledgementMode; + private readonly IRabbitMqConsumer _consumer; + private readonly bool _requeueOnFailure; + + public RabbitMqAutoAcknowledgeMessageProcessor( + IMessageProcessor target, + ILogger logger, + RabbitMqMessageAcknowledgementMode acknowledgementMode, + IRabbitMqConsumer consumer) + { + _target = target; + _logger = logger; + _acknowledgementMode = acknowledgementMode; + _consumer = consumer; + + _requeueOnFailure = _target.ConsumerSettings?.All(x => x.GetOrDefault(RabbitMqProperties.ReqeueOnFailure, false)) ?? false; + } + + public IReadOnlyCollection ConsumerSettings => _target.ConsumerSettings; public void Dispose() { - if (target is IDisposable targetDisposable) + if (_target is IDisposable targetDisposable) { targetDisposable.Dispose(); } @@ -25,22 +41,26 @@ public void Dispose() public async Task ProcessMessage(BasicDeliverEventArgs transportMessage, IReadOnlyDictionary messageHeaders, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) { - var r = await target.ProcessMessage(transportMessage, messageHeaders: messageHeaders, consumerContextProperties: consumerContextProperties, cancellationToken: cancellationToken); - - if (acknowledgementMode == RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade) + var r = await _target.ProcessMessage(transportMessage, messageHeaders: messageHeaders, consumerContextProperties: consumerContextProperties, cancellationToken: cancellationToken); + if (_acknowledgementMode == RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade) { // Acknowledge after processing - var confirmOption = r.Exception != null - ? RabbitMqMessageConfirmOptions.Nack // NAck after processing when message fails (unless the user already acknowledged in any way). - : RabbitMqMessageConfirmOptions.Ack; // Acknowledge after processing + var confirmOption = r.Result switch + { + ProcessResult.Abandon => RabbitMqMessageConfirmOptions.Nack, // NAck after processing when message fails with non-transient exception (unless the user already acknowledged in any way). + ProcessResult.Fail when (_requeueOnFailure) => RabbitMqMessageConfirmOptions.Nack | RabbitMqMessageConfirmOptions.Requeue, // Re-queue after processing on transient failure + ProcessResult.Fail when (!_requeueOnFailure) => RabbitMqMessageConfirmOptions.Nack, // Fail after processing failure (no re-queue) + ProcessResult.Success => RabbitMqMessageConfirmOptions.Ack, // Acknowledge after processing + _ => throw new NotImplementedException() + }; - consumer.ConfirmMessage(transportMessage, confirmOption, consumerContextProperties); + _consumer.ConfirmMessage(transportMessage, confirmOption, consumerContextProperties); } if (r.Exception != null) { // We rely on the IMessageProcessor to execute the ConsumerErrorHandler, but if it's not registered in the DI, it fails, or there is another fatal error then the message will be lost. - logger.LogError(r.Exception, "Exchange {Exchange} - Queue {Queue}: Error processing message {Message}, delivery tag {DeliveryTag}", transportMessage.Exchange, consumer.QueueName, transportMessage, transportMessage.DeliveryTag); + _logger.LogError(r.Exception, "Exchange {Exchange} - Queue {Queue}: Error processing message {Message}, delivery tag {DeliveryTag}", transportMessage.Exchange, _consumer.QueueName, transportMessage, transportMessage.DeliveryTag); } return r; } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs index f29c3e72..34150846 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs @@ -36,7 +36,7 @@ IMessageProcessor CreateMessageProcessor(IEnumerable)); @@ -44,7 +44,7 @@ IMessageProcessor CreateMessageProcessor(IEnumerable x.Instances); - // For a given rabbit channel, there is only 1 task that dispatches messages. We want to be be able to let each SMB consume process within its own task (1 or more) + // For a given rabbit channel, there is only 1 task that dispatches messages. We want to be able to let each SMB consume process within its own task (1 or more) messageProcessor = new ConcurrentMessageProcessorDecorator(instances, loggerFactory, messageProcessor); return messageProcessor; diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs index 59ed5c66..3ce14894 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs @@ -3,6 +3,7 @@ public class RabbitMqResponseConsumer : AbstractRabbitMqConsumer { private readonly IMessageProcessor _messageProcessor; + private readonly bool _requeueOnFailure; protected override RabbitMqMessageAcknowledgementMode AcknowledgementMode => RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade; @@ -18,19 +19,30 @@ public RabbitMqResponseConsumer( : base(loggerFactory.CreateLogger(), channel, queueName, headerValueConverter) { _messageProcessor = new ResponseMessageProcessor(loggerFactory, requestResponseSettings, messageProvider, pendingRequestStore, currentTimeProvider); + _requeueOnFailure = requestResponseSettings.GetOrDefault(RabbitMqProperties.ReqeueOnFailure, false); } protected override async Task OnMessageReceived(Dictionary messageHeaders, BasicDeliverEventArgs transportMessage) { var r = await _messageProcessor.ProcessMessage(transportMessage, messageHeaders: messageHeaders, cancellationToken: CancellationToken); - if (r.Exception == null) + switch (r.Result) { - AckMessage(transportMessage); - } - else - { - NackMessage(transportMessage, requeue: false); + case ProcessResult.Abandon: + NackMessage(transportMessage, requeue: false); + break; + + case ProcessResult.Fail: + NackMessage(transportMessage, requeue: _requeueOnFailure); + break; + + case ProcessResult.Success: + AckMessage(transportMessage); + break; + + default: + throw new NotImplementedException(); } + return r.Exception; } } diff --git a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBusSettingsValidationService.cs b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBusSettingsValidationService.cs index a20337c1..444d08dc 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBusSettingsValidationService.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBusSettingsValidationService.cs @@ -32,7 +32,7 @@ protected override void AssertProducer(ProducerSettings producerSettings) if (routingKeyProvider == null) { // Requirement for the routing key depends on the ExchangeType - var exchangeType = producerSettings.GetExchageType(ProviderSettings); + var exchangeType = producerSettings.GetExchangeType(ProviderSettings); if (exchangeType == global::RabbitMQ.Client.ExchangeType.Direct || exchangeType == global::RabbitMQ.Client.ExchangeType.Topic) { ThrowProducerFieldNotSet(producerSettings, nameof(RabbitMqProducerBuilderExtensions.RoutingKeyProvider), $"is neither provided on the producer for exchange {producerSettings.DefaultPath} nor a default provider exists at the bus level (check that .{nameof(RabbitMqProducerBuilderExtensions.RoutingKeyProvider)}() exists on the producer or bus level). Exchange type {exchangeType} requires the producer to has a routing key provider."); @@ -75,6 +75,5 @@ protected override void AssertRequestResponseSettings() ThrowRequestResponseFieldNotSet(nameof(RabbitMqConsumerBuilderExtensions.Queue)); } } - } } diff --git a/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs b/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs index d041b78b..e1272c10 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs @@ -2,4 +2,4 @@ public interface IRedisConsumerErrorHandler : IConsumerErrorHandler; -public abstract class RedisConsumerErrorHandler : ConsumerErrorHandler; \ No newline at end of file +public abstract class RedisConsumerErrorHandler : ConsumerErrorHandler, IRedisConsumerErrorHandler; \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs b/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs index f178cd80..1b33b4e4 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs @@ -74,6 +74,11 @@ protected async Task Run() var processor = queue.Processors[i]; var r = await processor.ProcessMessage(transportMessage, transportMessage.Headers, cancellationToken: CancellationToken).ConfigureAwait(false); + if (r.Result == ProcessResult.Abandon) + { + throw new NotSupportedException("Transport does not support abandoning messages"); + } + if (r.Exception != null) { Logger.LogError(r.Exception, "Error occurred while processing the list item on {Queue}", queue.Name); diff --git a/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs b/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs index 77204dff..099af9bd 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs @@ -45,6 +45,11 @@ private async Task OnMessage(ChannelMessage m) var messageWithHeaders = (MessageWithHeaders)_envelopeSerializer.Deserialize(typeof(MessageWithHeaders), m.Message); var r = await _messageProcessor.ProcessMessage(messageWithHeaders, messageWithHeaders.Headers, cancellationToken: CancellationToken); + if (r.Result == ProcessResult.Abandon) + { + throw new NotSupportedException("Transport does not support abandoning messages"); + } + exception = r.Exception; } catch (Exception e) diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs index 94d584c7..f031335f 100644 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs @@ -7,7 +7,8 @@ public abstract class ConsumerErrorHandler : BaseConsumerErrorHandler, IConsu public abstract class BaseConsumerErrorHandler { - public static ConsumerErrorHandlerResult Failure() => ConsumerErrorHandlerResult.Failure; - public static ConsumerErrorHandlerResult Retry() => ConsumerErrorHandlerResult.Retry; - public static ConsumerErrorHandlerResult Success(object response = null) => response == null ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.SuccessWithResponse(response); + public virtual ConsumerErrorHandlerResult Abandon() => ConsumerErrorHandlerResult.Abandon; + public virtual ConsumerErrorHandlerResult Failure() => ConsumerErrorHandlerResult.Failure; + public virtual ConsumerErrorHandlerResult Retry() => ConsumerErrorHandlerResult.Retry; + public virtual ConsumerErrorHandlerResult Success(object response = null) => response == null ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.SuccessWithResponse(response); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs index 7b914fee..fda3910e 100644 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs @@ -4,40 +4,41 @@ public record ConsumerErrorHandlerResult { private static readonly object _noResponse = new(); - private ConsumerErrorHandlerResult(ConsumerErrorHandlerResultEnum result, object response = null) + private ConsumerErrorHandlerResult(ProcessResult result, object response = null) { Result = result; Response = response ?? _noResponse; } - public ConsumerErrorHandlerResultEnum Result { get; private set; } + public ProcessResult Result { get; private set; } public object Response { get; private set; } public bool HasResponse => !ReferenceEquals(Response, _noResponse); + /// + /// the message should be abandoned and placed in the dead letter queue. + /// + /// + /// This feature is not supported by every transport. + /// + public static readonly ConsumerErrorHandlerResult Abandon = new(ProcessResult.Abandon); + /// /// The message should be placed back into the queue. /// - public static readonly ConsumerErrorHandlerResult Failure = new(ConsumerErrorHandlerResultEnum.Fail); + public static readonly ConsumerErrorHandlerResult Failure = new(ProcessResult.Fail); /// /// The message processor should evaluate the message as having been processed successfully. /// - public static readonly ConsumerErrorHandlerResult Success = new(ConsumerErrorHandlerResultEnum.Success); + public static readonly ConsumerErrorHandlerResult Success = new(ProcessResult.Success); /// /// The message processor should evaluate the message as having been processed successfully and use the specified fallback response for the or . /// - public static ConsumerErrorHandlerResult SuccessWithResponse(object response) => new(ConsumerErrorHandlerResultEnum.Success, response); + public static ConsumerErrorHandlerResult SuccessWithResponse(object response) => new(ProcessResult.Success, response); /// /// Retry processing the message without placing it back in the queue. /// - public static readonly ConsumerErrorHandlerResult Retry = new(ConsumerErrorHandlerResultEnum.Retry); + public static readonly ConsumerErrorHandlerResult Retry = new(ProcessResult.Retry); } - -public enum ConsumerErrorHandlerResultEnum -{ - Fail, - Retry, - Success -} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs index b02b64fd..1ccebe3a 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs @@ -1,7 +1,7 @@ -namespace SlimMessageBus.Host; +namespace SlimMessageBus.Host; /// -/// Decorator profor that increases the amount of messages being concurrentlycessed. +/// Decorator for that increases the amount of messages being concurrently processed. /// The expectation is that will be executed synchronously (in sequential order) by the caller on which we want to increase amount of concurrent transportMessage being processed. /// /// @@ -58,7 +58,7 @@ public async Task ProcessMessage(TMessage transportMessage { // report the last exception _lastException = null; - return new(e, _lastExceptionSettings, null); + return new(ProcessResult.Fail, e, _lastExceptionSettings, null); } Interlocked.Increment(ref _pendingCount); @@ -67,7 +67,7 @@ public async Task ProcessMessage(TMessage transportMessage _ = ProcessInBackground(transportMessage, messageHeaders, currentServiceProvider, consumerContextProperties, cancellationToken); // Not exception - we don't know yet - return new(null, null, null); + return new(ProcessResult.Success, null, null, null); } /// diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageHandler.cs index ef8c3d20..cf5381b7 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageHandler.cs @@ -2,6 +2,6 @@ public interface IMessageHandler { - Task<(object Response, Exception ResponseException, string RequestId)> DoHandle(object message, IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage = null, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default); + Task<(ProcessResult Result, object Response, Exception ResponseException, string RequestId)> DoHandle(object message, IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage = null, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default); Task ExecuteConsumer(object message, IConsumerContext consumerContext, IMessageTypeConsumerInvokerSettings consumerInvoker, Type responseType); } diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageProcessor.cs index d6ce7240..891090f0 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/IMessageProcessor.cs @@ -1,10 +1,11 @@ namespace SlimMessageBus.Host; -public readonly struct ProcessMessageResult(Exception exception, AbstractConsumerSettings consumerSettings, object response) +public readonly struct ProcessMessageResult(ProcessResult result, Exception exception, AbstractConsumerSettings consumerSettings, object response) { public Exception Exception { get; init; } = exception; public AbstractConsumerSettings ConsumerSettings { get; init; } = consumerSettings; public object Response { get; init; } = response; + public ProcessResult Result { get; init; } = result; } public interface IMessageProcessor diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index 21717605..c1dfd997 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -58,7 +58,7 @@ public MessageHandler( } } - public async Task<(object Response, Exception ResponseException, string RequestId)> DoHandle(object message, IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage = null, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) + public async Task<(ProcessResult Result, object Response, Exception ResponseException, string RequestId)> DoHandle(object message, IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage = null, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) { var messageType = message.GetType(); @@ -89,7 +89,7 @@ public MessageHandler( { // ToDo: Call interceptor // Do not process the expired message - return (ResponseForExpiredRequest, null, requestId); + return (ProcessResult.Success, ResponseForExpiredRequest, null, requestId); } var messageBusTarget = new MessageBusProxy(MessageBus, messageScope.ServiceProvider); @@ -103,23 +103,25 @@ public MessageHandler( try { var response = await DoHandleInternal(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext).ConfigureAwait(false); - return (response, null, requestId); + return (ProcessResult.Success, response, null, requestId); } catch (Exception ex) { attempts++; var handleErrorResult = await DoHandleError(message, messageType, messageScope, consumerContext, ex, attempts, cancellationToken).ConfigureAwait(false); - if (handleErrorResult.Result != ConsumerErrorHandlerResultEnum.Retry) + if (handleErrorResult.Result == ProcessResult.Retry) { - var exception = handleErrorResult.Result != ConsumerErrorHandlerResultEnum.Success ? ex : null; - var response = handleErrorResult.HasResponse ? handleErrorResult.Response : null; - return (response, exception, requestId); + continue; } + + var exception = handleErrorResult.Result != ProcessResult.Success ? ex : null; + var response = handleErrorResult.HasResponse ? handleErrorResult.Response : null; + return (handleErrorResult.Result, response, exception, requestId); } } catch (Exception e) { - return (null, e, requestId); + return (ProcessResult.Fail, null, e, requestId); } finally { diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs index 46e4fa8f..6c96d0e5 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs @@ -1,5 +1,7 @@ namespace SlimMessageBus.Host; +using System.Diagnostics; + /// /// Implementation of that performs orchestration around processing of a new message using an instance of the declared consumer ( or interface). /// @@ -66,6 +68,7 @@ protected override ConsumerContext CreateConsumerContext(IReadOnlyDictionary ProcessMessage(TTransportMessage transportMessage, IReadOnlyDictionary messageHeaders, IDictionary consumerContextProperties = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) { IMessageTypeConsumerInvokerSettings lastConsumerInvoker = null; + var result = ProcessResult.Success; Exception lastException = null; object lastResponse = null; @@ -94,7 +97,9 @@ public async virtual Task ProcessMessage(TTransportMessage break; } - (lastResponse, lastException, var requestId) = await DoHandle(message, messageHeaders, consumerInvoker, transportMessage, consumerContextProperties, currentServiceProvider, cancellationToken).ConfigureAwait(false); + (result, lastResponse, lastException, var requestId) = await DoHandle(message, messageHeaders, consumerInvoker, transportMessage, consumerContextProperties, currentServiceProvider, cancellationToken).ConfigureAwait(false); + + Debug.Assert(result != ProcessResult.Retry); if (consumerInvoker.ParentSettings.ConsumerMode == ConsumerMode.RequestResponse && _responseProducer != null) { @@ -136,7 +141,7 @@ public async virtual Task ProcessMessage(TTransportMessage _logger.LogDebug(e, "Processing of the message {TransportMessage} failed", transportMessage); lastException = e; } - return new(lastException, lastException != null ? lastConsumerInvoker?.ParentSettings : null, lastResponse); + return new(result, lastException, lastException != null ? lastConsumerInvoker?.ParentSettings : null, lastResponse); } protected Type GetMessageType(IReadOnlyDictionary headers) diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs new file mode 100644 index 00000000..6e100377 --- /dev/null +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host; + +public enum ProcessResult +{ + Abandon, + Fail, + Retry, + Success +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs index 10242fc2..69ee4ff2 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs @@ -1,8 +1,6 @@ namespace SlimMessageBus.Host; -public abstract class ResponseMessageProcessor -{ -} +public abstract class ResponseMessageProcessor; /// /// The implementation that processes the responses arriving to the bus. @@ -48,7 +46,9 @@ public Task ProcessMessage(TTransportMessage transportMess // We can only continue and process all messages in the lease ex = e; } - return Task.FromResult(new ProcessMessageResult(ex, _requestResponseSettings, null)); + + var result = ex == null ? ProcessResult.Success : ProcessResult.Fail; + return Task.FromResult(new ProcessMessageResult(result, ex, _requestResponseSettings, null)); } /// @@ -117,5 +117,4 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p return null; } - } diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs index db9d3e56..edeb6aa5 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs @@ -6,12 +6,14 @@ namespace SlimMessageBus.Host.AzureServiceBus.Test; using System.Runtime.CompilerServices; using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SlimMessageBus.Host; +using SlimMessageBus.Host.Interceptor; using SlimMessageBus.Host.Serialization.Json; using SlimMessageBus.Host.Test.Common.IntegrationTest; @@ -113,6 +115,95 @@ public async Task BasicPubSubOnQueue(bool bulkProduce) await BasicPubSub(1, bulkProduce: bulkProduce); } + [Fact] + public async Task AbandonedMessage_DeliveredToDeadLetterQueue() + { + // arrange + var queue = QueueName(); + + AddTestServices((services, configuration) => + { + services.AddTransient(sp => + { + var connectionString = Secrets.Service.PopulateSecrets(configuration["Azure:ServiceBus"]); + return ActivatorUtilities.CreateInstance(sp, connectionString); + }); + + services.AddTransient(sp => + { + var connectionString = Secrets.Service.PopulateSecrets(configuration["Azure:ServiceBus"]); + return ActivatorUtilities.CreateInstance(sp, connectionString); + }); + + services.AddScoped(typeof(IConsumerInterceptor<>), typeof(AbandonPingMessageInterceptor<>)); + services.AddScoped(typeof(IServiceBusConsumerErrorHandler<>), typeof(AbandonMessageConsumerErrorHandler<>)); + }); + + AddBusConfiguration(mbb => + { + mbb + .Produce(x => x.DefaultQueue(queue).WithModifier(MessageModifier)) + .Consume(x => x + .Queue(queue) + .WithConsumer() + .Instances(20)); + }); + + var adminClient = ServiceProvider.GetRequiredService(); + var testMetric = ServiceProvider.GetRequiredService(); + var consumedMessages = ServiceProvider.GetRequiredService>(); + var client = ServiceProvider.GetRequiredService(); + var deadLetterReceiver = client.CreateReceiver($"{queue}/$DeadLetterQueue"); + + // act + var messageBus = MessageBus; + + var producedMessages = Enumerable + .Range(0, NumberOfMessages) + .Select(i => new PingMessage { Counter = i }) + .ToList(); + + await messageBus.Publish(producedMessages); + await consumedMessages.WaitUntilArriving(newMessagesTimeout: 5); + + // assert + // ensure number of instances of consumers created matches + testMetric.CreatedConsumerCount.Should().Be(NumberOfMessages); + consumedMessages.Count.Should().Be(NumberOfMessages); + + // all messages should be in the DLQ + var properties = await adminClient.GetQueueRuntimePropertiesAsync(queue); + properties.Value.ActiveMessageCount.Should().Be(0); + properties.Value.DeadLetterMessageCount.Should().Be(NumberOfMessages); + + // all messages should have been sent directly to the DLQ + var messages = await deadLetterReceiver.PeekMessagesAsync(NumberOfMessages); + messages.Count.Should().Be(NumberOfMessages); + foreach (var message in messages) + { + message.DeliveryCount.Should().Be(0); + message.ApplicationProperties["DeadLetterReason"].Should().Be(nameof(ApplicationException)); + } + } + + public class AbandonPingMessageInterceptor : IConsumerInterceptor + { + public async Task OnHandle(T message, Func> next, IConsumerContext context) + { + await next(); + var pingMessage = message as PingMessage; + throw new ApplicationException($"Abandon message {pingMessage.Counter:000} on path {context.Path}."); + } + } + + public class AbandonMessageConsumerErrorHandler : ServiceBusConsumerErrorHandler + { + public override Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) + { + return Task.FromResult(Abandon()); + } + } + [Fact] public async Task BasicPubSubWithCustomConsumerOnQueue() { diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/ConcurrentMessageProcessorQueueTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/ConcurrentMessageProcessorQueueTests.cs index c548124b..f31aebdf 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/ConcurrentMessageProcessorQueueTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/ConcurrentMessageProcessorQueueTests.cs @@ -20,7 +20,7 @@ public async Task When_Enqueue_Given_FourMessagesEnqueued_Then_ProcessMessageIsC static async Task ProcessMessageFake(object transportMessage, IReadOnlyDictionary messageHeaders, IDictionary consumerContextProperties, IServiceProvider currentServiceProvider, CancellationToken cancellationToken) { await Task.Delay(500, cancellationToken); - return new ProcessMessageResult(null, null, null); + return new ProcessMessageResult(ProcessResult.Success, null, null, null); } messageProcessor diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/MessageProcessorQueueTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/MessageProcessorQueueTests.cs index 05fa159f..eb3bfc66 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/MessageProcessorQueueTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/Consumers/MessageProcessorQueueTests.cs @@ -18,7 +18,7 @@ public async Task When_Enqueue_Given_TwoMessagesEnqueued_Then_ProcessMessageIsCa static async Task ProcessMessageFake(object transportMessage, IReadOnlyDictionary messageHeaders, IDictionary consumerContextProperties, IServiceProvider currentServiceProvider, CancellationToken cancellationToken) { await Task.Delay(500, cancellationToken); - return new ProcessMessageResult(null, null, null); + return new ProcessMessageResult(ProcessResult.Success, null, null, null); } messageProcessor diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs index 5b1fd18b..43557685 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs @@ -4,16 +4,15 @@ public class RabbitMqAutoAcknowledgeMessageProcessorTests { - private readonly Mock> _targetMock; - private readonly Mock _targetDisposableMock; + private readonly Mock> _messageProcessorMock; + private readonly Mock _messageProcessorDisposableMock; private readonly Mock _consumerMock; private readonly BasicDeliverEventArgs _transportMessage; - private readonly RabbitMqAutoAcknowledgeMessageProcessor _subject; public RabbitMqAutoAcknowledgeMessageProcessorTests() { - _targetMock = new Mock>(); - _targetDisposableMock = _targetMock.As(); + _messageProcessorMock = new Mock>(); + _messageProcessorDisposableMock = _messageProcessorMock.As(); _consumerMock = new Mock(); _transportMessage = new BasicDeliverEventArgs @@ -21,29 +20,36 @@ public RabbitMqAutoAcknowledgeMessageProcessorTests() Exchange = "exchange", DeliveryTag = 1 }; - - _subject = new RabbitMqAutoAcknowledgeMessageProcessor(_targetMock.Object, NullLogger.Instance, RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade, _consumerMock.Object); } [Fact] public void When_Dispose_Then_CallsDisposeOnTarget() { // arrange + var subject = CreateSubject(); // act - _subject.Dispose(); + subject.Dispose(); // assert - _targetDisposableMock.Verify(x => x.Dispose(), Times.Once); + _messageProcessorDisposableMock.Verify(x => x.Dispose(), Times.Once); } - [Fact] - public async Task When_ProcessMessage_Then_AutoAcknowledge() + [Theory] + [InlineData(ProcessResult.Abandon, RabbitMqMessageConfirmOptions.Nack)] + [InlineData(ProcessResult.Fail, RabbitMqMessageConfirmOptions.Nack)] + [InlineData(ProcessResult.Success, RabbitMqMessageConfirmOptions.Ack)] + public async Task When_ProcessMessage_Then_AutoAcknowledge(ProcessResult processResult, RabbitMqMessageConfirmOptions expected) { // arrange + _messageProcessorMock + .Setup(x => x.ProcessMessage(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ProcessMessageResult(processResult, null, null, null))); + + var subject = CreateSubject(); // act - var result = await _subject.ProcessMessage( + var result = await subject.ProcessMessage( _transportMessage, new Dictionary(), null, @@ -53,8 +59,13 @@ public async Task When_ProcessMessage_Then_AutoAcknowledge() // assert _consumerMock.Verify(x => x.ConfirmMessage( _transportMessage, - RabbitMqMessageConfirmOptions.Ack, + expected, It.IsAny>(), It.IsAny()), Times.Once); } + + private RabbitMqAutoAcknowledgeMessageProcessor CreateSubject() + { + return new RabbitMqAutoAcknowledgeMessageProcessor(_messageProcessorMock.Object, NullLogger.Instance, RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade, _consumerMock.Object); + } } diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs index 9d5d1577..6fa82ba4 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs @@ -12,6 +12,7 @@ public abstract class BaseIntegrationTest : IAsyncLifetime { private readonly Lazy _serviceProvider; private Action messageBusBuilderAction = (mbb) => { }; + private Action testServicesBuilderAction = (services, configuration) => { }; private ILogger? _logger; protected ILogger Logger => _logger ??= ServiceProvider.GetRequiredService>(); @@ -45,6 +46,7 @@ protected BaseIntegrationTest(ITestOutputHelper output) services.AddSingleton(); SetupServices(services, Configuration); + ApplyTestServices(services, Configuration); return services.BuildServiceProvider(); }); @@ -62,7 +64,18 @@ protected void AddBusConfiguration(Action action) }; } + protected void AddTestServices(Action action) + { + var prevAction = testServicesBuilderAction; + testServicesBuilderAction = (services, configuration) => + { + prevAction(services, configuration); + action(services, configuration); + }; + } + protected void ApplyBusConfiguration(MessageBusBuilder mbb) => messageBusBuilderAction?.Invoke(mbb); + protected void ApplyTestServices(IServiceCollection services, IConfigurationRoot configuration) => testServicesBuilderAction?.Invoke(services, configuration); protected async Task EnsureConsumersStarted() { diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs index fabd0748..328f123a 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs @@ -41,7 +41,7 @@ public async Task When_WaitAll_Then_WaitsOnAllPendingMessageProcessToFinish(bool .Returns(async () => { await Task.Delay(TimeSpan.FromSeconds(1)); - return new(null, null, null); + return new(ProcessResult.Success, null, null, null); }); var subject = new ConcurrentMessageProcessorDecorator(1, NullLoggerFactory.Instance, _messageProcessorMock.Object); @@ -115,7 +115,7 @@ public async Task When_ProcessMessage_Given_NMessagesAndConcurrencySetToC_Then_N // Leaving critical section Interlocked.Decrement(ref currentSectionCount); - return new(null, null, null); + return new(ProcessResult.Success, null, null, null); }); // act @@ -147,7 +147,7 @@ public async Task When_ProcessMessage_Given_ExceptionHappensOnTarget_Then_Except _messageProcessorMock .Setup(x => x.ProcessMessage(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ProcessMessageResult(exception, null, null)); + .ReturnsAsync(new ProcessMessageResult(ProcessResult.Fail, exception, null, null)); var msg = new SomeMessage(); var msgHeaders = new Dictionary(); From d04154fa5266ec4cd83299893a0c9efcb73299e3 Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Thu, 2 Jan 2025 14:03:02 +0800 Subject: [PATCH 13/21] #356 Consumer error response per transport Signed-off-by: Richard Pringle --- docs/intro.md | 70 ++++++++----------- docs/intro.t.md | 68 ++++++++---------- .../Consumer/SqsBaseConsumer.cs | 5 -- .../Consumer/EhPartitionConsumer.cs | 5 -- .../Consumer/AsbBaseConsumer.cs | 6 +- .../IServiceBusConsumerErrorHandler.cs | 15 +++- .../Consumer/KafkaPartitionConsumer.cs | 5 -- .../MemoryMessageBus.cs | 5 -- .../RabbitMqConsumerBuilderExtensions.cs | 15 ---- .../Config/RabbitMqProperties.cs | 2 - .../IRabbitMqConsumerErrorHandler.cs | 15 +++- ...RabbitMqAutoAcknowledgeMessageProcessor.cs | 10 +-- .../Consumers/RabbitMqResponseConsumer.cs | 12 ++-- .../Consumers/RedisListCheckerConsumer.cs | 5 -- .../Consumers/RedisTopicConsumer.cs | 5 -- .../Collections/RuntimeTypeCache.cs | 4 +- .../ErrorHandling/ConsumerErrorHandler.cs | 9 ++- .../ConsumerErrorHandlerResult.cs | 44 ------------ .../ErrorHandling/IConsumerErrorHandler.cs | 2 +- .../Consumer/ErrorHandling/ProcessResult.cs | 39 +++++++++++ .../ConcurrentMessageProcessorDecorator.cs | 2 +- .../MessageProcessors/MessageHandler.cs | 12 ++-- .../MessageProcessors/MessageProcessor.cs | 2 +- .../MessageProcessors/ProcessResult.cs | 9 --- .../ResponseMessageProcessor.cs | 2 +- src/SlimMessageBus.sln | 13 +--- .../ServiceBusMessageBusIt.cs | 14 ++-- .../MemoryMessageBusTests.cs | 4 +- ...tMqAutoAcknowledgeMessageProcessorTests.cs | 24 +++++-- .../IntegrationTests/RabbitMqMessageBusIt.cs | 6 +- ...ConcurrentMessageProcessorDecoratorTest.cs | 2 +- .../Consumer/MessageHandlerTest.cs | 4 +- 32 files changed, 188 insertions(+), 247 deletions(-) delete mode 100644 src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs create mode 100644 src/SlimMessageBus.Host/Consumer/ErrorHandling/ProcessResult.cs delete mode 100644 src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs diff --git a/docs/intro.md b/docs/intro.md index 984d8886..09cacc67 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -38,6 +38,8 @@ - [Order of Execution](#order-of-execution) - [Generic interceptors](#generic-interceptors) - [Error Handling](#error-handling) + - [Azure Service Bus](#azure-service-bus) + - [RabbitMQ](#rabbitmq) - [Logging](#logging) - [Debugging](#debugging) - [Provider specific functionality](#provider-specific-functionality) @@ -1081,15 +1083,14 @@ public interface IConsumerErrorHandler /// Exception that occurred during message processing. /// The number of times the message has been attempted to be processed. /// The error handling result. - Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); + Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); } ``` -The returned `ConsumerErrorHandlerResult` object is used to override the execution for the remainder of the execution pipeline. +The returned `ProcessResult` object is used to override the execution for the remainder of the execution pipeline. Some transports provide additional options. -| Result | Description | +| ProcessResult | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Abandon | The message should be sent to the dead letter queue/exchange. **Not supported by all transports.** | | Failure | The message failed to be processed and should be returned to the queue | | Success | The pipeline must treat the message as having been processed successfully | | SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | @@ -1105,52 +1106,38 @@ services.AddTransient(typeof(IRabbitMqConsumerErrorHandler<>), typeof(CustomRabb services.AddTransient(typeof(IConsumerErrorHandler<>), typeof(CustomConsumerErrorHandler<>)); ``` -Transport plugins provide specialized error handling interfaces. Examples include: +Transport plugins provide specialized error handling interfaces with a default implementation that includes any additional `ProcessResult` options. Examples include: -- [IMemoryConsumerErrorHandler](../src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs) -- [IRabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) -- [IKafkaConsumerErrorHandler](../src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs) -- [IRedisConsumerErrorHandler](../src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs) -- [INatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) -- [IServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) -- [IEventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) -- [ISqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) +| Interface | Implementation including reference to additional options (if any) | +| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| [IMemoryConsumerErrorHandler](../src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs) | [MemoryConsumerErrorHandler](../src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs) | +| [IRabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) | [RabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) | +| [IKafkaConsumerErrorHandler](../src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs) | [KafkaConsumerErrorHandler](../src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs) | +| [IRedisConsumerErrorHandler](../src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs) | [RedisConsumerErrorHandler](../src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs) | +| [INatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) | [NatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) | +| [IServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) | [ServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) | +| [IEventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) | [EventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) | +| [ISqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) | [SqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) | > The message processing pipeline will always attempt to use the transport-specific error handler (e.g., `IMemoryConsumerErrorHandler`) first. If unavailable, it will then look for the generic error handler (`IConsumerErrorHandler`). This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. - -### Abandon #### Azure Service Bus -The Azure Service Bus transport has full support for abandoning messages to the dead letter queue. - -#### RabbitMQ -Abandon will issue a `Nack` with `requeue: false`. - -#### Other transports -No other transports currently support `Abandon` and calling `Abandon` will result in `NotSupportedException` being thrown. +| ProcessResult | Description | +| ------------- | ---------------------------------------------------------------------------------- | +| DeadLetter | Abandons further processing of the message by sending it to the dead letter queue. | -### Failure #### RabbitMQ -While RabbitMQ supports dead letter exchanges, SMB's default implementation is not to requeue messages on `Failure`. If requeuing is required, it can be enabled by setting `RequeueOnFailure()` when configuring a consumer/handler. +| ProcessResult | Description | +| ------------- | --------------------------------------------------------------- | +| Requeue | Return the message to the queue for re-processing 1. | -Please be aware that as RabbitMQ does not have a maximum delivery count and enabling requeue may result in an infinite message loop. When `RequeueOnFailure()` has been set, it is the developer's responsibility to configure an appropriate `IConsumerErrorHandler` that will `Abandon` all non-transient exceptions. - -```cs -.Handle(x => x - .Queue("echo-request-handler") - .ExchangeBinding("test-echo") - .DeadLetterExchange("echo-request-handler-dlq") - // requeue a message on failure - .RequeueOnFailure() - .WithHandler()) -``` +1 RabbitMQ does not have a maximum delivery count. Please use `Requeue` with caution as, if no other conditions are applied, it may result in an infinite message loop. -### Example usage -Retry with exponential back-off and short-curcuit dead letter on non-transient exceptions (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): +Example retry with exponential back-off and short-curcuit to dead letter exchange on non-transient exceptions (using the [RabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) abstract implementation): ```cs -public class RetryHandler : ConsumerErrorHandler +public class RetryHandler : RabbitMqConsumerErrorHandler { private static readonly Random _random = new(); @@ -1158,17 +1145,20 @@ public class RetryHandler : ConsumerErrorHandler { if (!IsTranientException(exception)) { - return Abandon(); + return Failure(); } if (attempts < 3) { var delay = (attempts * 1000) + (_random.Next(1000) - 500); await Task.Delay(delay, consumerContext.CancellationToken); + + // in process retry return Retry(); } - return Failure(); + // re-qeuue for out of process execution + return Requeue(); } private static bool IsTransientException(Exception exception) diff --git a/docs/intro.t.md b/docs/intro.t.md index 6d64a390..ee6857b7 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -38,6 +38,8 @@ - [Order of Execution](#order-of-execution) - [Generic interceptors](#generic-interceptors) - [Error Handling](#error-handling) + - [Azure Service Bus](#azure-service-bus) + - [RabbitMQ](#rabbitmq) - [Logging](#logging) - [Debugging](#debugging) - [Provider specific functionality](#provider-specific-functionality) @@ -1063,11 +1065,10 @@ Message processing by consumers or handlers may result in exceptions. The [ICons @[:cs](../src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs,Interface) -The returned `ConsumerErrorHandlerResult` object is used to override the execution for the remainder of the execution pipeline. +The returned `ProcessResult` object is used to override the execution for the remainder of the execution pipeline. Some transports provide additional options. -| Result | Description | +| ProcessResult | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Abandon | The message should be sent to the dead letter queue/exchange. **Not supported by all transports.** | | Failure | The message failed to be processed and should be returned to the queue | | Success | The pipeline must treat the message as having been processed successfully | | SuccessWithResponse | The pipeline to treat the message as having been processed successfully, returning the response to the request/response invocation ([IRequestResponseBus](../src/SlimMessageBus/RequestResponse/IRequestResponseBus.cs)) | @@ -1083,52 +1084,38 @@ services.AddTransient(typeof(IRabbitMqConsumerErrorHandler<>), typeof(CustomRabb services.AddTransient(typeof(IConsumerErrorHandler<>), typeof(CustomConsumerErrorHandler<>)); ``` -Transport plugins provide specialized error handling interfaces. Examples include: +Transport plugins provide specialized error handling interfaces with a default implementation that includes any additional `ProcessResult` options. Examples include: -- [IMemoryConsumerErrorHandler](../src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs) -- [IRabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) -- [IKafkaConsumerErrorHandler](../src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs) -- [IRedisConsumerErrorHandler](../src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs) -- [INatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) -- [IServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) -- [IEventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) -- [ISqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) +| Interface | Implementation including reference to additional options (if any) | +| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| [IMemoryConsumerErrorHandler](../src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs) | [MemoryConsumerErrorHandler](../src/SlimMessageBus.Host.Memory/Consumers/IMemoryConsumerErrorHandler.cs) | +| [IRabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) | [RabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) | +| [IKafkaConsumerErrorHandler](../src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs) | [KafkaConsumerErrorHandler](../src/SlimMessageBus.Host.Kafka/Consumer/IKafkaConsumerErrorHandler.cs) | +| [IRedisConsumerErrorHandler](../src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs) | [RedisConsumerErrorHandler](../src/SlimMessageBus.Host.Redis/Consumers/IRedisConsumerErrorHandler.cs) | +| [INatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) | [NatsConsumerErrorHandler](../src/SlimMessageBus.Host.Nats/INatsConsumerErrorHandler.cs) | +| [IServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) | [ServiceBusConsumerErrorHandler](../src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs) | +| [IEventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) | [EventHubConsumerErrorHandler](../src/SlimMessageBus.Host.AzureEventHub/Consumer/IEventHubConsumerErrorHandler.cs) | +| [ISqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) | [SqsConsumerErrorHandler](../src/SlimMessageBus.Host.AmazonSQS/Consumer/ISqsConsumerErrorHandler.cs) | > The message processing pipeline will always attempt to use the transport-specific error handler (e.g., `IMemoryConsumerErrorHandler`) first. If unavailable, it will then look for the generic error handler (`IConsumerErrorHandler`). This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. - -### Abandon #### Azure Service Bus -The Azure Service Bus transport has full support for abandoning messages to the dead letter queue. - -#### RabbitMQ -Abandon will issue a `Nack` with `requeue: false`. - -#### Other transports -No other transports currently support `Abandon` and calling `Abandon` will result in `NotSupportedException` being thrown. +| ProcessResult | Description | +| ------------- | ---------------------------------------------------------------------------------- | +| DeadLetter | Abandons further processing of the message by sending it to the dead letter queue. | -### Failure #### RabbitMQ -While RabbitMQ supports dead letter exchanges, SMB's default implementation is not to requeue messages on `Failure`. If requeuing is required, it can be enabled by setting `RequeueOnFailure()` when configuring a consumer/handler. +| ProcessResult | Description | +| ------------- | --------------------------------------------------------------- | +| Requeue | Return the message to the queue for re-processing 1. | -Please be aware that as RabbitMQ does not have a maximum delivery count and enabling requeue may result in an infinite message loop. When `RequeueOnFailure()` has been set, it is the developer's responsibility to configure an appropriate `IConsumerErrorHandler` that will `Abandon` all non-transient exceptions. - -```cs -.Handle(x => x - .Queue("echo-request-handler") - .ExchangeBinding("test-echo") - .DeadLetterExchange("echo-request-handler-dlq") - // requeue a message on failure - .RequeueOnFailure() - .WithHandler()) -``` +1 RabbitMQ does not have a maximum delivery count. Please use `Requeue` with caution as, if no other conditions are applied, it may result in an infinite message loop. -### Example usage -Retry with exponential back-off and short-curcuit dead letter on non-transient exceptions (using the [ConsumerErrorHandler](../src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs) abstract implementation): +Example retry with exponential back-off and short-curcuit to dead letter exchange on non-transient exceptions (using the [RabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) abstract implementation): ```cs -public class RetryHandler : ConsumerErrorHandler +public class RetryHandler : RabbitMqConsumerErrorHandler { private static readonly Random _random = new(); @@ -1136,17 +1123,20 @@ public class RetryHandler : ConsumerErrorHandler { if (!IsTranientException(exception)) { - return Abandon(); + return Failure(); } if (attempts < 3) { var delay = (attempts * 1000) + (_random.Next(1000) - 500); await Task.Delay(delay, consumerContext.CancellationToken); + + // in process retry return Retry(); } - return Failure(); + // re-qeuue for out of process execution + return Requeue(); } private static bool IsTransientException(Exception exception) diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs index b5fec9ad..95818eb7 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs @@ -116,11 +116,6 @@ protected async Task Run() .ToDictionary(x => x.Key, x => HeaderSerializer.Deserialize(x.Key, x.Value)); var r = await MessageProcessor.ProcessMessage(message, messageHeaders, cancellationToken: CancellationToken).ConfigureAwait(false); - if (r.Result == ProcessResult.Abandon) - { - throw new NotSupportedException("Transport does not support abandoning messages"); - } - if (r.Exception != null) { Logger.LogError(r.Exception, "Message processing error - Queue: {Queue}, MessageId: {MessageId}", Path, message.MessageId); diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs index 853b12da..f84f7a79 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhPartitionConsumer.cs @@ -42,11 +42,6 @@ public async Task ProcessEventAsync(ProcessEventArgs args) var headers = GetHeadersFromTransportMessage(args.Data); var r = await MessageProcessor.ProcessMessage(args.Data, headers, cancellationToken: args.CancellationToken).ConfigureAwait(false); - if (r.Result == ProcessResult.Abandon) - { - throw new NotSupportedException("Transport does not support abandoning messages"); - } - if (r.Exception != null) { // Note: The OnMessageFaulted was called at this point by the MessageProcessor. diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs index daba0eb8..326a0991 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs @@ -171,19 +171,19 @@ protected async Task ProcessMessageAsyncInternal( var r = await MessageProcessor.ProcessMessage(message, message.ApplicationProperties, cancellationToken: token).ConfigureAwait(false); switch (r.Result) { - case ProcessResult.Success: + case ProcessResult.SuccessState: // Complete the message so that it is not received again. // This can be done only if the subscriptionClient is created in ReceiveMode.PeekLock mode (which is the default). Logger.LogDebug("Complete message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); await completeMessage(message, token).ConfigureAwait(false); return; - case ProcessResult.Abandon: + case ServiceBusProcessResult.DeadLetterState: Logger.LogError(r.Exception, "Dead letter message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId); await deadLetterMessage(message, r.Exception?.GetType().Name ?? string.Empty, r.Exception?.Message ?? string.Empty, token).ConfigureAwait(false); return; - case ProcessResult.Fail: + case ProcessResult.FailureState: var messageProperties = new Dictionary(); { // Set the exception message diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs index 3e963ae2..21cfd89f 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs @@ -2,4 +2,17 @@ public interface IServiceBusConsumerErrorHandler : IConsumerErrorHandler; -public abstract class ServiceBusConsumerErrorHandler : ConsumerErrorHandler, IServiceBusConsumerErrorHandler; \ No newline at end of file +public abstract class ServiceBusConsumerErrorHandler : ConsumerErrorHandler, IServiceBusConsumerErrorHandler +{ + public ProcessResult DeadLetter() => ServiceBusProcessResult.DeadLetter; +} + +public record ServiceBusProcessResult : ProcessResult +{ + /// + /// The message must be sent to the dead letter queue. + /// + public static readonly ProcessResult DeadLetter = new DeadLetterState(); + + public record DeadLetterState() : ProcessResult(); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs index 0474acab..d4b73d02 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaPartitionConsumer.cs @@ -115,11 +115,6 @@ public async Task OnMessage(ConsumeResult message) } var r = await _messageProcessor.ProcessMessage(message, messageHeaders, cancellationToken: _cancellationTokenSource.Token).ConfigureAwait(false); - if (r.Result == ProcessResult.Abandon) - { - throw new NotSupportedException("Transport does not support abandoning messages"); - } - if (r.Exception != null) { // The IKafkaConsumerErrorHandler and OnMessageFaulted was called at this point by the MessageProcessor. diff --git a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs index ed5d7e74..20b36e1a 100644 --- a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs +++ b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs @@ -158,11 +158,6 @@ private async Task ProduceInternal(object me var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; // Execute the message processor in synchronous manner var r = await messageProcessor.ProcessMessage(transportMessage, messageHeadersReadOnly, currentServiceProvider: serviceProvider, cancellationToken: cancellationToken); - if (r.Result == ProcessResult.Abandon) - { - throw new NotSupportedException("Transport does not support abandoning messages"); - } - if (r.Exception != null) { // We want to pass the same exception to the sender as it happened in the handler/consumer diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs index e7b368fd..e417799d 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqConsumerBuilderExtensions.cs @@ -118,19 +118,4 @@ public static TConsumerBuilder AcknowledgementMode(this TConsu builder.ConsumerSettings.Properties[RabbitMqProperties.MessageAcknowledgementMode] = mode; return builder; } - - /// - /// Requeues a message on failure. Abandoned messages will not be requeued. - /// This may lead to an infinite loop if the reason for the failure is not transient. Responsibility lies with the developer to ensure that this flow does not occur. - /// - /// - /// - /// - /// - public static TConsumerBuilder RequeueOnFailure(this TConsumerBuilder builder, bool state = true) - where TConsumerBuilder : AbstractConsumerBuilder - { - builder.ConsumerSettings.Properties[RabbitMqProperties.ReqeueOnFailure] = state; - return builder; - } } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs index 1e32d821..2d56d75c 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProperties.cs @@ -31,6 +31,4 @@ static internal class RabbitMqProperties public static readonly string ProviderSettings = $"RabbitMQ_{nameof(ProviderSettings)}"; public static readonly string MessageAcknowledgementMode = $"RabbitMQ_{nameof(MessageAcknowledgementMode)}"; - - public static readonly string ReqeueOnFailure = $"RabbitMQ_{nameof(ReqeueOnFailure)}"; } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs index 4bac0cde..8778d657 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs @@ -2,4 +2,17 @@ public interface IRabbitMqConsumerErrorHandler : IConsumerErrorHandler; -public abstract class RabbitMqConsumerErrorHandler : ConsumerErrorHandler, IRabbitMqConsumerErrorHandler; \ No newline at end of file +public abstract class RabbitMqConsumerErrorHandler : ConsumerErrorHandler, IRabbitMqConsumerErrorHandler +{ + public virtual ProcessResult Requeue() => RabbitMqProcessResult.Requeue; +} + +public record RabbitMqProcessResult : ProcessResult +{ + /// + /// The message should be placed back into the queue. + /// + public static readonly ProcessResult Requeue = new RequeueState(); + + public record RequeueState() : ProcessResult(); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs index 439aef92..f2f18de2 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs @@ -13,7 +13,6 @@ internal sealed class RabbitMqAutoAcknowledgeMessageProcessor : IMessageProcesso private readonly ILogger _logger; private readonly RabbitMqMessageAcknowledgementMode _acknowledgementMode; private readonly IRabbitMqConsumer _consumer; - private readonly bool _requeueOnFailure; public RabbitMqAutoAcknowledgeMessageProcessor( IMessageProcessor target, @@ -25,8 +24,6 @@ public RabbitMqAutoAcknowledgeMessageProcessor( _logger = logger; _acknowledgementMode = acknowledgementMode; _consumer = consumer; - - _requeueOnFailure = _target.ConsumerSettings?.All(x => x.GetOrDefault(RabbitMqProperties.ReqeueOnFailure, false)) ?? false; } public IReadOnlyCollection ConsumerSettings => _target.ConsumerSettings; @@ -47,10 +44,9 @@ public async Task ProcessMessage(BasicDeliverEventArgs tra // Acknowledge after processing var confirmOption = r.Result switch { - ProcessResult.Abandon => RabbitMqMessageConfirmOptions.Nack, // NAck after processing when message fails with non-transient exception (unless the user already acknowledged in any way). - ProcessResult.Fail when (_requeueOnFailure) => RabbitMqMessageConfirmOptions.Nack | RabbitMqMessageConfirmOptions.Requeue, // Re-queue after processing on transient failure - ProcessResult.Fail when (!_requeueOnFailure) => RabbitMqMessageConfirmOptions.Nack, // Fail after processing failure (no re-queue) - ProcessResult.Success => RabbitMqMessageConfirmOptions.Ack, // Acknowledge after processing + RabbitMqProcessResult.RequeueState => RabbitMqMessageConfirmOptions.Nack | RabbitMqMessageConfirmOptions.Requeue, // Re-queue after processing on transient failure + ProcessResult.FailureState => RabbitMqMessageConfirmOptions.Nack, // Fail after processing failure (no re-queue) + ProcessResult.SuccessState => RabbitMqMessageConfirmOptions.Ack, // Acknowledge after processing _ => throw new NotImplementedException() }; diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs index 3ce14894..96b1c250 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs @@ -3,7 +3,6 @@ public class RabbitMqResponseConsumer : AbstractRabbitMqConsumer { private readonly IMessageProcessor _messageProcessor; - private readonly bool _requeueOnFailure; protected override RabbitMqMessageAcknowledgementMode AcknowledgementMode => RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade; @@ -19,7 +18,6 @@ public RabbitMqResponseConsumer( : base(loggerFactory.CreateLogger(), channel, queueName, headerValueConverter) { _messageProcessor = new ResponseMessageProcessor(loggerFactory, requestResponseSettings, messageProvider, pendingRequestStore, currentTimeProvider); - _requeueOnFailure = requestResponseSettings.GetOrDefault(RabbitMqProperties.ReqeueOnFailure, false); } protected override async Task OnMessageReceived(Dictionary messageHeaders, BasicDeliverEventArgs transportMessage) @@ -27,15 +25,15 @@ protected override async Task OnMessageReceived(Dictionary>, IConsumerContext, Task>> ConsumerInterceptorType { get; } public IGenericTypeCache2> HandlerInterceptorType { get; } - public IGenericTypeCache>> ConsumerErrorHandlerType { get; } + public IGenericTypeCache>> ConsumerErrorHandlerType { get; } public RuntimeTypeCache() { @@ -78,7 +78,7 @@ public RuntimeTypeCache() typeof(IRequestHandlerInterceptor<,>), nameof(IRequestHandlerInterceptor.OnHandle)); - ConsumerErrorHandlerType = new GenericTypeCache>>( + ConsumerErrorHandlerType = new GenericTypeCache>>( typeof(IConsumerErrorHandler<>), nameof(IConsumerErrorHandler.OnHandleError)); } diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs index f031335f..32f6fc89 100644 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandler.cs @@ -2,13 +2,12 @@ public abstract class ConsumerErrorHandler : BaseConsumerErrorHandler, IConsumerErrorHandler { - public abstract Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); + public abstract Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); } public abstract class BaseConsumerErrorHandler { - public virtual ConsumerErrorHandlerResult Abandon() => ConsumerErrorHandlerResult.Abandon; - public virtual ConsumerErrorHandlerResult Failure() => ConsumerErrorHandlerResult.Failure; - public virtual ConsumerErrorHandlerResult Retry() => ConsumerErrorHandlerResult.Retry; - public virtual ConsumerErrorHandlerResult Success(object response = null) => response == null ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.SuccessWithResponse(response); + public virtual ProcessResult Failure() => ProcessResult.Failure; + public virtual ProcessResult Retry() => ProcessResult.Retry; + public virtual ProcessResult Success(object response = null) => response == null ? ProcessResult.Success : ProcessResult.SuccessWithResponse(response); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs deleted file mode 100644 index fda3910e..00000000 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ConsumerErrorHandlerResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace SlimMessageBus.Host; - -public record ConsumerErrorHandlerResult -{ - private static readonly object _noResponse = new(); - - private ConsumerErrorHandlerResult(ProcessResult result, object response = null) - { - Result = result; - Response = response ?? _noResponse; - } - - public ProcessResult Result { get; private set; } - public object Response { get; private set; } - public bool HasResponse => !ReferenceEquals(Response, _noResponse); - - /// - /// the message should be abandoned and placed in the dead letter queue. - /// - /// - /// This feature is not supported by every transport. - /// - public static readonly ConsumerErrorHandlerResult Abandon = new(ProcessResult.Abandon); - - /// - /// The message should be placed back into the queue. - /// - public static readonly ConsumerErrorHandlerResult Failure = new(ProcessResult.Fail); - - /// - /// The message processor should evaluate the message as having been processed successfully. - /// - public static readonly ConsumerErrorHandlerResult Success = new(ProcessResult.Success); - - /// - /// The message processor should evaluate the message as having been processed successfully and use the specified fallback response for the or . - /// - public static ConsumerErrorHandlerResult SuccessWithResponse(object response) => new(ProcessResult.Success, response); - - /// - /// Retry processing the message without placing it back in the queue. - /// - public static readonly ConsumerErrorHandlerResult Retry = new(ProcessResult.Retry); -} diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs index 5c40030d..a742d3a7 100644 --- a/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/IConsumerErrorHandler.cs @@ -20,6 +20,6 @@ public interface IConsumerErrorHandler /// Exception that occurred during message processing. /// The number of times the message has been attempted to be processed. /// The error handling result. - Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); + Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts); } // doc:fragment:Interface \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/ErrorHandling/ProcessResult.cs b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ProcessResult.cs new file mode 100644 index 00000000..49d41460 --- /dev/null +++ b/src/SlimMessageBus.Host/Consumer/ErrorHandling/ProcessResult.cs @@ -0,0 +1,39 @@ +namespace SlimMessageBus.Host; + +public abstract record ProcessResult +{ + private static readonly object _noResponse = new(); + + protected ProcessResult(object response = null) + { + Response = response ?? _noResponse; + } + + public object Response { get; } + public bool HasResponse => !ReferenceEquals(Response, _noResponse); + + /// + /// The message should be placed back into the queue. + /// + public static readonly ProcessResult Failure = new FailureState(); + + /// + /// Retry processing the message without placing it back in the queue. + /// + public static readonly ProcessResult Retry = new RetryState(); + + /// + /// The message processor should evaluate the message as having been processed successfully. + /// + public static readonly ProcessResult Success = new SuccessState(); + + /// + /// The message processor should evaluate the message as having been processed successfully and use the specified fallback response for the or . + /// + public static ProcessResult SuccessWithResponse(object response) => new SuccessStateWithResponse(response); + + public record FailureState() : ProcessResult(); + public record RetryState() : ProcessResult(); + public record SuccessState(object Response = null) : ProcessResult(Response); + public record SuccessStateWithResponse(object Response) : SuccessState(Response); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs index 1ccebe3a..0fba6f97 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs @@ -58,7 +58,7 @@ public async Task ProcessMessage(TMessage transportMessage { // report the last exception _lastException = null; - return new(ProcessResult.Fail, e, _lastExceptionSettings, null); + return new(ProcessResult.Failure, e, _lastExceptionSettings, null); } Interlocked.Increment(ref _pendingCount); diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index c1dfd997..62d873ae 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -109,19 +109,19 @@ public MessageHandler( { attempts++; var handleErrorResult = await DoHandleError(message, messageType, messageScope, consumerContext, ex, attempts, cancellationToken).ConfigureAwait(false); - if (handleErrorResult.Result == ProcessResult.Retry) + if (handleErrorResult is ProcessResult.RetryState) { continue; } - var exception = handleErrorResult.Result != ProcessResult.Success ? ex : null; + var exception = handleErrorResult is not ProcessResult.SuccessState ? ex : null; var response = handleErrorResult.HasResponse ? handleErrorResult.Response : null; - return (handleErrorResult.Result, response, exception, requestId); + return (handleErrorResult, response, exception, requestId); } } catch (Exception e) { - return (ProcessResult.Fail, null, e, requestId); + return (ProcessResult.Failure, null, e, requestId); } finally { @@ -148,9 +148,9 @@ private async Task DoHandleInternal(object message, IMessageTypeConsumer return await ExecuteConsumer(message, consumerContext, consumerInvoker, responseType).ConfigureAwait(false); } - private async Task DoHandleError(object message, Type messageType, IMessageScope messageScope, IConsumerContext consumerContext, Exception ex, int attempts, CancellationToken cancellationToken) + private async Task DoHandleError(object message, Type messageType, IMessageScope messageScope, IConsumerContext consumerContext, Exception ex, int attempts, CancellationToken cancellationToken) { - var errorHandlerResult = ConsumerErrorHandlerResult.Failure; + var errorHandlerResult = ProcessResult.Failure; // Use the bus provider specific error handler type first (if provided) var consumerErrorHandler = ConsumerErrorHandlerOpenGenericType is not null diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs index 6c96d0e5..74f19aa2 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs @@ -99,7 +99,7 @@ public async virtual Task ProcessMessage(TTransportMessage (result, lastResponse, lastException, var requestId) = await DoHandle(message, messageHeaders, consumerInvoker, transportMessage, consumerContextProperties, currentServiceProvider, cancellationToken).ConfigureAwait(false); - Debug.Assert(result != ProcessResult.Retry); + Debug.Assert(result is not ProcessResult.RetryState); if (consumerInvoker.ParentSettings.ConsumerMode == ConsumerMode.RequestResponse && _responseProducer != null) { diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs deleted file mode 100644 index 6e100377..00000000 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ProcessResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SlimMessageBus.Host; - -public enum ProcessResult -{ - Abandon, - Fail, - Retry, - Success -} \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs index 69ee4ff2..2290b8f8 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs @@ -47,7 +47,7 @@ public Task ProcessMessage(TTransportMessage transportMess ex = e; } - var result = ex == null ? ProcessResult.Success : ProcessResult.Fail; + var result = ex == null ? ProcessResult.Success : ProcessResult.Failure; return Task.FromResult(new ProcessMessageResult(result, ex, _requestResponseSettings, null)); } diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index 39ff2b16..f6d03ace 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -12,42 +12,31 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{A5B1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Content", "Content", "{8835721D-ECA4-42CB-BA8B-F8C1A06340EC}" ProjectSection(SolutionItems) = preProject - Samples\Content\DSC0843.jpg = Samples\Content\DSC0843.jpg Samples\Content\DSC0862.jpg = Samples\Content\DSC0862.jpg - Samples\Content\DSC1042.jpg = Samples\Content\DSC1042.jpg Samples\Content\DSC1044.jpg = Samples\Content\DSC1044.jpg Samples\Content\DSC2145.jpg = Samples\Content\DSC2145.jpg Samples\Content\DSC2152.jpg = Samples\Content\DSC2152.jpg - Samples\Content\DSC2468.jpg = Samples\Content\DSC2468.jpg Samples\Content\DSC3714.jpg = Samples\Content\DSC3714.jpg Samples\Content\DSC3718.jpg = Samples\Content\DSC3718.jpg - Samples\Content\DSC3720.jpg = Samples\Content\DSC3720.jpg Samples\Content\DSC3781.jpg = Samples\Content\DSC3781.jpg Samples\Content\DSC3808.jpg = Samples\Content\DSC3808.jpg Samples\Content\DSC3884.jpg = Samples\Content\DSC3884.jpg - Samples\Content\DSC4029.jpg = Samples\Content\DSC4029.jpg Samples\Content\DSC4037.jpg = Samples\Content\DSC4037.jpg Samples\Content\DSC4038.jpg = Samples\Content\DSC4038.jpg - Samples\Content\DSC4205.jpg = Samples\Content\DSC4205.jpg Samples\Content\DSC4216.jpg = Samples\Content\DSC4216.jpg - Samples\Content\DSC5718.jpg = Samples\Content\DSC5718.jpg Samples\Content\DSC5819.jpg = Samples\Content\DSC5819.jpg Samples\Content\DSC5839.jpg = Samples\Content\DSC5839.jpg Samples\Content\DSC8169.jpg = Samples\Content\DSC8169.jpg Samples\Content\DSC8177.jpg = Samples\Content\DSC8177.jpg Samples\Content\DSC8327.jpg = Samples\Content\DSC8327.jpg - Samples\Content\DSC8333.jpg = Samples\Content\DSC8333.jpg Samples\Content\DSC8462.jpg = Samples\Content\DSC8462.jpg Samples\Content\DSC8789.jpg = Samples\Content\DSC8789.jpg - Samples\Content\DSC9135.jpg = Samples\Content\DSC9135.jpg Samples\Content\DSC9230.jpg = Samples\Content\DSC9230.jpg Samples\Content\DSC9235.jpg = Samples\Content\DSC9235.jpg - Samples\Content\DSC9319.jpg = Samples\Content\DSC9319.jpg Samples\Content\DSC9323.jpg = Samples\Content\DSC9323.jpg Samples\Content\DSC9329.jpg = Samples\Content\DSC9329.jpg Samples\Content\DSC9823.jpg = Samples\Content\DSC9823.jpg Samples\Content\DSC9827.jpg = Samples\Content\DSC9827.jpg - Samples\Content\DSC9831.jpg = Samples\Content\DSC9831.jpg Samples\Content\DSC9839.jpg = Samples\Content\DSC9839.jpg Samples\Content\DSC9892.jpg = Samples\Content\DSC9892.jpg Samples\Content\DSC9904.jpg = Samples\Content\DSC9904.jpg @@ -140,6 +129,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{CBE53E71-7 ..\docs\intro.t.md = ..\docs\intro.t.md ..\docs\NuGet.md = ..\docs\NuGet.md ..\docs\plugin_asyncapi.t.md = ..\docs\plugin_asyncapi.t.md + ..\docs\plugin_outbox.t.md = ..\docs\plugin_outbox.t.md ..\docs\provider_amazon_sqs.t.md = ..\docs\provider_amazon_sqs.t.md ..\docs\provider_azure_eventhubs.md = ..\docs\provider_azure_eventhubs.md ..\docs\provider_azure_servicebus.md = ..\docs\provider_azure_servicebus.md @@ -148,7 +138,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{CBE53E71-7 ..\docs\provider_memory.t.md = ..\docs\provider_memory.t.md ..\docs\provider_mqtt.md = ..\docs\provider_mqtt.md ..\docs\provider_nats.t.md = ..\docs\provider_nats.t.md - ..\docs\plugin_outbox.t.md = ..\docs\plugin_outbox.t.md ..\docs\provider_redis.md = ..\docs\provider_redis.md ..\docs\README.md = ..\docs\README.md ..\README.md = ..\README.md diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs index edeb6aa5..22560fd2 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs @@ -116,7 +116,7 @@ public async Task BasicPubSubOnQueue(bool bulkProduce) } [Fact] - public async Task AbandonedMessage_DeliveredToDeadLetterQueue() + public async Task DeadLetterMessage_IsDeliveredTo_DeadLetterQueue() { // arrange var queue = QueueName(); @@ -135,8 +135,8 @@ public async Task AbandonedMessage_DeliveredToDeadLetterQueue() return ActivatorUtilities.CreateInstance(sp, connectionString); }); - services.AddScoped(typeof(IConsumerInterceptor<>), typeof(AbandonPingMessageInterceptor<>)); - services.AddScoped(typeof(IServiceBusConsumerErrorHandler<>), typeof(AbandonMessageConsumerErrorHandler<>)); + services.AddScoped(typeof(IConsumerInterceptor<>), typeof(ThrowExceptionPingMessageInterceptor<>)); + services.AddScoped(typeof(IServiceBusConsumerErrorHandler<>), typeof(DeadLetterMessageConsumerErrorHandler<>)); }); AddBusConfiguration(mbb => @@ -186,7 +186,7 @@ public async Task AbandonedMessage_DeliveredToDeadLetterQueue() } } - public class AbandonPingMessageInterceptor : IConsumerInterceptor + public class ThrowExceptionPingMessageInterceptor : IConsumerInterceptor { public async Task OnHandle(T message, Func> next, IConsumerContext context) { @@ -196,11 +196,11 @@ public async Task OnHandle(T message, Func> next, IConsumer } } - public class AbandonMessageConsumerErrorHandler : ServiceBusConsumerErrorHandler + public class DeadLetterMessageConsumerErrorHandler : ServiceBusConsumerErrorHandler { - public override Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) + public override Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) { - return Task.FromResult(Abandon()); + return Task.FromResult(DeadLetter()); } } diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index 1e934b8b..6f0316d9 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -430,7 +430,7 @@ public async Task When_Publish_Given_AConsumersThatThrowsException_Then_Exceptio var consumerErrorHandlerMock = new Mock>(); consumerErrorHandlerMock .Setup(x => x.OnHandleError(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(() => errorHandlerHandlesError ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.Failure); + .ReturnsAsync(() => errorHandlerHandlesError ? ProcessResult.Success : ProcessResult.Failure); _serviceProviderMock.ProviderMock .Setup(x => x.GetService(typeof(IConsumer))) @@ -478,7 +478,7 @@ public async Task When_Send_Given_AHandlerThatThrowsException_Then_ExceptionIsBu var consumerErrorHandlerMock = new Mock>(); consumerErrorHandlerMock .Setup(x => x.OnHandleError(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(() => errorHandlerHandlesError ? ConsumerErrorHandlerResult.SuccessWithResponse(null) : ConsumerErrorHandlerResult.Failure); + .ReturnsAsync(() => errorHandlerHandlesError ? ProcessResult.SuccessWithResponse(null) : ProcessResult.Failure); _serviceProviderMock.ProviderMock .Setup(x => x.GetService(typeof(IRequestHandler))) diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs index 43557685..ed67f89e 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/Consumer/RabbitMqAutoAcknowledgeMessageProcessorTests.cs @@ -35,11 +35,25 @@ public void When_Dispose_Then_CallsDisposeOnTarget() _messageProcessorDisposableMock.Verify(x => x.Dispose(), Times.Once); } - [Theory] - [InlineData(ProcessResult.Abandon, RabbitMqMessageConfirmOptions.Nack)] - [InlineData(ProcessResult.Fail, RabbitMqMessageConfirmOptions.Nack)] - [InlineData(ProcessResult.Success, RabbitMqMessageConfirmOptions.Ack)] - public async Task When_ProcessMessage_Then_AutoAcknowledge(ProcessResult processResult, RabbitMqMessageConfirmOptions expected) + [Fact] + public Task When_Requeue_ThenAutoNackWithRequeue() + { + return When_ProcessMessage_Then_AutoAcknowledge(RabbitMqProcessResult.Requeue, RabbitMqMessageConfirmOptions.Nack | RabbitMqMessageConfirmOptions.Requeue); + } + + [Fact] + public Task When_Failure_ThenAutoNack() + { + return When_ProcessMessage_Then_AutoAcknowledge(RabbitMqProcessResult.Failure, RabbitMqMessageConfirmOptions.Nack); + } + + [Fact] + public Task When_Success_AutoAcknowledge() + { + return When_ProcessMessage_Then_AutoAcknowledge(RabbitMqProcessResult.Success, RabbitMqMessageConfirmOptions.Ack); + } + + private async Task When_ProcessMessage_Then_AutoAcknowledge(ProcessResult processResult, RabbitMqMessageConfirmOptions expected) { // arrange _messageProcessorMock diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs index 3c884a16..3b6c6413 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs @@ -371,7 +371,7 @@ public static Task SimulateFakeException(int counter) /// public class CustomRabbitMqConsumerErrorHandler : IRabbitMqConsumerErrorHandler { - public Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) + public Task OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts) { // Check if this is consumer context for RabbitMQ var isRabbitMqContext = consumerContext.GetTransportMessage() != null; @@ -390,7 +390,7 @@ public Task OnHandleError(T message, IConsumerContex } return Task.FromResult(isRabbitMqContext - ? ConsumerErrorHandlerResult.Success - : ConsumerErrorHandlerResult.Failure); + ? ProcessResult.Success + : ProcessResult.Failure); } } diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs index 328f123a..2829c30f 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/ConcurrentMessageProcessorDecoratorTest.cs @@ -147,7 +147,7 @@ public async Task When_ProcessMessage_Given_ExceptionHappensOnTarget_Then_Except _messageProcessorMock .Setup(x => x.ProcessMessage(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ProcessMessageResult(ProcessResult.Fail, exception, null, null)); + .ReturnsAsync(new ProcessMessageResult(ProcessResult.Failure, exception, null, null)); var msg = new SomeMessage(); var msgHeaders = new Dictionary(); diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs index 9c2030bf..060df69c 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageHandlerTest.cs @@ -102,7 +102,7 @@ public async Task When_DoHandle_Given_ConsumerThatThrowsExceptionAndErrorHandler consumerErrorHandlerMock .Setup(x => x.OnHandleError(someMessage, It.IsAny(), someException, It.IsAny())) - .ReturnsAsync(() => errorHandlerWasAbleToHandle ? ConsumerErrorHandlerResult.Success : ConsumerErrorHandlerResult.Failure); + .ReturnsAsync(() => errorHandlerWasAbleToHandle ? ProcessResult.Success : ProcessResult.Failure); if (errorHandlerRegistered) { @@ -177,7 +177,7 @@ public async Task When_DoHandle_Given_ConsumerThatThrowsExceptionAndErrorHandler consumerErrorHandlerMock .Setup(x => x.OnHandleError(someMessage, It.IsAny(), someException, It.IsAny())) - .ReturnsAsync(() => ConsumerErrorHandlerResult.Retry); + .ReturnsAsync(() => ProcessResult.Retry); busMock.ServiceProviderMock .Setup(x => x.GetService(typeof(IConsumerErrorHandler))) From c8250edc4e93c3deb89bcced354ee22717ab4e21 Mon Sep 17 00:00:00 2001 From: Richard Pringle Date: Wed, 17 Jul 2024 09:46:44 +0800 Subject: [PATCH 14/21] #251 Health check circuit breaker Signed-off-by: Richard Pringle --- build/tasks.ps1 | 2 + docs/intro.md | 27 ++ docs/intro.t.md | 27 ++ src/.editorconfig | 3 + .../Consumers/AddConsumer.cs | 17 + .../Consumers/SubtractConsumer.cs | 17 + .../GlobalUsings.cs | 17 + .../HealthChecks/AddRandomHealthCheck.cs | 11 + .../HealthChecks/RandomHealthCheck.cs | 20 ++ .../HealthChecks/SubtractRandomHealthCheck.cs | 11 + .../IntermittentMessagePublisher.cs | 28 ++ .../Models/Add.cs | 3 + .../Models/Subtract.cs | 3 + .../Program.cs | 91 +++++ .../Sample.CircuitBreaker.HealthCheck.csproj | 33 ++ .../appsettings.json | 19 ++ .../Consumer/SqsBaseConsumer.cs | 2 +- .../Consumer/EhGroupConsumer.cs | 4 +- .../EventHubMessageBus.cs | 4 +- .../Consumer/AsbBaseConsumer.cs | 2 +- .../Config/ConsumerBuilderExtensions.cs | 47 +++ .../Config/SettingsExtensions.cs | 47 +++ .../GlobalUsings.cs | 7 + .../HealthCheckBackgroundService.cs | 125 +++++++ .../HealthCheckCircuitBreaker.cs | 92 +++++ .../IHealthCheckHostBreaker.cs | 9 + ...Bus.Host.CircuitBreaker.HealthCheck.csproj | 30 ++ .../Builders/AbstractConsumerBuilder.cs | 5 +- .../Builders/MessageBusBuilder.cs | 301 ++++++++-------- .../GlobalUsings.cs | 3 +- .../Settings/AbstractConsumerSettings.cs | 10 + .../Settings/ConsumerSettings.cs | 9 +- .../TypeCollection.cs | 78 +++++ .../Consumer/KafkaGroupConsumer.cs | 254 +++++++------- .../KafkaMessageBus.cs | 8 +- .../MqttMessageBus.cs | 10 +- .../MqttTopicConsumer.cs | 3 +- .../NatsMessageBus.cs | 161 +++++---- .../NatsSubjectConsumer.cs | 73 ++-- .../Consumers/AbstractRabbitMqConsumer.cs | 4 +- .../Consumers/RabbitMqConsumer.cs | 2 +- .../Consumers/RabbitMqResponseConsumer.cs | 2 +- .../Consumers/RedisListCheckerConsumer.cs | 2 +- .../Consumers/RedisTopicConsumer.cs | 4 +- .../RedisMessageBus.cs | 7 +- .../Consumer/AbstractConsumer.cs | 109 +++++- src/SlimMessageBus.Host/IConsumerControl.cs | 2 +- src/SlimMessageBus.sln | 36 ++ src/SlimMessageBus/IConsumerCircuitBreaker.cs | 17 + .../GlobalUsings.cs | 12 + .../HealthCheckBackgroundServiceTests.cs | 323 ++++++++++++++++++ .../HealthCheckCircuitBreakerTests.cs | 184 ++++++++++ ...ost.CircuitBreaker.HealthCheck.Test.csproj | 23 ++ .../xunit.runner.json | 4 + .../TypeCollectionTests.cs | 217 ++++++++++++ .../Consumer/KafkaGroupConsumerTests.cs | 3 +- .../KafkaMessageBusIt.cs | 2 +- .../OutboxTests.cs | 2 + .../IntegrationTests/RabbitMqMessageBusIt.cs | 2 +- .../Consumer/AbstractConsumerTests.cs | 139 ++++++++ .../SlimMessageBus.Host.Test/GlobalUsings.cs | 6 +- .../Helpers/ReflectionUtilsTests.cs | 2 +- .../MessageBusTested.cs | 2 +- 63 files changed, 2298 insertions(+), 421 deletions(-) create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj create mode 100644 src/Samples/Sample.CircuitBreaker.HealthCheck/appsettings.json create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/IHealthCheckHostBreaker.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj create mode 100644 src/SlimMessageBus.Host.Configuration/TypeCollection.cs create mode 100644 src/SlimMessageBus/IConsumerCircuitBreaker.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/xunit.runner.json create mode 100644 src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs create mode 100644 src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs diff --git a/build/tasks.ps1 b/build/tasks.ps1 index 09afe10c..649a062d 100644 --- a/build/tasks.ps1 +++ b/build/tasks.ps1 @@ -43,6 +43,8 @@ $projects = @( "SlimMessageBus.Host.Outbox.Sql", "SlimMessageBus.Host.Outbox.Sql.DbContext", + "SlimMessageBus.Host.CircuitBreaker.HealthCheck", + "SlimMessageBus.Host.AsyncApi" ) diff --git a/docs/intro.md b/docs/intro.md index 09cacc67..5a873a3e 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -7,6 +7,7 @@ - [Set message headers](#set-message-headers) - [Consumer](#consumer) - [Start or Stop message consumption](#start-or-stop-message-consumption) + - [Health check circuit breaker](#health-check-circuit-breaker) - [Consumer context (additional message information)](#consumer-context-additional-message-information) - [Per-message DI container scope](#per-message-di-container-scope) - [Hybrid bus and message scope reuse](#hybrid-bus-and-message-scope-reuse) @@ -291,6 +292,32 @@ await consumerControl.Stop(); > Since version 1.15.5 +#### Health check circuit breaker + +Consumers can be linked to [.NET app health checks](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/diagnostic-health-checks) [tags](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks#register-health-check-services), enabling or disabling the consumer based on the health check status reported by the [Health Check Publisher](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks#health-check-publisher). A consumer associated with one or more tags will only be active if all health checks linked to the tags are passing. + +```cs + // add health checks with tags + builder.Services + .AddHealthChecks() + .AddCheck("Storage", tags: ["Storage"]); + .AddCheck("SqlServer", tags: ["Sql"]); + + builder.Services + .AddSlimMessageBus(mbb => { + ... + + mbb.Consume(cfg => { + ... + + // configure consumer to monitor tag/state + cfg.PauseOnUnhealthyCheck("Storage"); + cfg.PauseOnDegradedHealthCheck("Sql"); + }) + }) +``` +*Requires: SlimMessageBus.Host.CircuitBreaker.HealthCheck* + #### Consumer context (additional message information) The consumer can access the [`IConsumerContext`](../src/SlimMessageBus/IConsumerContext.cs) object which: diff --git a/docs/intro.t.md b/docs/intro.t.md index ee6857b7..7c1dc571 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -7,6 +7,7 @@ - [Set message headers](#set-message-headers) - [Consumer](#consumer) - [Start or Stop message consumption](#start-or-stop-message-consumption) + - [Health check circuit breaker](#health-check-circuit-breaker) - [Consumer context (additional message information)](#consumer-context-additional-message-information) - [Per-message DI container scope](#per-message-di-container-scope) - [Hybrid bus and message scope reuse](#hybrid-bus-and-message-scope-reuse) @@ -291,6 +292,32 @@ await consumerControl.Stop(); > Since version 1.15.5 +#### Health check circuit breaker + +Consumers can be linked to [.NET app health checks](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/diagnostic-health-checks) [tags](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks#register-health-check-services), enabling or disabling the consumer based on the health check status reported by the [Health Check Publisher](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks#health-check-publisher). A consumer associated with one or more tags will only be active if all health checks linked to the tags are passing. + +```cs + // add health checks with tags + builder.Services + .AddHealthChecks() + .AddCheck("Storage", tags: ["Storage"]); + .AddCheck("SqlServer", tags: ["Sql"]); + + builder.Services + .AddSlimMessageBus(mbb => { + ... + + mbb.Consume(cfg => { + ... + + // configure consumer to monitor tag/state + cfg.PauseOnUnhealthyCheck("Storage"); + cfg.PauseOnDegradedHealthCheck("Sql"); + }) + }) +``` +*Requires: SlimMessageBus.Host.CircuitBreaker.HealthCheck* + #### Consumer context (additional message information) The consumer can access the [`IConsumerContext`](../src/SlimMessageBus/IConsumerContext.cs) object which: diff --git a/src/.editorconfig b/src/.editorconfig index a0368644..53489ab9 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -178,11 +178,14 @@ dotnet_style_allow_multiple_blank_lines_experimental = true:silent dotnet_style_allow_statement_immediately_after_block_experimental = true:silent dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion dotnet_diagnostic.CA1859.severity = silent + dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion dotnet_diagnostic.VSTHRD200.severity = none +# not supported by .netstandard2.0 +dotnet_diagnostic.CA1510.severity = none [*.{csproj,xml}] indent_style = space diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs new file mode 100644 index 00000000..1da885ad --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs @@ -0,0 +1,17 @@ +namespace Sample.CircuitBreaker.HealthCheck.Consumers; + +public class AddConsumer : IConsumer +{ + private readonly ILogger _logger; + + public AddConsumer(ILogger logger) + { + _logger = logger; + } + + public Task OnHandle(Add message, CancellationToken cancellationToken) + { + _logger.LogInformation("{A} + {B} = {C}", message.a, message.b, message.a + message.b); + return Task.CompletedTask; + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs new file mode 100644 index 00000000..467a30d5 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs @@ -0,0 +1,17 @@ +namespace Sample.CircuitBreaker.HealthCheck.Consumers; + +public class SubtractConsumer : IConsumer +{ + private readonly ILogger _logger; + + public SubtractConsumer(ILogger logger) + { + _logger = logger; + } + + public Task OnHandle(Subtract message, CancellationToken cancellationToken) + { + _logger.LogInformation("{A} - {B} = {C}", message.a, message.b, message.a - message.b); + return Task.CompletedTask; + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs new file mode 100644 index 00000000..61fd0fa5 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs @@ -0,0 +1,17 @@ +global using System.Net.Mime; +global using System.Reflection; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; + +global using Sample.CircuitBreaker.HealthCheck.Consumers; +global using Sample.CircuitBreaker.HealthCheck.Models; + +global using SecretStore; + +global using SlimMessageBus; +global using SlimMessageBus.Host; +global using SlimMessageBus.Host.RabbitMQ; +global using SlimMessageBus.Host.Serialization.SystemTextJson; \ No newline at end of file diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs new file mode 100644 index 00000000..b74784dd --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs @@ -0,0 +1,11 @@ +namespace Sample.CircuitBreaker.HealthCheck.HealthChecks; + +using Microsoft.Extensions.Logging; + +public class AddRandomHealthCheck : RandomHealthCheck +{ + public AddRandomHealthCheck(ILogger logger) + : base(logger) + { + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs new file mode 100644 index 00000000..cf9ccf88 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs @@ -0,0 +1,20 @@ +namespace Sample.CircuitBreaker.HealthCheck.HealthChecks; + +using Microsoft.Extensions.Diagnostics.HealthChecks; + +public abstract class RandomHealthCheck : IHealthCheck +{ + private readonly ILogger _logger; + + protected RandomHealthCheck(ILogger logger) + { + _logger = logger; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var value = (HealthStatus)Random.Shared.Next(3); + _logger.LogInformation("{HealthCheck} evaluated as {HealthStatus}", this.GetType(), value); + return Task.FromResult(new HealthCheckResult(value, value.ToString())); + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs new file mode 100644 index 00000000..8a68b0b1 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs @@ -0,0 +1,11 @@ +namespace Sample.CircuitBreaker.HealthCheck.HealthChecks; + +using Microsoft.Extensions.Logging; + +public class SubtractRandomHealthCheck : RandomHealthCheck +{ + public SubtractRandomHealthCheck(ILogger logger) + : base(logger) + { + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs new file mode 100644 index 00000000..73110c15 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs @@ -0,0 +1,28 @@ +namespace Sample.CircuitBreaker.HealthCheck; +public class IntermittentMessagePublisher : BackgroundService +{ + private readonly ILogger _logger; + private readonly IMessageBus _messageBus; + + public IntermittentMessagePublisher(ILogger logger, IMessageBus messageBus) + { + _logger = logger; + _messageBus = messageBus; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var a = Random.Shared.Next(10); + var b = Random.Shared.Next(10); + + //_logger.LogInformation("Emitting {A} +- {B} = ?", a, b); + + await Task.WhenAll( + _messageBus.Publish(new Add(a, b)), + _messageBus.Publish(new Subtract(a, b)), + Task.Delay(1000, stoppingToken)); + } + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs new file mode 100644 index 00000000..97c5e418 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs @@ -0,0 +1,3 @@ +namespace Sample.CircuitBreaker.HealthCheck.Models; + +public record Add(int a, int b); \ No newline at end of file diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs new file mode 100644 index 00000000..51d2efc4 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs @@ -0,0 +1,3 @@ +namespace Sample.CircuitBreaker.HealthCheck.Models; + +public record Subtract(int a, int b); \ No newline at end of file diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs new file mode 100644 index 00000000..716253f5 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs @@ -0,0 +1,91 @@ +namespace Sample.CircuitBreaker.HealthCheck; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +using Sample.CircuitBreaker.HealthCheck.HealthChecks; + +using SlimMessageBus.Host.CircuitBreaker.HealthCheck.Config; + +public static class Program +{ + private static async Task Main(string[] args) + { + // Local file with secrets + Secrets.Load(@"..\..\..\..\..\secrets.txt"); + + await Host.CreateDefaultBuilder(args) + .ConfigureServices((builder, services) => + { + const string AddTag = "add"; + const string SubtractTag = "subtract"; + + services.AddSlimMessageBus(mbb => + { + var ticks = DateTimeOffset.UtcNow.Ticks; + var addTopic = $"Sample-CircuitBreaker-HealthCheck-add-{ticks}"; + var subtractTopic = $"Sample-CircuitBreaker-HealthCheck-subtract-{ticks}"; + + mbb + .WithProviderRabbitMQ( + cfg => + { + cfg.ConnectionString = Secrets.Service.PopulateSecrets(builder.Configuration.GetValue("RabbitMQ:ConnectionString")); + cfg.ConnectionFactory.ClientProvidedName = $"Sample_CircuitBreaker_HealthCheck_{Environment.MachineName}"; + + cfg.UseMessagePropertiesModifier((m, p) => p.ContentType = MediaTypeNames.Application.Json); + cfg.UseExchangeDefaults(durable: false); + cfg.UseQueueDefaults(durable: false); + }); + mbb + .Produce(x => x + .Exchange(addTopic, exchangeType: ExchangeType.Fanout, autoDelete: false) + .RoutingKeyProvider((m, p) => Guid.NewGuid().ToString())) + .Consume( + cfg => + { + cfg + .Queue(nameof(Add), autoDelete: false) + .Path(nameof(Add)) + .ExchangeBinding(addTopic) + .WithConsumer() + .PauseOnDegradedHealthCheck(AddTag); + }); + + mbb + .Produce(x => x + .Exchange(subtractTopic, exchangeType: ExchangeType.Fanout, autoDelete: false) + .RoutingKeyProvider((m, p) => Guid.NewGuid().ToString())) + .Consume( + cfg => + { + cfg + .Queue(nameof(Subtract), autoDelete: false) + .Path(nameof(Subtract)) + .ExchangeBinding(subtractTopic) + .WithConsumer() + .PauseOnUnhealthyCheck(SubtractTag); + }); + + mbb.AddServicesFromAssembly(Assembly.GetExecutingAssembly()); + mbb.AddJsonSerializer(); + }); + + services.AddHostedService(); + services.AddSingleton(); + services.AddSingleton(); + + services.Configure(cfg => + { + // aggressive to toggle health status often (sample only) + cfg.Delay = TimeSpan.FromSeconds(3); + cfg.Period = TimeSpan.FromSeconds(5); + }); + + services + .AddHealthChecks() + .AddCheck("Add", tags: [AddTag]) + .AddCheck("Subtract", tags: [SubtractTag]); + }) + .Build() + .RunAsync(); + } +} diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj b/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj new file mode 100644 index 00000000..8e4ac1af --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj @@ -0,0 +1,33 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + Always + true + PreserveNewest + + + + diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/appsettings.json b/src/Samples/Sample.CircuitBreaker.HealthCheck/appsettings.json new file mode 100644 index 00000000..fd742024 --- /dev/null +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "SingleLine": true, + "TimestampFormat": "HH:mm:ss.fff " + } + } + }, + "RabbitMQ": { + "ConnectionString": "{{rabbitmq_connectionstring}}" + } +} diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs index 95818eb7..c784f1b9 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs @@ -23,7 +23,7 @@ protected SqsBaseConsumer( IMessageProcessor messageProcessor, IEnumerable consumerSettings, ILogger logger) - : base(logger ?? throw new ArgumentNullException(nameof(logger))) + : base(logger, consumerSettings) { MessageBus = messageBus ?? throw new ArgumentNullException(nameof(messageBus)); _clientProvider = clientProvider ?? throw new ArgumentNullException(nameof(clientProvider)); diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs index ebf82dcd..a1bd113c 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs @@ -11,8 +11,8 @@ public class EhGroupConsumer : AbstractConsumer public EventHubMessageBus MessageBus { get; } - public EhGroupConsumer(EventHubMessageBus messageBus, GroupPath groupPath, Func partitionConsumerFactory) - : base(messageBus.LoggerFactory.CreateLogger()) + public EhGroupConsumer(IEnumerable consumerSettings, EventHubMessageBus messageBus, GroupPath groupPath, Func partitionConsumerFactory) + : base(messageBus.LoggerFactory.CreateLogger(), consumerSettings) { _groupPath = groupPath ?? throw new ArgumentNullException(nameof(groupPath)); if (partitionConsumerFactory == null) throw new ArgumentNullException(nameof(partitionConsumerFactory)); diff --git a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs index 3aae9ded..d3df802d 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs @@ -60,14 +60,14 @@ protected override async Task CreateConsumers() foreach (var (groupPath, consumerSettings) in Settings.Consumers.GroupBy(x => new GroupPath(path: x.Path, group: x.GetGroup())).ToDictionary(x => x.Key, x => x.ToList())) { _logger.LogInformation("Creating consumer for Path: {Path}, Group: {Group}", groupPath.Path, groupPath.Group); - AddConsumer(new EhGroupConsumer(this, groupPath, groupPathPartition => new EhPartitionConsumerForConsumers(this, consumerSettings, groupPathPartition, MessageProvider))); + AddConsumer(new EhGroupConsumer(consumerSettings, this, groupPath, groupPathPartition => new EhPartitionConsumerForConsumers(this, consumerSettings, groupPathPartition, MessageProvider))); } if (Settings.RequestResponse != null) { var pathGroup = new GroupPath(Settings.RequestResponse.Path, Settings.RequestResponse.GetGroup()); _logger.LogInformation("Creating response consumer for Path: {Path}, Group: {Group}", pathGroup.Path, pathGroup.Group); - AddConsumer(new EhGroupConsumer(this, pathGroup, groupPathPartition => new EhPartitionConsumerForResponses(this, Settings.RequestResponse, groupPathPartition, MessageProvider, PendingRequestStore, CurrentTimeProvider))); + AddConsumer(new EhGroupConsumer([Settings.RequestResponse], this, pathGroup, groupPathPartition => new EhPartitionConsumerForResponses(this, Settings.RequestResponse, groupPathPartition, MessageProvider, PendingRequestStore, CurrentTimeProvider))); } } diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs index 326a0991..a601e50c 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs @@ -10,7 +10,7 @@ public abstract class AsbBaseConsumer : AbstractConsumer protected TopicSubscriptionParams TopicSubscription { get; } protected AsbBaseConsumer(ServiceBusMessageBus messageBus, ServiceBusClient serviceBusClient, TopicSubscriptionParams subscriptionFactoryParams, IMessageProcessor messageProcessor, IEnumerable consumerSettings, ILogger logger) - : base(logger ?? throw new ArgumentNullException(nameof(logger))) + : base(logger ?? throw new ArgumentNullException(nameof(logger)), consumerSettings) { MessageBus = messageBus ?? throw new ArgumentNullException(nameof(messageBus)); TopicSubscription = subscriptionFactoryParams ?? throw new ArgumentNullException(nameof(subscriptionFactoryParams)); diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs new file mode 100644 index 00000000..c721f5dc --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs @@ -0,0 +1,47 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Config; + +using Microsoft.Extensions.DependencyInjection.Extensions; + +public static class ConsumerBuilderExtensions +{ + public static T PauseOnUnhealthyCheck(this T builder, params string[] tags) + where T : AbstractConsumerBuilder + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ConsumerSettings.PauseOnUnhealthy(tags); + RegisterHealthServices(builder); + return builder; + } + + public static T PauseOnDegradedHealthCheck(this T builder, params string[] tags) + where T : AbstractConsumerBuilder + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ConsumerSettings.PauseOnDegraded(tags); + RegisterHealthServices(builder); + return builder; + } + + private static void RegisterHealthServices(AbstractConsumerBuilder builder) + { + builder.ConsumerSettings.CircuitBreakers.TryAdd(); + builder.PostConfigurationActions.Add( + services => + { + services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + services.TryAdd(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + services.AddHostedService(sp => sp.GetRequiredService()); + + services.TryAddSingleton(); + }); + } +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs new file mode 100644 index 00000000..a2775f10 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs @@ -0,0 +1,47 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; + +static internal class SettingsExtensions +{ + private const string _key = nameof(HealthCheckCircuitBreaker); + + public static T PauseOnDegraded(this T consumerSettings, params string[] tags) + where T : AbstractConsumerSettings + { + if (tags.Length > 0) + { + var dict = consumerSettings.HealthBreakerTags(); + foreach (var tag in tags) + { + dict[tag] = HealthStatus.Degraded; + } + } + + return consumerSettings; + } + + public static T PauseOnUnhealthy(this T consumerSettings, params string[] tags) + where T : AbstractConsumerSettings + { + if (tags.Length > 0) + { + var dict = consumerSettings.HealthBreakerTags(); + foreach (var tag in tags) + { + dict[tag] = HealthStatus.Unhealthy; + } + } + + return consumerSettings; + } + + static internal IDictionary HealthBreakerTags(this AbstractConsumerSettings consumerSettings) + { + if (!consumerSettings.Properties.TryGetValue(_key, out var rawValue) || rawValue is not IDictionary value) + { + value = new Dictionary(); + consumerSettings.Properties[_key] = value; + } + + return value; + } +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs new file mode 100644 index 00000000..6d9be2c3 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using System; +global using System.Diagnostics; + +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs new file mode 100644 index 00000000..96bd55a0 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs @@ -0,0 +1,125 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; + +internal sealed class HealthCheckBackgroundService : IHealthCheckPublisher, IHostedService, IHealthCheckHostBreaker, IDisposable +{ + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly Dictionary _healthReportEntries; + private readonly List _onChangeDelegates; + private readonly SemaphoreSlim _semaphore; + private IReadOnlyDictionary _tagStatus; + + public HealthCheckBackgroundService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _healthReportEntries = []; + _onChangeDelegates = []; + _semaphore = new SemaphoreSlim(1, 1); + _tagStatus = new Dictionary(); + } + + public IReadOnlyDictionary TagStatus => _tagStatus; + + public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken).Token; + + await _semaphore.WaitAsync(linkedToken); + try + { + UpdateHealthReportEntries(report); + if (UpdateTagStatus() && !linkedToken.IsCancellationRequested) + { + await Task.WhenAll(_onChangeDelegates.Select(x => x(_tagStatus))); + } + } + finally + { + _semaphore.Release(); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource.Cancel(); + return Task.CompletedTask; + } + + public async Task Subscribe(OnChangeDelegate onChange) + { + _onChangeDelegates.Add(onChange); + await onChange(_tagStatus); + } + + public void Unsubscribe(OnChangeDelegate onChange) + { + _onChangeDelegates.Remove(onChange); + } + + private void UpdateHealthReportEntries(HealthReport report) + { + foreach (var entry in report.Entries.Where(x => x.Value.Tags.Any())) + { + _healthReportEntries[entry.Key] = entry.Value; + } + } + + private bool UpdateTagStatus() + { + var tagStatus = new Dictionary(); + foreach (var entry in _healthReportEntries.Values) + { + foreach (var tag in entry.Tags) + { + if (tagStatus.TryGetValue(tag, out var currentStatus)) + { + if ((entry.Status == HealthStatus.Degraded && currentStatus == HealthStatus.Healthy) + || (entry.Status == HealthStatus.Unhealthy && currentStatus != HealthStatus.Unhealthy)) + { + tagStatus[tag] = entry.Status; + } + + continue; + } + + tagStatus[tag] = entry.Status; + } + } + + if (!AreEqual(_tagStatus, tagStatus)) + { + _tagStatus = tagStatus; + return true; + } + + return false; + } + + internal static bool AreEqual(IReadOnlyDictionary dict1, IReadOnlyDictionary dict2) + { + if (dict1.Count != dict2.Count) + { + return false; + } + + foreach (var kvp in dict1) + { + if (!dict2.TryGetValue(kvp.Key, out var value) || !EqualityComparer.Default.Equals(kvp.Value, value)) + { + return false; + } + } + + return true; + } + + public void Dispose() + { + _cancellationTokenSource.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs new file mode 100644 index 00000000..8797fc9e --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs @@ -0,0 +1,92 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; + +internal sealed class HealthCheckCircuitBreaker : IConsumerCircuitBreaker +{ + private readonly IEnumerable _settings; + private readonly IHealthCheckHostBreaker _host; + private Func? _onChange; + private IDictionary? _monitoredTags; + + public HealthCheckCircuitBreaker(IEnumerable settings, IHealthCheckHostBreaker host) + { + _settings = settings; + _host = host; + State = Circuit.Open; + } + + public Circuit State { get; private set; } + + public async Task Subscribe(Func onChange) + { + Debug.Assert(_onChange == null); + _onChange = onChange; + + _monitoredTags = _settings + .Select(x => x.HealthBreakerTags()) + .Aggregate( + (a, b) => + { + var c = new Dictionary(a.Count + b.Count); + foreach (var kvp in a) + { + var status = kvp.Value; + if (b.TryGetValue(kvp.Key, out var altStatus)) + { + b.Remove(kvp.Key); + if (status != altStatus && altStatus == HealthStatus.Degraded) + { + status = HealthStatus.Degraded; + } + } + + c[kvp.Key] = status; + } + + foreach (var kvp in b) + { + c.Add(kvp.Key, kvp.Value); + } + + return c; + }); + + await _host.Subscribe(TagStatusChanged); + } + + public void Unsubscribe() + { + _host.Unsubscribe(TagStatusChanged); + _onChange = null; + _monitoredTags = null; + } + + internal async Task TagStatusChanged(IReadOnlyDictionary tags) + { + var newState = _monitoredTags! + .All( + monitoredTag => + { + if (!tags.TryGetValue(monitoredTag.Key, out var currentStatus)) + { + // unknown tag, assume healthy + return true; + } + + return currentStatus switch + { + HealthStatus.Healthy => true, + HealthStatus.Degraded => monitoredTag.Value != HealthStatus.Degraded, + HealthStatus.Unhealthy => false, + _ => throw new InvalidOperationException($"Unknown health status '{currentStatus}'") + }; + }) + ? Circuit.Open + : Circuit.Closed; + + if (State != newState) + { + State = newState; + await _onChange!(newState); + } + } +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/IHealthCheckHostBreaker.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/IHealthCheckHostBreaker.cs new file mode 100644 index 00000000..c87c7fcf --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/IHealthCheckHostBreaker.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; + +internal interface IHealthCheckHostBreaker +{ + Task Subscribe(OnChangeDelegate onChange); + void Unsubscribe(OnChangeDelegate onChange); +} + +public delegate Task OnChangeDelegate(IReadOnlyDictionary tags); \ No newline at end of file diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj new file mode 100644 index 00000000..611abab7 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj @@ -0,0 +1,30 @@ + + + + + + Health check circuit breaker for SlimMessageBus + Toggle consumer on health check status changes + icon.png + + enable + + + + + + + + + + + + + <_Parameter1>SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs index f7b5825a..7ae056e9 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/AbstractConsumerBuilder.cs @@ -1,11 +1,13 @@ namespace SlimMessageBus.Host; -public abstract class AbstractConsumerBuilder : IAbstractConsumerBuilder, IConsumerBuilder +public abstract class AbstractConsumerBuilder : IAbstractConsumerBuilder, IConsumerBuilder, IHasPostConfigurationActions { public MessageBusSettings Settings { get; } public ConsumerSettings ConsumerSettings { get; } + public IList> PostConfigurationActions { get; } = []; + AbstractConsumerSettings IAbstractConsumerBuilder.ConsumerSettings => ConsumerSettings; HasProviderExtensions IBuilderWithSettings.Settings => ConsumerSettings; @@ -16,6 +18,7 @@ protected AbstractConsumerBuilder(MessageBusSettings settings, Type messageType, ConsumerSettings = new ConsumerSettings { + MessageBusSettings = settings, MessageType = messageType, Path = path, }; diff --git a/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs index 21e0dd13..a2bc9329 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs @@ -2,8 +2,8 @@ namespace SlimMessageBus.Host; using Microsoft.Extensions.DependencyInjection.Extensions; -public class MessageBusBuilder : IHasPostConfigurationActions, ISerializationBuilder, IProducerBuilder -{ +public class MessageBusBuilder : IHasPostConfigurationActions, ISerializationBuilder, IProducerBuilder +{ /// /// Parent bus builder. /// @@ -11,25 +11,27 @@ public class MessageBusBuilder : IHasPostConfigurationActions, ISerializationBui /// /// Declared child buses. - /// - public IDictionary Children { get; } = new Dictionary(); + /// + public IDictionary Children { get; } = new Dictionary(); /// /// The current settings that are being built. - /// + /// public MessageBusSettings Settings { get; private set; } = new(); HasProviderExtensions IBuilderWithSettings.Settings => Settings; /// /// The bus factory method. - /// - public Func BusFactory { get; private set; } + /// + public Func BusFactory { get; private set; } public IList> PostConfigurationActions { get; } = []; - protected MessageBusBuilder() + protected IList ConsumerPostConfigurationActions { get; } = []; + + protected MessageBusBuilder() { } @@ -39,8 +41,16 @@ protected MessageBusBuilder(MessageBusBuilder other) Children = other.Children; BusFactory = other.BusFactory; PostConfigurationActions = other.PostConfigurationActions; - } - + ConsumerPostConfigurationActions = other.ConsumerPostConfigurationActions; + } + + public IEnumerable> GetPostConfigurationActions() + { + return PostConfigurationActions + .Concat(ConsumerPostConfigurationActions.SelectMany(x => x.PostConfigurationActions)) + .Concat(Children.Values.SelectMany(x => x.PostConfigurationActions.Concat(x.ConsumerPostConfigurationActions.SelectMany(z => z.PostConfigurationActions)))); + } + public static MessageBusBuilder Create() => new(); public MessageBusBuilder MergeFrom(MessageBusSettings settings) @@ -49,51 +59,51 @@ public MessageBusBuilder MergeFrom(MessageBusSettings settings) Settings.MergeFrom(settings); return this; - } - - /// - /// Configures (declares) the production (publishing for pub/sub or request sending in request/response) of a message - /// - /// Type of the message - /// - /// - public MessageBusBuilder Produce(Action> builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - var item = new ProducerSettings(); - builder(new ProducerBuilder(item)); - Settings.Producers.Add(item); - return this; - } - - /// - /// Configures (declares) the production (publishing for pub/sub or request sending in request/response) of a message - /// - /// Type of the message - /// - /// - public MessageBusBuilder Produce(Type messageType, Action> builder) - { + } + + /// + /// Configures (declares) the production (publishing for pub/sub or request sending in request/response) of a message + /// + /// Type of the message + /// + /// + public MessageBusBuilder Produce(Action> builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var item = new ProducerSettings(); + builder(new ProducerBuilder(item)); + Settings.Producers.Add(item); + return this; + } + + /// + /// Configures (declares) the production (publishing for pub/sub or request sending in request/response) of a message + /// + /// Type of the message + /// + /// + public MessageBusBuilder Produce(Type messageType, Action> builder) + { if (builder == null) throw new ArgumentNullException(nameof(builder)); - - var item = new ProducerSettings(); - builder(new ProducerBuilder(item, messageType)); - Settings.Producers.Add(item); - return this; + + var item = new ProducerSettings(); + builder(new ProducerBuilder(item, messageType)); + Settings.Producers.Add(item); + return this; } - /// - /// Configures (declares) the consumer of given message types in pub/sub or queue communication. - /// - /// Type of message - /// - /// - public MessageBusBuilder Consume(Action> builder) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - var b = new ConsumerBuilder(Settings); + /// + /// Configures (declares) the consumer of given message types in pub/sub or queue communication. + /// + /// Type of message + /// + /// + public MessageBusBuilder Consume(Action> builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var b = new ConsumerBuilder(Settings); builder(b); if (b.ConsumerSettings.ConsumerType is null) @@ -101,83 +111,98 @@ public MessageBusBuilder Consume(Action> bui // Apply default consumer type of not set b.WithConsumer>(); } - return this; - } - - /// - /// Configures (declares) the consumer of given message types in pub/sub or queue communication. - /// - /// Type of message - /// - /// - public MessageBusBuilder Consume(Type messageType, Action> builder) + + ConsumerPostConfigurationActions.Add(b); + + return this; + } + + /// + /// Configures (declares) the consumer of given message types in pub/sub or queue communication. + /// + /// Type of message + /// + /// + public MessageBusBuilder Consume(Type messageType, Action> builder) { if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder(new ConsumerBuilder(Settings, messageType)); - return this; - } - + + var b = new ConsumerBuilder(Settings, messageType); + builder(b); + + ConsumerPostConfigurationActions.Add(b); + + return this; + } + /// /// Configures (declares) the handler of a given request message type in request-response communication. /// /// /// /// - /// - public MessageBusBuilder Handle(Action> builder) + /// + public MessageBusBuilder Handle(Action> builder) { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - var b = new HandlerBuilder(Settings); + var b = new HandlerBuilder(Settings); builder(b); if (b.ConsumerSettings.ConsumerType is null) { // Apply default handler type of not set b.WithHandler>(); - } + } - return this; - } + ConsumerPostConfigurationActions.Add(b); + + return this; + } /// /// Configures (declares) the handler of a given request message type which has no response message type. /// /// /// - /// - public MessageBusBuilder Handle(Action> builder) + /// + public MessageBusBuilder Handle(Action> builder) { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - var b = new HandlerBuilder(Settings); + var b = new HandlerBuilder(Settings); builder(b); if (b.ConsumerSettings.ConsumerType is null) { // Apply default handler type of not set b.WithHandler>(); - } + } + + ConsumerPostConfigurationActions.Add(b); - return this; + return this; } - + /// /// Configures (declares) the handler of a given request message type in request-response communication. /// /// /// /// - /// - public MessageBusBuilder Handle(Type requestType, Type responseType, Action> builder) + /// + public MessageBusBuilder Handle(Type requestType, Type responseType, Action> builder) { if (requestType == null) throw new ArgumentNullException(nameof(requestType)); if (responseType == null) throw new ArgumentNullException(nameof(responseType)); - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder(new HandlerBuilder(Settings, requestType, responseType)); - return this; + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var b = new HandlerBuilder(Settings, requestType, responseType); + builder(b); + + ConsumerPostConfigurationActions.Add(b); + + return this; } /// @@ -186,26 +211,30 @@ public MessageBusBuilder Handle(Type requestType, Type responseType, Action /// /// - /// - public MessageBusBuilder Handle(Type requestType, Action> builder) + /// + public MessageBusBuilder Handle(Type requestType, Action> builder) { if (requestType == null) throw new ArgumentNullException(nameof(requestType)); - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder(new HandlerBuilder(Settings, requestType)); - return this; + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var b = new HandlerBuilder(Settings, requestType); + builder(b); + + ConsumerPostConfigurationActions.Add(b); + + return this; + } + + public MessageBusBuilder ExpectRequestResponses(Action reqRespBuilder) + { + if (reqRespBuilder == null) throw new ArgumentNullException(nameof(reqRespBuilder)); + + var item = new RequestResponseSettings(); + reqRespBuilder(new RequestResponseBuilder(item)); + Settings.RequestResponse = item; + return this; } - - public MessageBusBuilder ExpectRequestResponses(Action reqRespBuilder) - { - if (reqRespBuilder == null) throw new ArgumentNullException(nameof(reqRespBuilder)); - - var item = new RequestResponseSettings(); - reqRespBuilder(new RequestResponseBuilder(item)); - Settings.RequestResponse = item; - return this; - } - + /// /// Serializer type () to look up in the DI for this bus. /// @@ -218,15 +247,15 @@ public MessageBusBuilder ExpectRequestResponses(Action r /// /// /// - public MessageBusBuilder WithSerializer(Type serializerType) + public MessageBusBuilder WithSerializer(Type serializerType) { if (serializerType is not null && !typeof(IMessageSerializer).IsAssignableFrom(serializerType)) { throw new ConfigurationMessageBusException($"The serializer type {serializerType.FullName} does not implement the interface {nameof(IMessageSerializer)}"); } - - Settings.SerializerType = serializerType ?? throw new ArgumentNullException(nameof(serializerType)); - return this; + + Settings.SerializerType = serializerType ?? throw new ArgumentNullException(nameof(serializerType)); + return this; } public void RegisterSerializer(Action services) @@ -236,39 +265,39 @@ public void RegisterSerializer(Action se PostConfigurationActions.Add(services => services.TryAddSingleton(sp => sp.GetRequiredService())); } - public MessageBusBuilder WithDependencyResolver(IServiceProvider serviceProvider) - { - Settings.ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - return this; - } - - public MessageBusBuilder WithProvider(Func provider) - { - BusFactory = provider ?? throw new ArgumentNullException(nameof(provider)); - return this; - } - - public MessageBusBuilder Do(Action builder) + public MessageBusBuilder WithDependencyResolver(IServiceProvider serviceProvider) + { + Settings.ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + return this; + } + + public MessageBusBuilder WithProvider(Func provider) + { + BusFactory = provider ?? throw new ArgumentNullException(nameof(provider)); + return this; + } + + public MessageBusBuilder Do(Action builder) { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder(this); - return this; - } - + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder(this); + return this; + } + /// /// Sets the default enable (or disable) creation of DI child scope for each message. /// /// - /// + /// public MessageBusBuilder PerMessageScopeEnabled(bool enabled = true) { Settings.IsMessageScopeEnabled = enabled; return this; - } - + } + public MessageBusBuilder WithMessageTypeResolver(Type messageTypeResolverType) - { + { Settings.MessageTypeResolverType = messageTypeResolverType ?? throw new ArgumentNullException(nameof(messageTypeResolverType)); return this; } @@ -291,7 +320,7 @@ public MessageBusBuilder WithHeaderModifier(MessageHeaderModifier header previousHeaderModifier(headers, message); headerModifier(headers, message); }; - return this; + return this; } /// @@ -325,7 +354,7 @@ public MessageBusBuilder AutoStartConsumersEnabled(bool enabled) /// /// /// - /// + /// public MessageBusBuilder AddChildBus(string busName, Action builderAction) { if (busName is null) throw new ArgumentNullException(nameof(busName)); @@ -349,13 +378,13 @@ public MessageBusBuilder AddChildBus(string busName, Action b return this; } - public IMessageBusProvider Build() - { + public IMessageBusProvider Build() + { if (BusFactory is null) { var busName = Settings.Name != null ? $"Child bus [{Settings.Name}]: " : string.Empty; throw new ConfigurationMessageBusException($"{busName}The bus provider was not configured. Check the MessageBus configuration and ensure the has the '.WithProviderXxx()' setting for one of the available transports."); } - return BusFactory(Settings); + return BusFactory(Settings); } } diff --git a/src/SlimMessageBus.Host.Configuration/GlobalUsings.cs b/src/SlimMessageBus.Host.Configuration/GlobalUsings.cs index e3b57e70..4612d87f 100644 --- a/src/SlimMessageBus.Host.Configuration/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Configuration/GlobalUsings.cs @@ -1,4 +1,5 @@ -global using System.Reflection; +global using System.Collections; +global using System.Reflection; global using Microsoft.Extensions.DependencyInjection; diff --git a/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs b/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs index d044317e..a04fd491 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs @@ -2,6 +2,11 @@ namespace SlimMessageBus.Host; public abstract class AbstractConsumerSettings : HasProviderExtensions { + /// + /// The settings for the message bus to which the consumer belongs. + /// + public MessageBusSettings MessageBusSettings { get; set; } + /// /// The topic or queue name. /// @@ -18,6 +23,11 @@ public abstract class AbstractConsumerSettings : HasProviderExtensions /// public int Instances { get; set; } + /// + /// to be used with the consumer. + /// + public TypeCollection CircuitBreakers { get; } = []; + protected AbstractConsumerSettings() { Instances = 1; diff --git a/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs b/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs index 218a52aa..965d8c35 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/ConsumerSettings.cs @@ -2,15 +2,15 @@ namespace SlimMessageBus.Host; public class ConsumerSettings : AbstractConsumerSettings, IMessageTypeConsumerInvokerSettings { - private Type messageType; + private Type _messageType; /// public Type MessageType { - get => messageType; + get => _messageType; set { - messageType = value; + _messageType = value; CalculateResponseType(); } } @@ -18,10 +18,11 @@ public Type MessageType private void CalculateResponseType() { // Try to get T from IRequest - ResponseType = messageType.GetInterfaces() + ResponseType = _messageType.GetInterfaces() .SingleOrDefault(i => i.GetTypeInfo().IsGenericType && i.GetTypeInfo().GetGenericTypeDefinition() == typeof(IRequest<>))?.GetGenericArguments()[0]; } + /// /// Type of consumer that is configured (subscriber or request handler). /// public ConsumerMode ConsumerMode { get; set; } diff --git a/src/SlimMessageBus.Host.Configuration/TypeCollection.cs b/src/SlimMessageBus.Host.Configuration/TypeCollection.cs new file mode 100644 index 00000000..04246947 --- /dev/null +++ b/src/SlimMessageBus.Host.Configuration/TypeCollection.cs @@ -0,0 +1,78 @@ +namespace SlimMessageBus.Host; + +public class TypeCollection : IEnumerable where TInterface : class +{ + private readonly Type _interfaceType = typeof(TInterface); + private readonly List _innerList = []; + + public void Add(Type type) + { + if (!_interfaceType.IsAssignableFrom(type)) + { + throw new ArgumentException($"Type is not assignable to '{_interfaceType}'.", nameof(type)); + } + + if (_innerList.Contains(type)) + { + throw new ArgumentException("Type already exists in the collection.", nameof(type)); + } + + _innerList.Add(type); + } + + public void Add() where T : TInterface + { + var type = typeof(T); + if (_innerList.Contains(type)) + { + throw new ArgumentException("Type already exists in the collection.", nameof(type)); // NOSONAR + } + + _innerList.Add(type); + } + + public bool TryAdd() where T : TInterface + { + var type = typeof(T); + if (_innerList.Contains(type)) + { + return false; + } + + _innerList.Add(type); + return true; + } + + public void Clear() => _innerList.Clear(); + + public bool Contains() where T : TInterface + { + return _innerList.Contains(typeof(T)); + } + + public void CopyTo(Type[] array, int arrayIndex) => _innerList.CopyTo(array, arrayIndex); + + public bool Remove() where T : TInterface + { + return _innerList.Remove(typeof(T)); + } + + public bool Remove(Type type) + { + return _innerList.Remove(type); + } + + public int Count => _innerList.Count; + + public bool IsReadOnly => false; + + public IEnumerator GetEnumerator() + { + return _innerList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _innerList.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs index 169b5533..cfb70f0f 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs @@ -1,35 +1,35 @@ -namespace SlimMessageBus.Host.Kafka; +namespace SlimMessageBus.Host.Kafka; using ConsumeResult = ConsumeResult; -using IConsumer = IConsumer; +using IConsumer = IConsumer; -public class KafkaGroupConsumer : AbstractConsumer, IKafkaCommitController -{ +public class KafkaGroupConsumer : AbstractConsumer, IKafkaCommitController +{ private readonly SafeDictionaryWrapper _processors; private IConsumer _consumer; - private Task _consumerTask; - private CancellationTokenSource _consumerCts; - - public KafkaMessageBusSettings ProviderSettings { get; } - public string Group { get; } - public IReadOnlyCollection Topics { get; } - - public KafkaGroupConsumer(ILoggerFactory loggerFactory, KafkaMessageBusSettings providerSettings, string group, IReadOnlyCollection topics, Func processorFactory) - : base(loggerFactory.CreateLogger()) - { + private Task _consumerTask; + private CancellationTokenSource _consumerCts; + + public KafkaMessageBusSettings ProviderSettings { get; } + public string Group { get; } + public IReadOnlyCollection Topics { get; } + + public KafkaGroupConsumer(ILoggerFactory loggerFactory, KafkaMessageBusSettings providerSettings, IEnumerable consumerSettings, string group, IReadOnlyCollection topics, Func processorFactory) + : base(loggerFactory.CreateLogger(), consumerSettings) + { ProviderSettings = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); - Group = group ?? throw new ArgumentNullException(nameof(group)); + Group = group ?? throw new ArgumentNullException(nameof(group)); Topics = topics ?? throw new ArgumentNullException(nameof(topics)); - Logger.LogInformation("Creating for Group: {Group}, Topics: {Topics}", group, string.Join(", ", topics)); - + Logger.LogInformation("Creating for Group: {Group}, Topics: {Topics}", group, string.Join(", ", topics)); + _processors = new SafeDictionaryWrapper(tp => processorFactory(tp, this)); - _consumer = CreateConsumer(group); + _consumer = CreateConsumer(group); } - #region Implementation of IAsyncDisposable + #region Implementation of IAsyncDisposable protected override async ValueTask DisposeAsyncCore() { @@ -51,56 +51,56 @@ protected override async ValueTask DisposeAsyncCore() _consumer = null; } - #endregion - - protected IConsumer CreateConsumer(string group) - { + #endregion + + protected IConsumer CreateConsumer(string group) + { var config = new ConsumerConfig { GroupId = group, - BootstrapServers = ProviderSettings.BrokerList + BootstrapServers = ProviderSettings.BrokerList }; ProviderSettings.ConsumerConfig(config); // ToDo: add support for auto commit - config.EnableAutoCommit = false; - // Notify when we reach EoF, so that we can do a manual commit + config.EnableAutoCommit = false; + // Notify when we reach EoF, so that we can do a manual commit config.EnablePartitionEof = true; - - var consumer = ProviderSettings.ConsumerBuilderFactory(config) + + var consumer = ProviderSettings.ConsumerBuilderFactory(config) .SetStatisticsHandler((_, json) => OnStatistics(json)) - .SetPartitionsAssignedHandler((_, partitions) => OnPartitionAssigned(partitions)) - .SetPartitionsRevokedHandler((_, partitions) => OnPartitionRevoked(partitions)) + .SetPartitionsAssignedHandler((_, partitions) => OnPartitionAssigned(partitions)) + .SetPartitionsRevokedHandler((_, partitions) => OnPartitionRevoked(partitions)) .SetOffsetsCommittedHandler((_, offsets) => OnOffsetsCommitted(offsets)) - .Build(); - - return consumer; + .Build(); + + return consumer; } protected override Task OnStart() { - if (_consumerTask != null) - { - throw new MessageBusException($"Consumer for group {Group} already started"); - } - - _consumerCts = new CancellationTokenSource(); + if (_consumerTask != null) + { + throw new MessageBusException($"Consumer for group {Group} already started"); + } + + _consumerCts = new CancellationTokenSource(); _consumerTask = Task.Factory.StartNew(ConsumerLoop, _consumerCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); return Task.CompletedTask; - } - - /// - /// The consumer group loop - /// - protected async virtual Task ConsumerLoop() + } + + /// + /// The consumer group loop + /// + protected async virtual Task ConsumerLoop() { - Logger.LogInformation("Group [{Group}]: Subscribing to topics: {Topics}", Group, string.Join(", ", Topics)); - _consumer.Subscribe(Topics); - - Logger.LogInformation("Group [{Group}]: Consumer loop started", Group); - try - { + Logger.LogInformation("Group [{Group}]: Subscribing to topics: {Topics}", Group, string.Join(", ", Topics)); + _consumer.Subscribe(Topics); + + Logger.LogInformation("Group [{Group}]: Consumer loop started", Group); + try + { try { for (var cancellationToken = _consumerCts.Token; !cancellationToken.IsCancellationRequested;) @@ -132,8 +132,8 @@ protected async virtual Task ConsumerLoop() } Logger.LogInformation("Group [{Group}]: Unsubscribing from topics", Group); - _consumer.Unsubscribe(); - + _consumer.Unsubscribe(); + if (ProviderSettings.EnableCommitOnBusStop) { OnClose(); @@ -141,117 +141,117 @@ protected async virtual Task ConsumerLoop() // Ensure the consumer leaves the group cleanly and final offsets are committed. _consumer.Close(); - } - catch (Exception e) - { - Logger.LogError(e, "Group [{Group}]: Error occurred in group loop (terminated)", Group); - } - finally - { - Logger.LogInformation("Group [{Group}]: Consumer loop finished", Group); - } - } + } + catch (Exception e) + { + Logger.LogError(e, "Group [{Group}]: Error occurred in group loop (terminated)", Group); + } + finally + { + Logger.LogInformation("Group [{Group}]: Consumer loop finished", Group); + } + } protected override async Task OnStop() { - if (_consumerTask == null) - { - throw new MessageBusException($"Consumer for group {Group} not yet started"); + if (_consumerTask == null) + { + throw new MessageBusException($"Consumer for group {Group} not yet started"); } await _consumerCts.CancelAsync(); - try - { - await _consumerTask.ConfigureAwait(false); - } - finally - { - _consumerTask = null; - - _consumerCts.Dispose(); - _consumerCts = null; + try + { + await _consumerTask.ConfigureAwait(false); + } + finally + { + _consumerTask = null; + + _consumerCts.Dispose(); + _consumerCts = null; } } - - protected virtual void OnPartitionAssigned(ICollection partitions) - { - // Ensure processors exist for each assigned topic-partition + + protected virtual void OnPartitionAssigned(ICollection partitions) + { + // Ensure processors exist for each assigned topic-partition foreach (var partition in partitions) { Logger.LogDebug("Group [{Group}]: Assigned partition, Topic: {Topic}, Partition: {Partition}", Group, partition.Topic, partition.Partition); var processor = _processors[partition]; processor.OnPartitionAssigned(partition); - } - } - - protected virtual void OnPartitionRevoked(ICollection partitions) - { + } + } + + protected virtual void OnPartitionRevoked(ICollection partitions) + { foreach (var partition in partitions) { Logger.LogDebug("Group [{Group}]: Revoked Topic: {Topic}, Partition: {Partition}, Offset: {Offset}", Group, partition.Topic, partition.Partition, partition.Offset); - - var processor = _processors[partition.TopicPartition]; - processor.OnPartitionRevoked(); + + var processor = _processors[partition.TopicPartition]; + processor.OnPartitionRevoked(); } - } - - protected virtual void OnPartitionEndReached(TopicPartitionOffset offset) - { - Logger.LogDebug("Group [{Group}]: Reached end of partition, Topic: {Topic}, Partition: {Partition}, Offset: {Offset}", Group, offset.Topic, offset.Partition, offset.Offset); - - var processor = _processors[offset.TopicPartition]; - processor.OnPartitionEndReached(); - } - - protected async virtual ValueTask OnMessage(ConsumeResult message) - { - Logger.LogDebug("Group [{Group}]: Received message with Topic: {Topic}, Partition: {Partition}, Offset: {Offset}, payload size: {MessageSize}", Group, message.Topic, message.Partition, message.Offset, message.Message.Value?.Length ?? 0); - - var processor = _processors[message.TopicPartition]; - await processor.OnMessage(message).ConfigureAwait(false); - } - - protected internal virtual void OnOffsetsCommitted(CommittedOffsets e) - { - if (e.Error.IsError || e.Error.IsFatal) + } + + protected virtual void OnPartitionEndReached(TopicPartitionOffset offset) + { + Logger.LogDebug("Group [{Group}]: Reached end of partition, Topic: {Topic}, Partition: {Partition}, Offset: {Offset}", Group, offset.Topic, offset.Partition, offset.Offset); + + var processor = _processors[offset.TopicPartition]; + processor.OnPartitionEndReached(); + } + + protected async virtual ValueTask OnMessage(ConsumeResult message) + { + Logger.LogDebug("Group [{Group}]: Received message with Topic: {Topic}, Partition: {Partition}, Offset: {Offset}, payload size: {MessageSize}", Group, message.Topic, message.Partition, message.Offset, message.Message.Value?.Length ?? 0); + + var processor = _processors[message.TopicPartition]; + await processor.OnMessage(message).ConfigureAwait(false); + } + + protected internal virtual void OnOffsetsCommitted(CommittedOffsets e) + { + if (e.Error.IsError || e.Error.IsFatal) { if (Logger.IsEnabled(LogLevel.Warning)) { Logger.LogWarning("Group [{Group}]: Failed to commit offsets: [{Offsets}], error: {ErrorMessage}", Group, string.Join(", ", e.Offsets), e.Error.Reason); } - } - else + } + else { if (Logger.IsEnabled(LogLevel.Debug)) { Logger.LogDebug("Group [{Group}]: Successfully committed offsets: [{Offsets}]", Group, string.Join(", ", e.Offsets)); } - } - } - + } + } + protected virtual void OnClose() { var processors = _processors.Snapshot(); foreach (var processor in processors) { processor.OnClose(); - } - } - - protected virtual void OnStatistics(string json) + } + } + + protected virtual void OnStatistics(string json) + { + Logger.LogTrace("Group [{Group}]: Statistics: {statistics}", Group, json); + } + + #region Implementation of IKafkaCommitController + + public void Commit(TopicPartitionOffset offset) { - Logger.LogTrace("Group [{Group}]: Statistics: {statistics}", Group, json); - } - - #region Implementation of IKafkaCommitController - - public void Commit(TopicPartitionOffset offset) - { Logger.LogDebug("Group [{Group}]: Commit Offset, Topic: {Topic}, Partition: {Partition}, Offset: {Offset}", Group, offset.Topic, offset.Partition, offset.Offset); - _consumer.Commit([offset]); + _consumer.Commit([offset]); } - #endregion + #endregion } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs index 69ce65ea..1c1f68ba 100644 --- a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs +++ b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs @@ -61,10 +61,10 @@ protected override async Task CreateConsumers() var responseConsumerCreated = false; - void AddGroupConsumer(string group, IReadOnlyCollection topics, Func processorFactory) + void AddGroupConsumer(IEnumerable consumerSettings, string group, IReadOnlyCollection topics, Func processorFactory) { _logger.LogInformation("Creating consumer group {ConsumerGroup}", group); - AddConsumer(new KafkaGroupConsumer(LoggerFactory, ProviderSettings, group, topics, processorFactory)); + AddConsumer(new KafkaGroupConsumer(LoggerFactory, ProviderSettings, consumerSettings, group, topics, processorFactory)); } object MessageProvider(Type messageType, ConsumeResult transportMessage) @@ -97,12 +97,12 @@ IKafkaPartitionConsumer ConsumerProcessorFactory(TopicPartition tp, IKafkaCommit responseConsumerCreated = true; } - AddGroupConsumer(group, topics, processorFactory); + AddGroupConsumer(consumersByGroup, group, topics, processorFactory); } if (Settings.RequestResponse != null && !responseConsumerCreated) { - AddGroupConsumer(Settings.RequestResponse.GetGroup(), [Settings.RequestResponse.Path], ResponseProcessorFactory); + AddGroupConsumer([Settings.RequestResponse], Settings.RequestResponse.GetGroup(), new[] { Settings.RequestResponse.Path }, ResponseProcessorFactory); } } diff --git a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs index 24cc1cb8..102ee6d5 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs @@ -52,10 +52,10 @@ protected override async Task CreateConsumers() object MessageProvider(Type messageType, MqttApplicationMessage transportMessage) => Serializer.Deserialize(messageType, transportMessage.PayloadSegment.Array); - void AddTopicConsumer(string topic, IMessageProcessor messageProcessor) + void AddTopicConsumer(IEnumerable consumerSettings, string topic, IMessageProcessor messageProcessor) { _logger.LogInformation("Creating consumer for {Path}", topic); - var consumer = new MqttTopicConsumer(LoggerFactory.CreateLogger(), topic, messageProcessor); + var consumer = new MqttTopicConsumer(LoggerFactory.CreateLogger(), consumerSettings, topic, messageProcessor); AddConsumer(consumer); } @@ -69,7 +69,7 @@ void AddTopicConsumer(string topic, IMessageProcessor me responseProducer: this, consumerErrorHandlerOpenGenericType: typeof(IMqttConsumerErrorHandler<>)); - AddTopicConsumer(path, processor); + AddTopicConsumer(consumerSettings, path, processor); } if (Settings.RequestResponse != null) @@ -81,7 +81,7 @@ void AddTopicConsumer(string topic, IMessageProcessor me PendingRequestStore, CurrentTimeProvider); - AddTopicConsumer(Settings.RequestResponse.Path, processor); + AddTopicConsumer([Settings.RequestResponse], Settings.RequestResponse.Path, processor); } var topics = Consumers.Cast().Select(x => new MqttTopicFilterBuilder().WithTopic(x.Topic).Build()).ToList(); @@ -164,4 +164,4 @@ public override async Task ProduceToTransport(object message, Type messageType, throw new ProducerMessageBusException(GetProducerErrorMessage(path, message, messageType, ex), ex); } } -} +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs b/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs index f327bd21..7720782c 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs @@ -5,7 +5,8 @@ public class MqttTopicConsumer : AbstractConsumer public IMessageProcessor MessageProcessor { get; } public string Topic { get; } - public MqttTopicConsumer(ILogger logger, string topic, IMessageProcessor messageProcessor) : base(logger) + public MqttTopicConsumer(ILogger logger, IEnumerable consumerSettings, string topic, IMessageProcessor messageProcessor) + : base(logger, consumerSettings) { Topic = topic; MessageProcessor = messageProcessor; diff --git a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs index 3ea6254a..ec81318e 100644 --- a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs +++ b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs @@ -1,65 +1,62 @@ namespace SlimMessageBus.Host.Nats; -using System.Collections.Generic; -using System.Threading; - -using Microsoft.Extensions.Primitives; - -public class NatsMessageBus : MessageBusBase -{ - private readonly ILogger _logger; - private NatsConnection _connection; - - public NatsMessageBus(MessageBusSettings settings, NatsMessageBusSettings providerSettings) : base(settings, providerSettings) - { - _logger = LoggerFactory.CreateLogger(); - OnBuildProvider(); - } - - protected override IMessageBusSettingsValidationService ValidationService => new NatsMessageBusSettingsValidationService(Settings, ProviderSettings); - - public bool IsConnected => _connection is { ConnectionState: NatsConnectionState.Open }; - - protected override void Build() - { - base.Build(); - InitTaskList.Add(CreateConnectionAsync, CancellationToken); - } - - private Task CreateConnectionAsync() - { - try - { - _connection = new NatsConnection(new NatsOpts - { - Url = ProviderSettings.Endpoint, - LoggerFactory = LoggerFactory, - AuthOpts = ProviderSettings.AuthOpts, - Verbose = false, - Name = ProviderSettings.ClientName - }); - } - catch (Exception e) - { - _logger.LogError(e, "Could not initialize Nats connection: {ErrorMessage}", e.Message); - } - - return Task.CompletedTask; - } - - protected override async Task CreateConsumers() - { - if (_connection == null) - { - throw new ConsumerMessageBusException("The connection is not available at this time"); +using Microsoft.Extensions.Primitives; + +public class NatsMessageBus : MessageBusBase +{ + private readonly ILogger _logger; + private NatsConnection _connection; + + public NatsMessageBus(MessageBusSettings settings, NatsMessageBusSettings providerSettings) : base(settings, providerSettings) + { + _logger = LoggerFactory.CreateLogger(); + OnBuildProvider(); + } + + protected override IMessageBusSettingsValidationService ValidationService => new NatsMessageBusSettingsValidationService(Settings, ProviderSettings); + + public bool IsConnected => _connection is { ConnectionState: NatsConnectionState.Open }; + + protected override void Build() + { + base.Build(); + InitTaskList.Add(CreateConnectionAsync, CancellationToken); + } + + private Task CreateConnectionAsync() + { + try + { + _connection = new NatsConnection(new NatsOpts + { + Url = ProviderSettings.Endpoint, + LoggerFactory = LoggerFactory, + AuthOpts = ProviderSettings.AuthOpts, + Verbose = false, + Name = ProviderSettings.ClientName + }); + } + catch (Exception e) + { + _logger.LogError(e, "Could not initialize Nats connection: {ErrorMessage}", e.Message); + } + + return Task.CompletedTask; + } + + protected override async Task CreateConsumers() + { + if (_connection == null) + { + throw new ConsumerMessageBusException("The connection is not available at this time"); } await base.CreateConsumers(); object MessageProvider(Type messageType, NatsMsg transportMessage) => Serializer.Deserialize(messageType, transportMessage.Data); - foreach (var (subject, consumerSettings) in Settings.Consumers.GroupBy(x => x.Path).ToDictionary(x => x.Key, x => x.ToList())) - { + foreach (var (subject, consumerSettings) in Settings.Consumers.GroupBy(x => x.Path).ToDictionary(x => x.Key, x => x.ToList())) + { var processor = new MessageProcessor>( consumerSettings, messageBus: this, @@ -67,34 +64,34 @@ protected override async Task CreateConsumers() subject, this, consumerErrorHandlerOpenGenericType: typeof(INatsConsumerErrorHandler<>)); - - AddSubjectConsumer(subject, processor); - } - - if (Settings.RequestResponse != null) - { - var processor = new ResponseMessageProcessor>(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider); - AddSubjectConsumer(Settings.RequestResponse.Path, processor); - } - } - - private void AddSubjectConsumer(string subject, IMessageProcessor> processor) - { - _logger.LogInformation("Creating consumer for {Subject}", subject); - var consumer = new NatsSubjectConsumer(LoggerFactory.CreateLogger>(), subject, _connection, processor); - AddConsumer(consumer); - } - - protected override async ValueTask DisposeAsyncCore() - { - await base.DisposeAsyncCore().ConfigureAwait(false); - - if (_connection != null) - { - await _connection.DisposeAsync(); - _connection = null; - } - } + + AddSubjectConsumer(consumerSettings, subject, processor); + } + + if (Settings.RequestResponse != null) + { + var processor = new ResponseMessageProcessor>(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider); + AddSubjectConsumer([], Settings.RequestResponse.Path, processor); + } + } + + private void AddSubjectConsumer(IEnumerable consumerSettings, string subject, IMessageProcessor> processor) + { + _logger.LogInformation("Creating consumer for {Subject}", subject); + var consumer = new NatsSubjectConsumer(LoggerFactory.CreateLogger>(), consumerSettings, subject, _connection, processor); + AddConsumer(consumer); + } + + protected override async ValueTask DisposeAsyncCore() + { + await base.DisposeAsyncCore().ConfigureAwait(false); + + if (_connection != null) + { + await _connection.DisposeAsync(); + _connection = null; + } + } public override async Task ProduceToTransport(object message, Type messageType, string path, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { diff --git a/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs b/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs index bebc502f..43a9f2cf 100644 --- a/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs +++ b/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs @@ -1,30 +1,43 @@ -#nullable enable -namespace SlimMessageBus.Host.Nats; - -public class NatsSubjectConsumer(ILogger logger, string subject, INatsConnection connection, IMessageProcessor> messageProcessor) : AbstractConsumer(logger) -{ - private INatsSub? _subscription; - private Task? _messageConsumerTask; - - protected override async Task OnStart() - { - _subscription ??= await connection.SubscribeCoreAsync(subject, cancellationToken: CancellationToken); - - _messageConsumerTask = Task.Factory.StartNew(OnLoop, CancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); - } - - protected override async Task OnStop() - { - if (_messageConsumerTask != null) - { - await _messageConsumerTask.ConfigureAwait(false); - } - - if (_subscription != null) - { - await _subscription.UnsubscribeAsync().ConfigureAwait(false); - await _subscription.DisposeAsync(); - } +#nullable enable +namespace SlimMessageBus.Host.Nats; + +using System.Collections.Generic; + +public class NatsSubjectConsumer : AbstractConsumer +{ + private readonly string _subject; + private readonly INatsConnection _connection; + private readonly IMessageProcessor> _messageProcessor; + private INatsSub? _subscription; + private Task? _messageConsumerTask; + + public NatsSubjectConsumer(ILogger logger, IEnumerable consumerSettings, string subject, INatsConnection connection, IMessageProcessor> messageProcessor) + : base(logger, consumerSettings) + { + _subject = subject; + _connection = connection; + _messageProcessor = messageProcessor; + } + + protected override async Task OnStart() + { + _subscription ??= await _connection.SubscribeCoreAsync(_subject, cancellationToken: CancellationToken); + + _messageConsumerTask = Task.Factory.StartNew(OnLoop, CancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); + } + + protected override async Task OnStop() + { + if (_messageConsumerTask != null) + { + await _messageConsumerTask.ConfigureAwait(false); + } + + if (_subscription != null) + { + await _subscription.UnsubscribeAsync().ConfigureAwait(false); + await _subscription.DisposeAsync(); + } } private async Task OnLoop() @@ -35,13 +48,13 @@ private async Task OnLoop() { while (_subscription.Msgs.TryRead(out var msg)) { - await messageProcessor.ProcessMessage(msg, msg.Headers.ToReadOnlyDictionary(), cancellationToken: CancellationToken).ConfigureAwait(false); + await _messageProcessor.ProcessMessage(msg, msg.Headers.ToReadOnlyDictionary(), cancellationToken: CancellationToken).ConfigureAwait(false); } } } catch (OperationCanceledException ex) { Logger.LogInformation(ex, "Consumer task was cancelled"); - } - } + } + } } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs index 10161c51..47e4e630 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs @@ -13,8 +13,8 @@ public abstract class AbstractRabbitMqConsumer : AbstractConsumer public string QueueName { get; } protected abstract RabbitMqMessageAcknowledgementMode AcknowledgementMode { get; } - protected AbstractRabbitMqConsumer(ILogger logger, IRabbitMqChannel channel, string queueName, IHeaderValueConverter headerValueConverter) - : base(logger) + protected AbstractRabbitMqConsumer(ILogger logger, IEnumerable consumerSettings, IRabbitMqChannel channel, string queueName, IHeaderValueConverter headerValueConverter) + : base(logger, consumerSettings) { _channel = channel; _headerValueConverter = headerValueConverter; diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs index 34150846..e9018b58 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs @@ -24,7 +24,7 @@ public RabbitMqConsumer( MessageBusBase messageBus, MessageProvider messageProvider, IHeaderValueConverter headerValueConverter) - : base(loggerFactory.CreateLogger(), channel, queueName, headerValueConverter) + : base(loggerFactory.CreateLogger(), consumers, channel, queueName, headerValueConverter) { _acknowledgementMode = consumers.Select(x => x.GetOrDefault(RabbitMqProperties.MessageAcknowledgementMode, messageBus.Settings)).FirstOrDefault(x => x != null) ?? RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade; // be default choose the safer acknowledgement mode diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs index 96b1c250..ca63dc73 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqResponseConsumer.cs @@ -15,7 +15,7 @@ public RabbitMqResponseConsumer( IPendingRequestStore pendingRequestStore, ICurrentTimeProvider currentTimeProvider, IHeaderValueConverter headerValueConverter) - : base(loggerFactory.CreateLogger(), channel, queueName, headerValueConverter) + : base(loggerFactory.CreateLogger(), [requestResponseSettings], channel, queueName, headerValueConverter) { _messageProcessor = new ResponseMessageProcessor(loggerFactory, requestResponseSettings, messageProvider, pendingRequestStore, currentTimeProvider); } diff --git a/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs b/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs index f178cd80..8105bb1b 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs @@ -24,7 +24,7 @@ public QueueProcessors(string name, List> } public RedisListCheckerConsumer(ILogger logger, IDatabase database, TimeSpan? pollDelay, TimeSpan maxIdle, IEnumerable<(string QueueName, IMessageProcessor Processor)> queues, IMessageSerializer envelopeSerializer) - : base(logger) + : base(logger, []) { _database = database; _pollDelay = pollDelay; diff --git a/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs b/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs index 77204dff..ad79f383 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs @@ -9,8 +9,8 @@ public class RedisTopicConsumer : AbstractConsumer, IRedisConsumer public string Path { get; } - public RedisTopicConsumer(ILogger logger, string topic, ISubscriber subscriber, IMessageProcessor messageProcessor, IMessageSerializer envelopeSerializer) - : base(logger) + public RedisTopicConsumer(ILogger logger, IEnumerable consumerSettings, string topic, ISubscriber subscriber, IMessageProcessor messageProcessor, IMessageSerializer envelopeSerializer) + : base(logger, consumerSettings) { Path = topic; _messageProcessor = messageProcessor; diff --git a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs index 773f89eb..1802eb3a 100644 --- a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs +++ b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs @@ -90,10 +90,11 @@ protected override async Task CreateConsumers() object MessageProvider(Type messageType, MessageWithHeaders transportMessage) => Serializer.Deserialize(messageType, transportMessage.Payload); - void AddTopicConsumer(string topic, ISubscriber subscriber, IMessageProcessor messageProcessor) + void AddTopicConsumer(IEnumerable consumerSettings, string topic, ISubscriber subscriber, IMessageProcessor messageProcessor) { var consumer = new RedisTopicConsumer( LoggerFactory.CreateLogger(), + consumerSettings, topic, subscriber, messageProcessor, @@ -128,7 +129,7 @@ void AddTopicConsumer(string topic, ISubscriber subscriber, IMessageProcessor(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider)); + AddTopicConsumer([Settings.RequestResponse], Settings.RequestResponse.Path, subscriber, new ResponseMessageProcessor(LoggerFactory, Settings.RequestResponse, MessageProvider, PendingRequestStore, CurrentTimeProvider)); } else { diff --git a/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs b/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs index 7ea8acab..28745d69 100644 --- a/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs +++ b/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs @@ -2,63 +2,122 @@ public abstract class AbstractConsumer : IAsyncDisposable, IConsumerControl { + private readonly SemaphoreSlim _semaphore; + private readonly List _circuitBreakers; + private CancellationTokenSource _cancellationTokenSource; private bool _starting; private bool _stopping; - protected ILogger Logger { get; } - + public bool IsPaused { get; private set; } public bool IsStarted { get; private set; } - + protected ILogger Logger { get; } + protected IReadOnlyList Settings { get; } protected CancellationToken CancellationToken => _cancellationTokenSource.Token; - protected AbstractConsumer(ILogger logger) => Logger = logger; + protected AbstractConsumer(ILogger logger, IEnumerable consumerSettings) + { + _semaphore = new(1, 1); + _circuitBreakers = []; + + Logger = logger; + Settings = consumerSettings.ToList(); + } public async Task Start() { + async Task StartCircuitBreakers() + { + var types = Settings.SelectMany(x => x.CircuitBreakers).Distinct(); + if (!types.Any()) + { + return; + } + + var sp = Settings.Select(x => x.MessageBusSettings.ServiceProvider).FirstOrDefault(x => x != null); + foreach (var type in types.Distinct()) + { + var breaker = (IConsumerCircuitBreaker)ActivatorUtilities.CreateInstance(sp, type, Settings); + _circuitBreakers.Add(breaker); + await breaker.Subscribe(BreakerChanged); + } + } + if (IsStarted || _starting) { return; } + await _semaphore.WaitAsync(); _starting = true; try { - if (_cancellationTokenSource == null || _cancellationTokenSource.IsCancellationRequested) + if (_cancellationTokenSource?.IsCancellationRequested != false) { _cancellationTokenSource?.Dispose(); _cancellationTokenSource = new CancellationTokenSource(); } - await OnStart().ConfigureAwait(false); + await StartCircuitBreakers(); + IsPaused = _circuitBreakers.Exists(x => x.State == Circuit.Closed); + if (!IsPaused) + { + await OnStart().ConfigureAwait(false); + } IsStarted = true; } finally { _starting = false; + _semaphore.Release(); } } public async Task Stop() { + async Task StopCircuitBreakers() + { + foreach (var breaker in _circuitBreakers) + { + breaker.Unsubscribe(); + + if (breaker is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else if (breaker is IDisposable disposable) + { + disposable.Dispose(); + } + } + + _circuitBreakers.Clear(); + } + if (!IsStarted || _stopping) { return; } + await _semaphore.WaitAsync(); _stopping = true; try { await _cancellationTokenSource.CancelAsync(); - await OnStop().ConfigureAwait(false); + await StopCircuitBreakers(); + if (!IsPaused) + { + await OnStop().ConfigureAwait(false); + } IsStarted = false; } finally { _stopping = false; + _semaphore.Release(); } } @@ -82,4 +141,40 @@ protected async virtual ValueTask DisposeAsyncCore() } #endregion + + async internal Task BreakerChanged(Circuit state) + { + await _semaphore.WaitAsync(); + try + { + if (!IsStarted) + { + return; + } + + var shouldPause = state == Circuit.Closed || _circuitBreakers.Exists(x => x.State == Circuit.Closed); + if (shouldPause != IsPaused) + { + var settings = Settings.Count > 0 ? Settings[0] : null; + var path = settings?.Path ?? "[unknown path]"; + var bus = settings?.MessageBusSettings?.Name ?? "default"; + if (shouldPause) + { + Logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); + await OnStop().ConfigureAwait(false); + } + else + { + Logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); + await OnStart().ConfigureAwait(false); + } + + IsPaused = shouldPause; + } + } + finally + { + _semaphore.Release(); + } + } } diff --git a/src/SlimMessageBus.Host/IConsumerControl.cs b/src/SlimMessageBus.Host/IConsumerControl.cs index 451ccd19..eee96a7f 100644 --- a/src/SlimMessageBus.Host/IConsumerControl.cs +++ b/src/SlimMessageBus.Host/IConsumerControl.cs @@ -9,7 +9,7 @@ public interface IConsumerControl Task Start(); /// - /// Indicated whether the consumers are started. + /// Indicates whether the consumers are started. /// bool IsStarted { get; } diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index f6d03ace..7c230bbb 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -253,6 +253,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{1A71BB05 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecretStore.Test", "Tests\SecretStore.Test\SecretStore.Test.csproj", "{969AAB37-AEFC-40F9-9F89-B4B5E45E13C9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CircuitBreakers", "CircuitBreakers", "{FE36338C-0DA2-499E-92CA-F9D5CE594B9F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.CircuitBreaker.HealthCheck", "SlimMessageBus.Host.CircuitBreaker.HealthCheck\SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj", "{B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test", "Tests\SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test\SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj", "{CA02D82E-DACC-4AB5-BD6B-071341E9E664}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.CircuitBreaker.HealthCheck", "Samples\Sample.CircuitBreaker.HealthCheck\Sample.CircuitBreaker.HealthCheck.csproj", "{226FC4F3-01EF-4214-9566-942CA0FB71B0}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.Sql.Test", "Tests\SlimMessageBus.Host.Outbox.Sql.Test\SlimMessageBus.Host.Outbox.Sql.Test.csproj", "{CDF578D6-FE85-4A44-A99A-32490F047FDA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Nats", "SlimMessageBus.Host.Nats\SlimMessageBus.Host.Nats.csproj", "{57290E47-603D-46D0-BF13-AC1D6481380A}" @@ -802,6 +810,30 @@ Global {969AAB37-AEFC-40F9-9F89-B4B5E45E13C9}.Release|Any CPU.Build.0 = Release|Any CPU {969AAB37-AEFC-40F9-9F89-B4B5E45E13C9}.Release|x86.ActiveCfg = Release|Any CPU {969AAB37-AEFC-40F9-9F89-B4B5E45E13C9}.Release|x86.Build.0 = Release|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Debug|x86.Build.0 = Debug|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Release|Any CPU.Build.0 = Release|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Release|x86.ActiveCfg = Release|Any CPU + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8}.Release|x86.Build.0 = Release|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Debug|x86.Build.0 = Debug|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Release|Any CPU.Build.0 = Release|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Release|x86.ActiveCfg = Release|Any CPU + {CA02D82E-DACC-4AB5-BD6B-071341E9E664}.Release|x86.Build.0 = Release|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Debug|x86.Build.0 = Debug|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Release|Any CPU.Build.0 = Release|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Release|x86.ActiveCfg = Release|Any CPU + {226FC4F3-01EF-4214-9566-942CA0FB71B0}.Release|x86.Build.0 = Release|Any CPU {CDF578D6-FE85-4A44-A99A-32490F047FDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CDF578D6-FE85-4A44-A99A-32490F047FDA}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDF578D6-FE85-4A44-A99A-32490F047FDA}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -938,6 +970,10 @@ Global {137BFD32-CD0A-47CA-8884-209CD49DEE8C} = {0F4AD1B7-157D-4ABC-A379-68BF207F2FC3} {1A71BB05-58ED-4B27-B4A4-A03D9E608C1C} = {0F4AD1B7-157D-4ABC-A379-68BF207F2FC3} {969AAB37-AEFC-40F9-9F89-B4B5E45E13C9} = {D3D6FD9A-968A-45BB-86C7-4527C72A057E} + {FE36338C-0DA2-499E-92CA-F9D5CE594B9F} = {75BDDBB5-8DB8-4893-BD89-8FFC6C42244D} + {B71D4F74-B1D9-47A8-8DC1-C7D6A56DC6A8} = {FE36338C-0DA2-499E-92CA-F9D5CE594B9F} + {CA02D82E-DACC-4AB5-BD6B-071341E9E664} = {9F005B5C-A856-4351-8C0C-47A8B785C637} + {226FC4F3-01EF-4214-9566-942CA0FB71B0} = {A5B15524-93B8-4CCE-AC6D-A22984498BA0} {CDF578D6-FE85-4A44-A99A-32490F047FDA} = {9F005B5C-A856-4351-8C0C-47A8B785C637} {57290E47-603D-46D0-BF13-AC1D6481380A} = {9291D340-B4FA-44A3-8060-C14743FB1712} {9C464F95-B620-4BDF-B9AC-D95C465D9793} = {9F005B5C-A856-4351-8C0C-47A8B785C637} diff --git a/src/SlimMessageBus/IConsumerCircuitBreaker.cs b/src/SlimMessageBus/IConsumerCircuitBreaker.cs new file mode 100644 index 00000000..27154566 --- /dev/null +++ b/src/SlimMessageBus/IConsumerCircuitBreaker.cs @@ -0,0 +1,17 @@ +namespace SlimMessageBus; + +/// +/// Circuit breaker to toggle consumer status on an external event. +/// +public interface IConsumerCircuitBreaker +{ + Circuit State { get; } + Task Subscribe(Func onChange); + void Unsubscribe(); +} + +public enum Circuit +{ + Open, + Closed +} diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs new file mode 100644 index 00000000..9434d91d --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs @@ -0,0 +1,12 @@ +global using System; +global using System.Collections.Generic; +global using System.Threading.Tasks; + +global using FluentAssertions; + +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Logging.Abstractions; + +global using Moq; + +global using Xunit; diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs new file mode 100644 index 00000000..35986e86 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs @@ -0,0 +1,323 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test; + +public static class HealthCheckBackgroundServiceTests +{ + public class AreEqualTests + { + [Fact] + public void AreEqual_ShouldReturnTrue_WhenBothDictionariesAreEmpty() + { + // Arrange + var dict1 = new Dictionary(); + var dict2 = new Dictionary(); + + // Act + var result = HealthCheckBackgroundService.AreEqual(dict1, dict2); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void AreEqual_ShouldReturnFalse_WhenDictionariesHaveDifferentCounts() + { + // Arrange + var dict1 = new Dictionary { { "key1", 1 } }; + var dict2 = new Dictionary(); + + // Act + var result = HealthCheckBackgroundService.AreEqual(dict1, dict2); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void AreEqual_ShouldReturnFalse_WhenDictionariesHaveDifferentKeys() + { + // Arrange + var dict1 = new Dictionary { { "key1", 1 } }; + var dict2 = new Dictionary { { "key2", 1 } }; + + // Act + var result = HealthCheckBackgroundService.AreEqual(dict1, dict2); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void AreEqual_ShouldReturnFalse_WhenDictionariesHaveDifferentValues() + { + // Arrange + var dict1 = new Dictionary { { "key1", 1 } }; + var dict2 = new Dictionary { { "key1", 2 } }; + + // Act + var result = HealthCheckBackgroundService.AreEqual(dict1, dict2); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void AreEqual_ShouldReturnTrue_WhenDictionariesHaveSameKeysAndValues() + { + // Arrange + var dict1 = new Dictionary { { "key1", 1 } }; + var dict2 = new Dictionary { { "key1", 1 } }; + + // Act + var result = HealthCheckBackgroundService.AreEqual(dict1, dict2); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void AreEqual_ShouldReturnTrue_WhenDictionariesAreComplexButEqual() + { + // Arrange + var dict1 = new Dictionary + { + { "key1", 1 }, + { "key2", 2 }, + { "key3", 3 } + }; + var dict2 = new Dictionary + { + { "key1", 1 }, + { "key2", 2 }, + { "key3", 3 } + }; + + // Act + var result = HealthCheckBackgroundService.AreEqual(dict1, dict2); + + // Assert + result.Should().BeTrue(); + } + } + + public class PublishAsyncTests + { + [Fact] + public async Task PublishAsync_ShouldUpdateEntries_WhenServiceIsActive() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, tags: ["tag1"]) }, + { "check2", new HealthReportEntry(HealthStatus.Degraded, "Degraded", TimeSpan.Zero, null, null, tags: ["tag2"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + + // Act + await target.PublishAsync(report, CancellationToken.None); + + // Assert + var expectedTagStatus = new Dictionary + { + { "tag1", HealthStatus.Healthy }, + { "tag2", HealthStatus.Degraded } + }; + + target.TagStatus.Should().BeEquivalentTo(expectedTagStatus); + } + + [Fact] + public async Task PublishAsync_ShouldSetTagStatusToUnhealthy_WhenConflictingHealthStatusOccurs() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["sharedTag"]) }, + { "check2", new HealthReportEntry(HealthStatus.Unhealthy, "Unhealthy", TimeSpan.Zero, null, null, ["sharedTag"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + + // Act + await target.PublishAsync(report, CancellationToken.None); + + // Assert + var tagStatus = target.TagStatus; + tagStatus.Should().ContainKey("sharedTag").WhoseValue.Should().Be(HealthStatus.Unhealthy); + } + + [Fact] + public async Task PublishAsync_ShouldSetTagStatusToUnhealthy_WhenOneIsDegradedAndOtherIsUnHealthy() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Degraded, "Degraded", TimeSpan.Zero, null, null, ["sharedTag"]) }, + { "check2", new HealthReportEntry(HealthStatus.Unhealthy, "UnHealthy", TimeSpan.Zero, null, null, ["sharedTag"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + + // Act + await target.PublishAsync(report, CancellationToken.None); + + // Assert + var tagStatus = target.TagStatus; + tagStatus.Should().ContainKey("sharedTag").WhoseValue.Should().Be(HealthStatus.Unhealthy); + } + + [Fact] + public async Task PublishAsync_ShouldSetTagStatusToDegraded_WhenOneIsDegradedAndOtherIsHealthy_AndNoUnhealthyExists() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Degraded, "Degraded", TimeSpan.Zero, null, null, ["sharedTag"]) }, + { "check2", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["sharedTag"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + + // Act + await target.PublishAsync(report, CancellationToken.None); + + // Assert + var tagStatus = target.TagStatus; + tagStatus.Should().ContainKey("sharedTag").WhoseValue.Should().Be(HealthStatus.Degraded); + } + + [Fact] + public async Task PublishAsync_ShouldNotChangeTagStatus_WhenConflictingHealthStatusIsSame() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["sharedTag"]) }, + { "check2", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["sharedTag"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + + // Act + await target.PublishAsync(report, CancellationToken.None); + + // Assert + var tagStatus = target.TagStatus; + tagStatus.Should().ContainKey("sharedTag").WhoseValue.Should().Be(HealthStatus.Healthy); + } + + [Fact] + public async Task PublishAsync_ShouldNotCallDelegates_IfTagStatusHasNotChanged() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var delegateCalled = false; + await target.Subscribe( + _ => + { + delegateCalled = true; + return Task.CompletedTask; + }); + + var initialHealthReport = new HealthReport( + new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["tag1"]) } + }, + TimeSpan.Zero); + + await target.PublishAsync(initialHealthReport, CancellationToken.None); + delegateCalled = false; + + var unchangedHealthReport = new HealthReport( + new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["tag1"]) }, + { "check2", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["tag1"]) } + }, + TimeSpan.Zero); + + // Act + await target.PublishAsync(unchangedHealthReport, CancellationToken.None); + + // Assert + delegateCalled.Should().BeFalse(); + } + } + + public class SubscribeTests + { + [Fact] + public async Task Subscribe_ShouldInvokeDelegateImmediatelyWithCurrentStatus() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + await target.StartAsync(CancellationToken.None); + + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["tag1"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + await target.PublishAsync(report, CancellationToken.None); + + IReadOnlyDictionary? capturedStatus = null; + Task OnChange(IReadOnlyDictionary status) + { + capturedStatus = status; + return Task.CompletedTask; + } + + // Act + await target.Subscribe(OnChange); + + // Assert + capturedStatus.Should().NotBeNull(); + capturedStatus.Should().ContainKey("tag1").WhoseValue.Should().Be(HealthStatus.Healthy); + } + } + + public class UnsubscribeTests + { + [Fact] + public async Task Unsubscribe_ShouldRemoveDelegate() + { + // Arrange + using var target = new HealthCheckBackgroundService(); + var entries = new Dictionary + { + { "check1", new HealthReportEntry(HealthStatus.Healthy, "Healthy", TimeSpan.Zero, null, null, ["tag1"]) } + }; + var report = new HealthReport(entries, TimeSpan.Zero); + + IReadOnlyDictionary? capturedStatus = null; + Task OnChange(IReadOnlyDictionary status) + { + capturedStatus = status; + return Task.CompletedTask; + } + + // Act + await target.Subscribe(OnChange); + capturedStatus = null; + + target.Unsubscribe(OnChange); + await target.PublishAsync(report, CancellationToken.None); + + // Assert + capturedStatus.Should().BeNull(); + } + } +} diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs new file mode 100644 index 00000000..48499879 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs @@ -0,0 +1,184 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test; +public class HealthCheckCircuitBreakerTests +{ + private readonly Mock _hostMock; + private readonly HealthCheckCircuitBreaker _circuitBreaker; + private readonly TestConsumerSettings _testConsumerSettings; + private readonly TestConsumerSettings _testConsumerSettings2; + + public HealthCheckCircuitBreakerTests() + { + _testConsumerSettings = new TestConsumerSettings(); + _testConsumerSettings2 = new TestConsumerSettings(); + + _hostMock = new Mock(); + var _settings = new List + { + _testConsumerSettings, + _testConsumerSettings2 + }; + + _circuitBreaker = new HealthCheckCircuitBreaker( + _settings, + _hostMock.Object); + } + + [Fact] + public void Constructor_ShouldInitializeOpenState() + { + // assert + _circuitBreaker.State.Should().Be(Circuit.Open); + } + + [Fact] + public async Task Subscribe_ShouldSetOnChangeAndSubscribeToHost() + { + // arrange + static Task onChange(Circuit _) => Task.CompletedTask; + + // act + await _circuitBreaker.Subscribe(onChange); + + // assert + _hostMock.Verify(h => h.Subscribe(It.IsAny()), Times.Once); + _circuitBreaker.State.Should().Be(Circuit.Open); + } + + [Fact] + public async Task Subscribe_ShouldMergeTagsFromAllSettings() + { + const string degradedTag = "degraded"; + const string unhealthyTag = "unhealthy"; + + _testConsumerSettings.PauseOnUnhealthy(unhealthyTag); + _testConsumerSettings2.PauseOnDegraded(degradedTag); + + var expected = new Dictionary() + { + { unhealthyTag, HealthStatus.Unhealthy }, + { degradedTag, HealthStatus.Degraded } + }; + + // arrange + static Task onChange(Circuit _) => Task.CompletedTask; + await _circuitBreaker.Subscribe(onChange); + + // act + var actual = typeof(HealthCheckCircuitBreaker) + .GetField("_monitoredTags", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(_circuitBreaker) as IDictionary; + + // assert + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Subscribe_MergedTagsOfDifferentSeverity_ShouldUseLeastSevereCondition() + { + const string tag = "tag"; + _testConsumerSettings.PauseOnUnhealthy(tag); + _testConsumerSettings2.PauseOnDegraded(tag); + + var expected = new Dictionary() + { + { tag, HealthStatus.Degraded }, + }; + + // arrange + static Task onChange(Circuit _) => Task.CompletedTask; + await _circuitBreaker.Subscribe(onChange); + + // act + var actual = typeof(HealthCheckCircuitBreaker) + .GetField("_monitoredTags", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(_circuitBreaker) as IDictionary; + + // assert + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task TagStatusChanged_ShouldChangeStateToClosed_WhenUnhealthyTagIsUnhealthy() + { + // arrange + const string tag = "tag"; + _testConsumerSettings.PauseOnUnhealthy(tag); + + static Task onChange(Circuit _) => Task.CompletedTask; + await _circuitBreaker.Subscribe(onChange); + + var tags = new Dictionary + { + { tag, HealthStatus.Unhealthy } + }; + + // act + await _circuitBreaker.TagStatusChanged(tags); + + // assert + _circuitBreaker.State.Should().Be(Circuit.Closed); + } + + [Fact] + public async Task TagStatusChanged_ShouldChangeStateToClosed_WhenDegradedTagIsUnhealthy() + { + // arrange + const string tag = "tag"; + _testConsumerSettings.PauseOnDegraded(tag); + + static Task onChange(Circuit _) => Task.CompletedTask; + await _circuitBreaker.Subscribe(onChange); + + var tags = new Dictionary + { + { tag, HealthStatus.Unhealthy } + }; + + // act + await _circuitBreaker.TagStatusChanged(tags); + + // assert + _circuitBreaker.State.Should().Be(Circuit.Closed); + } + + [Fact] + public async Task TagStatusChanged_ShouldRemainOpen_WhenUnmonitoredTagsAreUnhealthyOrDegraded() + { + // arrange + _testConsumerSettings.PauseOnUnhealthy("tag1", "tag2"); + + Func onChange = _ => Task.CompletedTask; + await _circuitBreaker.Subscribe(onChange); + + var tags = new Dictionary + { + { "tag1", HealthStatus.Healthy }, + { "tag2", HealthStatus.Degraded }, + { "unmonitored1", HealthStatus.Unhealthy }, + { "unmonitored2", HealthStatus.Degraded } + }; + + // act + await _circuitBreaker.TagStatusChanged(tags); + + // assert + _circuitBreaker.State.Should().Be(Circuit.Open); + } + + [Fact] + public void Unsubscribe_ShouldUnsubscribeFromHostAndClearOnChange() + { + // act + _circuitBreaker.Unsubscribe(); + + // assert + _hostMock.Verify(h => h.Unsubscribe(It.IsAny()), Times.Once); + _circuitBreaker.State.Should().Be(Circuit.Open); + } + + public class TestConsumerSettings : AbstractConsumerSettings + { + } +} diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj new file mode 100644 index 00000000..909dcfbe --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj @@ -0,0 +1,23 @@ + + + + + + enable + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/xunit.runner.json b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/xunit.runner.json new file mode 100644 index 00000000..4e80a853 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "diagnosticMessages": true, + "longRunningTestSeconds": 120 +} \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs new file mode 100644 index 00000000..0a8ff343 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs @@ -0,0 +1,217 @@ +namespace SlimMessageBus.Host.Configuration.Test; + +public class TypeCollectionTests +{ + [Fact] + public void Add_Should_AddTypeToCollection_IfAssignableToGeneric() + { + // Arrange + var collection = new TypeCollection(); + + // Act + collection.Add(typeof(SampleClass)); + + // Assert + collection.Count.Should().Be(1); + collection.Should().Contain(typeof(SampleClass)); + } + + [Fact] + public void Add_Should_ThrowException_IfNotAssignableToGeneric() + { + // Arrange + var collection = new TypeCollection(); + + // Act + var act = () => collection.Add(typeof(object)); + + // Assert + act.Should().Throw().WithMessage($"Type is not assignable to '{typeof(ISampleInterface)}'. (Parameter 'type')"); + } + + [Fact] + public void Add_Should_ThrowException_WhenTypeIsAssignableToGenericButAlreadyExists() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + + // Act + Action act = () => collection.Add(typeof(SampleClass)); + + // Assert + act.Should().Throw().WithMessage("Type already exists in the collection. (Parameter 'type')"); + } + + [Fact] + public void Add_Should_AddTypeToCollection() + { + // Arrange + var collection = new TypeCollection(); + + // Act + collection.Add(); + + // Assert + collection.Count.Should().Be(1); + collection.Should().Contain(typeof(SampleClass)); + } + + [Fact] + public void Add_Should_ThrowException_WhenTypeAlreadyExists() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + + // Act + Action act = () => collection.Add(); + + // Assert + act.Should().Throw().WithMessage("Type already exists in the collection. (Parameter 'type')"); + } + + [Fact] + public void TryAdd_Should_AddTypeToCollection() + { + // Arrange + var collection = new TypeCollection(); + + // Act + var result = collection.TryAdd(); + + // Assert + result.Should().BeTrue(); + collection.Count.Should().Be(1); + collection.Should().Contain(typeof(SampleClass)); + } + + [Fact] + public void TryAdd_Should_ReturnFalse_WhenTypeAlreadyExists() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + + // Act + var result = collection.TryAdd(); + + // Assert + result.Should().BeFalse(); + collection.Count.Should().Be(1); + } + + [Fact] + public void Clear_Should_RemoveAllTypesFromCollection() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + collection.Add(); + + // Act + collection.Clear(); + + // Assert + collection.Count.Should().Be(0); + collection.Should().NotContain(typeof(SampleClass)); + collection.Should().NotContain(typeof(AnotherSampleClass)); + } + + [Fact] + public void Contains_Should_ReturnTrue_WhenTypeExistsInCollection() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + + // Act + var contains = collection.Contains(); + + // Assert + contains.Should().BeTrue(); + } + + [Fact] + public void Contains_Should_ReturnFalse_WhenTypeDoesNotExistInCollection() + { + // Arrange + var collection = new TypeCollection(); + + // Act + var contains = collection.Contains(); + + // Assert + contains.Should().BeFalse(); + } + + [Fact] + public void Remove_Should_RemoveTypeFromCollection_WhenSuppliedAsGenericParameter() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + + // Act + var removed = collection.Remove(); + + // Assert + removed.Should().BeTrue(); + collection.Count.Should().Be(0); + collection.Should().NotContain(typeof(SampleClass)); + } + + [Fact] + public void Remove_Should_RemoveTypeFromCollection_WhenSuppliedAsType() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + + // Act + var removed = collection.Remove(typeof(SampleClass)); + + // Assert + removed.Should().BeTrue(); + collection.Count.Should().Be(0); + collection.Should().NotContain(typeof(SampleClass)); + } + + [Fact] + public void Remove_Should_ReturnFalse_WhenTypeDoesNotExistInCollection() + { + // Arrange + var collection = new TypeCollection(); + + // Act + var removed = collection.Remove(); + + // Assert + removed.Should().BeFalse(); + } + + [Fact] + public void Enumerator_Should_IterateOverCollection() + { + // Arrange + var collection = new TypeCollection(); + collection.Add(); + collection.Add(); + + // Act + var types = new List(); + foreach (var type in collection) + { + types.Add(type); + } + + // Assert + types.Should().ContainInOrder(typeof(SampleClass), typeof(AnotherSampleClass)); + } + + public interface ISampleInterface { } + + public class SampleClass : ISampleInterface { } + + public class AnotherSampleClass : ISampleInterface { } +} diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs index 4dc38daa..258fd280 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs @@ -13,8 +13,9 @@ public KafkaGroupConsumerTests() var processorFactoryMock = new Mock>(); var providerSettings = new KafkaMessageBusSettings("host"); + var consumerSettings = Array.Empty(); - var subjectMock = new Mock(loggerFactoryMock.Object, providerSettings, "group", new List { "topic" }, processorFactoryMock.Object) { CallBase = true }; + var subjectMock = new Mock(loggerFactoryMock.Object, providerSettings, consumerSettings, "group", new List { "topic" }, processorFactoryMock.Object) { CallBase = true }; _subject = subjectMock.Object; } diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs index 4e4662ce..a16c44cb 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs @@ -244,7 +244,7 @@ await Task.WhenAll(requests.Select(async req => } })); - await responses.WaitUntilArriving(newMessagesTimeout: 5); + await responses.WaitUntilArriving(newMessagesTimeout: 10); // assert diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs index 8d67cb92..df760dae 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/OutboxTests.cs @@ -78,6 +78,8 @@ void ConfigureExternalBus(MessageBusBuilder mbb) } else { + mbb.PerMessageScopeEnabled(true); + // we test outbox with hybrid bus setup mbb.AddChildBus("Memory", mbb => { diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs index 3b6c6413..d8ca2150 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs @@ -189,7 +189,7 @@ private async Task BasicPubSub(int expectedMessageCopies, Action addit [InlineData(RabbitMqMessageAcknowledgementMode.AckMessageBeforeProcessing)] public async Task BasicReqRespOnTopic(RabbitMqMessageAcknowledgementMode acknowledgementMode) { - var topic = "test-echo"; + const string topic = "test-echo"; AddBusConfiguration(mbb => { diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs new file mode 100644 index 00000000..5d49be44 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs @@ -0,0 +1,139 @@ +namespace SlimMessageBus.Host.Test.Consumer; + +public class AbstractConsumerTests +{ + private class TestConsumer : AbstractConsumer + { + public TestConsumer(ILogger logger, IEnumerable settings) + : base(logger, settings) { } + + protected override Task OnStart() => Task.CompletedTask; + protected override Task OnStop() => Task.CompletedTask; + } + + private class TestConsumerSettings : AbstractConsumerSettings; + + public class CircuitBreakerAccessor + { + public Circuit State { get; set; } + public int SubscribeCallCount { get; set; } = 0; + public int UnsubscribeCallCount { get; set; } = 0; + public IEnumerable Settings { get; set; } + public Func OnChange { get; set; } + } + + private class TestCircuitBreaker : IConsumerCircuitBreaker + { + private readonly CircuitBreakerAccessor _accessor; + + public TestCircuitBreaker(CircuitBreakerAccessor accessor, IEnumerable settings) + { + _accessor = accessor; + Settings = settings; + State = Circuit.Open; + } + + public Circuit State + { + get => _accessor.State; + set => _accessor.State = value; + } + public IEnumerable Settings { get; } + + public Task Subscribe(Func onChange) + { + _accessor.SubscribeCallCount++; + _accessor.OnChange = onChange; + + return Task.CompletedTask; + } + + public void Unsubscribe() + { + _accessor.UnsubscribeCallCount++; + } + } + + private readonly List _settings; + private readonly TestConsumer _target; + private readonly CircuitBreakerAccessor accessor; + + public AbstractConsumerTests() + { + accessor = new CircuitBreakerAccessor(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.TryAddSingleton(accessor); + serviceCollection.TryAddTransient(); + + var testSettings = new TestConsumerSettings + { + MessageBusSettings = new MessageBusSettings { ServiceProvider = serviceCollection.BuildServiceProvider() } + }; + + testSettings.CircuitBreakers.Add(); + + _settings = [testSettings]; + + _target = new TestConsumer(NullLogger.Instance, _settings); + } + + [Fact] + public async Task Start_ShouldStartCircuitBreakers_WhenNotStarted() + { + // Arrange + + // Act + await _target.Start(); + + // Assert + _target.IsStarted.Should().BeTrue(); + accessor.SubscribeCallCount.Should().Be(1); + } + + [Fact] + public async Task Stop_ShouldStopCircuitBreakers_WhenStarted() + { + // Arrange + await _target.Start(); + + // Act + await _target.Stop(); + + // Assert + _target.IsStarted.Should().BeFalse(); + accessor.UnsubscribeCallCount.Should().Be(1); + } + + [Fact] + public async Task BreakerChanged_ShouldPauseConsumer_WhenBreakerClosed() + { + // Arrange + await _target.Start(); + + // Act + _target.IsPaused.Should().BeFalse(); + accessor.State = Circuit.Closed; + await _target.BreakerChanged(Circuit.Closed); + + // Assert + _target.IsPaused.Should().BeTrue(); + } + + [Fact] + public async Task BreakerChanged_ShouldResumeConsumer_WhenBreakerOpen() + { + // Arrange + await _target.Start(); + accessor.State = Circuit.Closed; + await _target.BreakerChanged(Circuit.Open); + + // Act + _target.IsPaused.Should().BeTrue(); + accessor.State = Circuit.Open; + await _target.BreakerChanged(Circuit.Open); + + // Assert + _target.IsPaused.Should().BeFalse(); + } +} diff --git a/src/Tests/SlimMessageBus.Host.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.Test/GlobalUsings.cs index a8f3ecbb..af20b140 100644 --- a/src/Tests/SlimMessageBus.Host.Test/GlobalUsings.cs +++ b/src/Tests/SlimMessageBus.Host.Test/GlobalUsings.cs @@ -1,8 +1,11 @@ -global using AutoFixture; +global using System; + +global using AutoFixture; global using FluentAssertions; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Logging.Abstractions; @@ -13,3 +16,4 @@ global using SlimMessageBus.Host.Serialization.Json; global using Xunit; + diff --git a/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs b/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs index 2f8dfb37..6fd1a4eb 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Helpers/ReflectionUtilsTests.cs @@ -94,7 +94,7 @@ internal record ClassWithGenericMethod(object Value) } [Fact] - public void When_GenerateGenericMethodCallToFunc_Given_GenericMethid_Then_MethodIsProperlyInvoked() + public void When_GenerateGenericMethodCallToFunc_Given_GenericMethod_Then_MethodIsProperlyInvoked() { // arrange var obj = new ClassWithGenericMethod(true); diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs index 771866b8..a1aeba2e 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs @@ -81,7 +81,7 @@ public void TriggerPendingRequestCleanup() PendingRequestManager.CleanPendingRequests(); } - public class MessageBusTestedConsumer(ILogger logger) : AbstractConsumer(logger) + public class MessageBusTestedConsumer(ILogger logger) : AbstractConsumer(logger, []) { protected override Task OnStart() => Task.CompletedTask; From 0abaafa655c4b4102de941190d4467a7d10c9f09 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Wed, 1 Jan 2025 19:19:01 +0100 Subject: [PATCH 15/21] Health check circuit breaker Signed-off-by: Tomasz Maruszak --- README.md | 1 + build/tasks.ps1 | 1 + docs/NuGet.md | 1 + src/Host.Plugin.Properties.xml | 2 +- .../Consumers/AddConsumer.cs | 13 +- .../Consumers/SubtractConsumer.cs | 13 +- .../GlobalUsings.cs | 2 +- .../HealthChecks/AddRandomHealthCheck.cs | 8 +- .../HealthChecks/RandomHealthCheck.cs | 13 +- .../HealthChecks/SubtractRandomHealthCheck.cs | 8 +- .../IntermittentMessagePublisher.cs | 19 +- .../Models/Add.cs | 2 +- .../Models/Subtract.cs | 2 +- .../Program.cs | 3 +- .../Consumer/SqsBaseConsumer.cs | 9 +- .../Consumer/EhGroupConsumer.cs | 7 +- .../Consumer/AsbBaseConsumer.cs | 18 +- .../TopicSubscriptionParams.cs | 12 +- .../Config/ConsumerBuilderExtensions.cs | 23 +-- ...sions.cs => ConsumerSettingsExtensions.cs} | 18 +- .../Config/ConsumerSettingsProperties.cs | 6 + .../GlobalUsings.cs | 4 +- .../HealthCheckBackgroundService.cs | 6 +- .../HealthCheckCircuitBreaker.cs | 6 +- ...Bus.Host.CircuitBreaker.HealthCheck.csproj | 9 +- .../Circuit.cs | 7 + .../Config/ConsumerBuilderExtensions.cs | 26 +++ .../Config/ConsumerSettingsProperties.cs | 9 + .../GlobalUsings.cs | 2 + .../IConsumerCircuitBreaker.cs | 10 +- .../AbstractConsumerExtensions.cs | 7 + .../AbstractConsumerProperties.cs | 7 + ...rcuitBreakerAbstractConsumerInterceptor.cs | 92 ++++++++++ .../SlimMessageBus.Host.CircuitBreaker.csproj | 22 +++ .../Settings/AbstractConsumerSettings.cs | 5 - .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../TypeCollection.cs | 33 ++-- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../Consumer/KafkaGroupConsumer.cs | 13 +- .../KafkaMessageBus.cs | 4 +- .../MqttMessageBus.cs | 8 +- .../MqttTopicConsumer.cs | 13 +- .../NatsMessageBus.cs | 3 +- .../NatsSubjectConsumer.cs | 18 +- .../Consumers/AbstractRabbitMqConsumer.cs | 20 +- ...RabbitMqAutoAcknowledgeMessageProcessor.cs | 2 +- .../Consumers/RabbitMqConsumer.cs | 15 +- .../Consumers/RabbitMqResponseConsumer.cs | 11 +- .../RabbitMqMessageBus.cs | 4 + .../Consumers/RedisListCheckerConsumer.cs | 13 +- .../Consumers/RedisTopicConsumer.cs | 16 +- .../RedisMessageBus.cs | 5 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- .../Consumer/AbstractConsumer.cs | 173 +++++++++--------- .../Consumer/IAbstractConsumerInterceptor.cs | 33 ++++ .../ServiceCollectionExtensions.cs | 2 +- .../SlimMessageBus.Host.csproj | 2 +- src/SlimMessageBus.sln | 24 ++- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- .../GlobalUsings.cs | 17 +- .../HealthCheckBackgroundServiceTests.cs | 4 +- .../HealthCheckCircuitBreakerTests.cs | 5 +- ...ost.CircuitBreaker.HealthCheck.Test.csproj | 1 + .../GlobalUsings.cs | 8 + ...BreakerAbstractConsumerInterceptorTests.cs | 140 ++++++++++++++ ...MessageBus.Host.CircuitBreaker.Test.csproj | 20 ++ .../Consumer/KafkaGroupConsumerTests.cs | 2 +- .../Consumer/AbstractConsumerTests.cs | 149 +++++---------- .../MessageBusTested.cs | 6 +- 69 files changed, 759 insertions(+), 406 deletions(-) rename src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/{SettingsExtensions.cs => ConsumerSettingsExtensions.cs} (70%) create mode 100644 src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsProperties.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/Circuit.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerSettingsProperties.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/GlobalUsings.cs rename src/{SlimMessageBus => SlimMessageBus.Host.CircuitBreaker}/IConsumerCircuitBreaker.cs (76%) create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerExtensions.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerProperties.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs create mode 100644 src/SlimMessageBus.Host.CircuitBreaker/SlimMessageBus.Host.CircuitBreaker.csproj create mode 100644 src/SlimMessageBus.Host/Consumer/IAbstractConsumerInterceptor.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/GlobalUsings.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs create mode 100644 src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/SlimMessageBus.Host.CircuitBreaker.Test.csproj diff --git a/README.md b/README.md index a5d018f5..fcbc9a23 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i | `.Host.Outbox.Sql` | Transactional Outbox using MSSQL | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.Sql.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) | | `.Host.Outbox.Sql.DbContext` | Transactional Outbox using MSSQL with EF DataContext integration | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.Sql.DbContext.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql.DbContext) | | `.Host.AsyncApi` | [AsyncAPI](https://www.asyncapi.com/) specification generation via [Saunter](https://github.com/tehmantra/saunter) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AsyncApi.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AsyncApi) | +| `.Host.CircuitBreaker.HealthCheck` | Consumer circuit breaker based on [health checks](docs/intro.md#health-check-circuit-breaker) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.CircuitBreaker.HealthCheck.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.CircuitBreaker.HealthCheck) | Typically the application layers (domain model, business logic) only need to depend on `SlimMessageBus` which is the facade, and ultimately the application hosting layer (ASP.NET, Console App, Windows Service) will reference and configure the other packages (`SlimMessageBus.Host.*`) which are the messaging transport providers and additional plugins. diff --git a/build/tasks.ps1 b/build/tasks.ps1 index 649a062d..1af71ba1 100644 --- a/build/tasks.ps1 +++ b/build/tasks.ps1 @@ -43,6 +43,7 @@ $projects = @( "SlimMessageBus.Host.Outbox.Sql", "SlimMessageBus.Host.Outbox.Sql.DbContext", + "SlimMessageBus.Host.CircuitBreaker", "SlimMessageBus.Host.CircuitBreaker.HealthCheck", "SlimMessageBus.Host.AsyncApi" diff --git a/docs/NuGet.md b/docs/NuGet.md index 23b474ef..ce59ffb3 100644 --- a/docs/NuGet.md +++ b/docs/NuGet.md @@ -24,5 +24,6 @@ Plugins: - Transactional Outbox pattern (SQL, DbContext) - Serialization using JSON, Avro, ProtoBuf - AsyncAPI specification generation +- Consumer Circuit Breaker based on Health Checks Find out more [https://github.com/zarusz/SlimMessageBus](https://github.com/zarusz/SlimMessageBus). diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index ea2f607f..a1eda2f8 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc901 + 3.0.0-rc902 \ No newline at end of file diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs index 1da885ad..fdfca42f 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/AddConsumer.cs @@ -1,17 +1,10 @@ namespace Sample.CircuitBreaker.HealthCheck.Consumers; -public class AddConsumer : IConsumer -{ - private readonly ILogger _logger; - - public AddConsumer(ILogger logger) - { - _logger = logger; - } - +public class AddConsumer(ILogger logger) : IConsumer +{ public Task OnHandle(Add message, CancellationToken cancellationToken) { - _logger.LogInformation("{A} + {B} = {C}", message.a, message.b, message.a + message.b); + logger.LogInformation("{A} + {B} = {C}", message.A, message.B, message.A + message.B); return Task.CompletedTask; } } diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs index 467a30d5..0d296333 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Consumers/SubtractConsumer.cs @@ -1,17 +1,10 @@ namespace Sample.CircuitBreaker.HealthCheck.Consumers; -public class SubtractConsumer : IConsumer -{ - private readonly ILogger _logger; - - public SubtractConsumer(ILogger logger) - { - _logger = logger; - } - +public class SubtractConsumer(ILogger logger) : IConsumer +{ public Task OnHandle(Subtract message, CancellationToken cancellationToken) { - _logger.LogInformation("{A} - {B} = {C}", message.a, message.b, message.a - message.b); + logger.LogInformation("{A} - {B} = {C}", message.A, message.B, message.A - message.B); return Task.CompletedTask; } } diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs index 61fd0fa5..ac87444b 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/GlobalUsings.cs @@ -14,4 +14,4 @@ global using SlimMessageBus; global using SlimMessageBus.Host; global using SlimMessageBus.Host.RabbitMQ; -global using SlimMessageBus.Host.Serialization.SystemTextJson; \ No newline at end of file +global using SlimMessageBus.Host.Serialization.SystemTextJson; diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs index b74784dd..93bc7607 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/AddRandomHealthCheck.cs @@ -1,11 +1,5 @@ namespace Sample.CircuitBreaker.HealthCheck.HealthChecks; -using Microsoft.Extensions.Logging; - -public class AddRandomHealthCheck : RandomHealthCheck +public class AddRandomHealthCheck(ILogger logger) : RandomHealthCheck(logger) { - public AddRandomHealthCheck(ILogger logger) - : base(logger) - { - } } diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs index cf9ccf88..a448c3ec 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/RandomHealthCheck.cs @@ -2,19 +2,12 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; -public abstract class RandomHealthCheck : IHealthCheck -{ - private readonly ILogger _logger; - - protected RandomHealthCheck(ILogger logger) - { - _logger = logger; - } - +public abstract class RandomHealthCheck(ILogger logger) : IHealthCheck +{ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { var value = (HealthStatus)Random.Shared.Next(3); - _logger.LogInformation("{HealthCheck} evaluated as {HealthStatus}", this.GetType(), value); + logger.LogInformation("{HealthCheck} evaluated as {HealthStatus}", GetType(), value); return Task.FromResult(new HealthCheckResult(value, value.ToString())); } } diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs index 8a68b0b1..27ce53e5 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/HealthChecks/SubtractRandomHealthCheck.cs @@ -1,11 +1,5 @@ namespace Sample.CircuitBreaker.HealthCheck.HealthChecks; -using Microsoft.Extensions.Logging; - -public class SubtractRandomHealthCheck : RandomHealthCheck +public class SubtractRandomHealthCheck(ILogger logger) : RandomHealthCheck(logger) { - public SubtractRandomHealthCheck(ILogger logger) - : base(logger) - { - } } diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs index 73110c15..7d887959 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/IntermittentMessagePublisher.cs @@ -1,15 +1,8 @@ namespace Sample.CircuitBreaker.HealthCheck; -public class IntermittentMessagePublisher : BackgroundService + +public class IntermittentMessagePublisher(ILogger logger, IMessageBus messageBus) + : BackgroundService { - private readonly ILogger _logger; - private readonly IMessageBus _messageBus; - - public IntermittentMessagePublisher(ILogger logger, IMessageBus messageBus) - { - _logger = logger; - _messageBus = messageBus; - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) @@ -17,11 +10,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var a = Random.Shared.Next(10); var b = Random.Shared.Next(10); - //_logger.LogInformation("Emitting {A} +- {B} = ?", a, b); + logger.LogInformation("Emitting {A} +- {B} = ?", a, b); await Task.WhenAll( - _messageBus.Publish(new Add(a, b)), - _messageBus.Publish(new Subtract(a, b)), + messageBus.Publish(new Add(a, b), cancellationToken: stoppingToken), + messageBus.Publish(new Subtract(a, b), cancellationToken: stoppingToken), Task.Delay(1000, stoppingToken)); } } diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs index 97c5e418..2208622f 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Add.cs @@ -1,3 +1,3 @@ namespace Sample.CircuitBreaker.HealthCheck.Models; -public record Add(int a, int b); \ No newline at end of file +public record Add(int A, int B); \ No newline at end of file diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs index 51d2efc4..d491f605 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Models/Subtract.cs @@ -1,3 +1,3 @@ namespace Sample.CircuitBreaker.HealthCheck.Models; -public record Subtract(int a, int b); \ No newline at end of file +public record Subtract(int A, int B); \ No newline at end of file diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs b/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs index 716253f5..718af413 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Program.cs @@ -1,9 +1,10 @@ namespace Sample.CircuitBreaker.HealthCheck; + using Microsoft.Extensions.Diagnostics.HealthChecks; using Sample.CircuitBreaker.HealthCheck.HealthChecks; -using SlimMessageBus.Host.CircuitBreaker.HealthCheck.Config; +using SlimMessageBus.Host.CircuitBreaker.HealthCheck; public static class Program { diff --git a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs index c784f1b9..1b978461 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AmazonSQS/Consumer/SqsBaseConsumer.cs @@ -13,7 +13,6 @@ public abstract class SqsBaseConsumer : AbstractConsumer public SqsMessageBus MessageBus { get; } protected IMessageProcessor MessageProcessor { get; } - protected string Path { get; } protected ISqsHeaderSerializer HeaderSerializer { get; } protected SqsBaseConsumer( @@ -23,11 +22,13 @@ protected SqsBaseConsumer( IMessageProcessor messageProcessor, IEnumerable consumerSettings, ILogger logger) - : base(logger, consumerSettings) + : base(logger, + consumerSettings, + path, + messageBus.Settings.ServiceProvider.GetServices()) { - MessageBus = messageBus ?? throw new ArgumentNullException(nameof(messageBus)); _clientProvider = clientProvider ?? throw new ArgumentNullException(nameof(clientProvider)); - Path = path ?? throw new ArgumentNullException(nameof(path)); + MessageBus = messageBus; MessageProcessor = messageProcessor ?? throw new ArgumentNullException(nameof(messageProcessor)); HeaderSerializer = messageBus.HeaderSerializer; T GetSingleValue(Func selector, string settingName, T defaultValue = default) diff --git a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs index a1bd113c..762ba2fd 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/Consumer/EhGroupConsumer.cs @@ -3,6 +3,8 @@ namespace SlimMessageBus.Host.AzureEventHub; using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Processor; +using Microsoft.Extensions.DependencyInjection; + public class EhGroupConsumer : AbstractConsumer { private readonly EventProcessorClient _processorClient; @@ -12,7 +14,10 @@ public class EhGroupConsumer : AbstractConsumer public EventHubMessageBus MessageBus { get; } public EhGroupConsumer(IEnumerable consumerSettings, EventHubMessageBus messageBus, GroupPath groupPath, Func partitionConsumerFactory) - : base(messageBus.LoggerFactory.CreateLogger(), consumerSettings) + : base(messageBus.LoggerFactory.CreateLogger(), + consumerSettings, + groupPath.Path, + messageBus.Settings.ServiceProvider.GetServices()) { _groupPath = groupPath ?? throw new ArgumentNullException(nameof(groupPath)); if (partitionConsumerFactory == null) throw new ArgumentNullException(nameof(partitionConsumerFactory)); diff --git a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs index a601e50c..09a5db8f 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs @@ -1,5 +1,7 @@ namespace SlimMessageBus.Host.AzureServiceBus.Consumer; +using Microsoft.Extensions.DependencyInjection; + public abstract class AsbBaseConsumer : AbstractConsumer { private ServiceBusProcessor _serviceBusProcessor; @@ -9,11 +11,19 @@ public abstract class AsbBaseConsumer : AbstractConsumer protected IMessageProcessor MessageProcessor { get; } protected TopicSubscriptionParams TopicSubscription { get; } - protected AsbBaseConsumer(ServiceBusMessageBus messageBus, ServiceBusClient serviceBusClient, TopicSubscriptionParams subscriptionFactoryParams, IMessageProcessor messageProcessor, IEnumerable consumerSettings, ILogger logger) - : base(logger ?? throw new ArgumentNullException(nameof(logger)), consumerSettings) + protected AsbBaseConsumer(ServiceBusMessageBus messageBus, + ServiceBusClient serviceBusClient, + TopicSubscriptionParams subscriptionFactoryParams, + IMessageProcessor messageProcessor, + IEnumerable consumerSettings, + ILogger logger) + : base(logger ?? throw new ArgumentNullException(nameof(logger)), + consumerSettings, + subscriptionFactoryParams.ToString(), + messageBus.Settings.ServiceProvider.GetServices()) { - MessageBus = messageBus ?? throw new ArgumentNullException(nameof(messageBus)); - TopicSubscription = subscriptionFactoryParams ?? throw new ArgumentNullException(nameof(subscriptionFactoryParams)); + MessageBus = messageBus; + TopicSubscription = subscriptionFactoryParams; MessageProcessor = messageProcessor ?? throw new ArgumentNullException(nameof(messageProcessor)); T GetSingleValue(Func selector, string settingName) diff --git a/src/SlimMessageBus.Host.AzureServiceBus/TopicSubscriptionParams.cs b/src/SlimMessageBus.Host.AzureServiceBus/TopicSubscriptionParams.cs index cf19244c..4cd056a4 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/TopicSubscriptionParams.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/TopicSubscriptionParams.cs @@ -1,15 +1,9 @@ namespace SlimMessageBus.Host.AzureServiceBus; -public class TopicSubscriptionParams +public class TopicSubscriptionParams(string path, string subscriptionName) { - public string Path { get; set; } - public string SubscriptionName { get; set; } - - public TopicSubscriptionParams(string path, string subscriptionName) - { - Path = path; - SubscriptionName = subscriptionName; - } + public string Path { get; set; } = path; + public string SubscriptionName { get; set; } = subscriptionName; public override string ToString() => SubscriptionName == null ? Path : $"{Path}/{SubscriptionName}"; diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs index c721f5dc..cdb92ff0 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerBuilderExtensions.cs @@ -1,6 +1,4 @@ -namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Config; - -using Microsoft.Extensions.DependencyInjection.Extensions; +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; public static class ConsumerBuilderExtensions { @@ -32,16 +30,15 @@ public static T PauseOnDegradedHealthCheck(this T builder, params string[] ta private static void RegisterHealthServices(AbstractConsumerBuilder builder) { - builder.ConsumerSettings.CircuitBreakers.TryAdd(); - builder.PostConfigurationActions.Add( - services => - { - services.TryAddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); - services.TryAdd(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); - services.AddHostedService(sp => sp.GetRequiredService()); + builder.AddConsumerCircuitBreakerType(); + builder.PostConfigurationActions.Add(services => + { + services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + services.TryAdd(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + services.AddHostedService(sp => sp.GetRequiredService()); - services.TryAddSingleton(); - }); + services.TryAddTransient(); + }); } } diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsExtensions.cs similarity index 70% rename from src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs rename to src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsExtensions.cs index a2775f10..18bebac4 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/SettingsExtensions.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsExtensions.cs @@ -1,9 +1,7 @@ namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; -static internal class SettingsExtensions +static internal class ConsumerSettingsExtensions { - private const string _key = nameof(HealthCheckCircuitBreaker); - public static T PauseOnDegraded(this T consumerSettings, params string[] tags) where T : AbstractConsumerSettings { @@ -15,7 +13,6 @@ public static T PauseOnDegraded(this T consumerSettings, params string[] tags dict[tag] = HealthStatus.Degraded; } } - return consumerSettings; } @@ -30,18 +27,9 @@ public static T PauseOnUnhealthy(this T consumerSettings, params string[] tag dict[tag] = HealthStatus.Unhealthy; } } - return consumerSettings; } - static internal IDictionary HealthBreakerTags(this AbstractConsumerSettings consumerSettings) - { - if (!consumerSettings.Properties.TryGetValue(_key, out var rawValue) || rawValue is not IDictionary value) - { - value = new Dictionary(); - consumerSettings.Properties[_key] = value; - } - - return value; - } + static internal IDictionary HealthBreakerTags(this AbstractConsumerSettings consumerSettings) + => consumerSettings.GetOrCreate(ConsumerSettingsProperties.HealthStatusTags, () => []); } diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsProperties.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsProperties.cs new file mode 100644 index 00000000..a9fa2e91 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/Config/ConsumerSettingsProperties.cs @@ -0,0 +1,6 @@ +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; + +static internal class ConsumerSettingsProperties +{ + static readonly internal ProviderExtensionProperty> HealthStatusTags = new("CircuitBreaker_HealthStatusTags"); +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs index 6d9be2c3..af29883e 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/GlobalUsings.cs @@ -1,7 +1,7 @@ global using System; global using System.Diagnostics; - + global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs index 96bd55a0..a2cd303d 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckBackgroundService.cs @@ -38,10 +38,8 @@ public async Task PublishAsync(HealthReport report, CancellationToken cancellati } } - public Task StartAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) { diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs index 8797fc9e..52a5f33e 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/HealthCheckCircuitBreaker.cs @@ -1,5 +1,7 @@ -namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; - +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck; + +using SlimMessageBus.Host.CircuitBreaker; + internal sealed class HealthCheckCircuitBreaker : IConsumerCircuitBreaker { private readonly IEnumerable _settings; diff --git a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj index 611abab7..7f63ccfb 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj +++ b/src/SlimMessageBus.Host.CircuitBreaker.HealthCheck/SlimMessageBus.Host.CircuitBreaker.HealthCheck.csproj @@ -15,16 +15,13 @@ + - - <_Parameter1>SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test - - - <_Parameter1>DynamicProxyGenAssembly2 - + + diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Circuit.cs b/src/SlimMessageBus.Host.CircuitBreaker/Circuit.cs new file mode 100644 index 00000000..349530a4 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/Circuit.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host.CircuitBreaker; + +public enum Circuit +{ + Open, + Closed +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs new file mode 100644 index 00000000..e1258a05 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs @@ -0,0 +1,26 @@ +namespace SlimMessageBus.Host.CircuitBreaker; + +using Microsoft.Extensions.DependencyInjection.Extensions; + +public static class ConsumerBuilderExtensions +{ + public static T AddConsumerCircuitBreakerType(this T builder) + where T : AbstractConsumerBuilder + where TConsumerCircuitBreaker : IConsumerCircuitBreaker + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var breakersTypes = builder.ConsumerSettings.GetOrCreate(ConsumerSettingsProperties.CircuitBreakerTypes, () => []); + breakersTypes.TryAdd(); + + builder.PostConfigurationActions.Add(services => + { + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerSettingsProperties.cs b/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerSettingsProperties.cs new file mode 100644 index 00000000..5e441547 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerSettingsProperties.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host.CircuitBreaker; + +static internal class ConsumerSettingsProperties +{ + /// + /// to be used with the consumer. + /// + static readonly internal ProviderExtensionProperty> CircuitBreakerTypes = new("CircuitBreaker_Types"); +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker/GlobalUsings.cs b/src/SlimMessageBus.Host.CircuitBreaker/GlobalUsings.cs new file mode 100644 index 00000000..a4d02342 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; diff --git a/src/SlimMessageBus/IConsumerCircuitBreaker.cs b/src/SlimMessageBus.Host.CircuitBreaker/IConsumerCircuitBreaker.cs similarity index 76% rename from src/SlimMessageBus/IConsumerCircuitBreaker.cs rename to src/SlimMessageBus.Host.CircuitBreaker/IConsumerCircuitBreaker.cs index 27154566..453d149b 100644 --- a/src/SlimMessageBus/IConsumerCircuitBreaker.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker/IConsumerCircuitBreaker.cs @@ -1,5 +1,5 @@ -namespace SlimMessageBus; - +namespace SlimMessageBus.Host.CircuitBreaker; + /// /// Circuit breaker to toggle consumer status on an external event. /// @@ -9,9 +9,3 @@ public interface IConsumerCircuitBreaker Task Subscribe(Func onChange); void Unsubscribe(); } - -public enum Circuit -{ - Open, - Closed -} diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerExtensions.cs new file mode 100644 index 00000000..ebbc1b2f --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerExtensions.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host.CircuitBreaker; + +internal static class AbstractConsumerExtensions +{ + public static bool IsPaused(this AbstractConsumer consumer) => consumer.GetOrDefault(AbstractConsumerProperties.IsPaused, false); + public static void SetIsPaused(this AbstractConsumer consumer, bool isPaused) => AbstractConsumerProperties.IsPaused.Set(consumer, isPaused); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerProperties.cs b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerProperties.cs new file mode 100644 index 00000000..5daffa07 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/AbstractConsumerProperties.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host.CircuitBreaker; + +static internal class AbstractConsumerProperties +{ + static readonly internal ProviderExtensionProperty IsPaused = new("CircuitBreaker_IsPaused"); + static readonly internal ProviderExtensionProperty?> Breakers = new("CircuitBreaker_Breakers"); +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs new file mode 100644 index 00000000..30658f06 --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs @@ -0,0 +1,92 @@ +namespace SlimMessageBus.Host.CircuitBreaker; + +/// +/// Circuit breaker to toggle consumer status on an external events. +/// +internal sealed class CircuitBreakerAbstractConsumerInterceptor : IAbstractConsumerInterceptor +{ + public int Order => 100; + + public async Task CanStart(AbstractConsumer consumer) + { + var breakerTypes = consumer.Settings.SelectMany(x => x.GetOrDefault(ConsumerSettingsProperties.CircuitBreakerTypes, [])).ToHashSet(); + if (breakerTypes.Count == 0) + { + // no breakers, allow to pass + return true; + } + + var breakers = consumer.GetOrCreate(AbstractConsumerProperties.Breakers, () => [])!; + + async Task BreakerChanged(Circuit state) + { + if (!consumer.IsStarted) + { + return; + } + + var isPaused = consumer.IsPaused(); + var shouldPause = state == Circuit.Closed || breakers.Exists(x => x.State == Circuit.Closed); + if (shouldPause != isPaused) + { + var path = consumer.Path; + var bus = consumer.Settings[0].MessageBusSettings.Name ?? "default"; + if (shouldPause) + { + consumer.Logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); + await consumer.DoStop().ConfigureAwait(false); + } + else + { + consumer.Logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); + await consumer.DoStart().ConfigureAwait(false); + } + consumer.SetIsPaused(shouldPause); + } + } + + var sp = consumer.Settings.Select(x => x.MessageBusSettings.ServiceProvider).First(x => x != null); + foreach (var breakerType in breakerTypes) + { + var breaker = (IConsumerCircuitBreaker)ActivatorUtilities.CreateInstance(sp, breakerType, consumer.Settings); + breakers.Add(breaker); + + await breaker.Subscribe(BreakerChanged); + } + + var isPaused = breakers.Exists(x => x.State == Circuit.Closed); + consumer.SetIsPaused(isPaused); + return !isPaused; + } + + public async Task CanStop(AbstractConsumer consumer) + { + var breakers = consumer.GetOrDefault(AbstractConsumerProperties.Breakers, null); + if (breakers == null || breakers.Count == 0) + { + // no breakers, allow to pass + return true; + } + + foreach (var breaker in breakers) + { + breaker.Unsubscribe(); + + if (breaker is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else if (breaker is IDisposable disposable) + { + disposable.Dispose(); + } + } + breakers.Clear(); + + return !consumer.IsPaused(); + } + + public Task Started(AbstractConsumer consumer) => Task.CompletedTask; + + public Task Stopped(AbstractConsumer consumer) => Task.CompletedTask; +} diff --git a/src/SlimMessageBus.Host.CircuitBreaker/SlimMessageBus.Host.CircuitBreaker.csproj b/src/SlimMessageBus.Host.CircuitBreaker/SlimMessageBus.Host.CircuitBreaker.csproj new file mode 100644 index 00000000..5b7fe10c --- /dev/null +++ b/src/SlimMessageBus.Host.CircuitBreaker/SlimMessageBus.Host.CircuitBreaker.csproj @@ -0,0 +1,22 @@ + + + + + + Circuit breaker abstractions for SlimMessageBus + Toggle consumer on or off + icon.png + + enable + + + + + + + + + + + + diff --git a/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs b/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs index a04fd491..71260121 100644 --- a/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs +++ b/src/SlimMessageBus.Host.Configuration/Settings/AbstractConsumerSettings.cs @@ -23,11 +23,6 @@ public abstract class AbstractConsumerSettings : HasProviderExtensions /// public int Instances { get; set; } - /// - /// to be used with the consumer. - /// - public TypeCollection CircuitBreakers { get; } = []; - protected AbstractConsumerSettings() { Instances = 1; diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index 36463945..b01a3b49 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc900 + 3.0.0-rc901 diff --git a/src/SlimMessageBus.Host.Configuration/TypeCollection.cs b/src/SlimMessageBus.Host.Configuration/TypeCollection.cs index 04246947..f936bcfb 100644 --- a/src/SlimMessageBus.Host.Configuration/TypeCollection.cs +++ b/src/SlimMessageBus.Host.Configuration/TypeCollection.cs @@ -45,34 +45,25 @@ public bool TryAdd() where T : TInterface public void Clear() => _innerList.Clear(); - public bool Contains() where T : TInterface - { - return _innerList.Contains(typeof(T)); - } + public bool Contains() where T : TInterface + => _innerList.Contains(typeof(T)); - public void CopyTo(Type[] array, int arrayIndex) => _innerList.CopyTo(array, arrayIndex); + public void CopyTo(Type[] array, int arrayIndex) + => _innerList.CopyTo(array, arrayIndex); - public bool Remove() where T : TInterface - { - return _innerList.Remove(typeof(T)); - } + public bool Remove() where T : TInterface + => _innerList.Remove(typeof(T)); - public bool Remove(Type type) - { - return _innerList.Remove(type); - } + public bool Remove(Type type) + => _innerList.Remove(type); public int Count => _innerList.Count; public bool IsReadOnly => false; - public IEnumerator GetEnumerator() - { - return _innerList.GetEnumerator(); - } + public IEnumerator GetEnumerator() + => _innerList.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return _innerList.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + => _innerList.GetEnumerator(); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index 97abd9cc..b98b06a5 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc900 + 3.0.0-rc901 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs index cfb70f0f..13278bf0 100644 --- a/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs +++ b/src/SlimMessageBus.Host.Kafka/Consumer/KafkaGroupConsumer.cs @@ -15,8 +15,17 @@ public class KafkaGroupConsumer : AbstractConsumer, IKafkaCommitController public string Group { get; } public IReadOnlyCollection Topics { get; } - public KafkaGroupConsumer(ILoggerFactory loggerFactory, KafkaMessageBusSettings providerSettings, IEnumerable consumerSettings, string group, IReadOnlyCollection topics, Func processorFactory) - : base(loggerFactory.CreateLogger(), consumerSettings) + public KafkaGroupConsumer(ILoggerFactory loggerFactory, + KafkaMessageBusSettings providerSettings, + IEnumerable consumerSettings, + IEnumerable interceptors, + string group, + IReadOnlyCollection topics, + Func processorFactory) + : base(loggerFactory.CreateLogger(), + consumerSettings, + group, + interceptors) { ProviderSettings = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); Group = group ?? throw new ArgumentNullException(nameof(group)); diff --git a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs index 1c1f68ba..5bc1d1aa 100644 --- a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs +++ b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs @@ -1,5 +1,7 @@ namespace SlimMessageBus.Host.Kafka; +using Microsoft.Extensions.DependencyInjection; + using IProducer = Confluent.Kafka.IProducer; using Message = Confluent.Kafka.Message; @@ -64,7 +66,7 @@ protected override async Task CreateConsumers() void AddGroupConsumer(IEnumerable consumerSettings, string group, IReadOnlyCollection topics, Func processorFactory) { _logger.LogInformation("Creating consumer group {ConsumerGroup}", group); - AddConsumer(new KafkaGroupConsumer(LoggerFactory, ProviderSettings, consumerSettings, group, topics, processorFactory)); + AddConsumer(new KafkaGroupConsumer(LoggerFactory, ProviderSettings, consumerSettings, interceptors: Settings.ServiceProvider.GetServices(), group, topics, processorFactory)); } object MessageProvider(Type messageType, ConsumeResult transportMessage) diff --git a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs index 102ee6d5..2002f298 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Threading; +using Microsoft.Extensions.DependencyInjection; + using MQTTnet.Extensions.ManagedClient; public class MqttMessageBus : MessageBusBase @@ -55,7 +57,7 @@ protected override async Task CreateConsumers() void AddTopicConsumer(IEnumerable consumerSettings, string topic, IMessageProcessor messageProcessor) { _logger.LogInformation("Creating consumer for {Path}", topic); - var consumer = new MqttTopicConsumer(LoggerFactory.CreateLogger(), consumerSettings, topic, messageProcessor); + var consumer = new MqttTopicConsumer(LoggerFactory.CreateLogger(), consumerSettings, interceptors: Settings.ServiceProvider.GetServices(), topic, messageProcessor); AddConsumer(consumer); } @@ -84,7 +86,7 @@ void AddTopicConsumer(IEnumerable consumerSettings, st AddTopicConsumer([Settings.RequestResponse], Settings.RequestResponse.Path, processor); } - var topics = Consumers.Cast().Select(x => new MqttTopicFilterBuilder().WithTopic(x.Topic).Build()).ToList(); + var topics = Consumers.Cast().Select(x => new MqttTopicFilterBuilder().WithTopic(x.Path).Build()).ToList(); await _mqttClient.SubscribeAsync(topics).ConfigureAwait(false); } @@ -101,7 +103,7 @@ protected override async Task DestroyConsumers() private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg) { - var consumer = Consumers.Cast().FirstOrDefault(x => x.Topic == arg.ApplicationMessage.Topic); + var consumer = Consumers.Cast().FirstOrDefault(x => x.Path == arg.ApplicationMessage.Topic); if (consumer != null) { var headers = new Dictionary(); diff --git a/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs b/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs index 7720782c..738abb8b 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttTopicConsumer.cs @@ -3,12 +3,17 @@ public class MqttTopicConsumer : AbstractConsumer { public IMessageProcessor MessageProcessor { get; } - public string Topic { get; } - public MqttTopicConsumer(ILogger logger, IEnumerable consumerSettings, string topic, IMessageProcessor messageProcessor) - : base(logger, consumerSettings) + public MqttTopicConsumer(ILogger logger, + IEnumerable consumerSettings, + IEnumerable interceptors, + string topic, + IMessageProcessor messageProcessor) + : base(logger, + consumerSettings, + topic, + interceptors) { - Topic = topic; MessageProcessor = messageProcessor; } diff --git a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs index ec81318e..4a9681a6 100644 --- a/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs +++ b/src/SlimMessageBus.Host.Nats/NatsMessageBus.cs @@ -1,5 +1,6 @@ namespace SlimMessageBus.Host.Nats; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; public class NatsMessageBus : MessageBusBase @@ -78,7 +79,7 @@ protected override async Task CreateConsumers() private void AddSubjectConsumer(IEnumerable consumerSettings, string subject, IMessageProcessor> processor) { _logger.LogInformation("Creating consumer for {Subject}", subject); - var consumer = new NatsSubjectConsumer(LoggerFactory.CreateLogger>(), consumerSettings, subject, _connection, processor); + var consumer = new NatsSubjectConsumer(LoggerFactory.CreateLogger>(), consumerSettings, interceptors: Settings.ServiceProvider.GetServices(), subject, _connection, processor); AddConsumer(consumer); } diff --git a/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs b/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs index 43a9f2cf..625dec57 100644 --- a/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs +++ b/src/SlimMessageBus.Host.Nats/NatsSubjectConsumer.cs @@ -1,27 +1,31 @@ #nullable enable namespace SlimMessageBus.Host.Nats; -using System.Collections.Generic; - public class NatsSubjectConsumer : AbstractConsumer { - private readonly string _subject; private readonly INatsConnection _connection; private readonly IMessageProcessor> _messageProcessor; private INatsSub? _subscription; private Task? _messageConsumerTask; - public NatsSubjectConsumer(ILogger logger, IEnumerable consumerSettings, string subject, INatsConnection connection, IMessageProcessor> messageProcessor) - : base(logger, consumerSettings) + public NatsSubjectConsumer(ILogger logger, + IEnumerable consumerSettings, + IEnumerable interceptors, + string subject, + INatsConnection connection, + IMessageProcessor> messageProcessor) + : base(logger, + consumerSettings, + path: subject, + interceptors) { - _subject = subject; _connection = connection; _messageProcessor = messageProcessor; } protected override async Task OnStart() { - _subscription ??= await _connection.SubscribeCoreAsync(_subject, cancellationToken: CancellationToken); + _subscription ??= await _connection.SubscribeCoreAsync(Path, cancellationToken: CancellationToken); _messageConsumerTask = Task.Factory.StartNew(OnLoop, CancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs index 47e4e630..05c40cc5 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/AbstractRabbitMqConsumer.cs @@ -10,15 +10,21 @@ public abstract class AbstractRabbitMqConsumer : AbstractConsumer private AsyncEventingBasicConsumer _consumer; private string _consumerTag; - public string QueueName { get; } protected abstract RabbitMqMessageAcknowledgementMode AcknowledgementMode { get; } - protected AbstractRabbitMqConsumer(ILogger logger, IEnumerable consumerSettings, IRabbitMqChannel channel, string queueName, IHeaderValueConverter headerValueConverter) - : base(logger, consumerSettings) + protected AbstractRabbitMqConsumer(ILogger logger, + IEnumerable consumerSettings, + IEnumerable interceptors, + IRabbitMqChannel channel, + string queueName, + IHeaderValueConverter headerValueConverter) + : base(logger, + consumerSettings, + path: queueName, + interceptors) { _channel = channel; _headerValueConverter = headerValueConverter; - QueueName = queueName; } protected override Task OnStart() @@ -28,7 +34,7 @@ protected override Task OnStart() lock (_channel.ChannelLock) { - _consumerTag = _channel.Channel.BasicConsume(QueueName, autoAck: AcknowledgementMode == RabbitMqMessageAcknowledgementMode.AckAutomaticByRabbit, _consumer); + _consumerTag = _channel.Channel.BasicConsume(Path, autoAck: AcknowledgementMode == RabbitMqMessageAcknowledgementMode.AckAutomaticByRabbit, _consumer); } return Task.CompletedTask; @@ -54,7 +60,7 @@ protected async Task OnMessageReceived(object sender, BasicDeliverEventArgs @eve return; } - Logger.LogDebug("Message arrived on queue {QueueName} from exchange {ExchangeName} with delivery tag {DeliveryTag}", QueueName, @event.Exchange, @event.DeliveryTag); + Logger.LogDebug("Message arrived on queue {QueueName} from exchange {ExchangeName} with delivery tag {DeliveryTag}", Path, @event.Exchange, @event.DeliveryTag); Exception exception; try { @@ -76,7 +82,7 @@ protected async Task OnMessageReceived(object sender, BasicDeliverEventArgs @eve } if (exception != null) { - Logger.LogError(exception, "Error while processing message on queue {QueueName} from exchange {ExchangeName}: {ErrorMessage}", QueueName, @event.Exchange, exception.Message); + Logger.LogError(exception, "Error while processing message on queue {QueueName} from exchange {ExchangeName}: {ErrorMessage}", Path, @event.Exchange, exception.Message); } } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs index f2f18de2..f12ad27a 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqAutoAcknowledgeMessageProcessor.cs @@ -56,7 +56,7 @@ public async Task ProcessMessage(BasicDeliverEventArgs tra if (r.Exception != null) { // We rely on the IMessageProcessor to execute the ConsumerErrorHandler, but if it's not registered in the DI, it fails, or there is another fatal error then the message will be lost. - _logger.LogError(r.Exception, "Exchange {Exchange} - Queue {Queue}: Error processing message {Message}, delivery tag {DeliveryTag}", transportMessage.Exchange, _consumer.QueueName, transportMessage, transportMessage.DeliveryTag); + _logger.LogError(r.Exception, "Exchange {Exchange} - Queue {Queue}: Error processing message {Message}, delivery tag {DeliveryTag}", transportMessage.Exchange, _consumer.Path, transportMessage, transportMessage.DeliveryTag); } return r; } diff --git a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs index e9018b58..f6ed3ec5 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Consumers/RabbitMqConsumer.cs @@ -1,8 +1,10 @@ namespace SlimMessageBus.Host.RabbitMQ; +using Microsoft.Extensions.DependencyInjection; + public interface IRabbitMqConsumer { - string QueueName { get; } + string Path { get; } void ConfirmMessage(BasicDeliverEventArgs transportMessage, RabbitMqMessageConfirmOptions option, IDictionary properties, bool warnIfAlreadyConfirmed = false); } @@ -24,7 +26,12 @@ public RabbitMqConsumer( MessageBusBase messageBus, MessageProvider messageProvider, IHeaderValueConverter headerValueConverter) - : base(loggerFactory.CreateLogger(), consumers, channel, queueName, headerValueConverter) + : base(loggerFactory.CreateLogger(), + consumers, + messageBus.Settings.ServiceProvider.GetServices(), + channel, + queueName, + headerValueConverter) { _acknowledgementMode = consumers.Select(x => x.GetOrDefault(RabbitMqProperties.MessageAcknowledgementMode, messageBus.Settings)).FirstOrDefault(x => x != null) ?? RabbitMqMessageAcknowledgementMode.ConfirmAfterMessageProcessingWhenNoManualConfirmMade; // be default choose the safer acknowledgement mode @@ -99,7 +106,7 @@ public void ConfirmMessage(BasicDeliverEventArgs transportMessage, RabbitMqMessa // Note: We want to makes sure the 1st message confirmation is handled if (warnIfAlreadyConfirmed) { - Logger.LogWarning("Exchange {Exchange} - Queue {Queue}: The message (delivery tag {MessageDeliveryTag}) was already confirmed, subsequent message confirmation will have no effect", transportMessage.Exchange, QueueName, transportMessage.DeliveryTag); + Logger.LogWarning("Exchange {Exchange} - Queue {Queue}: The message (delivery tag {MessageDeliveryTag}) was already confirmed, subsequent message confirmation will have no effect", transportMessage.Exchange, Path, transportMessage.DeliveryTag); } return; } @@ -139,7 +146,7 @@ protected override async Task OnMessageReceived(Dictionary _messageProcessor; @@ -8,6 +10,7 @@ public class RabbitMqResponseConsumer : AbstractRabbitMqConsumer public RabbitMqResponseConsumer( ILoggerFactory loggerFactory, + IEnumerable interceptors, IRabbitMqChannel channel, string queueName, RequestResponseSettings requestResponseSettings, @@ -15,7 +18,13 @@ public RabbitMqResponseConsumer( IPendingRequestStore pendingRequestStore, ICurrentTimeProvider currentTimeProvider, IHeaderValueConverter headerValueConverter) - : base(loggerFactory.CreateLogger(), [requestResponseSettings], channel, queueName, headerValueConverter) + + : base(loggerFactory.CreateLogger(), + [requestResponseSettings], + interceptors, + channel, + queueName, + headerValueConverter) { _messageProcessor = new ResponseMessageProcessor(loggerFactory, requestResponseSettings, messageProvider, pendingRequestStore, currentTimeProvider); } diff --git a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs index 5a354453..2e5d034c 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs @@ -1,4 +1,7 @@ namespace SlimMessageBus.Host.RabbitMQ; + +using Microsoft.Extensions.DependencyInjection; + public class RabbitMqMessageBus : MessageBusBase, IRabbitMqChannel { private readonly ILogger _logger; @@ -51,6 +54,7 @@ protected override async Task CreateConsumers() if (Settings.RequestResponse != null) { AddConsumer(new RabbitMqResponseConsumer(LoggerFactory, + interceptors: Settings.ServiceProvider.GetServices(), channel: this, queueName: Settings.RequestResponse.GetQueueName(), Settings.RequestResponse, diff --git a/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs b/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs index 8105bb1b..466b772a 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/RedisListCheckerConsumer.cs @@ -23,8 +23,17 @@ public QueueProcessors(string name, List> } } - public RedisListCheckerConsumer(ILogger logger, IDatabase database, TimeSpan? pollDelay, TimeSpan maxIdle, IEnumerable<(string QueueName, IMessageProcessor Processor)> queues, IMessageSerializer envelopeSerializer) - : base(logger, []) + public RedisListCheckerConsumer(ILogger logger, + IEnumerable interceptors, + IDatabase database, + TimeSpan? pollDelay, + TimeSpan maxIdle, + IEnumerable<(string QueueName, IMessageProcessor Processor)> queues, + IMessageSerializer envelopeSerializer) + : base(logger, + [], + path: string.Join("|", queues.Select(x => x.QueueName)), + interceptors) { _database = database; _pollDelay = pollDelay; diff --git a/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs b/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs index ad79f383..a336aee4 100644 --- a/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs +++ b/src/SlimMessageBus.Host.Redis/Consumers/RedisTopicConsumer.cs @@ -7,12 +7,18 @@ public class RedisTopicConsumer : AbstractConsumer, IRedisConsumer private ChannelMessageQueue _channelMessageQueue; private readonly IMessageProcessor _messageProcessor; - public string Path { get; } - - public RedisTopicConsumer(ILogger logger, IEnumerable consumerSettings, string topic, ISubscriber subscriber, IMessageProcessor messageProcessor, IMessageSerializer envelopeSerializer) - : base(logger, consumerSettings) + public RedisTopicConsumer(ILogger logger, + IEnumerable consumerSettings, + IEnumerable interceptors, + string topic, + ISubscriber subscriber, + IMessageProcessor messageProcessor, + IMessageSerializer envelopeSerializer) + : base(logger, + consumerSettings, + path: topic, + interceptors) { - Path = topic; _messageProcessor = messageProcessor; _envelopeSerializer = envelopeSerializer; _subscriber = subscriber ?? throw new ArgumentNullException(nameof(subscriber)); diff --git a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs index 1802eb3a..d7016aec 100644 --- a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs +++ b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs @@ -1,5 +1,7 @@ namespace SlimMessageBus.Host.Redis; +using Microsoft.Extensions.DependencyInjection; + public class RedisMessageBus : MessageBusBase { private readonly ILogger _logger; @@ -95,6 +97,7 @@ void AddTopicConsumer(IEnumerable consumerSettings, st var consumer = new RedisTopicConsumer( LoggerFactory.CreateLogger(), consumerSettings, + interceptors: Settings.ServiceProvider.GetServices(), topic, subscriber, messageProcessor, @@ -152,7 +155,7 @@ void AddTopicConsumer(IEnumerable consumerSettings, st if (queues.Count > 0) { - AddConsumer(new RedisListCheckerConsumer(LoggerFactory.CreateLogger(), Database, ProviderSettings.QueuePollDelay, ProviderSettings.QueuePollMaxIdle, queues, ProviderSettings.EnvelopeSerializer)); + AddConsumer(new RedisListCheckerConsumer(LoggerFactory.CreateLogger(), Settings.ServiceProvider.GetServices(), Database, ProviderSettings.QueuePollDelay, ProviderSettings.QueuePollMaxIdle, queues, ProviderSettings.EnvelopeSerializer)); } } diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index 16df001a..4a23fc6e 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc900 + 3.0.0-rc901 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs b/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs index 28745d69..c5c8d736 100644 --- a/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs +++ b/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs @@ -1,48 +1,98 @@ namespace SlimMessageBus.Host; -public abstract class AbstractConsumer : IAsyncDisposable, IConsumerControl +public abstract class AbstractConsumer : HasProviderExtensions, IAsyncDisposable, IConsumerControl { private readonly SemaphoreSlim _semaphore; - private readonly List _circuitBreakers; - + private readonly IReadOnlyList _interceptors; private CancellationTokenSource _cancellationTokenSource; private bool _starting; private bool _stopping; - public bool IsPaused { get; private set; } public bool IsStarted { get; private set; } - protected ILogger Logger { get; } - protected IReadOnlyList Settings { get; } + public string Path { get; } + public ILogger Logger { get; } + public IReadOnlyList Settings { get; } protected CancellationToken CancellationToken => _cancellationTokenSource.Token; - protected AbstractConsumer(ILogger logger, IEnumerable consumerSettings) + protected AbstractConsumer(ILogger logger, + IEnumerable consumerSettings, + string path, + IEnumerable interceptors) { _semaphore = new(1, 1); - _circuitBreakers = []; - + _interceptors = [.. interceptors.OrderBy(x => x.Order)]; Logger = logger; - Settings = consumerSettings.ToList(); + Settings = [.. consumerSettings]; + Path = path; } - public async Task Start() + private async Task CallInterceptor(Func> func) { - async Task StartCircuitBreakers() + foreach (var interceptor in _interceptors) { - var types = Settings.SelectMany(x => x.CircuitBreakers).Distinct(); - if (!types.Any()) + try { - return; + if (!await func(interceptor).ConfigureAwait(false)) + { + return false; + } } - - var sp = Settings.Select(x => x.MessageBusSettings.ServiceProvider).FirstOrDefault(x => x != null); - foreach (var type in types.Distinct()) + catch (Exception e) { - var breaker = (IConsumerCircuitBreaker)ActivatorUtilities.CreateInstance(sp, type, Settings); - _circuitBreakers.Add(breaker); - await breaker.Subscribe(BreakerChanged); + Logger.LogError(e, "Interceptor {Interceptor} failed with error: {Error}", interceptor.GetType().Name, e.Message); } } + return true; + } + + /// + /// Starts the underlying transport consumer (synchronized). + /// + /// + public async Task DoStart() + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + await InternalOnStart().ConfigureAwait(false); + } + finally + { + _semaphore.Release(); + } + } + + private async Task InternalOnStart() + { + await OnStart().ConfigureAwait(false); + await CallInterceptor(async x => { await x.Started(this); return true; }).ConfigureAwait(false); + } + + private async Task InternalOnStop() + { + await OnStop().ConfigureAwait(false); + await CallInterceptor(async x => { await x.Stopped(this); return true; }).ConfigureAwait(false); + } + + /// + /// Stops the underlying transport consumer (synchronized). + /// + /// + public async Task DoStop() + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + await InternalOnStop().ConfigureAwait(false); + } + finally + { + _semaphore.Release(); + } + } + public async Task Start() + { if (IsStarted || _starting) { return; @@ -58,11 +108,9 @@ async Task StartCircuitBreakers() _cancellationTokenSource = new CancellationTokenSource(); } - await StartCircuitBreakers(); - IsPaused = _circuitBreakers.Exists(x => x.State == Circuit.Closed); - if (!IsPaused) + if (await CallInterceptor(x => x.CanStart(this)).ConfigureAwait(false)) { - await OnStart().ConfigureAwait(false); + await InternalOnStart().ConfigureAwait(false); } IsStarted = true; @@ -76,25 +124,6 @@ async Task StartCircuitBreakers() public async Task Stop() { - async Task StopCircuitBreakers() - { - foreach (var breaker in _circuitBreakers) - { - breaker.Unsubscribe(); - - if (breaker is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else if (breaker is IDisposable disposable) - { - disposable.Dispose(); - } - } - - _circuitBreakers.Clear(); - } - if (!IsStarted || _stopping) { return; @@ -104,12 +133,11 @@ async Task StopCircuitBreakers() _stopping = true; try { - await _cancellationTokenSource.CancelAsync(); + await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); - await StopCircuitBreakers(); - if (!IsPaused) + if (await CallInterceptor(x => x.CanStop(this)).ConfigureAwait(false)) { - await OnStop().ConfigureAwait(false); + await InternalOnStop().ConfigureAwait(false); } IsStarted = false; @@ -121,8 +149,17 @@ async Task StopCircuitBreakers() } } - protected abstract Task OnStart(); - protected abstract Task OnStop(); + /// + /// Initializes the transport specific consumer loop after the consumer has been started. + /// + /// + internal protected abstract Task OnStart(); + + /// + /// Destroys the transport specific consumer loop before the consumer is stopped. + /// + /// + internal protected abstract Task OnStop(); #region IAsyncDisposable @@ -141,40 +178,4 @@ protected async virtual ValueTask DisposeAsyncCore() } #endregion - - async internal Task BreakerChanged(Circuit state) - { - await _semaphore.WaitAsync(); - try - { - if (!IsStarted) - { - return; - } - - var shouldPause = state == Circuit.Closed || _circuitBreakers.Exists(x => x.State == Circuit.Closed); - if (shouldPause != IsPaused) - { - var settings = Settings.Count > 0 ? Settings[0] : null; - var path = settings?.Path ?? "[unknown path]"; - var bus = settings?.MessageBusSettings?.Name ?? "default"; - if (shouldPause) - { - Logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); - await OnStop().ConfigureAwait(false); - } - else - { - Logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); - await OnStart().ConfigureAwait(false); - } - - IsPaused = shouldPause; - } - } - finally - { - _semaphore.Release(); - } - } } diff --git a/src/SlimMessageBus.Host/Consumer/IAbstractConsumerInterceptor.cs b/src/SlimMessageBus.Host/Consumer/IAbstractConsumerInterceptor.cs new file mode 100644 index 00000000..953be4d9 --- /dev/null +++ b/src/SlimMessageBus.Host/Consumer/IAbstractConsumerInterceptor.cs @@ -0,0 +1,33 @@ +namespace SlimMessageBus.Host; + +/// +/// Interceptor for consumers that are of type . +/// +public interface IAbstractConsumerInterceptor : IInterceptorWithOrder +{ + /// + /// Called to check if the consumer can be started. + /// + /// + /// True if the start is allowed + Task CanStart(AbstractConsumer consumer); + + /// + /// Called to check if the consumer can be stopped. + /// + /// + /// True if the stop is allowed + Task CanStop(AbstractConsumer consumer); + + /// + /// Called when the consumer is started. + /// + /// + Task Started(AbstractConsumer consumer); + + /// + /// Called when the consumer is stopped. + /// + /// + Task Stopped(AbstractConsumer consumer); +} diff --git a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs index 7378e100..8e3a4ae8 100644 --- a/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs +++ b/src/SlimMessageBus.Host/DependencyResolver/ServiceCollectionExtensions.cs @@ -26,7 +26,7 @@ public static IServiceCollection AddSlimMessageBus(this IServiceCollection servi configure(mbb); // Execute post config actions for the master bus and its children - foreach (var postConfigure in mbb.PostConfigurationActions.Concat(mbb.Children.Values.SelectMany(x => x.PostConfigurationActions))) + foreach (var postConfigure in mbb.GetPostConfigurationActions()) { postConfigure(services); } diff --git a/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj b/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj index 1bbb7a7a..b07b9317 100644 --- a/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj +++ b/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index 7c230bbb..ae2b40a4 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -259,7 +259,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Circuit EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test", "Tests\SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test\SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj", "{CA02D82E-DACC-4AB5-BD6B-071341E9E664}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.CircuitBreaker.HealthCheck", "Samples\Sample.CircuitBreaker.HealthCheck\Sample.CircuitBreaker.HealthCheck.csproj", "{226FC4F3-01EF-4214-9566-942CA0FB71B0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.CircuitBreaker.HealthCheck", "Samples\Sample.CircuitBreaker.HealthCheck\Sample.CircuitBreaker.HealthCheck.csproj", "{226FC4F3-01EF-4214-9566-942CA0FB71B0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.Outbox.Sql.Test", "Tests\SlimMessageBus.Host.Outbox.Sql.Test\SlimMessageBus.Host.Outbox.Sql.Test.csproj", "{CDF578D6-FE85-4A44-A99A-32490F047FDA}" EndProject @@ -282,6 +282,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.AmazonS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.AmazonSQS.Test", "Tests\SlimMessageBus.Host.AmazonSQS.Test\SlimMessageBus.Host.AmazonSQS.Test.csproj", "{9255A33D-9697-4E69-9418-AD31656FF8AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimMessageBus.Host.CircuitBreaker", "SlimMessageBus.Host.CircuitBreaker\SlimMessageBus.Host.CircuitBreaker.csproj", "{2FC8813B-D882-4B08-A886-5C6C6F8CB334}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimMessageBus.Host.CircuitBreaker.Test", "Tests\SlimMessageBus.Host.CircuitBreaker.Test\SlimMessageBus.Host.CircuitBreaker.Test.csproj", "{B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -890,6 +894,22 @@ Global {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|Any CPU.Build.0 = Release|Any CPU {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|x86.ActiveCfg = Release|Any CPU {9255A33D-9697-4E69-9418-AD31656FF8AC}.Release|x86.Build.0 = Release|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Debug|x86.ActiveCfg = Debug|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Debug|x86.Build.0 = Debug|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Release|Any CPU.Build.0 = Release|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Release|x86.ActiveCfg = Release|Any CPU + {2FC8813B-D882-4B08-A886-5C6C6F8CB334}.Release|x86.Build.0 = Release|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Debug|x86.Build.0 = Debug|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Release|Any CPU.Build.0 = Release|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Release|x86.ActiveCfg = Release|Any CPU + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -982,6 +1002,8 @@ Global {9FCBF788-1F0C-43E2-909D-1F96B2685F38} = {9F005B5C-A856-4351-8C0C-47A8B785C637} {4DF4BC7C-5EE3-4310-BC40-054C1494444E} = {9291D340-B4FA-44A3-8060-C14743FB1712} {9255A33D-9697-4E69-9418-AD31656FF8AC} = {9F005B5C-A856-4351-8C0C-47A8B785C637} + {2FC8813B-D882-4B08-A886-5C6C6F8CB334} = {FE36338C-0DA2-499E-92CA-F9D5CE594B9F} + {B05BA0C5-8E47-4361-8C62-BC6B8682B7AA} = {9F005B5C-A856-4351-8C0C-47A8B785C637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {435A0D65-610C-4B84-B1AA-2C7FBE72DB80} diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index c8a0c17f..569adb31 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc900 + 3.0.0-rc902 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs index 9434d91d..6bd34e2a 100644 --- a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/GlobalUsings.cs @@ -1,12 +1,7 @@ -global using System; -global using System.Collections.Generic; -global using System.Threading.Tasks; - -global using FluentAssertions; - -global using Microsoft.Extensions.Diagnostics.HealthChecks; -global using Microsoft.Extensions.Logging.Abstractions; - -global using Moq; - +global using FluentAssertions; + +global using Microsoft.Extensions.Diagnostics.HealthChecks; + +global using Moq; + global using Xunit; diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs index 35986e86..b83e0ab3 100644 --- a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckBackgroundServiceTests.cs @@ -1,5 +1,5 @@ -namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test; - +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test; + public static class HealthCheckBackgroundServiceTests { public class AreEqualTests diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs index 48499879..d7ce443a 100644 --- a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/HealthCheckCircuitBreakerTests.cs @@ -1,4 +1,7 @@ -namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test; +namespace SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test; + +using Microsoft.Extensions.Diagnostics.HealthChecks; + public class HealthCheckCircuitBreakerTests { private readonly Mock _hostMock; diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj index 909dcfbe..74f0b935 100644 --- a/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test/SlimMessageBus.Host.CircuitBreaker.HealthCheck.Test.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/GlobalUsings.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/GlobalUsings.cs new file mode 100644 index 00000000..820b49b3 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using FluentAssertions; + +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Abstractions; + +global using Xunit; diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs new file mode 100644 index 00000000..8003c373 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs @@ -0,0 +1,140 @@ +namespace SlimMessageBus.Host.CircuitBreaker.Test; + +public class CircuitBreakerAbstractConsumerInterceptorTests +{ + private class TestConsumer(ILogger logger, IEnumerable settings, IEnumerable interceptors) + : AbstractConsumer(logger, settings, "path", interceptors) + { + protected override Task OnStart() => Task.CompletedTask; + protected override Task OnStop() => Task.CompletedTask; + } + + private class TestConsumerSettings : AbstractConsumerSettings; + + public class CircuitBreakerAccessor + { + public Circuit State { get; set; } + public int SubscribeCallCount { get; set; } = 0; + public int UnsubscribeCallCount { get; set; } = 0; + public Func? OnChange { get; set; } + } + + private class TestCircuitBreaker : IConsumerCircuitBreaker + { + private readonly CircuitBreakerAccessor _accessor; + + public TestCircuitBreaker(CircuitBreakerAccessor accessor, IEnumerable settings) + { + _accessor = accessor; + Settings = settings; + State = Circuit.Open; + } + + public Circuit State + { + get => _accessor.State; + set => _accessor.State = value; + } + public IEnumerable Settings { get; } + + public Task Subscribe(Func onChange) + { + _accessor.SubscribeCallCount++; + _accessor.OnChange = onChange; + + return Task.CompletedTask; + } + + public void Unsubscribe() + { + _accessor.UnsubscribeCallCount++; + } + } + + private readonly List _settings; + private readonly TestConsumer _target; + private readonly CircuitBreakerAccessor accessor; + + public CircuitBreakerAbstractConsumerInterceptorTests() + { + accessor = new CircuitBreakerAccessor(); + + var h = new CircuitBreakerAbstractConsumerInterceptor(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.TryAddSingleton(accessor); + serviceCollection.TryAddTransient(); + serviceCollection.TryAddEnumerable(ServiceDescriptor.Singleton(h)); + + var testSettings = new TestConsumerSettings + { + MessageBusSettings = new MessageBusSettings { ServiceProvider = serviceCollection.BuildServiceProvider() } + }; + + var breakers = testSettings.GetOrCreate(ConsumerSettingsProperties.CircuitBreakerTypes, () => []); + breakers.Add(); + + _settings = [testSettings]; + + _target = new TestConsumer(NullLogger.Instance, _settings, [h]); + } + + [Fact] + public async Task When_Start_ShouldStartCircuitBreakers_WhenNotStarted() + { + // Arrange + + // Act + await _target.Start(); + + // Assert + _target.IsStarted.Should().BeTrue(); + accessor.SubscribeCallCount.Should().Be(1); + } + + [Fact] + public async Task When_Stop_ShouldStopCircuitBreakers_WhenStarted() + { + // Arrange + await _target.Start(); + + // Act + await _target.Stop(); + + // Assert + _target.IsStarted.Should().BeFalse(); + accessor.UnsubscribeCallCount.Should().Be(1); + } + + [Fact] + public async Task When_BreakerChanged_Should_PauseConsumer_Given_BreakerClosed() + { + // Arrange + await _target.Start(); + + // Act + _target.GetOrDefault(AbstractConsumerProperties.IsPaused, false).Should().BeFalse(); + accessor.State = Circuit.Closed; + await accessor.OnChange!(Circuit.Closed); + + // Assert + _target.GetOrDefault(AbstractConsumerProperties.IsPaused, false).Should().BeTrue(); + } + + [Fact] + public async Task When_BreakerChanged_Should_ResumeConsumer_Given_BreakerOpen() + { + // Arrange + await _target.Start(); + accessor.State = Circuit.Closed; + await accessor.OnChange!(Circuit.Open); + + // Act + _target.GetOrDefault(AbstractConsumerProperties.IsPaused, false).Should().BeTrue(); + accessor.State = Circuit.Open; + await accessor.OnChange(Circuit.Open); + + // Assert + _target.GetOrDefault(AbstractConsumerProperties.IsPaused, false).Should().BeFalse(); + } +} diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/SlimMessageBus.Host.CircuitBreaker.Test.csproj b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/SlimMessageBus.Host.CircuitBreaker.Test.csproj new file mode 100644 index 00000000..0d44b542 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/SlimMessageBus.Host.CircuitBreaker.Test.csproj @@ -0,0 +1,20 @@ + + + + + + enable + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs index 258fd280..32e70db3 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/Consumer/KafkaGroupConsumerTests.cs @@ -15,7 +15,7 @@ public KafkaGroupConsumerTests() var providerSettings = new KafkaMessageBusSettings("host"); var consumerSettings = Array.Empty(); - var subjectMock = new Mock(loggerFactoryMock.Object, providerSettings, consumerSettings, "group", new List { "topic" }, processorFactoryMock.Object) { CallBase = true }; + var subjectMock = new Mock(loggerFactoryMock.Object, providerSettings, consumerSettings, Array.Empty(), "group", new List { "topic" }, processorFactoryMock.Object) { CallBase = true }; _subject = subjectMock.Object; } diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs index 5d49be44..a05b74cf 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs @@ -2,138 +2,87 @@ public class AbstractConsumerTests { - private class TestConsumer : AbstractConsumer - { - public TestConsumer(ILogger logger, IEnumerable settings) - : base(logger, settings) { } - - protected override Task OnStart() => Task.CompletedTask; - protected override Task OnStop() => Task.CompletedTask; + private class TestConsumer(ILogger logger, IEnumerable settings, IEnumerable interceptors) + : AbstractConsumer(logger, settings, path: "path", interceptors) + { + internal protected override Task OnStart() => Task.CompletedTask; + internal protected override Task OnStop() => Task.CompletedTask; } private class TestConsumerSettings : AbstractConsumerSettings; - public class CircuitBreakerAccessor - { - public Circuit State { get; set; } - public int SubscribeCallCount { get; set; } = 0; - public int UnsubscribeCallCount { get; set; } = 0; - public IEnumerable Settings { get; set; } - public Func OnChange { get; set; } - } - - private class TestCircuitBreaker : IConsumerCircuitBreaker - { - private readonly CircuitBreakerAccessor _accessor; - - public TestCircuitBreaker(CircuitBreakerAccessor accessor, IEnumerable settings) - { - _accessor = accessor; - Settings = settings; - State = Circuit.Open; - } - - public Circuit State - { - get => _accessor.State; - set => _accessor.State = value; - } - public IEnumerable Settings { get; } - - public Task Subscribe(Func onChange) - { - _accessor.SubscribeCallCount++; - _accessor.OnChange = onChange; - - return Task.CompletedTask; - } - - public void Unsubscribe() - { - _accessor.UnsubscribeCallCount++; - } - } - - private readonly List _settings; - private readonly TestConsumer _target; - private readonly CircuitBreakerAccessor accessor; + private readonly List _settings; + private readonly Mock _targetMock; + private readonly AbstractConsumer _target; + private readonly Mock _interceptor; public AbstractConsumerTests() { - accessor = new CircuitBreakerAccessor(); + _interceptor = new Mock(); var serviceCollection = new ServiceCollection(); - serviceCollection.TryAddSingleton(accessor); - serviceCollection.TryAddTransient(); + serviceCollection.TryAddEnumerable(ServiceDescriptor.Singleton(_interceptor.Object)); var testSettings = new TestConsumerSettings { MessageBusSettings = new MessageBusSettings { ServiceProvider = serviceCollection.BuildServiceProvider() } }; - testSettings.CircuitBreakers.Add(); - _settings = [testSettings]; - _target = new TestConsumer(NullLogger.Instance, _settings); + _targetMock = new Mock(NullLogger.Instance, _settings, "path", new IAbstractConsumerInterceptor[] { _interceptor.Object }) { CallBase = true }; + _target = _targetMock.Object; } - [Fact] - public async Task Start_ShouldStartCircuitBreakers_WhenNotStarted() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_Start_Then_Interceptor_CanStartIsCalled(bool canStart) { // Arrange + _interceptor.Setup(x => x.CanStart(_target)).ReturnsAsync(canStart); // Act await _target.Start(); // Assert - _target.IsStarted.Should().BeTrue(); - accessor.SubscribeCallCount.Should().Be(1); - } - - [Fact] - public async Task Stop_ShouldStopCircuitBreakers_WhenStarted() + _target.IsStarted.Should().BeTrue(); + + _interceptor.Verify(x => x.CanStart(_target), Times.Once); + _interceptor.Verify(x => x.Started(_target), canStart ? Times.Once : Times.Never); + _interceptor.VerifyGet(x => x.Order, Times.Once); + _interceptor.VerifyNoOtherCalls(); + + _targetMock.Verify(x => x.OnStart(), canStart ? Times.Once : Times.Never); + _targetMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_Stop_Then_Interceptor_CanStopIsCalled(bool canStop) { // Arrange - await _target.Start(); + _interceptor.Setup(x => x.CanStart(_target)).ReturnsAsync(true); + _interceptor.Setup(x => x.CanStop(_target)).ReturnsAsync(canStop); + + await _target.Start(); // Act await _target.Stop(); // Assert - _target.IsStarted.Should().BeFalse(); - accessor.UnsubscribeCallCount.Should().Be(1); - } - - [Fact] - public async Task BreakerChanged_ShouldPauseConsumer_WhenBreakerClosed() - { - // Arrange - await _target.Start(); - - // Act - _target.IsPaused.Should().BeFalse(); - accessor.State = Circuit.Closed; - await _target.BreakerChanged(Circuit.Closed); - - // Assert - _target.IsPaused.Should().BeTrue(); - } - - [Fact] - public async Task BreakerChanged_ShouldResumeConsumer_WhenBreakerOpen() - { - // Arrange - await _target.Start(); - accessor.State = Circuit.Closed; - await _target.BreakerChanged(Circuit.Open); - - // Act - _target.IsPaused.Should().BeTrue(); - accessor.State = Circuit.Open; - await _target.BreakerChanged(Circuit.Open); - - // Assert - _target.IsPaused.Should().BeFalse(); + _target.IsStarted.Should().BeFalse(); + + _interceptor.Verify(x => x.CanStart(_target), Times.Once); + _interceptor.Verify(x => x.CanStop(_target), Times.Once); + _interceptor.Verify(x => x.Started(_target), Times.Once); + _interceptor.Verify(x => x.Stopped(_target), canStop ? Times.Once : Times.Never); + _interceptor.VerifyGet(x => x.Order, Times.Once); + _interceptor.VerifyNoOtherCalls(); + + _targetMock.Verify(x => x.OnStart(), Times.Once); + _targetMock.Verify(x => x.OnStop(), canStop ? Times.Once : Times.Never); + _targetMock.VerifyNoOtherCalls(); } } diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs index a1aeba2e..4217e9a7 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs @@ -81,10 +81,10 @@ public void TriggerPendingRequestCleanup() PendingRequestManager.CleanPendingRequests(); } - public class MessageBusTestedConsumer(ILogger logger) : AbstractConsumer(logger, []) + public class MessageBusTestedConsumer(ILogger logger) : AbstractConsumer(logger, [], "path", []) { - protected override Task OnStart() => Task.CompletedTask; + internal protected override Task OnStart() => Task.CompletedTask; - protected override Task OnStop() => Task.CompletedTask; + internal protected override Task OnStop() => Task.CompletedTask; } } From b3ce7246649f0b771b14607e5cdbec989c715e09 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Wed, 4 Dec 2024 00:38:29 +0100 Subject: [PATCH 16/21] Evolve benchmark Signed-off-by: Tomasz Maruszak --- src/Host.Plugin.Properties.xml | 2 +- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../Consumers/MessageProcessorQueue.cs | 5 +- .../AvroMessageSerializer.cs | 10 ++-- .../SlimMessageBus.Host.Serialization.csproj | 2 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- .../AbstractMemoryBenchmark.cs | 42 +++++++++++------ .../PubSubBenchmark.cs | 32 ++++++------- .../README.md | 46 +++++++++++++++++++ .../ReqRespBenchmark.cs | 37 +++++++-------- 11 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index a1eda2f8..6953d847 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc902 + 3.0.0-rc903 \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index b01a3b49..c601cb53 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc901 + 3.0.0-rc903 diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index b98b06a5..93e2c206 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc901 + 3.0.0-rc903 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Memory/Consumers/MessageProcessorQueue.cs b/src/SlimMessageBus.Host.Memory/Consumers/MessageProcessorQueue.cs index 5177abbc..090771fa 100644 --- a/src/SlimMessageBus.Host.Memory/Consumers/MessageProcessorQueue.cs +++ b/src/SlimMessageBus.Host.Memory/Consumers/MessageProcessorQueue.cs @@ -1,6 +1,9 @@ namespace SlimMessageBus.Host.Memory; -public class MessageProcessorQueue(IMessageProcessor messageProcessor, ILogger logger, CancellationToken cancellationToken) : AbstractMessageProcessorQueue(messageProcessor, logger) +public class MessageProcessorQueue(IMessageProcessor messageProcessor, + ILogger logger, + CancellationToken cancellationToken) + : AbstractMessageProcessorQueue(messageProcessor, logger) { private readonly object _prevTaskLock = new(); private Task _prevTask = null; diff --git a/src/SlimMessageBus.Host.Serialization.Avro/AvroMessageSerializer.cs b/src/SlimMessageBus.Host.Serialization.Avro/AvroMessageSerializer.cs index 8b49db64..35c48e16 100644 --- a/src/SlimMessageBus.Host.Serialization.Avro/AvroMessageSerializer.cs +++ b/src/SlimMessageBus.Host.Serialization.Avro/AvroMessageSerializer.cs @@ -52,8 +52,8 @@ public AvroMessageSerializer(ILoggerFactory loggerFactory = null) var mf = new ReflectionMessageCreationStrategy(loggerFactory.CreateLogger()); var ml = new ReflectionSchemaLookupStrategy(loggerFactory.CreateLogger()); - MessageFactory = (Type type) => mf.Create(type); - WriteSchemaLookup = (Type type) => ml.Lookup(type); + MessageFactory = mf.Create; + WriteSchemaLookup = ml.Lookup; ReadSchemaLookup = WriteSchemaLookup; } @@ -73,7 +73,7 @@ public AvroMessageSerializer(ILoggerFactory loggerFactory, IMessageCreationStrat public object Deserialize(Type t, byte[] payload) { using var ms = ReadMemoryStreamFactory(payload); - + var dec = new BinaryDecoder(ms); var message = MessageFactory(t); @@ -84,7 +84,7 @@ public object Deserialize(Type t, byte[] payload) var writerSchema = WriteSchemaLookup(t); AssertSchemaNotNull(t, writerSchema, true); - _logger.LogDebug("Type {0} writer schema: {1}, reader schema: {2}", t, writerSchema, readerSchema); + _logger.LogDebug("Type {Type} writer schema: {WriterSchema}, reader schema: {ReaderSchema}", t, writerSchema, readerSchema); var reader = new SpecificDefaultReader(writerSchema, readerSchema); reader.Read(message, dec); @@ -108,7 +108,7 @@ public byte[] Serialize(Type t, object message) var writerSchema = WriteSchemaLookup(t); AssertSchemaNotNull(t, writerSchema, true); - _logger.LogDebug("Type {0} writer schema: {1}", t, writerSchema); + _logger.LogDebug("Type {Type} writer schema: {WriterSchema}", t, writerSchema); var writer = new SpecificDefaultWriter(writerSchema); // Schema comes from pre-compiled, code-gen phase writer.Write(message, enc); diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index 4a23fc6e..cb4aaa3e 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc901 + 3.0.0-rc903 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index 569adb31..54d2cc6e 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc902 + 3.0.0-rc903 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/AbstractMemoryBenchmark.cs b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/AbstractMemoryBenchmark.cs index a60f2d16..505ea7ee 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/AbstractMemoryBenchmark.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/AbstractMemoryBenchmark.cs @@ -1,29 +1,41 @@ namespace SlimMessageBus.Host.Memory.Benchmark; +using System.Reflection; + using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using SlimMessageBus.Host; -using System.Reflection; - public abstract class AbstractMemoryBenchmark : IDisposable { - protected ServiceProvider svp; - protected readonly IMessageBus bus; + private Lazy _serviceProvider; + + protected IServiceProvider ServiceProvider => _serviceProvider.Value; + + protected bool PerMessageScopeEnabled { get; set; } + + protected IMessageBus Bus => ServiceProvider.GetRequiredService(); protected AbstractMemoryBenchmark() { - var services = new ServiceCollection(); - - services.AddSlimMessageBus(mbb => mbb.WithProviderMemory().AutoDeclareFrom(Assembly.GetExecutingAssembly())); + _serviceProvider = new Lazy(() => + { + var services = new ServiceCollection(); - services.AddSingleton(); - services.AddTransient(); - Setup(services); + services.AddSlimMessageBus(mbb => mbb + .WithProviderMemory() + .AutoDeclareFrom(Assembly.GetExecutingAssembly()) + .PerMessageScopeEnabled(PerMessageScopeEnabled)); - svp = services.BuildServiceProvider(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(); + services.AddTransient(); + Setup(services); - bus = svp.GetRequiredService(); + return services.BuildServiceProvider(); + }); } protected virtual void Setup(ServiceCollection services) @@ -32,10 +44,10 @@ protected virtual void Setup(ServiceCollection services) public void Dispose() { - if (svp != null) + if (_serviceProvider.Value != null) { - svp.Dispose(); - svp = null; + _serviceProvider.Value.Dispose(); + _serviceProvider = null; } } } diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs index d9ae5834..b3b622e3 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/PubSubBenchmark.cs @@ -8,25 +8,21 @@ public abstract class PubSubBaseBenchmark : AbstractMemoryBenchmark { - private readonly TestResult testResult; - - public PubSubBaseBenchmark() - { - testResult = svp.GetRequiredService(); - } - protected override void Setup(ServiceCollection services) { services.AddSingleton(); services.AddTransient(); } - protected async Task RunTest(int messageCount) + protected async Task RunTest(int messageCount, bool createMessageScope) { + PerMessageScopeEnabled = createMessageScope; + var bus = Bus; var publishTasks = Enumerable.Range(0, messageCount).Select(x => bus.Publish(new SomeEvent(DateTimeOffset.Now, x))); await Task.WhenAll(publishTasks); + var testResult = ServiceProvider.GetRequiredService(); while (testResult.ArrivedCount < messageCount) { await Task.Yield(); @@ -38,8 +34,9 @@ protected async Task RunTest(int messageCount) public class PubSubBenchmark : PubSubBaseBenchmark { [Benchmark] - [Arguments(1000000)] - public Task PubSub(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task PubSub(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -53,8 +50,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task PubSubWithProducerInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task PubSubWithProducerInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -68,8 +66,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task PubSubWithPublishInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task PubSubWithPublishInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -83,8 +82,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task PubSubWithConsumerInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task PubSubWithConsumerInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } public record SomeEvent(DateTimeOffset Timestamp, long Id); diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md new file mode 100644 index 00000000..f49fbebc --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md @@ -0,0 +1,46 @@ +Sample Benchmark results + +``` +// * Summary * + +BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2454) +12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores +.NET SDK 9.0.100 + [Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2 + Job-WFUQPN : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2 + +MaxIterationCount=30 MaxWarmupIterationCount=10 +``` + +| Type | Method | messageCount | createMessageScope | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +| -------------------------------------- | ----------------------------- | ------------ | ------------------ | -------: | -------: | -------: | ----------: | --------: | --------: | --------: | +| PubSubBenchmark | PubSub | 1000000 | False | 730.6 ms | 14.58 ms | 17.36 ms | 119000.0000 | 3000.0000 | 3000.0000 | 1.04 GB | +| PubSubWithConsumerInterceptorBenchmark | PubSubWithConsumerInterceptor | 1000000 | False | 810.5 ms | 16.16 ms | 16.59 ms | 146000.0000 | 3000.0000 | 3000.0000 | 1.28 GB | +| PubSubWithProducerInterceptorBenchmark | PubSubWithProducerInterceptor | 1000000 | False | 823.5 ms | 7.74 ms | 6.86 ms | 156000.0000 | 3000.0000 | 3000.0000 | 1.37 GB | +| PubSubWithPublishInterceptorBenchmark | PubSubWithPublishInterceptor | 1000000 | False | 831.8 ms | 9.43 ms | 7.87 ms | 156000.0000 | 3000.0000 | 3000.0000 | 1.37 GB | +| PubSubBenchmark | PubSub | 1000000 | True | 794.8 ms | 5.44 ms | 4.54 ms | 137000.0000 | 3000.0000 | 3000.0000 | 1.2 GB | +| PubSubWithConsumerInterceptorBenchmark | PubSubWithConsumerInterceptor | 1000000 | True | 900.9 ms | 11.65 ms | 10.32 ms | 164000.0000 | 3000.0000 | 3000.0000 | 1.44 GB | +| PubSubWithProducerInterceptorBenchmark | PubSubWithProducerInterceptor | 1000000 | True | 934.5 ms | 14.15 ms | 13.24 ms | 174000.0000 | 3000.0000 | 3000.0000 | 1.53 GB | +| PubSubWithPublishInterceptorBenchmark | PubSubWithPublishInterceptor | 1000000 | True | 930.6 ms | 14.29 ms | 13.37 ms | 174000.0000 | 3000.0000 | 3000.0000 | 1.53 GB | + +``` +// * Hints * + +Outliers + PubSubWithProducerInterceptorBenchmark.PubSubWithProducerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 1 outlier was removed (840.20 ms) + PubSubWithPublishInterceptorBenchmark.PubSubWithPublishInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 2 outliers were removed (862.16 ms, 863.90 ms) + PubSubBenchmark.PubSub: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 2 outliers were removed (810.49 ms, 823.65 ms) + PubSubWithConsumerInterceptorBenchmark.PubSubWithConsumerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 1 outlier was removed (947.16 ms) + +// * Legends * + messageCount : Value of the 'messageCount' parameter + createMessageScope : Value of the 'createMessageScope' parameter + Mean : Arithmetic mean of all measurements + Error : Half of 99.9% confidence interval + StdDev : Standard deviation of all measurements + Gen0 : GC Generation 0 collects per 1000 operations + Gen1 : GC Generation 1 collects per 1000 operations + Gen2 : GC Generation 2 collects per 1000 operations + Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) + 1 ms : 1 Millisecond (0.001 sec) +``` diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs index c3dfeb82..2c379f03 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/ReqRespBenchmark.cs @@ -8,25 +8,21 @@ public abstract class ReqRespBaseBenchmark : AbstractMemoryBenchmark { - private readonly TestResult testResult; - - protected ReqRespBaseBenchmark() - { - testResult = svp.GetRequiredService(); - } - protected override void Setup(ServiceCollection services) { services.AddSingleton(); services.AddTransient(); } - public async Task RunTest(int messageCount) + public async Task RunTest(int messageCount, bool createMessageScope) { + PerMessageScopeEnabled = createMessageScope; + var bus = Bus; var sendRequests = Enumerable.Range(0, messageCount).Select(x => bus.Send(new SomeRequest(DateTimeOffset.Now, x))); await Task.WhenAll(sendRequests); + var testResult = ServiceProvider.GetRequiredService(); while (testResult.ArrivedCount < messageCount) { await Task.Yield(); @@ -38,8 +34,9 @@ public async Task RunTest(int messageCount) public class ReqRespBenchmark : ReqRespBaseBenchmark { [Benchmark] - [Arguments(1000000)] - public Task RequestResponse(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task RequestResponse(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -53,8 +50,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task ReqRespWithProducerInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task ReqRespWithProducerInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -68,8 +66,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task ReqRespWithSendInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task ReqRespWithSendInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -83,8 +82,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task ReqRespWithConsumerInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task ReqRespWithConsumerInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } [MemoryDiagnoser] @@ -98,8 +98,9 @@ protected override void Setup(ServiceCollection services) } [Benchmark] - [Arguments(1000000)] - public Task ReqRespWithRequestHandlerInterceptor(int messageCount) => RunTest(messageCount); + [Arguments(1000000, true)] + [Arguments(1000000, false)] + public Task ReqRespWithRequestHandlerInterceptor(int messageCount, bool createMessageScope) => RunTest(messageCount, createMessageScope); } public record SomeRequest(DateTimeOffset Timestamp, long Id) : IRequest; From dd54670bb1d3ff10400d8bdfaed3d063a409c8b7 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sun, 12 Jan 2025 00:46:35 +0100 Subject: [PATCH 17/21] High performance logging Signed-off-by: Tomasz Maruszak --- src/Host.Plugin.Properties.xml | 2 +- ...ontextAccessorCurrentMessageBusProvider.cs | 28 ++- ...rcuitBreakerAbstractConsumerInterceptor.cs | 6 +- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../AbstractMessageProcessorQueue.cs | 26 ++- .../MemoryMessageBus.cs | 29 ++- .../SlimMessageBus.Host.Serialization.csproj | 2 +- .../Collections/KindMapping.cs | 4 +- .../Collections/ProducerByMessageTypeCache.cs | 37 +++- .../Consumer/AbstractConsumer.cs | 27 ++- .../Checkpointing/CheckpointTrigger.cs | 33 ++- .../ConcurrentMessageProcessorDecorator.cs | 38 +++- .../MessageProcessors/MessageHandler.cs | 41 +++- .../MessageProcessors/MessageProcessor.cs | 82 ++++++- .../ResponseMessageProcessor.cs | 70 +++++- .../Helpers/CompatAttributes.cs | 45 ++++ .../Helpers/CompatMethods.cs | 5 +- .../Helpers/CompatRecord.cs | 2 +- .../{ => Helpers}/Retry.cs | 9 - src/SlimMessageBus.Host/Helpers/Utils.cs | 22 +- .../Hybrid/HybridMessageBus.cs | 36 +++- src/SlimMessageBus.Host/MessageBusBase.cs | 204 +++++++++++++++--- .../RequestResponse/PendingRequestManager.cs | 27 ++- .../RequestResponse/PendingRequestState.cs | 9 +- .../Services/MessageHeaderService.cs | 25 ++- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- ...BreakerAbstractConsumerInterceptorTests.cs | 2 +- .../README.md | 46 ++-- .../MemoryMessageBusTests.cs | 32 +++ .../Consumer/AbstractConsumerTests.cs | 53 ++++- .../Consumer/ResponseMessageProcessorTest.cs | 151 +++++++++++++ .../Hybrid/HybridMessageBusTest.cs | 7 +- .../ReqestResponse/PendingRequestStateTest.cs | 21 ++ 34 files changed, 963 insertions(+), 164 deletions(-) create mode 100644 src/SlimMessageBus.Host/Helpers/CompatAttributes.cs rename src/SlimMessageBus.Host/{ => Helpers}/Retry.cs (88%) create mode 100644 src/Tests/SlimMessageBus.Host.Test/Consumer/ResponseMessageProcessorTest.cs create mode 100644 src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestStateTest.cs diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index 6953d847..52d1bb2e 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc903 + 3.0.0-rc904 \ No newline at end of file diff --git a/src/SlimMessageBus.Host.AspNetCore/HttpContextAccessorCurrentMessageBusProvider.cs b/src/SlimMessageBus.Host.AspNetCore/HttpContextAccessorCurrentMessageBusProvider.cs index ef231f89..2bd0f717 100644 --- a/src/SlimMessageBus.Host.AspNetCore/HttpContextAccessorCurrentMessageBusProvider.cs +++ b/src/SlimMessageBus.Host.AspNetCore/HttpContextAccessorCurrentMessageBusProvider.cs @@ -3,24 +3,46 @@ /// /// Resolves the from the current ASP.NET Core web request (if present, otherwise falls back to the application root container). /// -public class HttpContextAccessorCurrentMessageBusProvider( +public partial class HttpContextAccessorCurrentMessageBusProvider( ILogger logger, IHttpContextAccessor httpContextAccessor, IServiceProvider serviceProvider) : CurrentMessageBusProvider(serviceProvider) { + private readonly ILogger _logger = logger; + public override IMessageBus GetCurrent() { // When the call to resolve the given type is made within an HTTP Request, use the request scope service provider var httpContext = httpContextAccessor?.HttpContext; if (httpContext != null) { - logger.LogTrace("The type IMessageBus will be requested from the per-request scope"); + LogCurrentFrom("request"); return httpContext.RequestServices.GetService(); } // otherwise use the app wide scope provider - logger.LogTrace("The type IMessageBus will be requested from the app scope"); + LogCurrentFrom("root"); return base.GetCurrent(); } + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "The type IMessageBus will be requested from the {ScopeName} scope")] + private partial void LogCurrentFrom(string scopeName); + + #endregion } + +#if NETSTANDARD2_0 + +public partial class HttpContextAccessorCurrentMessageBusProvider +{ + private partial void LogCurrentFrom(string scopeName) + => _logger.LogTrace("The type IMessageBus will be requested from the {ScopeName} scope", scopeName); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs index 30658f06..f46f14d4 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs @@ -3,7 +3,7 @@ /// /// Circuit breaker to toggle consumer status on an external events. /// -internal sealed class CircuitBreakerAbstractConsumerInterceptor : IAbstractConsumerInterceptor +internal sealed class CircuitBreakerAbstractConsumerInterceptor(ILogger logger) : IAbstractConsumerInterceptor { public int Order => 100; @@ -33,12 +33,12 @@ async Task BreakerChanged(Circuit state) var bus = consumer.Settings[0].MessageBusSettings.Name ?? "default"; if (shouldPause) { - consumer.Logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); + logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); await consumer.DoStop().ConfigureAwait(false); } else { - consumer.Logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); + logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); await consumer.DoStart().ConfigureAwait(false); } consumer.SetIsPaused(shouldPause); diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index c601cb53..46567f42 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc903 + 3.0.0-rc904 diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index 93e2c206..e44ce8c0 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc903 + 3.0.0-rc904 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Memory/Consumers/AbstractMessageProcessorQueue.cs b/src/SlimMessageBus.Host.Memory/Consumers/AbstractMessageProcessorQueue.cs index 4ceb8ce0..d634c221 100644 --- a/src/SlimMessageBus.Host.Memory/Consumers/AbstractMessageProcessorQueue.cs +++ b/src/SlimMessageBus.Host.Memory/Consumers/AbstractMessageProcessorQueue.cs @@ -1,7 +1,9 @@ namespace SlimMessageBus.Host.Memory; -public abstract class AbstractMessageProcessorQueue(IMessageProcessor messageProcessor, ILogger logger) : IMessageProcessorQueue +public abstract partial class AbstractMessageProcessorQueue(IMessageProcessor messageProcessor, ILogger logger) : IMessageProcessorQueue { + private readonly ILogger _logger = logger; + public abstract void Enqueue(object transportMessage, IReadOnlyDictionary messageHeaders); protected async Task ProcessMessage(object transportMessage, IReadOnlyDictionary messageHeaders, CancellationToken cancellationToken) @@ -23,7 +25,27 @@ protected async Task ProcessMessage(object transportMessage, IReadOnlyDictionary if (r.Exception != null) { // We rely on the IMessageProcessor to execute the ConsumerErrorHandler, but if it's not registered in the DI, it fails, or there is another fatal error then the message will be lost. - logger.LogError(r.Exception, "Error processing message {Message} of type {MessageType}", transportMessage, transportMessage.GetType()); + LogMessageError(transportMessage, transportMessage.GetType(), r.Exception); } } + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Error processing message {TransportMessage} of type {TransportMessageType}")] + private partial void LogMessageError(object transportMessage, Type transportMessageType, Exception e); + + #endregion } + +#if NETSTANDARD2_0 + +public abstract partial class AbstractMessageProcessorQueue +{ + private partial void LogMessageError(object transportMessage, Type transportMessageType, Exception e) + => _logger.LogError(e, "Error processing message {TransportMessage} of type {TransportMessageType}", transportMessage, transportMessageType); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs index 20b36e1a..44d1a7af 100644 --- a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs +++ b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs @@ -5,7 +5,7 @@ /// /// In-memory message bus implementation to use for in process message passing. /// -public class MemoryMessageBus : MessageBusBase +public partial class MemoryMessageBus : MessageBusBase { private readonly ILogger _logger; private IDictionary> _messageProcessorByPath; @@ -63,11 +63,8 @@ public override IDictionary CreateHeaders() public override bool IsMessageScopeEnabled(ConsumerSettings consumerSettings, IDictionary consumerContextProperties) { -#if NETSTANDARD2_0 if (consumerSettings is null) throw new ArgumentNullException(nameof(consumerSettings)); -#else - ArgumentNullException.ThrowIfNull(consumerSettings); -#endif + if (consumerContextProperties != null && consumerContextProperties.ContainsKey(MemoryMessageBusProperties.CreateScope)) { return true; @@ -133,7 +130,7 @@ private async Task ProduceInternal(object me path ??= GetDefaultPath(producerSettings.MessageType, producerSettings); if (!_messageProcessorByPath.TryGetValue(path, out var messageProcessor)) { - _logger.LogDebug("No consumers interested in message type {MessageType} on path {Path}", messageType, path); + LogNoConsumerInterestedInMessageType(path, messageType); return default; } @@ -165,4 +162,24 @@ private async Task ProduceInternal(object me } return (TResponseMessage)r.Response; } + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "No consumers interested in message type {MessageType} on path {Path}")] + private partial void LogNoConsumerInterestedInMessageType(string path, Type messageType); + + #endregion } + +#if NETSTANDARD2_0 + +public partial class MemoryMessageBus +{ + private partial void LogNoConsumerInterestedInMessageType(string path, Type messageType) + => _logger.LogDebug("No consumers interested in message type {MessageType} on path {Path}", messageType, path); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index cb4aaa3e..ca482ff8 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc903 + 3.0.0-rc904 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host/Collections/KindMapping.cs b/src/SlimMessageBus.Host/Collections/KindMapping.cs index 21acf909..a14f6bb5 100644 --- a/src/SlimMessageBus.Host/Collections/KindMapping.cs +++ b/src/SlimMessageBus.Host/Collections/KindMapping.cs @@ -2,8 +2,8 @@ public class KindMapping { - private readonly Dictionary _kindByTopic = new(); - private readonly Dictionary _kindByMessageType = new(); + private readonly Dictionary _kindByTopic = []; + private readonly Dictionary _kindByMessageType = []; public void Configure(MessageBusSettings settings) { diff --git a/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs b/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs index ff6e2bcb..614416a2 100644 --- a/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs +++ b/src/SlimMessageBus.Host/Collections/ProducerByMessageTypeCache.cs @@ -5,7 +5,7 @@ /// The message type hierarchy is discovered at runtime and cached for faster access. /// /// The producer type -public class ProducerByMessageTypeCache : IReadOnlyCache +public partial class ProducerByMessageTypeCache : IReadOnlyCache where TProducer : class { private readonly ILogger _logger; @@ -37,7 +37,7 @@ private TProducer CalculateProducer(Type messageType) var assignableProducer = assignableProducers.FirstOrDefault(); if (assignableProducer.Key != null) { - _logger.LogDebug("Matched producer for message type {ProducerMessageType} for dispatched message type {MessageType}", assignableProducer.Key, messageType); + LogMatchedProducerForMessageType(messageType, assignableProducer.Key); return assignableProducer.Value; } @@ -52,7 +52,7 @@ private TProducer CalculateProducer(Type messageType) } } - _logger.LogDebug("Unable to match any declared producer for dispatched message type {MessageType}", messageType); + LogUnmatchedProducerForMessageType(messageType); // Note: Nulls are also added to dictionary, so that we don't look them up using reflection next time (cached). return null; @@ -74,4 +74,33 @@ private static int CalculateBaseClassDistance(Type type, Type baseType) return distance; } -} \ No newline at end of file + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Matched producer for message type {ProducerMessageType} for dispatched message type {MessageType}")] + private partial void LogMatchedProducerForMessageType(Type messageType, Type producerMessageType); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "Unable to match any declared producer for dispatched message type {MessageType}")] + private partial void LogUnmatchedProducerForMessageType(Type messageType); + + #endregion +} + +#if NETSTANDARD2_0 + +public partial class ProducerByMessageTypeCache +{ + private partial void LogMatchedProducerForMessageType(Type messageType, Type producerMessageType) + => _logger.LogDebug("Matched producer for message type {ProducerMessageType} for dispatched message type {MessageType}", producerMessageType, messageType); + + private partial void LogUnmatchedProducerForMessageType(Type messageType) + => _logger.LogDebug("Unable to match any declared producer for dispatched message type {MessageType}", messageType); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs b/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs index c5c8d736..364485c0 100644 --- a/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs +++ b/src/SlimMessageBus.Host/Consumer/AbstractConsumer.cs @@ -1,7 +1,8 @@ namespace SlimMessageBus.Host; -public abstract class AbstractConsumer : HasProviderExtensions, IAsyncDisposable, IConsumerControl +public abstract partial class AbstractConsumer : HasProviderExtensions, IAsyncDisposable, IConsumerControl { + protected readonly ILogger Logger; private readonly SemaphoreSlim _semaphore; private readonly IReadOnlyList _interceptors; private CancellationTokenSource _cancellationTokenSource; @@ -10,7 +11,6 @@ public abstract class AbstractConsumer : HasProviderExtensions, IAsyncDisposable public bool IsStarted { get; private set; } public string Path { get; } - public ILogger Logger { get; } public IReadOnlyList Settings { get; } protected CancellationToken CancellationToken => _cancellationTokenSource.Token; @@ -39,7 +39,7 @@ private async Task CallInterceptor(Func Logger.LogError(ex, "Interceptor {InterceptorType} failed with error: {Error}", interceptorType, error); } + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/Checkpointing/CheckpointTrigger.cs b/src/SlimMessageBus.Host/Consumer/Checkpointing/CheckpointTrigger.cs index 49f474a9..7787b853 100644 --- a/src/SlimMessageBus.Host/Consumer/Checkpointing/CheckpointTrigger.cs +++ b/src/SlimMessageBus.Host/Consumer/Checkpointing/CheckpointTrigger.cs @@ -2,7 +2,7 @@ using System.Diagnostics; -public class CheckpointTrigger : ICheckpointTrigger +public partial class CheckpointTrigger : ICheckpointTrigger { private readonly ILogger _logger; @@ -35,7 +35,6 @@ public static CheckpointValue GetCheckpointValue(HasProviderExtensions settings) => new(settings.GetOrDefault(CheckpointSettings.CheckpointCount, CheckpointSettings.CheckpointCountDefault), settings.GetOrDefault(CheckpointSettings.CheckpointDuration, CheckpointSettings.CheckpointDurationDefault)); - #region Implementation of ICheckpointTrigger public bool IsEnabled @@ -53,17 +52,37 @@ public bool Increment() var enabled = IsEnabled; if (enabled && _logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Checkpoint triggered after Count: {CheckpointCount}, Duration: {CheckpointDuration} (s)", _lastCheckpointCount, _lastCheckpointDuration.Elapsed.Seconds); + LogCheckpointTriggered(_lastCheckpointCount, _lastCheckpointDuration.Elapsed.Seconds); } return enabled; - } - + } + public void Reset() { _lastCheckpointCount = 0; _lastCheckpointDuration.Restart(); - } - + } + + #endregion + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Checkpoint triggered after Count: {CheckpointCount}, Duration: {CheckpointDuration} (s)")] + private partial void LogCheckpointTriggered(int checkpointCount, int checkpointDuration); + #endregion } + +#if NETSTANDARD2_0 + +public partial class CheckpointTrigger +{ + private partial void LogCheckpointTriggered(int checkpointCount, int checkpointDuration) + => _logger.LogDebug("Checkpoint triggered after Count: {CheckpointCount}, Duration: {CheckpointDuration} (s)", checkpointCount, checkpointDuration); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs index 0fba6f97..5c0230ae 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ConcurrentMessageProcessorDecorator.cs @@ -5,7 +5,7 @@ /// The expectation is that will be executed synchronously (in sequential order) by the caller on which we want to increase amount of concurrent transportMessage being processed. /// /// -public sealed class ConcurrentMessageProcessorDecorator : IMessageProcessor, IDisposable +public sealed partial class ConcurrentMessageProcessorDecorator : IMessageProcessor, IDisposable { private readonly ILogger _logger; private SemaphoreSlim _concurrentSemaphore; @@ -87,7 +87,8 @@ private async Task ProcessInBackground(TMessage transportMessage, IReadOnlyDicti { try { - _logger.LogDebug("Entering ProcessMessages for message {MessageType}", typeof(TMessage)); + LogEntering(typeof(TMessage)); + var r = await _target.ProcessMessage(transportMessage, messageHeaders, consumerContextProperties, currentServiceProvider, cancellationToken).ConfigureAwait(false); if (r.Exception != null) { @@ -105,10 +106,39 @@ private async Task ProcessInBackground(TMessage transportMessage, IReadOnlyDicti } finally { - _logger.LogDebug("Leaving ProcessMessages for message {MessageType}", typeof(TMessage)); + LogLeaving(typeof(TMessage)); _concurrentSemaphore?.Release(); Interlocked.Decrement(ref _pendingCount); } } -} \ No newline at end of file + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Entering ProcessMessages for message {MessageType}")] + private partial void LogEntering(Type messageType); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "Leaving ProcessMessages for message {MessageType}")] + private partial void LogLeaving(Type messageType); + + #endregion +} + +#if NETSTANDARD2_0 + +public partial class ConcurrentMessageProcessorDecorator +{ + private partial void LogEntering(Type messageType) + => _logger.LogDebug("Entering ProcessMessages for message {MessageType}", messageType); + + private partial void LogLeaving(Type messageType) + => _logger.LogDebug("Leaving ProcessMessages for message {MessageType}", messageType); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index 62d873ae..e785d199 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -2,7 +2,7 @@ using SlimMessageBus.Host.Consumer; -public class MessageHandler : IMessageHandler +public partial class MessageHandler : IMessageHandler { private readonly ILogger _logger; private readonly IMessageScopeFactory _messageScopeFactory; @@ -31,11 +31,7 @@ public MessageHandler( string path, Type consumerErrorHandlerOpenGenericType = null) { -#if NETSTANDARD2_0 if (messageBus is null) throw new ArgumentNullException(nameof(messageBus)); -#else - ArgumentNullException.ThrowIfNull(messageBus); -#endif _logger = messageBus.LoggerFactory.CreateLogger(); _messageScopeFactory = messageScopeFactory; @@ -127,7 +123,7 @@ public MessageHandler( { if (consumerInvoker.ParentSettings.IsDisposeConsumerEnabled && consumerInstance is IDisposable consumerInstanceDisposable) { - _logger.LogDebug("Disposing consumer instance {Consumer} of type {ConsumerType}", consumerInstance, consumerType); + LogDisposingConsumer(consumerType, consumerInstance); consumerInstanceDisposable.DisposeSilently("ConsumerInstance", _logger); } } @@ -162,7 +158,7 @@ private async Task DoHandleError(object message, Type messageType if (consumerErrorHandler != null) { - _logger.LogDebug(ex, "Consumer error handler of type {ConsumerErrorHandlerType} will be used to handle the exception during processing of message of type {MessageType}", consumerErrorHandler.GetType(), messageType); + LogConsumerErrorHandlerWillBeUsed(messageType, consumerErrorHandler.GetType(), ex); var consumerErrorHandlerMethod = RuntimeTypeCache.ConsumerErrorHandlerType[messageType]; errorHandlerResult = await consumerErrorHandlerMethod(consumerErrorHandler, message, consumerContext, ex, attempts).ConfigureAwait(false); @@ -211,4 +207,33 @@ public async Task ExecuteConsumer(object message, IConsumerContext consu return null; } -} \ No newline at end of file + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Disposing consumer instance {Consumer} of type {ConsumerType}")] + private partial void LogDisposingConsumer(Type consumerType, object consumer); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "Consumer error handler of type {ConsumerErrorHandlerType} will be used to handle the exception during processing of message of type {MessageType}")] + private partial void LogConsumerErrorHandlerWillBeUsed(Type messageType, Type consumerErrorHandlerType, Exception ex); + + #endregion +} + +#if NETSTANDARD2_0 + +public partial class MessageHandler +{ + private partial void LogDisposingConsumer(Type consumerType, object consumer) + => _logger.LogDebug("Disposing consumer instance {Consumer} of type {ConsumerType}", consumer, consumerType); + + private partial void LogConsumerErrorHandlerWillBeUsed(Type messageType, Type consumerErrorHandlerType, Exception ex) + => _logger.LogDebug(ex, "Consumer error handler of type {ConsumerErrorHandlerType} will be used to handle the exception during processing of message of type {MessageType}", consumerErrorHandlerType, messageType); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs index 74f19aa2..47d4b727 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs @@ -6,7 +6,7 @@ /// Implementation of that performs orchestration around processing of a new message using an instance of the declared consumer ( or interface). /// /// -public class MessageProcessor : MessageHandler, IMessageProcessor +public partial class MessageProcessor : MessageHandler, IMessageProcessor { private readonly ILogger _logger; private readonly MessageProvider _messageProvider; @@ -131,14 +131,14 @@ public async virtual Task ProcessMessage(TTransportMessage } catch (Exception e) { - _logger.LogDebug(e, "Processing of the message {TransportMessage} of type {MessageType} failed", transportMessage, messageType); + LogProcessingMessageFailedTypeKnown(transportMessage, messageType, e); lastException ??= e; } } } catch (Exception e) { - _logger.LogDebug(e, "Processing of the message {TransportMessage} failed", transportMessage); + LogProcessingMessageFailed(transportMessage, e); lastException = e; } return new(result, lastException, lastException != null ? lastConsumerInvoker?.ParentSettings : null, lastResponse); @@ -151,7 +151,7 @@ protected Type GetMessageType(IReadOnlyDictionary headers) var messageType = MessageTypeResolver.ToType(messageTypeName); if (messageType != null) { - _logger.LogDebug("Message type {MessageType} was declared in the message header", messageType); + LogMessageTypeDeclaredInHeader(messageType); return messageType; } @@ -164,11 +164,11 @@ protected Type GetMessageType(IReadOnlyDictionary headers) if (_singleInvoker != null) { - _logger.LogDebug("No message type header was present, defaulting to the only declared message type {MessageType}", _singleInvoker.MessageType); + LogMessageTypeHeaderMissingAndDefaulting(_singleInvoker.MessageType); return _singleInvoker.MessageType; } - _logger.LogDebug("No message type header was present in the message header, multiple consumer types declared therefore cannot infer the message type"); + LogNoMessageTypeHeaderPresent(); if (_shouldFailWhenUnrecognizedMessageType) { @@ -198,7 +198,8 @@ protected IEnumerable TryMatchConsumerInvok { if (_shouldLogWhenUnrecognizedMessageType) { - _logger.LogInformation("The message on path {Path} declared {HeaderName} header of type {MessageType}, but none of the known consumer types {ConsumerTypes} was able to handle it", Path, MessageHeaders.MessageType, messageType, string.Join(",", _invokers.Select(x => x.ConsumerType.Name))); + var consumerTypes = string.Join(",", _invokers.Select(x => x.ConsumerType.Name)); + LogNoConsumerTypeMatched(messageType, Path, MessageHeaders.MessageType, consumerTypes); } if (_shouldFailWhenUnrecognizedMessageType) @@ -208,4 +209,69 @@ protected IEnumerable TryMatchConsumerInvok } } } -} \ No newline at end of file + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Processing of the message {TransportMessage} of type {MessageType} failed")] + private partial void LogProcessingMessageFailedTypeKnown(TTransportMessage transportMessage, Type messageType, Exception e); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "Processing of the message {TransportMessage} failed")] + private partial void LogProcessingMessageFailed(TTransportMessage transportMessage, Exception e); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Debug, + Message = "Message type {MessageType} was declared in the message header")] + private partial void LogMessageTypeDeclaredInHeader(Type messageType); + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Debug, + Message = "No message type header was present, defaulting to the only declared message type {MessageType}")] + private partial void LogMessageTypeHeaderMissingAndDefaulting(Type messageType); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Debug, + Message = "No message type header was present in the message header, multiple consumer types declared therefore cannot infer the message type")] + private partial void LogNoMessageTypeHeaderPresent(); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "The message on path {Path} declared {HeaderName} header of type {MessageType}, but none of the known consumer types {ConsumerTypes} was able to handle it")] + private partial void LogNoConsumerTypeMatched(Type messageType, string path, string headerName, string consumerTypes); + + #endregion +} + +#if NETSTANDARD2_0 + +public partial class MessageProcessor +{ + private partial void LogProcessingMessageFailedTypeKnown(TTransportMessage transportMessage, Type messageType, Exception e) + => _logger.LogDebug(e, "Processing of the message {TransportMessage} of type {MessageType} failed", transportMessage, messageType); + + private partial void LogProcessingMessageFailed(TTransportMessage transportMessage, Exception e) + => _logger.LogDebug(e, "Processing of the message {TransportMessage} failed", transportMessage); + + private partial void LogMessageTypeDeclaredInHeader(Type messageType) + => _logger.LogDebug("Message type {MessageType} was declared in the message header", messageType); + + private partial void LogMessageTypeHeaderMissingAndDefaulting(Type messageType) + => _logger.LogDebug("No message type header was present, defaulting to the only declared message type {MessageType}", messageType); + + private partial void LogNoMessageTypeHeaderPresent() + => _logger.LogDebug("No message type header was present in the message header, multiple consumer types declared therefore cannot infer the message type"); + + private partial void LogNoConsumerTypeMatched(Type messageType, string path, string headerName, string consumerTypes) + => _logger.LogInformation("The message on path {Path} declared {HeaderName} header of type {MessageType}, but none of the known consumer types {ConsumerTypes} was able to handle it", path, headerName, messageType, consumerTypes); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs index 2290b8f8..b1f02b40 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs @@ -6,7 +6,7 @@ public abstract class ResponseMessageProcessor; /// The implementation that processes the responses arriving to the bus. /// /// -public class ResponseMessageProcessor : ResponseMessageProcessor, IMessageProcessor +public partial class ResponseMessageProcessor : ResponseMessageProcessor, IMessageProcessor { private readonly ILogger _logger; private readonly RequestResponseSettings _requestResponseSettings; @@ -42,7 +42,7 @@ public Task ProcessMessage(TTransportMessage transportMess } catch (Exception e) { - _logger.LogError(e, "Error occurred while consuming response message, {Message}", transportMessage); + LogErrorConsumingResponse(transportMessage, e); // We can only continue and process all messages in the lease ex = e; } @@ -68,7 +68,7 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p var requestState = _pendingRequestStore.GetById(requestId); if (requestState == null) { - _logger.LogDebug("The response message for request id {RequestId} arriving on path {Path} will be disregarded. Either the request had already expired, had been cancelled or it was already handled (this response message is a duplicate).", requestId, path); + LogResponseWillBeDiscarded(path, requestId); // ToDo: add and API hook to these kind of situation return null; } @@ -77,8 +77,8 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p { if (_logger.IsEnabled(LogLevel.Debug)) { - var tookTimespan = _currentTimeProvider.CurrentTime.Subtract(requestState.Created); - _logger.LogDebug("Response arrived for {Request} on path {Path} (time: {RequestTime} ms)", requestState, path, tookTimespan); + var requestTime = _currentTimeProvider.CurrentTime.Subtract(requestState.Created); + LogResponseArrived(path, requestState, requestTime); } if (responseHeaders.TryGetHeader(ReqRespMessageHeaders.Error, out string errorMessage)) @@ -86,7 +86,7 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p // error response arrived var responseException = new RequestHandlerFaultedMessageBusException(errorMessage); - _logger.LogDebug(responseException, "Response arrived for {Request} on path {Path} with error: {ResponseError}", requestState, path, responseException.Message); + LogResponseArrivedWithError(path, requestState, responseException, responseException.Message); requestState.TaskCompletionSource.TrySetException(responseException); } else @@ -104,7 +104,7 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p } catch (Exception e) { - _logger.LogDebug(e, "Could not deserialize the response message for {Request} arriving on path {Path}", requestState, path); + LogResponseCouldNotDeserialize(path, requestState, e); requestState.TaskCompletionSource.TrySetException(e); } } @@ -117,4 +117,60 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p return null; } + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Error occurred while consuming response message, {Message}")] + private partial void LogErrorConsumingResponse(TTransportMessage message, Exception e); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "The response message for request id {RequestId} arriving on path {Path} will be disregarded. Either the request had already expired, had been cancelled or it was already handled (this response message is a duplicate).")] + private partial void LogResponseWillBeDiscarded(string path, string requestId); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Debug, + Message = "Response arrived for {RequestState} on path {Path} (time: {RequestTime} ms)")] + private partial void LogResponseArrived(string path, PendingRequestState requestState, TimeSpan requestTime); + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Debug, + Message = "Response arrived for {RequestState} on path {Path} with error: {ResponseError}")] + private partial void LogResponseArrivedWithError(string path, PendingRequestState requestState, Exception e, string responseError); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Debug, + Message = "Could not deserialize the response message for {RequestState} arriving on path {Path}")] + private partial void LogResponseCouldNotDeserialize(string path, PendingRequestState requestState, Exception e); + + #endregion +} + +#if NETSTANDARD2_0 + +public partial class ResponseMessageProcessor +{ + private partial void LogErrorConsumingResponse(TTransportMessage message, Exception e) + => _logger.LogError(e, "Error occurred while consuming response message, {Message}", message); + + private partial void LogResponseWillBeDiscarded(string path, string requestId) + => _logger.LogDebug("The response message for request id {RequestId} arriving on path {Path} will be disregarded. Either the request had already expired, had been cancelled or it was already handled (this response message is a duplicate).", requestId, path); + + private partial void LogResponseArrived(string path, PendingRequestState requestState, TimeSpan requestTime) + => _logger.LogDebug("Response arrived for {RequestState} on path {Path} (time: {RequestTime} ms)", requestState, path, requestTime); + + private partial void LogResponseArrivedWithError(string path, PendingRequestState requestState, Exception e, string responseError) + => _logger.LogDebug(e, "Response arrived for {RequestState} on path {Path} with error: {ResponseError}", requestState, path, responseError); + + private partial void LogResponseCouldNotDeserialize(string path, PendingRequestState requestState, Exception e) + => _logger.LogDebug(e, "Could not deserialize the response message for {RequestState} arriving on path {Path}", requestState, path); } + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Helpers/CompatAttributes.cs b/src/SlimMessageBus.Host/Helpers/CompatAttributes.cs new file mode 100644 index 00000000..3e322b75 --- /dev/null +++ b/src/SlimMessageBus.Host/Helpers/CompatAttributes.cs @@ -0,0 +1,45 @@ +#if NETSTANDARD2_0 + +namespace SlimMessageBus.Host; + +[AttributeUsage(AttributeTargets.Method)] +public class LoggerMessageAttribute : Attribute +{ + /// + /// Gets the logging event id for the logging method. + /// + public int EventId { get; set; } = -1; + + /// + /// Gets or sets the logging event name for the logging method. + /// + /// + /// This will equal the method name if not specified. + /// + public string EventName { get; set; } + + /// + /// Gets the logging level for the logging method. + /// + public LogLevel Level { get; set; } = LogLevel.None; + + /// + /// Gets the message text for the logging method. + /// + public string Message { get; set; } = ""; + + /// + /// Gets the flag to skip IsEnabled check for the logging method. + /// + public bool SkipEnabledCheck { get; set; } + + public LoggerMessageAttribute() + { + } + + public LoggerMessageAttribute(int eventId, LogLevel level, string message) + { + } +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Helpers/CompatMethods.cs b/src/SlimMessageBus.Host/Helpers/CompatMethods.cs index 145d8865..d02ebaa3 100644 --- a/src/SlimMessageBus.Host/Helpers/CompatMethods.cs +++ b/src/SlimMessageBus.Host/Helpers/CompatMethods.cs @@ -25,8 +25,6 @@ public static bool TryAdd(this IDictionary dict, K key, V value) public static HashSet ToHashSet(this IEnumerable items) => new(items); -#if NETSTANDARD2_0 - public static IEnumerable> Chunk(this IEnumerable items, int size) { var chunk = new List(size); @@ -50,7 +48,6 @@ public static IEnumerable> Chunk(this IEnumerable i } } -#endif } public static class TimeSpanExtensions @@ -59,4 +56,4 @@ public static TimeSpan Multiply(this TimeSpan timeSpan, double factor) => TimeSpan.FromMilliseconds(timeSpan.TotalMilliseconds * factor); } -#endif +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Helpers/CompatRecord.cs b/src/SlimMessageBus.Host/Helpers/CompatRecord.cs index 56a4fc7d..118d95fe 100644 --- a/src/SlimMessageBus.Host/Helpers/CompatRecord.cs +++ b/src/SlimMessageBus.Host/Helpers/CompatRecord.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 +#if NETSTANDARD2_0 // See https://github.com/dotnet/roslyn/issues/45510#issuecomment-725091019 diff --git a/src/SlimMessageBus.Host/Retry.cs b/src/SlimMessageBus.Host/Helpers/Retry.cs similarity index 88% rename from src/SlimMessageBus.Host/Retry.cs rename to src/SlimMessageBus.Host/Helpers/Retry.cs index 5c58579d..b56cbdc7 100644 --- a/src/SlimMessageBus.Host/Retry.cs +++ b/src/SlimMessageBus.Host/Helpers/Retry.cs @@ -6,17 +6,8 @@ public static class Retry public static async Task WithDelay(Func operation, Func shouldRetry, TimeSpan? delay, TimeSpan? jitter = default, CancellationToken cancellationToken = default) { -#if NETSTANDARD2_0 if (operation is null) throw new ArgumentNullException(nameof(operation)); -#else - ArgumentNullException.ThrowIfNull(operation); -#endif - -#if NETSTANDARD2_0 if (shouldRetry is null) throw new ArgumentNullException(nameof(shouldRetry)); -#else - ArgumentNullException.ThrowIfNull(shouldRetry); -#endif var attempt = 0; do diff --git a/src/SlimMessageBus.Host/Helpers/Utils.cs b/src/SlimMessageBus.Host/Helpers/Utils.cs index 8d1cd9a1..8d4c544d 100644 --- a/src/SlimMessageBus.Host/Helpers/Utils.cs +++ b/src/SlimMessageBus.Host/Helpers/Utils.cs @@ -30,22 +30,18 @@ public static async ValueTask DisposeSilently(this IAsyncDisposable disposable, } public static void DisposeSilently(this IDisposable disposable, string name, ILogger logger) - { - disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", name)); - } - - public static void DisposeSilently(this IDisposable disposable, Func nameFunc, ILogger logger) - { - disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", nameFunc())); - } + => disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", name)); public static ValueTask DisposeSilently(this IAsyncDisposable disposable, Func nameFunc, ILogger logger) - { - return disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", nameFunc())); - } + => disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", nameFunc())); public static ValueTask DisposeSilently(this IAsyncDisposable disposable, string name, ILogger logger) + => disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", name)); + + public static string JoinOrSingle(this T[] values, Func selector, string separator = ",") => values.Length switch { - return disposable.DisposeSilently(e => logger.LogWarning(e, "Error occurred while disposing {Name}", name)); - } + 0 => string.Empty, + 1 => selector(values[0]), + _ => string.Join(separator, values.Select(selector)) + }; } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs b/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs index 0cffd609..66a47ba9 100644 --- a/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs +++ b/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs @@ -4,7 +4,7 @@ using SlimMessageBus.Host.Serialization; -public class HybridMessageBus : IMasterMessageBus, ICompositeMessageBus, IDisposable, IAsyncDisposable +public partial class HybridMessageBus : IMasterMessageBus, ICompositeMessageBus, IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly Dictionary _busByName; @@ -121,7 +121,8 @@ protected virtual MessageBusBase[] Route(object message, string path) { if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Resolved bus {BusName} for message type: {MessageType} and path {Path}", string.Join(",", buses.Select(x => x.Settings.Name)), messageType, path); + var busName = buses.JoinOrSingle(x => x.Settings.Name); + LogResolvedBus(path, messageType, busName); } return buses; } @@ -134,7 +135,7 @@ protected virtual MessageBusBase[] Route(object message, string path) // Add the message type, so that we only emit warn log once if (ProviderSettings.UndeclaredMessageTypeMode == UndeclaredMessageTypeMode.RaiseOneTimeLog && _undeclaredMessageType.TryAdd(messageType, true)) { - _logger.LogInformation("Could not find any bus that produces the message type: {MessageType}. Messages of that type will not be delivered to any child bus. Double check the message bus configuration.", messageType); + LogCouldNotFindBus(messageType); } return []; @@ -198,4 +199,33 @@ public IMasterMessageBus GetChildBus(string name) public IEnumerable GetChildBuses() => _busByName.Values; #endregion + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Resolved bus {BusName} for message type {MessageType} and path {Path}")] + private partial void LogResolvedBus(string path, Type messageType, string busName); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Information, + Message = "Could not find any bus that produces the message type {MessageType}. Messages of that type will not be delivered to any child bus. Double check the message bus configuration.")] + private partial void LogCouldNotFindBus(Type messageType); + + #endregion } + +#if NETSTANDARD2_0 + +public partial class HybridMessageBus +{ + private partial void LogResolvedBus(string path, Type messageType, string busName) + => _logger.LogDebug("Resolved bus {BusName} for message type {MessageType} and path {Path}", busName, messageType, path); + + private partial void LogCouldNotFindBus(Type messageType) + => _logger.LogInformation("Could not find any bus that produces the message type {MessageType}. Messages of that type will not be delivered to any child bus. Double check the message bus configuration.", messageType); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index 02384e3a..827bf499 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -12,7 +12,7 @@ public abstract class MessageBusBase(MessageBusSettings setti public TProviderSettings ProviderSettings { get; } = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); } -public abstract class MessageBusBase : IDisposable, IAsyncDisposable, +public abstract partial class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, IMessageHeadersFactory, @@ -111,7 +111,7 @@ protected MessageBusBase(MessageBusSettings settings) protected virtual IMessageSerializer GetSerializer() => Settings.GetSerializer(Settings.ServiceProvider); - protected virtual IMessageBusSettingsValidationService ValidationService { get => new DefaultMessageBusSettingsValidationService(Settings); } + protected virtual IMessageBusSettingsValidationService ValidationService => new DefaultMessageBusSettingsValidationService(Settings); /// /// Called by the provider to initialize the bus. @@ -137,7 +137,7 @@ protected void OnBuildProvider() } catch (Exception e) { - _logger.LogError(e, "Could not auto start consumers"); + LogCouldNotStartConsumers(e); } }); } @@ -191,17 +191,16 @@ public async Task Start() try { - await InitTaskList.EnsureAllFinished(); - - _logger.LogInformation("Starting consumers for {BusName} bus...", Name); + await InitTaskList.EnsureAllFinished().ConfigureAwait(false); + LogStartingConsumers(Name); await OnBusLifecycle(MessageBusLifecycleEventType.Starting).ConfigureAwait(false); - await CreateConsumers(); + await CreateConsumers().ConfigureAwait(false); await OnStart().ConfigureAwait(false); await Task.WhenAll(_consumers.Select(x => x.Start())).ConfigureAwait(false); await OnBusLifecycle(MessageBusLifecycleEventType.Started).ConfigureAwait(false); - _logger.LogInformation("Started consumers for {BusName} bus", Name); + LogStartedConsumers(Name); lock (_startLock) { @@ -230,9 +229,9 @@ public async Task Stop() try { - await InitTaskList.EnsureAllFinished(); + await InitTaskList.EnsureAllFinished().ConfigureAwait(false); - _logger.LogInformation("Stopping consumers for {BusName} bus...", Name); + LogStoppingConsumers(Name); await OnBusLifecycle(MessageBusLifecycleEventType.Stopping).ConfigureAwait(false); await Task.WhenAll(_consumers.Select(x => x.Stop())).ConfigureAwait(false); @@ -240,7 +239,7 @@ public async Task Stop() await DestroyConsumers().ConfigureAwait(false); await OnBusLifecycle(MessageBusLifecycleEventType.Stopped).ConfigureAwait(false); - _logger.LogInformation("Stopped consumers for {BusName} bus", Name); + LogStoppedConsumers(Name); lock (_startLock) { @@ -332,13 +331,13 @@ protected async virtual ValueTask DisposeAsyncCore() protected virtual Task CreateConsumers() { - _logger.LogInformation("Creating consumers for {BusName} bus...", Name); + LogCreatingConsumers(Name); return Task.CompletedTask; } protected async virtual Task DestroyConsumers() { - _logger.LogInformation("Destroying consumers for {BusName} bus...", Name); + LogDestroyingConsumers(Name); foreach (var consumer in _consumers) { @@ -370,7 +369,7 @@ protected virtual string GetDefaultPath(Type messageType, ProducerSettings produ var path = producerSettings.DefaultPath ?? throw new ProducerMessageBusException($"An attempt to produce message of type {messageType} without specifying path, but there was no default path configured. Double check your configuration."); - _logger.LogDebug("Applying default path {Path} for message type {MessageType}", path, messageType); + LogApplyingDefaultPath(messageType, path); return path; } @@ -383,10 +382,10 @@ public abstract Task ProduceToTransport( CancellationToken cancellationToken); protected void OnProduceToTransport(object message, - Type messageType, - string path, - IDictionary messageHeaders) - => _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, messageType, path); + Type messageType, + string path, + IDictionary messageHeaders) + => LogProducingMessageToPath(message, messageType, path); public virtual int? MaxMessagesPerTransaction => null; @@ -509,7 +508,7 @@ protected virtual TimeSpan GetDefaultRequestTimeout(Type requestType, ProducerSe if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); var timeout = producerSettings.Timeout ?? Settings.RequestResponse.Timeout; - _logger.LogDebug("Applying default timeout {MessageTimeout} for message type {MessageType}", timeout, requestType); + LogApplyingDefaultTimeout(requestType, timeout); return timeout; } @@ -584,12 +583,12 @@ protected async internal virtual Task SendInternal SendInternal consumerContextProperties, IServiceProvider currentServiceProvider) { var createMessageScope = IsMessageScopeEnabled(consumerSettings, consumerContextProperties); - if (createMessageScope) { - _logger.LogDebug("Creating message scope for {Message} of type {MessageType}", message, message.GetType()); + LogCreatingScope(message, message.GetType()); } return new MessageScopeWrapper(currentServiceProvider ?? Settings.ServiceProvider, createMessageScope); } public virtual Task ProvisionTopology() => Task.CompletedTask; + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Could not auto start consumers")] + private partial void LogCouldNotStartConsumers(Exception ex); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "Creating message scope for {Message} of type {MessageType}")] + private partial void LogCreatingScope(object message, Type messageType); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Debug, + Message = "Publishing of request message failed")] + private partial void LogPublishOfRequestFailed(Exception ex); + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Information, + Message = "Starting consumers for {BusName} bus...")] + private partial void LogStartingConsumers(string busName); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Information, + Message = "Started consumers for {BusName} bus")] + private partial void LogStartedConsumers(string busName); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "Stopping consumers for {BusName} bus...")] + private partial void LogStoppingConsumers(string busName); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Information, + Message = "Stopped consumers for {BusName} bus")] + private partial void LogStoppedConsumers(string busName); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Information, + Message = "Creating consumers for {BusName} bus...")] + private partial void LogCreatingConsumers(string busName); + + [LoggerMessage( + EventId = 8, + Level = LogLevel.Information, + Message = "Destroying consumers for {BusName} bus...")] + private partial void LogDestroyingConsumers(string busName); + + [LoggerMessage( + EventId = 9, + Level = LogLevel.Debug, + Message = "Applying default path {Path} for message type {MessageType}")] + private partial void LogApplyingDefaultPath(Type messageType, string path); + + [LoggerMessage( + EventId = 10, + Level = LogLevel.Debug, + Message = "Applying default timeout {MessageTimeout} for message type {MessageType}")] + private partial void LogApplyingDefaultTimeout(Type messageType, TimeSpan messageTimeout); + + [LoggerMessage( + EventId = 11, + Level = LogLevel.Debug, + Message = "Producing message {Message} of type {MessageType} to path {Path}")] + private partial void LogProducingMessageToPath(object message, Type messageType, string path); + + [LoggerMessage( + EventId = 12, + Level = LogLevel.Trace, + Message = "Added to PendingRequests, total is {RequestCount}")] + private partial void LogAddedToPendingRequests(int requestCount); + + [LoggerMessage( + EventId = 13, + Level = LogLevel.Debug, + Message = "Sending request message {MessageType} to path {Path} with reply to {ReplyTo}")] + private partial void LogSendingRequestMessage(string path, Type messageType, string replyTo); + + [LoggerMessage( + EventId = 14, + Level = LogLevel.Debug, + Message = "Skipping sending response {Response} of type {MessageType} as the header {HeaderName} is missing for RequestId: {RequestId}")] + private partial void LogSkippingSendingResponseMessage(string requestId, object response, Type messageType, string headerName); + + [LoggerMessage( + EventId = 15, + Level = LogLevel.Debug, + Message = "Sending the response {Response} of type {MessageType} for RequestId: {RequestId}...")] + private partial void LogSendingResponseMessage(string requestId, object response, Type messageType); + + #endregion +} + +#if NETSTANDARD2_0 +public abstract partial class MessageBusBase +{ + private partial void LogCouldNotStartConsumers(Exception ex) + => _logger.LogError(ex, "Could not auto start consumers"); + + private partial void LogCreatingScope(object message, Type messageType) + => _logger.LogDebug("Creating message scope for {Message} of type {MessageType}", message, messageType); + + private partial void LogPublishOfRequestFailed(Exception ex) + => _logger.LogDebug(ex, "Publishing of request message failed"); + + private partial void LogStartingConsumers(string busName) + => _logger.LogInformation("Starting consumers for {BusName} bus...", busName); + + private partial void LogStartedConsumers(string busName) + => _logger.LogInformation("Started consumers for {BusName} bus", busName); + + private partial void LogStoppingConsumers(string busName) + => _logger.LogInformation("Stopping consumers for {BusName} bus...", busName); + + private partial void LogStoppedConsumers(string busName) + => _logger.LogInformation("Stopped consumers for {BusName} bus", busName); + + private partial void LogCreatingConsumers(string busName) + => _logger.LogInformation("Creating consumers for {BusName} bus...", busName); + + private partial void LogDestroyingConsumers(string busName) + => _logger.LogInformation("Destroying consumers for {BusName} bus...", busName); + + private partial void LogApplyingDefaultPath(Type messageType, string path) + => _logger.LogDebug("Applying default path {Path} for message type {MessageType}", path, messageType); + + private partial void LogApplyingDefaultTimeout(Type messageType, TimeSpan messageTimeout) + => _logger.LogDebug("Applying default timeout {MessageTimeout} for message type {MessageType}", messageTimeout, messageType); + + private partial void LogProducingMessageToPath(object message, Type messageType, string path) + => _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, messageType, path); + + private partial void LogAddedToPendingRequests(int requestCount) + => _logger.LogTrace("Added to PendingRequests, total is {RequestCount}", requestCount); + + private partial void LogSendingRequestMessage(string path, Type messageType, string replyTo) + => _logger.LogDebug("Sending request message {MessageType} to path {Path} with reply to {ReplyTo}", messageType, path, replyTo); + + private partial void LogSkippingSendingResponseMessage(string requestId, object response, Type messageType, string headerName) + => _logger.LogDebug("Skipping sending response {Response} of type {MessageType} as the header {HeaderName} is missing for RequestId: {RequestId}", response, messageType, headerName, requestId); + + private partial void LogSendingResponseMessage(string requestId, object response, Type messageType) + => _logger.LogDebug("Sending the response {Response} of type {MessageType} for RequestId: {RequestId}...", response, messageType, requestId); } + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs b/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs index a4731927..f37f0dcb 100644 --- a/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs +++ b/src/SlimMessageBus.Host/RequestResponse/PendingRequestManager.cs @@ -3,7 +3,7 @@ /// /// Manages the pending requests - ensure requests which exceeded the allotted timeout period are removed. /// -public class PendingRequestManager : IPendingRequestManager, IDisposable +public partial class PendingRequestManager : IPendingRequestManager, IDisposable { private readonly ILogger _logger; @@ -85,10 +85,31 @@ public virtual void CleanPendingRequests() if (canceled) { - _logger.LogDebug("Pending request timed-out: {RequestState}, now: {TimeNow}", requestState, now); + LogPendingRequestTimeout(now, requestState); _onRequestTimeout?.Invoke(requestState.Request); } } Store.RemoveAll(requestsToCancel.Select(x => x.Id)); - } + } + + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Pending request timed-out: {RequestState}, now: {TimeNow}")] + private partial void LogPendingRequestTimeout(DateTimeOffset timeNow, PendingRequestState requestState); + + #endregion } + +#if NETSTANDARD2_0 + +public partial class PendingRequestManager +{ + private partial void LogPendingRequestTimeout(DateTimeOffset timeNow, PendingRequestState requestState) + => _logger.LogDebug("Pending request timed-out: {RequestState}, now: {TimeNow}", requestState, timeNow); +} + +#endif diff --git a/src/SlimMessageBus.Host/RequestResponse/PendingRequestState.cs b/src/SlimMessageBus.Host/RequestResponse/PendingRequestState.cs index 96f92fcf..73a0ef56 100644 --- a/src/SlimMessageBus.Host/RequestResponse/PendingRequestState.cs +++ b/src/SlimMessageBus.Host/RequestResponse/PendingRequestState.cs @@ -24,12 +24,5 @@ public PendingRequestState(string id, object request, Type requestType, Type res CancellationToken = cancellationToken; } - #region Overrides of Object - - public override string ToString() - { - return $"Request(Id: {Id}, RequestType: {RequestType}, ResponseType: {ResponseType}, Created: {Created}, Expires: {Expires})"; - } - - #endregion + public override string ToString() => $"Request(Id: {Id}, RequestType: {RequestType}, ResponseType: {ResponseType}, Created: {Created}, Expires: {Expires})"; } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Services/MessageHeaderService.cs b/src/SlimMessageBus.Host/Services/MessageHeaderService.cs index f6877245..1357c55a 100644 --- a/src/SlimMessageBus.Host/Services/MessageHeaderService.cs +++ b/src/SlimMessageBus.Host/Services/MessageHeaderService.cs @@ -6,7 +6,7 @@ internal interface IMessageHeaderService void AddMessageTypeHeader(object message, IDictionary headers); } -internal class MessageHeaderService : IMessageHeaderService +internal partial class MessageHeaderService : IMessageHeaderService { private readonly ILogger _logger; private readonly MessageBusSettings _settings; @@ -35,14 +35,14 @@ public void AddMessageHeaders(IDictionary messageHeaders, IDicti if (producerSettings.HeaderModifier != null) { // Call header hook - _logger.LogTrace($"Executing producer {nameof(ProducerSettings.HeaderModifier)}"); + LogExecutingHeaderModifier("producer"); producerSettings.HeaderModifier(messageHeaders, message); } if (_settings.HeaderModifier != null) { // Call header hook - _logger.LogTrace($"Executing bus {nameof(MessageBusSettings.HeaderModifier)}"); + LogExecutingHeaderModifier("bus"); _settings.HeaderModifier(messageHeaders, message); } } @@ -54,5 +54,24 @@ public void AddMessageTypeHeader(object message, IDictionary hea headers.SetHeader(MessageHeaders.MessageType, _messageTypeResolver.ToName(message.GetType())); } } + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = $"Executing {{ConfigLevel}} {nameof(ProducerSettings.HeaderModifier)}")] + private partial void LogExecutingHeaderModifier(string configLevel); + + #endregion +} + +#if NETSTANDARD2_0 + +internal partial class MessageHeaderService +{ + private partial void LogExecutingHeaderModifier(string configLevel) + => _logger.LogTrace($"Executing {{ConfigLevel}} {nameof(ProducerSettings.HeaderModifier)}", configLevel); } +#endif \ No newline at end of file diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index 54d2cc6e..e6d6296f 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc903 + 3.0.0-rc904 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs index 8003c373..74aa2b9c 100644 --- a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs @@ -59,7 +59,7 @@ public CircuitBreakerAbstractConsumerInterceptorTests() { accessor = new CircuitBreakerAccessor(); - var h = new CircuitBreakerAbstractConsumerInterceptor(); + var h = new CircuitBreakerAbstractConsumerInterceptor(NullLogger.Instance); var serviceCollection = new ServiceCollection(); serviceCollection.TryAddSingleton(accessor); diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md index f49fbebc..ddee27b3 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/README.md @@ -3,34 +3,38 @@ Sample Benchmark results ``` // * Summary * -BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2454) +BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) 12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores .NET SDK 9.0.100 [Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2 - Job-WFUQPN : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2 + Job-SXUBYX : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2 MaxIterationCount=30 MaxWarmupIterationCount=10 + ``` -| Type | Method | messageCount | createMessageScope | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -| -------------------------------------- | ----------------------------- | ------------ | ------------------ | -------: | -------: | -------: | ----------: | --------: | --------: | --------: | -| PubSubBenchmark | PubSub | 1000000 | False | 730.6 ms | 14.58 ms | 17.36 ms | 119000.0000 | 3000.0000 | 3000.0000 | 1.04 GB | -| PubSubWithConsumerInterceptorBenchmark | PubSubWithConsumerInterceptor | 1000000 | False | 810.5 ms | 16.16 ms | 16.59 ms | 146000.0000 | 3000.0000 | 3000.0000 | 1.28 GB | -| PubSubWithProducerInterceptorBenchmark | PubSubWithProducerInterceptor | 1000000 | False | 823.5 ms | 7.74 ms | 6.86 ms | 156000.0000 | 3000.0000 | 3000.0000 | 1.37 GB | -| PubSubWithPublishInterceptorBenchmark | PubSubWithPublishInterceptor | 1000000 | False | 831.8 ms | 9.43 ms | 7.87 ms | 156000.0000 | 3000.0000 | 3000.0000 | 1.37 GB | -| PubSubBenchmark | PubSub | 1000000 | True | 794.8 ms | 5.44 ms | 4.54 ms | 137000.0000 | 3000.0000 | 3000.0000 | 1.2 GB | -| PubSubWithConsumerInterceptorBenchmark | PubSubWithConsumerInterceptor | 1000000 | True | 900.9 ms | 11.65 ms | 10.32 ms | 164000.0000 | 3000.0000 | 3000.0000 | 1.44 GB | -| PubSubWithProducerInterceptorBenchmark | PubSubWithProducerInterceptor | 1000000 | True | 934.5 ms | 14.15 ms | 13.24 ms | 174000.0000 | 3000.0000 | 3000.0000 | 1.53 GB | -| PubSubWithPublishInterceptorBenchmark | PubSubWithPublishInterceptor | 1000000 | True | 930.6 ms | 14.29 ms | 13.37 ms | 174000.0000 | 3000.0000 | 3000.0000 | 1.53 GB | +| Type | Method | messageCount | createMessageScope | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|--------------------------------------- |------------------------------ |------------- |------------------- |---------:|---------:|---------:|------------:|----------:|----------:|----------:| +| PubSubBenchmark | PubSub | 1000000 | False | 651.3 ms | 11.20 ms | 11.51 ms | 116000.0000 | 3000.0000 | 3000.0000 | 1.02 GB | +| PubSubWithConsumerInterceptorBenchmark | PubSubWithConsumerInterceptor | 1000000 | False | 729.8 ms | 14.52 ms | 12.12 ms | 144000.0000 | 3000.0000 | 3000.0000 | 1.26 GB | +| PubSubWithProducerInterceptorBenchmark | PubSubWithProducerInterceptor | 1000000 | False | 759.4 ms | 12.06 ms | 11.28 ms | 154000.0000 | 3000.0000 | 3000.0000 | 1.35 GB | +| PubSubWithPublishInterceptorBenchmark | PubSubWithPublishInterceptor | 1000000 | False | 752.2 ms | 10.63 ms | 9.94 ms | 154000.0000 | 3000.0000 | 3000.0000 | 1.35 GB | +| PubSubBenchmark | PubSub | 1000000 | True | 673.1 ms | 6.32 ms | 5.91 ms | 130000.0000 | 3000.0000 | 3000.0000 | 1.14 GB | +| PubSubWithConsumerInterceptorBenchmark | PubSubWithConsumerInterceptor | 1000000 | True | 769.8 ms | 10.16 ms | 9.01 ms | 157000.0000 | 3000.0000 | 3000.0000 | 1.38 GB | +| PubSubWithProducerInterceptorBenchmark | PubSubWithProducerInterceptor | 1000000 | True | 789.1 ms | 14.11 ms | 12.51 ms | 167000.0000 | 3000.0000 | 3000.0000 | 1.47 GB | +| PubSubWithPublishInterceptorBenchmark | PubSubWithPublishInterceptor | 1000000 | True | 802.6 ms | 9.42 ms | 7.87 ms | 167000.0000 | 3000.0000 | 3000.0000 | 1.47 GB | ``` // * Hints * - Outliers - PubSubWithProducerInterceptorBenchmark.PubSubWithProducerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 1 outlier was removed (840.20 ms) - PubSubWithPublishInterceptorBenchmark.PubSubWithPublishInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 2 outliers were removed (862.16 ms, 863.90 ms) - PubSubBenchmark.PubSub: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 2 outliers were removed (810.49 ms, 823.65 ms) - PubSubWithConsumerInterceptorBenchmark.PubSubWithConsumerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 1 outlier was removed (947.16 ms) + PubSubBenchmark.PubSub: MaxIterationCount=30, MaxWarmupIterationCount=10 + -> 2 outliers were removed (689.29 ms, 692.70 ms) + PubSubWithConsumerInterceptorBenchmark.PubSubWithConsumerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 2 outliers were removed (771.75 ms, 783.11 ms) + PubSubBenchmark.PubSub: MaxIterationCount=30, MaxWarmupIterationCount=10 + -> 1 outlier was detected (662.74 ms) + PubSubWithConsumerInterceptorBenchmark.PubSubWithConsumerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 1 outlier was removed (797.48 ms) + PubSubWithProducerInterceptorBenchmark.PubSubWithProducerInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 1 outlier was removed (842.72 ms) + PubSubWithPublishInterceptorBenchmark.PubSubWithPublishInterceptor: MaxIterationCount=30, MaxWarmupIterationCount=10 -> 2 outliers were removed, 3 outliers were detected (786.09 ms, 821.03 ms, 827.84 ms) // * Legends * messageCount : Value of the 'messageCount' parameter @@ -43,4 +47,12 @@ Outliers Gen2 : GC Generation 2 collects per 1000 operations Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) 1 ms : 1 Millisecond (0.001 sec) + +// * Diagnostic Output - MemoryDiagnoser * + + +// ***** BenchmarkRunner: End ***** +Global total time: 00:03:23 (203.54 sec), executed benchmarks: 8 +// * Artifacts cleanup * +Artifacts cleanup is finished ``` diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index 6f0316d9..00a61811 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -507,6 +507,38 @@ public async Task When_Send_Given_AHandlerThatThrowsException_Then_ExceptionIsBu await act.Should().ThrowAsync(); } } + + [Fact] + public async Task When_Publish_Given_NoConsumerRegistered_Then_NoOp() + { + const string topic = "topic-a"; + + _builder.Produce(x => x.DefaultTopic(topic)); + + var request = new SomeRequest(Guid.NewGuid()); + + // act + Func act = () => _subject.Value.ProducePublish(request); + + // assert + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task When_Send_Given_NoHandlerRegistered_Then_ResponseIsNull() + { + const string topic = "topic-a"; + + _builder.Produce(x => x.DefaultTopic(topic)); + + var request = new SomeRequest(Guid.NewGuid()); + + // act + var response = await _subject.Value.ProduceSend(request); + + // assert + response.Should().BeNull(); + } } public record SomeMessageA(Guid Value); diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs index a05b74cf..280ed2f5 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/AbstractConsumerTests.cs @@ -1,5 +1,4 @@ -namespace SlimMessageBus.Host.Test.Consumer; - +namespace SlimMessageBus.Host.Test.Consumer; public class AbstractConsumerTests { private class TestConsumer(ILogger logger, IEnumerable settings, IEnumerable interceptors) @@ -35,12 +34,21 @@ public AbstractConsumerTests() } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task When_Start_Then_Interceptor_CanStartIsCalled(bool canStart) + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task When_Start_Then_Interceptor_CanStartIsCalled(bool canStart, bool interceptorThrowsException) { - // Arrange - _interceptor.Setup(x => x.CanStart(_target)).ReturnsAsync(canStart); + // Arrange + if (interceptorThrowsException) + { + _interceptor.Setup(x => x.CanStart(_target)).ThrowsAsync(new Exception()); + } + else + { + _interceptor.Setup(x => x.CanStart(_target)).ReturnsAsync(canStart); + } // Act await _target.Start(); @@ -48,15 +56,38 @@ public async Task When_Start_Then_Interceptor_CanStartIsCalled(bool canStart) // Assert _target.IsStarted.Should().BeTrue(); - _interceptor.Verify(x => x.CanStart(_target), Times.Once); - _interceptor.Verify(x => x.Started(_target), canStart ? Times.Once : Times.Never); _interceptor.VerifyGet(x => x.Order, Times.Once); + _interceptor.Verify(x => x.CanStart(_target), Times.Once); + _interceptor.Verify(x => x.Started(_target), canStart || interceptorThrowsException ? Times.Once : Times.Never); _interceptor.VerifyNoOtherCalls(); - _targetMock.Verify(x => x.OnStart(), canStart ? Times.Once : Times.Never); + _targetMock.Verify(x => x.OnStart(), canStart || interceptorThrowsException ? Times.Once : Times.Never); _targetMock.VerifyNoOtherCalls(); } + [Fact] + public async Task When_Start_Givn_CalledConcurrently_Then_ItWillStartOnce() + { + // Arrange + _interceptor.Setup(x => x.CanStart(_target)).ReturnsAsync(true); + + var startTasks = Enumerable.Range(0, 100).Select(_ => _target.Start()).ToArray(); + + // Act + await Task.WhenAll(startTasks); + + // Assert + _target.IsStarted.Should().BeTrue(); + + _interceptor.VerifyGet(x => x.Order, Times.Once); + _interceptor.Verify(x => x.CanStart(_target), Times.Once); + _interceptor.Verify(x => x.Started(_target), Times.Once); + _interceptor.VerifyNoOtherCalls(); + + _targetMock.Verify(x => x.OnStart(), Times.Once); + _targetMock.VerifyNoOtherCalls(); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -74,11 +105,11 @@ public async Task When_Stop_Then_Interceptor_CanStopIsCalled(bool canStop) // Assert _target.IsStarted.Should().BeFalse(); + _interceptor.VerifyGet(x => x.Order, Times.Once); _interceptor.Verify(x => x.CanStart(_target), Times.Once); _interceptor.Verify(x => x.CanStop(_target), Times.Once); _interceptor.Verify(x => x.Started(_target), Times.Once); _interceptor.Verify(x => x.Stopped(_target), canStop ? Times.Once : Times.Never); - _interceptor.VerifyGet(x => x.Order, Times.Once); _interceptor.VerifyNoOtherCalls(); _targetMock.Verify(x => x.OnStart(), Times.Once); diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/ResponseMessageProcessorTest.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/ResponseMessageProcessorTest.cs new file mode 100644 index 00000000..d82ff5e6 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/ResponseMessageProcessorTest.cs @@ -0,0 +1,151 @@ +namespace SlimMessageBus.Host.Test.Consumer; + +public class ResponseMessageProcessorTest +{ + private readonly RequestResponseSettings _settings; + private readonly Mock> _messageProviderMock; + private readonly Mock _pendingRequestStoreMock; + private readonly ResponseMessageProcessor _subject; + private readonly object _transportMessage; + private readonly Dictionary _messageHeaders; + + public ResponseMessageProcessorTest() + { + _settings = new RequestResponseSettings(); + _messageProviderMock = new Mock>(); + _pendingRequestStoreMock = new Mock(); + _subject = new ResponseMessageProcessor(NullLoggerFactory.Instance, + _settings, + _messageProviderMock.Object, + _pendingRequestStoreMock.Object, + new CurrentTimeProvider()); + _transportMessage = new object(); + _messageHeaders = []; + } + + [Fact] + public async Task When_ProcessMessage_Given_NoRequestIdHeader_Then_ExceptionResult() + { + // arrange + + // act + var r = await _subject.ProcessMessage(_transportMessage, _messageHeaders, null, null); + + // assert + r.Exception.Should().BeOfType(); + r.Response.Should().BeNull(); + r.Result.Should().Be(ProcessResult.Failure); + } + + [Fact] + public async Task When_ProcessMessage_Given_NonExistendRequestId_Then_ResponseIsNullAndNoError() + { + // arrange + _messageHeaders[ReqRespMessageHeaders.RequestId] = "requestId"; + + // act + var r = await _subject.ProcessMessage(_transportMessage, _messageHeaders, null, null); + + // assert + r.Exception.Should().BeNull(); + r.Response.Should().BeNull(); + r.Result.Should().Be(ProcessResult.Success); + } + + [Fact] + public async Task When_ProcessMessage_Given_ResponseIsFaulted_Then_ExceptionResult() + { + // arrange + var requestId = "requestId"; + var responseError = "the error"; + _messageHeaders[ReqRespMessageHeaders.RequestId] = requestId; + _messageHeaders[ReqRespMessageHeaders.Error] = responseError; + + var pendingRequestState = new PendingRequestState(requestId, + new object(), + typeof(object), + typeof(object), + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(2), + default); + + _pendingRequestStoreMock.Setup(x => x.GetById(requestId)).Returns(pendingRequestState); + + // act + var r = await _subject.ProcessMessage(_transportMessage, _messageHeaders, null, null); + + // assert + r.Exception.Should().BeNull(); + r.Response.Should().BeNull(); + r.Result.Should().Be(ProcessResult.Success); + + Func act = () => pendingRequestState.TaskCompletionSource.Task; + await act.Should().ThrowAsync().WithMessage(responseError); + } + + [Fact] + public async Task When_ProcessMessage_Given_ResponseArrivedOnTime_Then_TaskSourceIsResolved() + { + // arrange + var requestId = "requestId"; + var response = new object(); + + _messageProviderMock.Setup(x => x(response.GetType(), _transportMessage)).Returns(response); + + _messageHeaders[ReqRespMessageHeaders.RequestId] = requestId; + + var pendingRequestState = new PendingRequestState(requestId, + new object(), + typeof(object), + typeof(object), + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(2), + default); + + _pendingRequestStoreMock.Setup(x => x.GetById(requestId)).Returns(pendingRequestState); + + // act + var r = await _subject.ProcessMessage(_transportMessage, _messageHeaders, null, null); + + // assert + r.Exception.Should().BeNull(); + r.Response.Should().BeNull(); + r.Result.Should().Be(ProcessResult.Success); + + var responseReturned = await pendingRequestState.TaskCompletionSource.Task; + responseReturned.Should().BeSameAs(response); + } + + [Fact] + public async Task When_ProcessMessage_Given_ResponseCannotBeDeserialized_Then_TaskSourceIsException() + { + // arrange + var requestId = "requestId"; + var ex = new Exception("Boom!"); + + _messageProviderMock.Setup(x => x(typeof(object), _transportMessage)).Throws(ex); + + _messageHeaders[ReqRespMessageHeaders.RequestId] = requestId; + + var pendingRequestState = new PendingRequestState(requestId, + new object(), + typeof(object), + typeof(object), + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(2), + default); + + _pendingRequestStoreMock.Setup(x => x.GetById(requestId)).Returns(pendingRequestState); + + // act + var r = await _subject.ProcessMessage(_transportMessage, _messageHeaders, null, null); + + // assert + r.Exception.Should().BeNull(); + r.Response.Should().BeNull(); + r.Result.Should().Be(ProcessResult.Success); + + Func act = () => pendingRequestState.TaskCompletionSource.Task; + await act.Should().ThrowAsync(); + } +} diff --git a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs index 6a3350aa..482f0243 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs @@ -44,6 +44,7 @@ public HybridMessageBusTest() _serviceProviderMock.Setup(x => x.GetService(typeof(RuntimeTypeCache))).Returns(new RuntimeTypeCache()); _serviceProviderMock.Setup(x => x.GetService(typeof(IPendingRequestManager))).Returns(() => new PendingRequestManager(new InMemoryPendingRequestStore(), new CurrentTimeProvider(), NullLoggerFactory.Instance)); + _loggerMock.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); _loggerFactoryMock.Setup(x => x.CreateLogger(It.IsAny())).Returns(_loggerMock.Object); _messageBusBuilder.AddChildBus("bus1", (mbb) => @@ -179,7 +180,7 @@ public async Task Given_UndeclareMessageType_When_Publish_Then_FollowsSettingsMo _loggerMock.Verify(x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((x, _) => MoqMatchers.LogMessageMatcher(x, m => m.StartsWith("Could not find any bus that produces the message type: "))), + It.Is((x, _) => MoqMatchers.LogMessageMatcher(x, m => m.StartsWith("Could not find any bus that produces the message type "))), It.IsAny(), It.IsAny>()), mode == UndeclaredMessageTypeMode.RaiseOneTimeLog ? Times.Once : Times.Never); } @@ -218,7 +219,7 @@ public async Task Given_UndeclaredRequestType_When_Send_Then_FollowsSettingsMode _loggerMock.Verify(x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((x, _) => MoqMatchers.LogMessageMatcher(x, m => m.StartsWith("Could not find any bus that produces the message type: "))), + It.Is((x, _) => MoqMatchers.LogMessageMatcher(x, m => m.StartsWith("Could not find any bus that produces the message type "))), It.IsAny(), It.IsAny>()), mode == UndeclaredMessageTypeMode.RaiseOneTimeLog ? Times.Once : Times.Never); } @@ -256,7 +257,7 @@ public async Task Given_UndeclaredRequestTypeWithoutResponse_When_Send_Then_Foll _loggerMock.Verify(x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((x, _) => MoqMatchers.LogMessageMatcher(x, m => m.StartsWith("Could not find any bus that produces the message type: "))), + It.Is((x, _) => MoqMatchers.LogMessageMatcher(x, m => m.StartsWith("Could not find any bus that produces the message type "))), It.IsAny(), It.IsAny>()), mode == UndeclaredMessageTypeMode.RaiseOneTimeLog ? Times.Once : Times.Never); } diff --git a/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestStateTest.cs b/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestStateTest.cs new file mode 100644 index 00000000..2e4b2b17 --- /dev/null +++ b/src/Tests/SlimMessageBus.Host.Test/ReqestResponse/PendingRequestStateTest.cs @@ -0,0 +1,21 @@ +namespace SlimMessageBus.Host.Test; + +public class PendingRequestStateTest +{ + [Fact] + public void When_ToString_Then_ReturnsExpectedValue() + { + // arrange + var request = new object(); + var requestId = "r1"; + var requestType = typeof(object); + var responseType = typeof(object); + var state = new PendingRequestState(requestId, request, requestType, responseType, DateTimeOffset.Now, DateTimeOffset.Now.AddSeconds(30), CancellationToken.None); + + // act + var result = state.ToString(); + + // assert + result.Should().StartWith($"Request(Id: {requestId}, RequestType: {requestType}, ResponseType: {responseType}"); + } +} From e17e74de645c59c8932434f7c3add69811921548 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Mon, 13 Jan 2025 23:33:48 +0100 Subject: [PATCH 18/21] Update libs Signed-off-by: Tomasz Maruszak --- src/Host.Plugin.Properties.xml | 2 +- .../Sample.AsyncApi.Service.csproj | 2 +- .../Sample.DomainEvents.WebApi.csproj | 2 +- .../Sample.Nats.WebApi/Sample.Nats.WebApi.csproj | 2 +- .../Sample.OutboxWebApi/Sample.OutboxWebApi.csproj | 2 +- .../Sample.ValidatingWebApi.csproj | 4 ++-- .../SlimMessageBus.Host.AmazonSQS.csproj | 4 ++-- .../SlimMessageBus.Host.AsyncApi.csproj | 2 +- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.FluentValidation.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../SlimMessageBus.Host.Kafka.csproj | 2 +- .../SlimMessageBus.Host.Nats.csproj | 2 +- .../SlimMessageBus.Host.Redis.csproj | 2 +- ...ssageBus.Host.Serialization.GoogleProtobuf.csproj | 2 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- src/Tests/Host.Test.Properties.xml | 12 ++++++------ .../SlimMessageBus.Host.Outbox.Sql.Test.csproj | 6 +++--- ...limMessageBus.Host.Serialization.Benchmark.csproj | 4 ++-- ...Bus.Host.Serialization.GoogleProtobuf.Test.csproj | 10 +++++----- .../SlimMessageBus.Host.Test.Common.csproj | 6 +++--- src/Tools/SecretStore/SecretStore.csproj | 2 +- 23 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index 52d1bb2e..0adb42be 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc904 + 3.0.0-rc905 \ No newline at end of file diff --git a/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj b/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj index 4ab654b2..4e5f5789 100644 --- a/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj +++ b/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj b/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj index 04608631..5ef959a3 100644 --- a/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj +++ b/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj b/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj index 561b2c7f..52233346 100644 --- a/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj +++ b/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj b/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj index c4ef505f..dd3632c1 100644 --- a/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj +++ b/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj b/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj index 6432a850..842c4a4b 100644 --- a/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj +++ b/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj b/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj index 6c0f4335..3fa55fa3 100644 --- a/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj +++ b/src/SlimMessageBus.Host.AmazonSQS/SlimMessageBus.Host.AmazonSQS.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/SlimMessageBus.Host.AsyncApi/SlimMessageBus.Host.AsyncApi.csproj b/src/SlimMessageBus.Host.AsyncApi/SlimMessageBus.Host.AsyncApi.csproj index 4a24c9b4..c702e648 100644 --- a/src/SlimMessageBus.Host.AsyncApi/SlimMessageBus.Host.AsyncApi.csproj +++ b/src/SlimMessageBus.Host.AsyncApi/SlimMessageBus.Host.AsyncApi.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index 46567f42..1aaa00b2 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc904 + 3.0.0-rc905 diff --git a/src/SlimMessageBus.Host.FluentValidation/SlimMessageBus.Host.FluentValidation.csproj b/src/SlimMessageBus.Host.FluentValidation/SlimMessageBus.Host.FluentValidation.csproj index cedefee9..9ce85c38 100644 --- a/src/SlimMessageBus.Host.FluentValidation/SlimMessageBus.Host.FluentValidation.csproj +++ b/src/SlimMessageBus.Host.FluentValidation/SlimMessageBus.Host.FluentValidation.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index e44ce8c0..1bce66bc 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc904 + 3.0.0-rc905 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Kafka/SlimMessageBus.Host.Kafka.csproj b/src/SlimMessageBus.Host.Kafka/SlimMessageBus.Host.Kafka.csproj index 9c41663c..9236f4a9 100644 --- a/src/SlimMessageBus.Host.Kafka/SlimMessageBus.Host.Kafka.csproj +++ b/src/SlimMessageBus.Host.Kafka/SlimMessageBus.Host.Kafka.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/SlimMessageBus.Host.Nats/SlimMessageBus.Host.Nats.csproj b/src/SlimMessageBus.Host.Nats/SlimMessageBus.Host.Nats.csproj index 9cbe52d7..cc3d953b 100644 --- a/src/SlimMessageBus.Host.Nats/SlimMessageBus.Host.Nats.csproj +++ b/src/SlimMessageBus.Host.Nats/SlimMessageBus.Host.Nats.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/SlimMessageBus.Host.Redis/SlimMessageBus.Host.Redis.csproj b/src/SlimMessageBus.Host.Redis/SlimMessageBus.Host.Redis.csproj index 8ce29b62..41e82d92 100644 --- a/src/SlimMessageBus.Host.Redis/SlimMessageBus.Host.Redis.csproj +++ b/src/SlimMessageBus.Host.Redis/SlimMessageBus.Host.Redis.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj index 77e9f0a9..ebbcbb22 100644 --- a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj +++ b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index ca482ff8..fc2226d9 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc904 + 3.0.0-rc905 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index e6d6296f..ab41c887 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc904 + 3.0.0-rc905 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/Host.Test.Properties.xml b/src/Tests/Host.Test.Properties.xml index 02ee75d2..29729b92 100644 --- a/src/Tests/Host.Test.Properties.xml +++ b/src/Tests/Host.Test.Properties.xml @@ -10,16 +10,16 @@ - + - - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj index c6fcffff..26ff12a9 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SlimMessageBus.Host.Outbox.Sql.Test.csproj @@ -8,9 +8,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj b/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj index c64abeca..42effafd 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj +++ b/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj @@ -19,8 +19,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj b/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj index d4b0734f..a87410d8 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj @@ -7,12 +7,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj b/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj index 2d190d20..e14458d4 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj +++ b/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj @@ -13,7 +13,7 @@ - + @@ -22,8 +22,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tools/SecretStore/SecretStore.csproj b/src/Tools/SecretStore/SecretStore.csproj index d3cf157b..0a7eb816 100644 --- a/src/Tools/SecretStore/SecretStore.csproj +++ b/src/Tools/SecretStore/SecretStore.csproj @@ -1,7 +1,7 @@ - netstandard2.1 + net8.0 latest enable From c653493248ff9aefbe4255b4571a05d8699094d7 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sat, 5 Oct 2024 09:01:04 +0200 Subject: [PATCH 19/21] Include net9.0 in multi target. Upgrade tests and samples to net9 Signed-off-by: Tomasz Maruszak --- .github/workflows/build.yml | 2 +- src/Common.NuGet.Properties.xml | 2 +- src/Host.Plugin.Properties.xml | 2 +- .../Sample.AsyncApi.Service.csproj | 4 ++-- .../Sample.CircuitBreaker.HealthCheck.csproj | 8 ++++---- .../Sample.DomainEvents.Application.csproj | 4 ++-- .../Sample.DomainEvents.Domain.csproj | 4 ++-- .../Sample.DomainEvents.WebApi.csproj | 6 +++--- .../Sample.Hybrid.ConsoleApp.csproj | 14 +++++++------- .../Sample.Images.FileStore.csproj | 2 +- .../Sample.Images.Messages.csproj | 2 +- .../Sample.Images.WebApi.csproj | 8 ++++---- .../Sample.Images.Worker.csproj | 12 ++++++------ .../Sample.Nats.WebApi/Sample.Nats.WebApi.csproj | 4 ++-- .../Sample.OutboxWebApi/Sample.OutboxWebApi.csproj | 8 ++++---- .../Sample.Serialization.ConsoleApp.csproj | 8 ++++---- .../Sample.Serialization.MessagesAvro.csproj | 2 +- .../Sample.Simple.ConsoleApp.csproj | 4 ++-- .../Sample.ValidatingWebApi.csproj | 2 +- .../SlimMessageBus.Host.AspNetCore.csproj | 6 ------ .../Config/ConsumerBuilderExtensions.cs | 2 +- ...tor.cs => CircuitBreakerConsumerInterceptor.cs} | 2 +- .../SlimMessageBus.Host.Configuration.csproj | 3 ++- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../SlimMessageBus.Host.Mqtt.csproj | 6 +----- ...SlimMessageBus.Host.Outbox.Sql.DbContext.csproj | 1 + .../SlimMessageBus.Host.RabbitMQ.csproj | 4 +--- .../SlimMessageBus.Host.Serialization.Avro.csproj | 1 + ...ageBus.Host.Serialization.GoogleProtobuf.csproj | 1 + ...SlimMessageBus.Host.Serialization.Hybrid.csproj | 1 + .../SlimMessageBus.Host.Serialization.Json.csproj | 1 + ...ageBus.Host.Serialization.SystemTextJson.csproj | 1 + .../SlimMessageBus.Host.Serialization.csproj | 2 +- src/SlimMessageBus.Host/SlimMessageBus.Host.csproj | 4 +++- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- src/Tests/Host.Test.Properties.xml | 4 ++-- ...rcuitBreakerAbstractConsumerInterceptorTests.cs | 2 +- .../SlimMessageBus.Host.Memory.Benchmark.csproj | 2 +- ...essageBus.Host.Outbox.Sql.DbContext.Test.csproj | 4 ++-- .../SqlOutboxRepositoryTests.cs | 3 +++ .../SlimMessageBus.Host.Test.Common.csproj | 10 +++++----- src/Tools/SecretStore/SecretStore.csproj | 2 +- 42 files changed, 82 insertions(+), 82 deletions(-) rename src/SlimMessageBus.Host.CircuitBreaker/Implementation/{CircuitBreakerAbstractConsumerInterceptor.cs => CircuitBreakerConsumerInterceptor.cs} (93%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d2e8947..bf2d097c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,7 @@ jobs: - name: Setup .NET 8.0 uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x cache: false - name: Setup .NET diff --git a/src/Common.NuGet.Properties.xml b/src/Common.NuGet.Properties.xml index a2e5c611..8d9675d6 100644 --- a/src/Common.NuGet.Properties.xml +++ b/src/Common.NuGet.Properties.xml @@ -3,7 +3,7 @@ - net8.0;net6.0;netstandard2.0 + net9.0;net8.0;net6.0;netstandard2.0 true NuGet.md icon.png diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index 0adb42be..b40bc2f8 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc905 + 3.0.0-rc906 \ No newline at end of file diff --git a/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj b/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj index 4e5f5789..54c715ac 100644 --- a/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj +++ b/src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable true @@ -9,7 +9,7 @@ - + diff --git a/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj b/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj index 8e4ac1af..bd133c9b 100644 --- a/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj +++ b/src/Samples/Sample.CircuitBreaker.HealthCheck/Sample.CircuitBreaker.HealthCheck.csproj @@ -2,15 +2,15 @@ Exe - net8.0 + net9.0 enable enable - - - + + + diff --git a/src/Samples/Sample.DomainEvents.Application/Sample.DomainEvents.Application.csproj b/src/Samples/Sample.DomainEvents.Application/Sample.DomainEvents.Application.csproj index 251dcf6f..2aee5fb1 100644 --- a/src/Samples/Sample.DomainEvents.Application/Sample.DomainEvents.Application.csproj +++ b/src/Samples/Sample.DomainEvents.Application/Sample.DomainEvents.Application.csproj @@ -1,14 +1,14 @@  - net8.0 + net9.0 Library latest enable - + diff --git a/src/Samples/Sample.DomainEvents.Domain/Sample.DomainEvents.Domain.csproj b/src/Samples/Sample.DomainEvents.Domain/Sample.DomainEvents.Domain.csproj index 104d9879..db119147 100644 --- a/src/Samples/Sample.DomainEvents.Domain/Sample.DomainEvents.Domain.csproj +++ b/src/Samples/Sample.DomainEvents.Domain/Sample.DomainEvents.Domain.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 Library latest enable @@ -10,7 +10,7 @@ - + diff --git a/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj b/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj index 5ef959a3..cd054958 100644 --- a/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj +++ b/src/Samples/Sample.DomainEvents.WebApi/Sample.DomainEvents.WebApi.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 latest enable true @@ -14,9 +14,9 @@ - + - + diff --git a/src/Samples/Sample.Hybrid.ConsoleApp/Sample.Hybrid.ConsoleApp.csproj b/src/Samples/Sample.Hybrid.ConsoleApp/Sample.Hybrid.ConsoleApp.csproj index 05d6b51a..bc71ef48 100644 --- a/src/Samples/Sample.Hybrid.ConsoleApp/Sample.Hybrid.ConsoleApp.csproj +++ b/src/Samples/Sample.Hybrid.ConsoleApp/Sample.Hybrid.ConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 latest enable @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/src/Samples/Sample.Images.FileStore/Sample.Images.FileStore.csproj b/src/Samples/Sample.Images.FileStore/Sample.Images.FileStore.csproj index 155a2701..cdb3101a 100644 --- a/src/Samples/Sample.Images.FileStore/Sample.Images.FileStore.csproj +++ b/src/Samples/Sample.Images.FileStore/Sample.Images.FileStore.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 latest enable diff --git a/src/Samples/Sample.Images.Messages/Sample.Images.Messages.csproj b/src/Samples/Sample.Images.Messages/Sample.Images.Messages.csproj index e81a1001..468f7fed 100644 --- a/src/Samples/Sample.Images.Messages/Sample.Images.Messages.csproj +++ b/src/Samples/Sample.Images.Messages/Sample.Images.Messages.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 latest enable diff --git a/src/Samples/Sample.Images.WebApi/Sample.Images.WebApi.csproj b/src/Samples/Sample.Images.WebApi/Sample.Images.WebApi.csproj index 0ad4ea03..2ee32c13 100644 --- a/src/Samples/Sample.Images.WebApi/Sample.Images.WebApi.csproj +++ b/src/Samples/Sample.Images.WebApi/Sample.Images.WebApi.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 latest enable @@ -15,9 +15,9 @@ - - - + + + diff --git a/src/Samples/Sample.Images.Worker/Sample.Images.Worker.csproj b/src/Samples/Sample.Images.Worker/Sample.Images.Worker.csproj index 4242d30f..b68fe7e4 100644 --- a/src/Samples/Sample.Images.Worker/Sample.Images.Worker.csproj +++ b/src/Samples/Sample.Images.Worker/Sample.Images.Worker.csproj @@ -1,18 +1,18 @@  - net8.0 + net9.0 latest enable Exe - - - - - + + + + + diff --git a/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj b/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj index 52233346..304ff404 100644 --- a/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj +++ b/src/Samples/Sample.Nats.WebApi/Sample.Nats.WebApi.csproj @@ -1,13 +1,13 @@ - net8.0 + net9.0 enable enable - + diff --git a/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj b/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj index dd3632c1..67525f8a 100644 --- a/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj +++ b/src/Samples/Sample.OutboxWebApi/Sample.OutboxWebApi.csproj @@ -1,18 +1,18 @@  - net8.0 + net9.0 enable enable - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Samples/Sample.Serialization.ConsoleApp/Sample.Serialization.ConsoleApp.csproj b/src/Samples/Sample.Serialization.ConsoleApp/Sample.Serialization.ConsoleApp.csproj index 55f417ca..3c48a4b2 100644 --- a/src/Samples/Sample.Serialization.ConsoleApp/Sample.Serialization.ConsoleApp.csproj +++ b/src/Samples/Sample.Serialization.ConsoleApp/Sample.Serialization.ConsoleApp.csproj @@ -2,15 +2,15 @@ Exe - net8.0 + net9.0 latest enable - - - + + + diff --git a/src/Samples/Sample.Serialization.MessagesAvro/Sample.Serialization.MessagesAvro.csproj b/src/Samples/Sample.Serialization.MessagesAvro/Sample.Serialization.MessagesAvro.csproj index 3c4d40ff..9abe3e70 100644 --- a/src/Samples/Sample.Serialization.MessagesAvro/Sample.Serialization.MessagesAvro.csproj +++ b/src/Samples/Sample.Serialization.MessagesAvro/Sample.Serialization.MessagesAvro.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 latest enable diff --git a/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj b/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj index 27646aa4..ec184e79 100644 --- a/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj +++ b/src/Samples/Sample.Simple.ConsoleApp/Sample.Simple.ConsoleApp.csproj @@ -2,12 +2,12 @@ Exe - net8.0 + net9.0 enable - + diff --git a/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj b/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj index 842c4a4b..7d8b9d3a 100644 --- a/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj +++ b/src/Samples/Sample.ValidatingWebApi/Sample.ValidatingWebApi.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/src/SlimMessageBus.Host.AspNetCore/SlimMessageBus.Host.AspNetCore.csproj b/src/SlimMessageBus.Host.AspNetCore/SlimMessageBus.Host.AspNetCore.csproj index 71110403..5bfcd507 100644 --- a/src/SlimMessageBus.Host.AspNetCore/SlimMessageBus.Host.AspNetCore.csproj +++ b/src/SlimMessageBus.Host.AspNetCore/SlimMessageBus.Host.AspNetCore.csproj @@ -8,12 +8,6 @@ icon.png - - - - - - diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs b/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs index e1258a05..20927f81 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker/Config/ConsumerBuilderExtensions.cs @@ -18,7 +18,7 @@ public static T AddConsumerCircuitBreakerType(this T builder.PostConfigurationActions.Add(services => { - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); }); return builder; diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs similarity index 93% rename from src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs rename to src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs index f46f14d4..1a059661 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerAbstractConsumerInterceptor.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs @@ -3,7 +3,7 @@ /// /// Circuit breaker to toggle consumer status on an external events. /// -internal sealed class CircuitBreakerAbstractConsumerInterceptor(ILogger logger) : IAbstractConsumerInterceptor +internal sealed class CircuitBreakerConsumerInterceptor(ILogger logger) : IAbstractConsumerInterceptor { public int Order => 100; diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index 1aaa00b2..aaf193cc 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,13 +6,14 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc905 + 3.0.0-rc906 + diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index 1bce66bc..500f4fce 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc905 + 3.0.0-rc906 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Mqtt/SlimMessageBus.Host.Mqtt.csproj b/src/SlimMessageBus.Host.Mqtt/SlimMessageBus.Host.Mqtt.csproj index 1d87f2df..cd4a6eb9 100644 --- a/src/SlimMessageBus.Host.Mqtt/SlimMessageBus.Host.Mqtt.csproj +++ b/src/SlimMessageBus.Host.Mqtt/SlimMessageBus.Host.Mqtt.csproj @@ -9,7 +9,7 @@ - + @@ -17,8 +17,4 @@ - - - - diff --git a/src/SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj index c40dda05..caf16cba 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj +++ b/src/SlimMessageBus.Host.Outbox.Sql.DbContext/SlimMessageBus.Host.Outbox.Sql.DbContext.csproj @@ -15,6 +15,7 @@ + diff --git a/src/SlimMessageBus.Host.RabbitMQ/SlimMessageBus.Host.RabbitMQ.csproj b/src/SlimMessageBus.Host.RabbitMQ/SlimMessageBus.Host.RabbitMQ.csproj index 82add62e..0629e2bf 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/SlimMessageBus.Host.RabbitMQ.csproj +++ b/src/SlimMessageBus.Host.RabbitMQ/SlimMessageBus.Host.RabbitMQ.csproj @@ -17,9 +17,7 @@ - - <_Parameter1>SlimMessageBus.Host.RabbitMQ.Test - + diff --git a/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj b/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj index 9d41a782..bcf8a4b3 100644 --- a/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj +++ b/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj @@ -14,6 +14,7 @@ + diff --git a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj index ebbcbb22..897a3c2e 100644 --- a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj +++ b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj @@ -14,6 +14,7 @@ + diff --git a/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj b/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj index bc61ff4c..df179d57 100644 --- a/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj +++ b/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj @@ -16,6 +16,7 @@ + diff --git a/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj b/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj index 6e9ab2e2..4f91f6b7 100644 --- a/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj +++ b/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj @@ -14,6 +14,7 @@ + diff --git a/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj b/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj index 4b0f16ff..fb72242a 100644 --- a/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj +++ b/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj @@ -13,6 +13,7 @@ + diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index fc2226d9..4c8c310d 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc905 + 3.0.0-rc906 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj b/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj index b07b9317..cda164ea 100644 --- a/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj +++ b/src/SlimMessageBus.Host/SlimMessageBus.Host.csproj @@ -14,10 +14,12 @@ - + + + diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index ab41c887..3159f398 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc905 + 3.0.0-rc906 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication. diff --git a/src/Tests/Host.Test.Properties.xml b/src/Tests/Host.Test.Properties.xml index 29729b92..41ce0fde 100644 --- a/src/Tests/Host.Test.Properties.xml +++ b/src/Tests/Host.Test.Properties.xml @@ -4,13 +4,13 @@ - net8.0 + net9.0 false 1.0.0 - + diff --git a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs index 74aa2b9c..736c7198 100644 --- a/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs +++ b/src/Tests/SlimMessageBus.Host.CircuitBreaker.Test/HealthCheckCircuitBreakerAbstractConsumerInterceptorTests.cs @@ -59,7 +59,7 @@ public CircuitBreakerAbstractConsumerInterceptorTests() { accessor = new CircuitBreakerAccessor(); - var h = new CircuitBreakerAbstractConsumerInterceptor(NullLogger.Instance); + var h = new CircuitBreakerConsumerInterceptor(NullLogger.Instance); var serviceCollection = new ServiceCollection(); serviceCollection.TryAddSingleton(accessor); diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj index 14c770d7..208f7851 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj index 86a731ac..c3331929 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.DbContext.Test/SlimMessageBus.Host.Outbox.Sql.DbContext.Test.csproj @@ -3,11 +3,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs index f14b2630..61fc6cca 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Sql.Test/SqlOutboxRepositoryTests.cs @@ -1,4 +1,7 @@ namespace SlimMessageBus.Host.Outbox.Sql.Test; + +[Trait("Category", "Integration")] +[Trait("Transport", "Outbox.Sql")] public static class SqlOutboxRepositoryTests { public class SaveTests : BaseSqlOutboxRepositoryTest diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj b/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj index e14458d4..b32ac76d 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj +++ b/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/src/Tools/SecretStore/SecretStore.csproj b/src/Tools/SecretStore/SecretStore.csproj index 0a7eb816..89ef7c5c 100644 --- a/src/Tools/SecretStore/SecretStore.csproj +++ b/src/Tools/SecretStore/SecretStore.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 latest enable From cbcdee2637757cded5692bcc2c540504e6cc2182 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sun, 26 Jan 2025 12:56:07 +0100 Subject: [PATCH 20/21] Cleanup + more high performance logging Signed-off-by: Tomasz Maruszak --- src/.editorconfig | 3 + .../CircuitBreakerConsumerInterceptor.cs | 36 ++++++++- .../Services/OutboxLockRenewalTimerFactory.cs | 2 +- ...mMessageBus.Host.Serialization.Avro.csproj | 2 +- .../GoogleProtobufMessageSerializer.cs | 8 +- ...s.Host.Serialization.GoogleProtobuf.csproj | 2 +- ...essageBus.Host.Serialization.Hybrid.csproj | 2 +- .../JsonMessageSerializer.cs | 74 ++++++++++++++++--- ...mMessageBus.Host.Serialization.Json.csproj | 2 +- ...s.Host.Serialization.SystemTextJson.csproj | 2 +- .../SqlHelper.cs | 35 ++++++++- .../MessageProcessors/MessageHandler.cs | 4 +- .../ResponseMessageProcessor.cs | 1 - src/SlimMessageBus.Host/MessageBusBase.cs | 1 + .../ConsumerBuilderTest.cs | 50 ++++++------- .../HandlerBuilderTest.cs | 12 +-- .../TypeCollectionTests.cs | 6 +- .../HybridTests.cs | 4 +- .../OutboxLockRenewalTimerTests.cs | 6 +- .../JsonMessageSerializerTests.cs | 22 +++--- .../JsonMessageSerializerTests.cs | 22 +++--- .../Collections/RuntimeTypeCacheTests.cs | 13 +--- 22 files changed, 204 insertions(+), 105 deletions(-) diff --git a/src/.editorconfig b/src/.editorconfig index 53489ab9..2cc329eb 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -103,6 +103,8 @@ csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +dotnet_diagnostic.xUnit1045.severity = silent [*.{cs,vb}] #### Naming styles #### @@ -186,6 +188,7 @@ dotnet_style_qualification_for_event = false:suggestion dotnet_diagnostic.VSTHRD200.severity = none # not supported by .netstandard2.0 dotnet_diagnostic.CA1510.severity = none +dotnet_diagnostic.CA1512.severity = none [*.{csproj,xml}] indent_style = space diff --git a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs index 1a059661..40ad3ec4 100644 --- a/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs +++ b/src/SlimMessageBus.Host.CircuitBreaker/Implementation/CircuitBreakerConsumerInterceptor.cs @@ -3,8 +3,10 @@ /// /// Circuit breaker to toggle consumer status on an external events. /// -internal sealed class CircuitBreakerConsumerInterceptor(ILogger logger) : IAbstractConsumerInterceptor +internal sealed partial class CircuitBreakerConsumerInterceptor(ILogger logger) : IAbstractConsumerInterceptor { + private readonly ILogger _logger = logger; + public int Order => 100; public async Task CanStart(AbstractConsumer consumer) @@ -33,12 +35,12 @@ async Task BreakerChanged(Circuit state) var bus = consumer.Settings[0].MessageBusSettings.Name ?? "default"; if (shouldPause) { - logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); + LogCircuitTripped(path, bus); await consumer.DoStop().ConfigureAwait(false); } else { - logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); + LogCircuitRestored(path, bus); await consumer.DoStart().ConfigureAwait(false); } consumer.SetIsPaused(shouldPause); @@ -89,4 +91,32 @@ public async Task CanStop(AbstractConsumer consumer) public Task Started(AbstractConsumer consumer) => Task.CompletedTask; public Task Stopped(AbstractConsumer consumer) => Task.CompletedTask; + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Warning, + Message = "Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.")] + private partial void LogCircuitTripped(string path, string bus); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Information, + Message = "Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.")] + private partial void LogCircuitRestored(string path, string bus); + + #endregion +} + +#if NETSTANDARD2_0 + +partial class CircuitBreakerConsumerInterceptor +{ + private partial void LogCircuitTripped(string path, string bus) + => _logger.LogWarning("Circuit breaker tripped for '{Path}' on '{Bus}' bus. Consumer paused.", path, bus); + + private partial void LogCircuitRestored(string path, string bus) + => _logger.LogInformation("Circuit breaker restored for '{Path}' on '{Bus}' bus. Consumer resumed.", path, bus); } +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs index 56254736..c22ef388 100644 --- a/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs +++ b/src/SlimMessageBus.Host.Outbox/Services/OutboxLockRenewalTimerFactory.cs @@ -9,7 +9,7 @@ public class OutboxLockRenewalTimerFactory(IS private bool _isDisposed = false; public IOutboxLockRenewalTimer CreateRenewalTimer(TimeSpan lockDuration, TimeSpan interval, Action lockLost, CancellationToken cancellationToken) - => (OutboxLockRenewalTimer)ActivatorUtilities.CreateInstance(_scope.ServiceProvider, typeof(OutboxLockRenewalTimer), lockDuration, interval, lockLost, cancellationToken); + => ActivatorUtilities.CreateInstance>(_scope.ServiceProvider, lockDuration, interval, lockLost, cancellationToken); public async ValueTask DisposeAsync() { diff --git a/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj b/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj index bcf8a4b3..0986f359 100644 --- a/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj +++ b/src/SlimMessageBus.Host.Serialization.Avro/SlimMessageBus.Host.Serialization.Avro.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/GoogleProtobufMessageSerializer.cs b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/GoogleProtobufMessageSerializer.cs index 997c8386..1d48bd9e 100644 --- a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/GoogleProtobufMessageSerializer.cs +++ b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/GoogleProtobufMessageSerializer.cs @@ -15,10 +15,8 @@ public GoogleProtobufMessageSerializer(ILoggerFactory loggerFactory, IMessagePar _messageParserFactory = messageParserFactory ?? new MessageParserFactory(); } - public byte[] Serialize(Type t, object message) - { - return ((IMessage)message).ToByteArray(); - } + public byte[] Serialize(Type t, object message) + => ((IMessage)message).ToByteArray(); public object Deserialize(Type t, byte[] payload) { @@ -31,7 +29,7 @@ public object Deserialize(Type t, byte[] payload) BindingFlags.Instance, null, messageParser, - new object[] { payload }); + [payload]); return message; } diff --git a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj index 897a3c2e..822fc28c 100644 --- a/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj +++ b/src/SlimMessageBus.Host.Serialization.GoogleProtobuf/SlimMessageBus.Host.Serialization.GoogleProtobuf.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj b/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj index df179d57..c769784c 100644 --- a/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj +++ b/src/SlimMessageBus.Host.Serialization.Hybrid/SlimMessageBus.Host.Serialization.Hybrid.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs b/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs index c3998f70..de4d9930 100644 --- a/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs +++ b/src/SlimMessageBus.Host.Serialization.Json/JsonMessageSerializer.cs @@ -7,7 +7,7 @@ namespace SlimMessageBus.Host.Serialization.Json; using Newtonsoft.Json; -public class JsonMessageSerializer : IMessageSerializer, IMessageSerializer +public partial class JsonMessageSerializer : IMessageSerializer, IMessageSerializer { private readonly ILogger _logger; private readonly Encoding _encoding; @@ -30,10 +30,10 @@ public JsonMessageSerializer() public byte[] Serialize(Type t, object message) { var jsonPayload = JsonConvert.SerializeObject(message, t, _serializerSettings); - _logger.LogDebug("Type {MessageType} serialized from {Message} to JSON {MessageJson}", t, message, jsonPayload); + LogSerialized(t, message, jsonPayload); return _encoding.GetBytes(jsonPayload); - } - + } + public object Deserialize(Type t, byte[] payload) { var jsonPayload = string.Empty; @@ -44,7 +44,11 @@ public object Deserialize(Type t, byte[] payload) } catch (Exception e) { - _logger.LogError(e, "Type {MessageType} could not been deserialized, payload: {MessagePayload}, JSON: {MessageJson}", t, _logger.IsEnabled(LogLevel.Debug) ? Convert.ToBase64String(payload) : "(...)", jsonPayload); + var base64Payload = _logger.IsEnabled(LogLevel.Debug) + ? Convert.ToBase64String(payload) + : "(...)"; + + LogDeserializationFailed(t, jsonPayload, base64Payload, e); throw; } } @@ -56,16 +60,66 @@ public object Deserialize(Type t, byte[] payload) string IMessageSerializer.Serialize(Type t, object message) { var payload = JsonConvert.SerializeObject(message, t, _serializerSettings); - _logger.LogDebug("Type {MessageType} serialized from {Message} to JSON {MessageJson}", t, message, payload); + LogSerialized(t, message, payload); return payload; } public object Deserialize(Type t, string payload) { - var message = JsonConvert.DeserializeObject(payload, t, _serializerSettings); - _logger.LogDebug("Type {MessageType} deserialized from JSON {MessageJson} to {Message}", t, payload, message); - return message; + try + { + var message = JsonConvert.DeserializeObject(payload, t, _serializerSettings); + LogDeserializedFromString(t, payload, message); + return message; + } + catch (Exception e) + { + LogDeserializationFailed(t, payload, string.Empty, e); + throw; + } } + #endregion + + #region Logging + +#if !NETSTANDARD2_0 + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Type {MessageType} serialized from {Message} to JSON {MessageJson}")] + private partial void LogSerialized(Type messageType, object message, string messageJson); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "Type {MessageType} deserialized from JSON {MessageJson} to {Message}")] + private partial void LogDeserializedFromString(Type messageType, string messageJson, object message); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Error, + Message = "Type {MessageType} could not been deserialized, payload: {MessagePayload}, JSON: {MessageJson}")] + private partial void LogDeserializationFailed(Type messageType, string messageJson, string messagePayload, Exception e); + +#endif + #endregion -} \ No newline at end of file +} + +#if NETSTANDARD2_0 + +public partial class JsonMessageSerializer +{ + private void LogSerialized(Type messageType, object message, string messageJson) + => _logger.LogDebug("Type {MessageType} serialized from {Message} to JSON {MessageJson}", messageType, message, messageJson); + + private void LogDeserializedFromString(Type messageType, string messageJson, object message) + => _logger.LogDebug("Type {MessageType} deserialized from JSON {MessageJson} to {Message}", messageType, messageJson, message); + + private void LogDeserializationFailed(Type messageType, string messageJson, string messagePayload, Exception e) + => _logger.LogError(e, "Type {MessageType} could not been deserialized, payload: {MessagePayload}, JSON: {MessageJson}", messageType, messagePayload, messageJson); +} + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj b/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj index 4f91f6b7..d0d77129 100644 --- a/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj +++ b/src/SlimMessageBus.Host.Serialization.Json/SlimMessageBus.Host.Serialization.Json.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj b/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj index fb72242a..1287d0c1 100644 --- a/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj +++ b/src/SlimMessageBus.Host.Serialization.SystemTextJson/SlimMessageBus.Host.Serialization.SystemTextJson.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs b/src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs index bcd7d1cb..47865ebd 100644 --- a/src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs +++ b/src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs @@ -3,7 +3,7 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -public static class SqlHelper +public static partial class SqlHelper { private static readonly HashSet TransientErrorNumbers = [ @@ -20,7 +20,7 @@ public static async Task RetryIfError(ILogger logger, SqlRetry { if (tries > 1) { - logger.LogInformation("SQL error encountered. Will begin attempt number {SqlRetryNumber} of {SqlRetryCount} max...", tries, retrySettings.RetryCount); + LogSqlError(logger, retrySettings.RetryCount, tries); await Task.Delay(nextRetryInterval, token); nextRetryInterval = nextRetryInterval.Multiply(retrySettings.RetryIntervalFactor); } @@ -36,7 +36,7 @@ public static async Task RetryIfError(ILogger logger, SqlRetry } // transient SQL error - continue trying lastTransientException = sqlEx; - logger.LogDebug(sqlEx, "SQL error occurred {SqlErrorCode}. Will retry operation", sqlEx.Number); + LogWillRetry(logger, sqlEx.Number, sqlEx); } } throw lastTransientException; @@ -47,4 +47,33 @@ public static Task RetryIfTransientError(ILogger logger, SqlRe public static Task RetryIfTransientError(ILogger logger, SqlRetrySettings retrySettings, Func operation, CancellationToken token) => RetryIfTransientError(logger, retrySettings, async () => { await operation(); return null; }, token); + + #region Logging + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "SQL error encountered. Will begin attempt number {SqlRetryNumber} of {SqlRetryCount} max...")] + private static partial void LogSqlError(ILogger logger, int sqlRetryCount, int sqlRetryNumber); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Debug, + Message = "SQL error occurred {SqlErrorCode}. Will retry operation")] + private static partial void LogWillRetry(ILogger logger, int sqlErrorCode, SqlException e); + + #endregion +} + +#if NETSTANDARD2_0 + +partial class SqlHelper +{ + private static partial void LogSqlError(ILogger logger, int sqlRetryCount, int sqlRetryNumber) + => logger.LogInformation("SQL error encountered. Will begin attempt number {SqlRetryNumber} of {SqlRetryCount} max...", sqlRetryNumber, sqlRetryCount); + + private static partial void LogWillRetry(ILogger logger, int sqlErrorCode, SqlException e) + => logger.LogDebug(e, "SQL error occurred {SqlErrorCode}. Will retry operation", sqlErrorCode); } + +#endif \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index e785d199..393ddcac 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -104,7 +104,7 @@ public MessageHandler( catch (Exception ex) { attempts++; - var handleErrorResult = await DoHandleError(message, messageType, messageScope, consumerContext, ex, attempts, cancellationToken).ConfigureAwait(false); + var handleErrorResult = await DoHandleError(message, messageType, messageScope, consumerContext, ex, attempts).ConfigureAwait(false); if (handleErrorResult is ProcessResult.RetryState) { continue; @@ -144,7 +144,7 @@ private async Task DoHandleInternal(object message, IMessageTypeConsumer return await ExecuteConsumer(message, consumerContext, consumerInvoker, responseType).ConfigureAwait(false); } - private async Task DoHandleError(object message, Type messageType, IMessageScope messageScope, IConsumerContext consumerContext, Exception ex, int attempts, CancellationToken cancellationToken) + private async Task DoHandleError(object message, Type messageType, IMessageScope messageScope, IConsumerContext consumerContext, Exception ex, int attempts) { var errorHandlerResult = ProcessResult.Failure; diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs index b1f02b40..6f68faa1 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/ResponseMessageProcessor.cs @@ -69,7 +69,6 @@ private Exception OnResponseArrived(TTransportMessage transportMessage, string p if (requestState == null) { LogResponseWillBeDiscarded(path, requestId); - // ToDo: add and API hook to these kind of situation return null; } diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index 827bf499..3199a90d 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -760,6 +760,7 @@ public virtual IMessageScope CreateMessageScope(ConsumerSettings consumerSetting } #if NETSTANDARD2_0 + public abstract partial class MessageBusBase { private partial void LogCouldNotStartConsumers(Exception ex) diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs index 19d8c1fe..378d28f6 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/ConsumerBuilderTest.cs @@ -22,7 +22,7 @@ public void Given_MessageType_When_Configured_Then_MessageType_ProperlySet_And_C // assert subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); subject.ConsumerSettings.ConsumerType.Should().BeNull(); - subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeMessage)); + subject.ConsumerSettings.MessageType.Should().Be(); } [Theory] @@ -95,36 +95,36 @@ public void Given_BaseMessageType_And_ItsHierarchy_When_WithConsumer_ForTheBaseT subject.ConsumerSettings.ResponseType.Should().BeNull(); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); - subject.ConsumerSettings.ConsumerType.Should().Be(typeof(BaseMessageConsumer)); + subject.ConsumerSettings.ConsumerType.Should().Be(); Func call = () => subject.ConsumerSettings.ConsumerMethod(new BaseMessageConsumer(), new BaseMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(BaseMessage)); subject.ConsumerSettings.Invokers.Count.Should().Be(4); var consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(BaseMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(BaseMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(BaseMessageConsumer)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new BaseMessageConsumer(), new BaseMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(BaseMessage)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedAMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedAMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedAMessageConsumer)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new DerivedAMessageConsumer(), new DerivedAMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(DerivedAMessage)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedBMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedBMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedBMessageConsumer)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new DerivedBMessageConsumer(), new DerivedBMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(DerivedBMessage)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(Derived2AMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(Derived2AMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(Derived2AMessageConsumer)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new Derived2AMessageConsumer(), new Derived2AMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(Derived2AMessage)); @@ -149,36 +149,36 @@ public void Given_BaseMessageType_And_ItsHierarchy_And_ConsumerOfContext_When_Wi subject.ConsumerSettings.ResponseType.Should().BeNull(); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); - subject.ConsumerSettings.ConsumerType.Should().Be(typeof(BaseMessageConsumerOfContext)); + subject.ConsumerSettings.ConsumerType.Should().Be(); Func call = () => subject.ConsumerSettings.ConsumerMethod(new BaseMessageConsumerOfContext(), new BaseMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(BaseMessage)); subject.ConsumerSettings.Invokers.Count.Should().Be(4); var consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(BaseMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(BaseMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(BaseMessageConsumerOfContext)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new BaseMessageConsumerOfContext(), new BaseMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(BaseMessage)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedAMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedAMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedAMessageConsumerOfContext)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new DerivedAMessageConsumerOfContext(), new DerivedAMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(DerivedAMessage)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedBMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedBMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedBMessageConsumerOfContext)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new DerivedBMessageConsumerOfContext(), new DerivedBMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(DerivedBMessage)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(Derived2AMessage)); - consumerInvokerSettings.MessageType.Should().Be(typeof(Derived2AMessage)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(Derived2AMessageConsumerOfContext)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new Derived2AMessageConsumerOfContext(), new Derived2AMessage(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(Derived2AMessage)); @@ -198,25 +198,25 @@ public void Given_BaseRequestType_And_ItsHierarchy_When_WithConsumer_ForTheBaseT .WithConsumer(); // assert - subject.ConsumerSettings.ResponseType.Should().Be(typeof(BaseResponse)); + subject.ConsumerSettings.ResponseType.Should().Be(); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.Consumer); - subject.ConsumerSettings.ConsumerType.Should().Be(typeof(BaseRequestConsumer)); + subject.ConsumerSettings.ConsumerType.Should().Be(); Func call = () => subject.ConsumerSettings.ConsumerMethod(new BaseRequestConsumer(), new BaseRequest(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(BaseRequest)); subject.ConsumerSettings.Invokers.Count.Should().Be(2); var consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(BaseRequest)); - consumerInvokerSettings.MessageType.Should().Be(typeof(BaseRequest)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(BaseRequestConsumer)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new BaseRequestConsumer(), new BaseRequest(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(BaseRequest)); consumerInvokerSettings = subject.ConsumerSettings.Invokers.Single(x => x.MessageType == typeof(DerivedRequest)); - consumerInvokerSettings.MessageType.Should().Be(typeof(DerivedRequest)); - consumerInvokerSettings.ConsumerType.Should().Be(typeof(DerivedRequestConsumer)); + consumerInvokerSettings.MessageType.Should().Be(); + consumerInvokerSettings.ConsumerType.Should().Be(); consumerInvokerSettings.ParentSettings.Should().BeSameAs(subject.ConsumerSettings); call = () => consumerInvokerSettings.ConsumerMethod(new DerivedRequestConsumer(), new DerivedRequest(), consumerContextMock.Object, consumerContextMock.Object.CancellationToken); call.Should().ThrowAsync().WithMessage(nameof(DerivedRequest)); diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs index 82e95fed..4e305534 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/HandlerBuilderTest.cs @@ -20,8 +20,8 @@ public void When_Created_Given_RequestAndResposeType_Then_MessageType_And_Respon var subject = new HandlerBuilder(_messageBusSettings); // assert - subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); - subject.ConsumerSettings.ResponseType.Should().Be(typeof(SomeResponse)); + subject.ConsumerSettings.MessageType.Should().Be(); + subject.ConsumerSettings.ResponseType.Should().Be(); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); subject.ConsumerSettings.ConsumerType.Should().BeNull(); subject.ConsumerSettings.Invokers.Should().BeEmpty(); @@ -34,7 +34,7 @@ public void When_Created_Given_RequestWithoutResposeType_Then_MessageType_And_De var subject = new HandlerBuilder(_messageBusSettings); // assert - subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequestWithoutResponse)); + subject.ConsumerSettings.MessageType.Should().Be(); subject.ConsumerSettings.ResponseType.Should().BeNull(); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); subject.ConsumerSettings.ConsumerType.Should().BeNull(); @@ -101,14 +101,14 @@ public void When_Configured_Given_RequestResponse_Then_ProperSettings(bool ofCon } // assert - subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequest)); + subject.ConsumerSettings.MessageType.Should().Be(); subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.Instances.Should().Be(3); subject.ConsumerSettings.ConsumerType.Should().Be(consumerType); subject.ConsumerSettings.ConsumerMode.Should().Be(ConsumerMode.RequestResponse); - subject.ConsumerSettings.ResponseType.Should().Be(typeof(SomeResponse)); + subject.ConsumerSettings.ResponseType.Should().Be(); subject.ConsumerSettings.Invokers.Count.Should().Be(2); @@ -153,7 +153,7 @@ public void When_Configured_Given_RequestWithoutResponse_And_HandlersWithDerived } // assert - subject.ConsumerSettings.MessageType.Should().Be(typeof(SomeRequestWithoutResponse)); + subject.ConsumerSettings.MessageType.Should().Be(); subject.ConsumerSettings.Path.Should().Be(_path); subject.ConsumerSettings.Instances.Should().Be(3); diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs index 0a8ff343..ca296748 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/TypeCollectionTests.cs @@ -9,7 +9,7 @@ public void Add_Should_AddTypeToCollection_IfAssignableToGeneric() var collection = new TypeCollection(); // Act - collection.Add(typeof(SampleClass)); + collection.Add(); // Assert collection.Count.Should().Be(1); @@ -37,7 +37,7 @@ public void Add_Should_ThrowException_WhenTypeIsAssignableToGenericButAlreadyExi collection.Add(); // Act - Action act = () => collection.Add(typeof(SampleClass)); + Action act = () => collection.Add(); // Assert act.Should().Throw().WithMessage("Type already exists in the collection. (Parameter 'type')"); @@ -169,7 +169,7 @@ public void Remove_Should_RemoveTypeFromCollection_WhenSuppliedAsType() collection.Add(); // Act - var removed = collection.Remove(typeof(SampleClass)); + var removed = collection.Remove(); // Assert removed.Should().BeTrue(); diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs index cc0cd751..a44c1198 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs @@ -115,12 +115,12 @@ public async Task When_PublishToMemoryBus_Given_InsideConsumerWithMessageScope_T // all the internal messages should be processed by Memory bus store .Where(x => x.Name == nameof(InternalMessageConsumer) || x.Name == nameof(InternalMessageConsumerInterceptor) || x.Name == nameof(InternalMessageProducerInterceptor) || x.Name == nameof(InternalMessagePublishInterceptor)) - .Should().AllSatisfy(x => x.ContextMessageBusType.Should().Be(typeof(MemoryMessageBus))); + .Should().AllSatisfy(x => x.ContextMessageBusType.Should().Be()); // all the external messages should be processed by Azure Service Bus store .Where(x => x.Name == nameof(ExternalMessageConsumer) || x.Name == nameof(ExternalMessageConsumerInterceptor)) - .Should().AllSatisfy(x => x.ContextMessageBusType.Should().Be(typeof(ServiceBusMessageBus))); + .Should().AllSatisfy(x => x.ContextMessageBusType.Should().Be()); // in this order var eventsThatHappenedWhenExternalWasPublished = grouping.Values.SingleOrDefault(x => x.Count == 2); diff --git a/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs index 0ca55039..0287b848 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.Test/OutboxLockRenewalTimerTests.cs @@ -111,9 +111,8 @@ public async Task CallbackAsync_ShouldReturnGracefullyIfTokenCancelled() lockLostActionMock.Verify(a => a(It.IsAny()), Times.Never); } - private OutboxLockRenewalTimer, Guid> CreateTimer(Action lockLostAction = null) - { - return new OutboxLockRenewalTimer, Guid>( + private OutboxLockRenewalTimer, Guid> CreateTimer(Action lockLostAction = null) + => new( _loggerMock.Object, _outboxRepositoryMock.Object, _instanceIdProviderMock.Object, @@ -121,7 +120,6 @@ private OutboxLockRenewalTimer, Guid> CreateTimer(Action, Guid> timer) { diff --git a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs index 0a237e38..41098586 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/JsonMessageSerializerTests.cs @@ -4,20 +4,16 @@ namespace SlimMessageBus.Host.Serialization.Json.Test; public class JsonMessageSerializerTests { - public static IEnumerable Data => - [ - [null, null], - [10, 10], - [false, false], - [true, true], - ["string", "string"], - [DateTime.Now.Date, DateTime.Now.Date], - [Guid.Empty, "00000000-0000-0000-0000-000000000000"], - ]; - - public JsonMessageSerializerTests() + public static TheoryData Data => new() { - } + { null, null }, + { 10, 10 }, + { false, false }, + { true, true}, + { "string", "string"}, + { DateTime.Now.Date, DateTime.Now.Date}, + { Guid.Empty, "00000000-0000-0000-0000-000000000000"}, + }; [Theory] [MemberData(nameof(Data))] diff --git a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs index b4af80c1..0423ef30 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs +++ b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs @@ -2,16 +2,16 @@ namespace SlimMessageBus.Host.Serialization.SystemTextJson.Test; public class JsonMessageSerializerTests { - public static IEnumerable Data => - [ - [null, null], - [10, 10], - [false, false], - [true, true], - ["string", "string"], - [DateTime.Now.Date, DateTime.Now.Date], - [Guid.Empty, "00000000-0000-0000-0000-000000000000"], - ]; + public static TheoryData Data => new() + { + { null, null}, + { 10, 10}, + { false, false}, + { true, true}, + { "string", "string"}, + { DateTime.Now.Date, DateTime.Now.Date}, + { Guid.Empty, "00000000-0000-0000-0000-000000000000"}, + }; [Theory] [MemberData(nameof(Data))] @@ -58,7 +58,7 @@ public void When_RegisterSerializer_Then_UsesOptionsFromContainerIfAvailable(boo // Simulate the options have been used in an serializer already (see https://github.com/zarusz/SlimMessageBus/issues/252) // Modifying options (adding converters) when the options are already in use by a serializer will throw an exception: System.InvalidOperationException : This JsonSerializerOptions instance is read-only or has already been used in serialization or deserialization. - JsonSerializer.SerializeToUtf8Bytes(new Dictionary(), typeof(Dictionary), jsonOptions); + JsonSerializer.SerializeToUtf8Bytes(new Dictionary(), jsonOptions); if (jsonOptionComeFromContainer) { diff --git a/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs b/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs index d412d632..3ac15e98 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Collections/RuntimeTypeCacheTests.cs @@ -5,13 +5,8 @@ public class RuntimeTypeCacheTests { - private readonly RuntimeTypeCache _subject; + private readonly RuntimeTypeCache _subject = new(); - public RuntimeTypeCacheTests() - { - _subject = new RuntimeTypeCache(); - } - [Theory] [InlineData(typeof(bool), typeof(int), false)] [InlineData(typeof(SomeMessageConsumer), typeof(IConsumer), true)] @@ -86,13 +81,9 @@ public void When_GetClosedGenericType(Type openGenericType, Type genericParamete public static TheoryData Data => new() { { (new SomeMessage[] { new() }).Concat([new SomeMessage()]), true }, - { new List { new(), new() }, true }, - { new SomeMessage[] { new(), new() }, true}, - { new HashSet { new(), new() }, true }, - { new object(), false }, }; @@ -109,7 +100,7 @@ public void Given_ObjectThatIsCollection_When_Then(object collection, bool isCol if (isCollection) { collectionInfo.Should().NotBeNull(); - collectionInfo.ItemType.Should().Be(typeof(SomeMessage)); + collectionInfo.ItemType.Should().Be(); var col = collectionInfo.ToCollection(collection); col.Should().NotBeNull(); col.Should().BeSameAs((IEnumerable)collection); From cb4f6c227b581cb6f002d025b3754c5cab376a90 Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Sun, 26 Jan 2025 22:56:33 +0100 Subject: [PATCH 21/21] Prepare for merge. Signed-off-by: Tomasz Maruszak --- README.md | 3 +-- docs/intro.md | 23 ++++++++++++++----- docs/intro.t.md | 21 +++++++++++++---- src/Host.Plugin.Properties.xml | 2 +- .../SlimMessageBus.Host.Configuration.csproj | 2 +- .../SlimMessageBus.Host.Interceptor.csproj | 2 +- .../SlimMessageBus.Host.Serialization.csproj | 2 +- src/SlimMessageBus/SlimMessageBus.csproj | 2 +- 8 files changed, 39 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fcbc9a23..e9861860 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,7 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zarusz_SlimMessageBus&metric=vulnerabilities)](https://sonarcloud.io/summary/overall?id=zarusz_SlimMessageBus) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zarusz_SlimMessageBus&metric=alert_status)](https://sonarcloud.io/summary/overall?id=zarusz_SlimMessageBus) -> The v3 release is [under construction](https://github.com/zarusz/SlimMessageBus/tree/release/v3). -> The v2 release is available (see [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/Host.Transport-2.0.0)). +> The v3 release is [available](https://github.com/zarusz/SlimMessageBus/releases/tag/3.0.0). - [Key elements of SlimMessageBus](#key-elements-of-slimmessagebus) - [Docs](#docs) diff --git a/docs/intro.md b/docs/intro.md index 5a873a3e..93617bd1 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -7,7 +7,7 @@ - [Set message headers](#set-message-headers) - [Consumer](#consumer) - [Start or Stop message consumption](#start-or-stop-message-consumption) - - [Health check circuit breaker](#health-check-circuit-breaker) + - [Health check circuit breaker](#health-check-circuit-breaker) - [Consumer context (additional message information)](#consumer-context-additional-message-information) - [Per-message DI container scope](#per-message-di-container-scope) - [Hybrid bus and message scope reuse](#hybrid-bus-and-message-scope-reuse) @@ -39,13 +39,14 @@ - [Order of Execution](#order-of-execution) - [Generic interceptors](#generic-interceptors) - [Error Handling](#error-handling) - - [Azure Service Bus](#azure-service-bus) - - [RabbitMQ](#rabbitmq) + - [Azure Service Bus](#azure-service-bus) + - [RabbitMQ](#rabbitmq) - [Logging](#logging) - [Debugging](#debugging) - [Provider specific functionality](#provider-specific-functionality) - [Topology Provisioning](#topology-provisioning) - [Triggering Topology Provisioning](#triggering-topology-provisioning) +- [Versions](#versions) ## Configuration @@ -316,7 +317,8 @@ Consumers can be linked to [.NET app health checks](https://learn.microsoft.com/ }) }) ``` -*Requires: SlimMessageBus.Host.CircuitBreaker.HealthCheck* + +_Requires: SlimMessageBus.Host.CircuitBreaker.HealthCheck_ #### Consumer context (additional message information) @@ -1151,11 +1153,13 @@ Transport plugins provide specialized error handling interfaces with a default i This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. #### Azure Service Bus + | ProcessResult | Description | | ------------- | ---------------------------------------------------------------------------------- | | DeadLetter | Abandons further processing of the message by sending it to the dead letter queue. | #### RabbitMQ + | ProcessResult | Description | | ------------- | --------------------------------------------------------------- | | Requeue | Return the message to the queue for re-processing 1. | @@ -1163,6 +1167,7 @@ This approach allows for transport-specific error handling, ensuring that specia 1 RabbitMQ does not have a maximum delivery count. Please use `Requeue` with caution as, if no other conditions are applied, it may result in an infinite message loop. Example retry with exponential back-off and short-curcuit to dead letter exchange on non-transient exceptions (using the [RabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) abstract implementation): + ```cs public class RetryHandler : RabbitMqConsumerErrorHandler { @@ -1179,7 +1184,7 @@ public class RetryHandler : RabbitMqConsumerErrorHandler { var delay = (attempts * 1000) + (_random.Next(1000) - 500); await Task.Delay(delay, consumerContext.CancellationToken); - + // in process retry return Retry(); } @@ -1199,6 +1204,7 @@ public class RetryHandler : RabbitMqConsumerErrorHandler } } ``` + ## Logging SlimMessageBus uses [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions): @@ -1253,4 +1259,9 @@ ITopologyControl ctrl = // injected await ctrl.ProvisionTopology(); ``` -This allows to recreate missing elements in the infrastructure without restarting the whole application. \ No newline at end of file +This allows to recreate missing elements in the infrastructure without restarting the whole application. + +## Versions + +- The v3 release [migration guide](https://github.com/zarusz/SlimMessageBus/tree/release/v3). +- The v2 release [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/Host.Transport-2.0.0). \ No newline at end of file diff --git a/docs/intro.t.md b/docs/intro.t.md index 7c1dc571..b962f339 100644 --- a/docs/intro.t.md +++ b/docs/intro.t.md @@ -7,7 +7,7 @@ - [Set message headers](#set-message-headers) - [Consumer](#consumer) - [Start or Stop message consumption](#start-or-stop-message-consumption) - - [Health check circuit breaker](#health-check-circuit-breaker) + - [Health check circuit breaker](#health-check-circuit-breaker) - [Consumer context (additional message information)](#consumer-context-additional-message-information) - [Per-message DI container scope](#per-message-di-container-scope) - [Hybrid bus and message scope reuse](#hybrid-bus-and-message-scope-reuse) @@ -39,13 +39,14 @@ - [Order of Execution](#order-of-execution) - [Generic interceptors](#generic-interceptors) - [Error Handling](#error-handling) - - [Azure Service Bus](#azure-service-bus) - - [RabbitMQ](#rabbitmq) + - [Azure Service Bus](#azure-service-bus) + - [RabbitMQ](#rabbitmq) - [Logging](#logging) - [Debugging](#debugging) - [Provider specific functionality](#provider-specific-functionality) - [Topology Provisioning](#topology-provisioning) - [Triggering Topology Provisioning](#triggering-topology-provisioning) +- [Versions](#versions) ## Configuration @@ -316,7 +317,8 @@ Consumers can be linked to [.NET app health checks](https://learn.microsoft.com/ }) }) ``` -*Requires: SlimMessageBus.Host.CircuitBreaker.HealthCheck* + +_Requires: SlimMessageBus.Host.CircuitBreaker.HealthCheck_ #### Consumer context (additional message information) @@ -1129,11 +1131,13 @@ Transport plugins provide specialized error handling interfaces with a default i This approach allows for transport-specific error handling, ensuring that specialized handlers can be prioritized. #### Azure Service Bus + | ProcessResult | Description | | ------------- | ---------------------------------------------------------------------------------- | | DeadLetter | Abandons further processing of the message by sending it to the dead letter queue. | #### RabbitMQ + | ProcessResult | Description | | ------------- | --------------------------------------------------------------- | | Requeue | Return the message to the queue for re-processing 1. | @@ -1141,6 +1145,7 @@ This approach allows for transport-specific error handling, ensuring that specia 1 RabbitMQ does not have a maximum delivery count. Please use `Requeue` with caution as, if no other conditions are applied, it may result in an infinite message loop. Example retry with exponential back-off and short-curcuit to dead letter exchange on non-transient exceptions (using the [RabbitMqConsumerErrorHandler](../src/SlimMessageBus.Host.RabbitMQ/Consumers/IRabbitMqConsumerErrorHandler.cs) abstract implementation): + ```cs public class RetryHandler : RabbitMqConsumerErrorHandler { @@ -1157,7 +1162,7 @@ public class RetryHandler : RabbitMqConsumerErrorHandler { var delay = (attempts * 1000) + (_random.Next(1000) - 500); await Task.Delay(delay, consumerContext.CancellationToken); - + // in process retry return Retry(); } @@ -1177,6 +1182,7 @@ public class RetryHandler : RabbitMqConsumerErrorHandler } } ``` + ## Logging SlimMessageBus uses [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions): @@ -1232,3 +1238,8 @@ await ctrl.ProvisionTopology(); ``` This allows to recreate missing elements in the infrastructure without restarting the whole application. + +## Versions + +- The v3 release [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/3.0.0). +- The v2 release [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/Host.Transport-2.0.0). diff --git a/src/Host.Plugin.Properties.xml b/src/Host.Plugin.Properties.xml index b40bc2f8..c6d7421e 100644 --- a/src/Host.Plugin.Properties.xml +++ b/src/Host.Plugin.Properties.xml @@ -4,7 +4,7 @@ - 3.0.0-rc906 + 3.0.0 \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index aaf193cc..ee264109 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -6,7 +6,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host - 3.0.0-rc906 + 3.0.0 diff --git a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj index 500f4fce..bce93b11 100644 --- a/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj +++ b/src/SlimMessageBus.Host.Interceptor/SlimMessageBus.Host.Interceptor.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc906 + 3.0.0 Core interceptor interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj index 4c8c310d..f77b6552 100644 --- a/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj +++ b/src/SlimMessageBus.Host.Serialization/SlimMessageBus.Host.Serialization.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc906 + 3.0.0 Core serialization interfaces of SlimMessageBus SlimMessageBus diff --git a/src/SlimMessageBus/SlimMessageBus.csproj b/src/SlimMessageBus/SlimMessageBus.csproj index 3159f398..8948b434 100644 --- a/src/SlimMessageBus/SlimMessageBus.csproj +++ b/src/SlimMessageBus/SlimMessageBus.csproj @@ -3,7 +3,7 @@ - 3.0.0-rc906 + 3.0.0 This library provides a lightweight, easy-to-use message bus interface for .NET, offering a simplified facade for working with messaging brokers. It supports multiple transport providers for popular messaging systems, as well as in-memory (in-process) messaging for efficient local communication.