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)); }