diff --git a/README.md b/README.md index 2767d5a6..55c7dbe8 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,15 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i - [Introduction](docs/intro.md) - Providers: + - [Apache Kafka](docs/provider_kafka.md) - [Azure EventHubs](docs/provider_azure_eventhubs.md) - [Azure ServiceBus](docs/provider_azure_servicebus.md) - - [Apache Kafka](docs/provider_kafka.md) - [Hybrid](docs/provider_hybrid.md) - - [Memory](docs/provider_memory.md) - [MQTT](docs/provider_mqtt.md) + - [Memory](docs/provider_memory.md) - [RabbitMQ](docs/provider_rabbitmq.md) - [Redis](docs/provider_redis.md) + - [SQL](docs/provider_sql.md) - Plugins: - [Serialization](docs/serialization.md) - [Transactional Outbox](docs/plugin_outbox.md) @@ -74,6 +75,7 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i | `.Host.MQTT` | Transport provider for MQTT | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.MQTT.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.MQTT) | | `.Host.RabbitMQ` | Transport provider for RabbitMQ | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.RabbitMQ.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.RabbitMQ) | | `.Host.Redis` | Transport provider for Redis | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Redis.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Redis) | +| `.Host.Sql` | Transport provider implementation for SQL database message passing | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Sql.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Sql) | | **Serialization** | | | | `.Host.Serialization.Json` | Serialization plugin for JSON (Newtonsoft.Json library) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Serialization.Json.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Serialization.Json) | | `.Host.Serialization.SystemTextJson` | Serialization plugin for JSON (System.Text.Json library) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Serialization.SystemTextJson.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Serialization.SystemTextJson) | diff --git a/build/tasks.ps1 b/build/tasks.ps1 index 20cd8a4d..2fd36167 100644 --- a/build/tasks.ps1 +++ b/build/tasks.ps1 @@ -32,6 +32,7 @@ $projects = @( "SlimMessageBus.Host.Redis", "SlimMessageBus.Host.Mqtt", "SlimMessageBus.Host.RabbitMQ", + "SlimMessageBus.Host.Sql", "SlimMessageBus.Host.FluentValidation", diff --git a/docs/NuGet.md b/docs/NuGet.md index 51590a17..b088e300 100644 --- a/docs/NuGet.md +++ b/docs/NuGet.md @@ -7,18 +7,19 @@ SlimMessageBus additionally provides request-response implementation over messag Transports: - Apache Kafka -- Azure Service Bus - Azure Event Hub -- Redis +- Azure Service Bus +- Hybrid (composition of the bus out of many transports) +- In-Memory transport (domain events, mediator) - MQTT / Azure IoT Hub - RabbitMQ -- In-Memory transport (domain events, mediator) -- Hybrid (composition of the bus out of many transports) +- Redis +- SQL (MS SQL, PostgreSql) Plugins: - Message validation via Fluent Validation -- Transactional Outbox pattern +- Transactional Outbox pattern (SQL, DbContext) - Serialization using JSON, Avro, ProtoBuf - AsyncAPI specification generation diff --git a/docs/README.md b/docs/README.md index f6b68525..98833b97 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,8 +3,12 @@ - [Introduction](intro.md) - 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) + - [RabbitMq](provider_rabbitmq.md) + - [Redis](provider_redis.md) + - [SQL](provider_sql.md) - [Serialization Plugins](serialization.md) diff --git a/docs/provider_sql.md b/docs/provider_sql.md new file mode 100644 index 00000000..c35384df --- /dev/null +++ b/docs/provider_sql.md @@ -0,0 +1,66 @@ +# SQL transport provider for SlimMessageBus + +Please read the [Introduction](intro.md) before reading this provider documentation. + +- [About](#about) +- [SQL Compatibility](#sql-compatibility) +- [Configuration](#configuration) +- [How it works](#how-it-works) + +## About + +The SQL transport provider allows to leverage a single shared SQL database instance as a messaging broker for all the collaborating producers and consumers. + +This transport might be optimal for simpler applications that do not have a dedicated messaging infrastructure available, do not have high throughput needs, or want to target a simplistic deployment model. + +When the application grows over time, and given that SMB is an abstraction, the migration from SQL towards a dedicated messaging system should be super easy. + +## SQL Compatibility + +This transport has been tested on SQL Azure (T-SQL), and should work on most other databases. +If you see an issue, please raise an github issue. + +## Configuration + +ToDo: Finish + +The configuration is arranged via the `.WithProviderMqtt(cfg => {})` method on the message bus builder. + +```cs +services.AddSlimMessageBus(mbb => +{ + mbb.WithProviderMqtt(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); + }); + + mbb.AddServicesFromAssemblyContaining(); + mbb.AddJsonSerializer(); +}); +``` + +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. +Therefore ensure all of the service instances point to the same database cluster. + +- Single table is used to store all the exchanged messages (by default table is called `Messages`). +- Producers send messages to the messages table. + - There are two types of entities (queues, and topics for pub/sub). + - In the case of a topic: + - Each subscription gets a copy of the message. + - Subscription has a lifetime, and can expire after certain idle time. Along with it, all the messages placed on the subscription. +- Consumers (queue consumers, or subscribers in pub/sub) long poll the table to pick up their respective message. + - Queue consumers compete for the message, and ensure only one consumer instance is processing the message. + - Topic subscribers complete for the message within the same subscription. +- In the future we might consider: + - Table per each entity, so that we can minimize table locking. + - Sessions to ensure order of processing within the same message session ID - similar to how Azure Service Bus feature or Apache Kafka topic-partition works. diff --git a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs index 4a5345bb..520bd298 100644 --- a/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureEventHub/EventHubMessageBus.cs @@ -102,7 +102,7 @@ protected override async Task OnStart() } } - protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { AssertActive(); diff --git a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs index 608a8a70..8ee49042 100644 --- a/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs +++ b/src/SlimMessageBus.Host.AzureServiceBus/ServiceBusMessageBus.cs @@ -16,6 +16,28 @@ public ServiceBusMessageBus(MessageBusSettings settings, ServiceBusMessageBusSet OnBuildProvider(); } + protected override async ValueTask DisposeAsyncCore() + { + await base.DisposeAsyncCore().ConfigureAwait(false); + + var producers = _producerByPath.ClearAndSnapshot(); + if (producers.Count > 0) + { + var producerCloseTasks = producers.Select(x => + { + _logger.LogDebug("Closing sender client for path {Path}", x.EntityPath); + return x.CloseAsync(); + }); + await Task.WhenAll(producerCloseTasks).ConfigureAwait(false); + } + + if (_client != null) + { + await _client.DisposeAsync().ConfigureAwait(false); + _client = null; + } + } + protected override IMessageBusSettingsValidationService ValidationService => new ServiceBusMessageBusSettingsValidationService(Settings, ProviderSettings); public override async Task ProvisionTopology() @@ -26,7 +48,6 @@ public override async Task ProvisionTopology() await provisioningService.ProvisionTopology(); // provisining happens asynchronously } - #region Overrides of MessageBusBase protected override void Build() @@ -92,29 +113,7 @@ void AddConsumerFrom(TopicSubscriptionParams topicSubscription, IMessageProcesso } } - protected override async ValueTask DisposeAsyncCore() - { - await base.DisposeAsyncCore().ConfigureAwait(false); - - var producers = _producerByPath.ClearAndSnapshot(); - if (producers.Count > 0) - { - var producerCloseTasks = producers.Select(x => - { - _logger.LogDebug("Closing sender client for path {Path}", x.EntityPath); - return x.CloseAsync(); - }); - await Task.WhenAll(producerCloseTasks).ConfigureAwait(false); - } - - if (_client != null) - { - await _client.DisposeAsync().ConfigureAwait(false); - _client = null; - } - } - - protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { var messageType = message?.GetType(); @@ -172,12 +171,5 @@ private void InvokeMessageModifier(object message, Type messageType, ServiceBusM } } - public override Task ProduceRequest(object request, IDictionary requestHeaders, string path, ProducerSettings producerSettings) - { - if (requestHeaders is null) throw new ArgumentNullException(nameof(requestHeaders)); - - return base.ProduceRequest(request, requestHeaders, path, producerSettings); - } - #endregion } diff --git a/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs b/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs index 0124b395..f739c295 100644 --- a/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs +++ b/src/SlimMessageBus.Host.Configuration/Builders/MessageBusBuilder.cs @@ -20,7 +20,7 @@ public class MessageBusBuilder /// /// The bus factory method. /// - public Func BusFactory { get; private set; } + public Func BusFactory { get; private set; } public IList> PostConfigurationActions { get; } = new List>(); @@ -230,7 +230,7 @@ public MessageBusBuilder WithDependencyResolver(IServiceProvider serviceProvider return this; } - public MessageBusBuilder WithProvider(Func provider) + public MessageBusBuilder WithProvider(Func provider) { BusFactory = provider ?? throw new ArgumentNullException(nameof(provider)); return this; @@ -330,7 +330,7 @@ public MessageBusBuilder AddChildBus(string busName, Action b return this; } - public IMessageBus Build() + public IMessageBusProvider Build() { if (BusFactory is null) { @@ -339,4 +339,4 @@ public IMessageBus Build() } return BusFactory(Settings); } -} \ No newline at end of file +} diff --git a/src/SlimMessageBus.Host.Configuration/IMessageBusProvider.cs b/src/SlimMessageBus.Host.Configuration/IMessageBusProvider.cs new file mode 100644 index 00000000..aa853cd4 --- /dev/null +++ b/src/SlimMessageBus.Host.Configuration/IMessageBusProvider.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host; + +public interface IMessageBusProvider +{ + string Name { get; } + MessageBusSettings Settings { get; } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj index c0e4aa53..5900a695 100644 --- a/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj +++ b/src/SlimMessageBus.Host.Configuration/SlimMessageBus.Host.Configuration.csproj @@ -7,6 +7,7 @@ Core configuration interfaces of SlimMessageBus SlimMessageBus SlimMessageBus.Host + 2.0.6-rc1 diff --git a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs index 8b0d133f..a9cc2fe5 100644 --- a/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs +++ b/src/SlimMessageBus.Host.Kafka/KafkaMessageBus.cs @@ -114,7 +114,7 @@ protected override async ValueTask DisposeAsyncCore() } } - protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { AssertActive(); diff --git a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs index 44e15b47..8badbd25 100644 --- a/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs +++ b/src/SlimMessageBus.Host.Memory/MemoryMessageBus.cs @@ -123,21 +123,21 @@ private IMessageProcessorQueue CreateMessageProcessorQueue(IMessageProcessor(), CancellationToken); } - protected override Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default) + protected override Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) => Task.CompletedTask; // Not used public override Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker) => Task.CompletedTask; // Not used to responses - protected override Task PublishInternal(object message, string path, IDictionary messageHeaders, CancellationToken cancellationToken, ProducerSettings producerSettings, IServiceProvider currentServiceProvider) - => ProduceInternal(message, path, messageHeaders, currentServiceProvider, isPublish: true, cancellationToken); + protected override Task PublishInternal(object message, string path, IDictionary messageHeaders, CancellationToken cancellationToken, ProducerSettings producerSettings, IMessageBusTarget targetBus) + => ProduceInternal(message, path, messageHeaders, targetBus, isPublish: true, cancellationToken); - protected override Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IServiceProvider currentServiceProvider, CancellationToken cancellationToken) - => ProduceInternal(request, path, requestHeaders, currentServiceProvider, isPublish: false, cancellationToken); + protected override Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) + => ProduceInternal(request, path, requestHeaders, targetBus, isPublish: false, cancellationToken); #endregion - private async Task ProduceInternal(object message, string path, IDictionary requestHeaders, IServiceProvider currentServiceProvider, bool isPublish, CancellationToken cancellationToken) + private async Task ProduceInternal(object message, string path, IDictionary requestHeaders, IMessageBusTarget targetBus, bool isPublish, CancellationToken cancellationToken) { var messageType = message.GetType(); var producerSettings = GetProducerSettings(messageType); @@ -166,8 +166,9 @@ private async Task ProduceInternal(object me return default; } + var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; // Execute the message processor in synchronous manner - var r = await messageProcessor.ProcessMessage(transportMessage, messageHeadersReadOnly, currentServiceProvider: currentServiceProvider, cancellationToken: cancellationToken); + var r = await messageProcessor.ProcessMessage(transportMessage, messageHeadersReadOnly, currentServiceProvider: serviceProvider, cancellationToken: cancellationToken); if (r.Exception != null) { // We want to pass the same exception to the sender as it happened in the handler/consumer diff --git a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs index 80d2f90d..0f4bf4f2 100644 --- a/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs +++ b/src/SlimMessageBus.Host.Mqtt/MqttMessageBus.cs @@ -99,7 +99,7 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg) return Task.CompletedTask; } - protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { var m = new MqttApplicationMessage { diff --git a/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs index dff6c0ca..a84abdb4 100644 --- a/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs +++ b/src/SlimMessageBus.Host.Outbox.DbContext/DbContextOutboxRepository.cs @@ -22,7 +22,7 @@ public DbContextOutboxRepository(ILogger logger, SqlOutboxS public override async ValueTask BeginTransaction() { ValidateNoTransactionStarted(); - await DbContext.Database.BeginTransactionAsync(Settings.TransactionIsolationLevel); + await DbContext.Database.BeginTransactionAsync(Settings.SqlSettings.TransactionIsolationLevel); } public override ValueTask CommitTransaction() diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs index 44082069..e7eabeb7 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/MessageBusBuilderExtensions.cs @@ -26,6 +26,7 @@ public static MessageBusBuilder AddOutboxUsingSql(this Messag services.Replace(ServiceDescriptor.Scoped(svp => svp.GetRequiredService())); services.TryAddSingleton(); + services.TryAddSingleton(); }); return mbb; } diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs index c973e3fd..3a652d40 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/Configuration/SqlOutboxSettings.cs @@ -1,39 +1,6 @@ namespace SlimMessageBus.Host.Outbox.Sql; -using System.Data; - public class SqlOutboxSettings : OutboxSettings { - public string DatabaseSchemaName { get; set; } = "dbo"; - public string DatabaseTableName { get; set; } = "Outbox"; - public string DatabaseMigrationsTableName { get; set; } = "__EFMigrationsHistory"; - public SqlDialect Dialect { get; set; } = SqlDialect.SqlServer; - /// - /// Desired of the transaction scope created by the consumers (when is enabled). - /// - public IsolationLevel TransactionIsolationLevel { get; set; } = IsolationLevel.RepeatableRead; - - public SqlRetrySettings SchemaCreationRetry { get; set; } = new() - { - RetryCount = 3, - RetryIntervalFactor = 1.2f, - RetryInterval = TimeSpan.FromSeconds(2), - }; - - public SqlRetrySettings OperationRetry { get; set; } = new() - { - RetryCount = 5, - RetryIntervalFactor = 1.5f, - RetryInterval = TimeSpan.FromSeconds(2), - }; - /// - /// Initializes the connection when set to a value. - /// - public TimeSpan? CommandTimeout { get; set; } + public CommonSqlSettings SqlSettings { get; set; } = new(); } - -public enum SqlDialect -{ - SqlServer = 1 - // ToDo: More to come -} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql/GlobalUsings.cs b/src/SlimMessageBus.Host.Outbox.Sql/GlobalUsings.cs index 8a0b79e6..43031c66 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/GlobalUsings.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/GlobalUsings.cs @@ -1,4 +1,12 @@ -global using Microsoft.Extensions.DependencyInjection; +global using System.Data; +global using System.Text.Json; + +global using Microsoft.Data.SqlClient; +global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; global using SlimMessageBus.Host.Interceptor; +global using SlimMessageBus.Host.Sql.Common; + + diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs b/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs index ed261363..11d61eaa 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/Interceptors/SqlTransactionConsumerInterceptor.cs @@ -13,16 +13,13 @@ public abstract class SqlTransactionConsumerInterceptor /// Wraps the consumer in an (conditionally). /// /// -public class SqlTransactionConsumerInterceptor : SqlTransactionConsumerInterceptor, IConsumerInterceptor where T : class +public class SqlTransactionConsumerInterceptor( + ILogger logger, + ISqlOutboxRepository outboxRepository) + : SqlTransactionConsumerInterceptor, IConsumerInterceptor where T : class { - private readonly ILogger _logger; - private readonly ISqlOutboxRepository _outboxRepository; - - public SqlTransactionConsumerInterceptor(ILogger logger, ISqlOutboxRepository outboxRepository) - { - _logger = logger; - _outboxRepository = outboxRepository; - } + private readonly ILogger _logger = logger; + private readonly ISqlOutboxRepository _outboxRepository = outboxRepository; public async Task OnHandle(T message, Func> next, IConsumerContext context) { @@ -58,8 +55,8 @@ public async Task OnHandle(T message, Func> next, IConsumer private static bool IsSqlTransactionEnabled(IConsumerContext context) { - var bus = context.Bus as MessageBusBase; - if (bus is null || context is not ConsumerContext consumerContext) + var bus = context.GetMasterMessageBus(); + if (bus == null || context is not ConsumerContext consumerContext) { return false; } diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SlimMessageBus.Host.Outbox.Sql.csproj b/src/SlimMessageBus.Host.Outbox.Sql/SlimMessageBus.Host.Outbox.Sql.csproj index fde274a4..f9b17fb5 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SlimMessageBus.Host.Outbox.Sql.csproj +++ b/src/SlimMessageBus.Host.Outbox.Sql/SlimMessageBus.Host.Outbox.Sql.csproj @@ -9,10 +9,10 @@ + - diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs new file mode 100644 index 00000000..f42372d5 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxMigrationService.cs @@ -0,0 +1,54 @@ +namespace SlimMessageBus.Host.Outbox.Sql; + +public class SqlOutboxMigrationService : CommonSqlMigrationService, IOutboxMigrationService +{ + public SqlOutboxMigrationService(ILogger logger, ISqlOutboxRepository repository, SqlOutboxSettings settings) + : base(logger, (CommonSqlRepository)repository, settings.SqlSettings) + { + } + + protected override async Task OnMigrate(CancellationToken token) + { + var qualifiedTableName = Repository.GetTableName(Settings.DatabaseTableName); + +#pragma warning disable CA1861 + await CreateTable(Settings.DatabaseTableName, [ + "Id uniqueidentifier NOT NULL", + "Timestamp datetime2(7) NOT NULL", + "BusName nvarchar(64) NOT NULL", + "MessageType nvarchar(256) NOT NULL", + "MessagePayload varbinary(max) NOT NULL", + "Headers nvarchar(max)", + "Path nvarchar(128)", + "InstanceId nvarchar(128) NOT NULL", + "LockInstanceId nvarchar(128) NOT NULL", + "LockExpiresOn datetime2(7) NOT NULL", + "DeliveryAttempt int NOT NULL", + "DeliveryComplete bit NOT NULL", + $"CONSTRAINT [PK_{Settings.DatabaseTableName}] PRIMARY KEY CLUSTERED([Id] ASC)" + ], + token); + + await CreateIndex("IX_Outbox_InstanceId", Settings.DatabaseTableName, [ + "DeliveryComplete", + "InstanceId" + ], token); + + await CreateIndex("IX_Outbox_LockExpiresOn", Settings.DatabaseTableName, [ + "DeliveryComplete", + "LockExpiresOn" + ], token); + + await CreateIndex("IX_Outbox_Timestamp_LockInstanceId", Settings.DatabaseTableName, [ + "DeliveryComplete", + "Timestamp", + "LockInstanceId", + ], token); +#pragma warning restore CA1861 + + await TryApplyMigration("20230120000000_SMB_Init", null, token); + + await TryApplyMigration("20230128225000_SMB_BusNameOptional", + @$"ALTER TABLE {qualifiedTableName} ALTER COLUMN BusName nvarchar(64) NULL", token); + } +} diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs index af7a7f55..3f1b1dec 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxRepository.cs @@ -1,251 +1,29 @@ namespace SlimMessageBus.Host.Outbox.Sql; -using System.Data; -using System.Reflection; -using System.Text.Json; - -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging; - -public class SqlOutboxRepository : ISqlOutboxRepository, IAsyncDisposable +public class SqlOutboxRepository : CommonSqlRepository, ISqlOutboxRepository { - private readonly ILogger _logger; private readonly SqlOutboxTemplate _sqlTemplate; private readonly JsonSerializerOptions _jsonOptions; - private SqlTransaction _transaction; protected SqlOutboxSettings Settings { get; } - protected SqlConnection Connection { get; } public SqlOutboxRepository(ILogger logger, SqlOutboxSettings settings, SqlOutboxTemplate sqlOutboxTemplate, SqlConnection connection) + : base(logger, settings.SqlSettings, connection) { - _logger = logger; _sqlTemplate = sqlOutboxTemplate; _jsonOptions = new(); _jsonOptions.Converters.Add(new ObjectToInferredTypesConverter()); Settings = settings; - Connection = connection; - } - - private async Task EnsureConnection() - { - if (Connection.State != ConnectionState.Open) - { - await Connection.OpenAsync(); - } - } - - protected virtual SqlCommand CreateCommand() - { - var cmd = Connection.CreateCommand(); - cmd.Transaction = CurrentTransaction; - - if (Settings.CommandTimeout != null) - { - cmd.CommandTimeout = (int)Settings.CommandTimeout.Value.TotalSeconds; - } - - return cmd; - } - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public virtual SqlTransaction CurrentTransaction => _transaction; - - public async virtual ValueTask BeginTransaction() - { - ValidateNoTransactionStarted(); -#if NETSTANDARD2_0 - _transaction = Connection.BeginTransaction(Settings.TransactionIsolationLevel); -#else - _transaction = (SqlTransaction)await Connection.BeginTransactionAsync(Settings.TransactionIsolationLevel); -#endif } - public async virtual ValueTask CommitTransaction() - { - ValidateTransactionStarted(); - -#if NETSTANDARD2_0 - _transaction.Commit(); - _transaction.Dispose(); -#else - await _transaction.CommitAsync(); - await _transaction.DisposeAsync(); -#endif - _transaction = null; - } - - public async virtual ValueTask RollbackTransaction() - { - ValidateTransactionStarted(); - -#if NETSTANDARD2_0 - _transaction.Rollback(); - _transaction.Dispose(); -#else - await _transaction.RollbackAsync(); - await _transaction.DisposeAsync(); -#endif - _transaction = null; - } -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - protected void ValidateNoTransactionStarted() - { - if (CurrentTransaction != null) - { - throw new MessageBusException("Transaction is already in progress"); - } - } - - protected void ValidateTransactionStarted() - { - if (CurrentTransaction == null) - { - throw new MessageBusException("Transaction has not been started"); - } - } - - public async virtual Task Initialize(CancellationToken token) - { - await EnsureConnection(); - try - { - _logger.LogInformation("Outbox database schema provisioning started..."); - - // Retry few times to create the schema - perhaps there are concurrently running other service process-es that attempt to do the same (distributed micro-service). - await SqlHelper.RetryIfError(_logger, Settings.SchemaCreationRetry, _ => true, async () => - { - await BeginTransaction(); - try - { - _logger.LogDebug("Ensuring table {TableName} is created", _sqlTemplate.MigrationsTableNameQualified); - await ExecuteNonQuery(Settings.SchemaCreationRetry, - @$"IF OBJECT_ID('{_sqlTemplate.MigrationsTableNameQualified}') IS NULL - BEGIN - CREATE TABLE {_sqlTemplate.MigrationsTableNameQualified} ( - MigrationId nvarchar(150) NOT NULL, - ProductVersion nvarchar(32) NOT NULL, - CONSTRAINT [PK_{Settings.DatabaseMigrationsTableName}] PRIMARY KEY CLUSTERED ([MigrationId] ASC) - ) - END", token: token); - - _logger.LogDebug("Ensuring table {TableName} is created", _sqlTemplate.TableNameQualified); - await ExecuteNonQuery(Settings.SchemaCreationRetry, - @$"IF OBJECT_ID('{_sqlTemplate.TableNameQualified}') IS NULL - BEGIN - CREATE TABLE {_sqlTemplate.TableNameQualified} ( - Id uniqueidentifier NOT NULL, - Timestamp datetime2(7) NOT NULL, - BusName nvarchar(64) NOT NULL, - MessageType nvarchar(256) NOT NULL, - MessagePayload varbinary(max) NOT NULL, - Headers nvarchar(max), - Path nvarchar(128), - InstanceId nvarchar(128) NOT NULL, - LockInstanceId nvarchar(128) NOT NULL, - LockExpiresOn datetime2(7) NOT NULL, - DeliveryAttempt int NOT NULL, - DeliveryComplete bit NOT NULL, - CONSTRAINT [PK_{Settings.DatabaseTableName}] PRIMARY KEY CLUSTERED ([Id] ASC) - ) - END", token: token); - -#pragma warning disable CA1861 - await CreateIndex("IX_Outbox_InstanceId", new string[] { - "DeliveryComplete", - "InstanceId" - }, token); - - await CreateIndex("IX_Outbox_LockExpiresOn", new string[] { - "DeliveryComplete", - "LockExpiresOn" - }, token); - - await CreateIndex("IX_Outbox_Timestamp_LockInstanceId", new string[] { - "DeliveryComplete", - "Timestamp", - "LockInstanceId", - }, token); -#pragma warning restore CA1861 - - await TryApplyMigration("20230120000000_SMB_Init", null, token); - - await TryApplyMigration("20230128225000_SMB_BusNameOptional", - @$"ALTER TABLE {_sqlTemplate.TableNameQualified} ALTER COLUMN BusName nvarchar(64) NULL", token); - - await CommitTransaction(); - return true; - } - catch (Exception) - { - await RollbackTransaction(); - throw; - } - }, token); - - _logger.LogInformation("Outbox database schema provisioning finished"); - } - catch (SqlException e) - { - _logger.LogError(e, "Outbox database schema provisioning enocuntered a non-recoverable SQL error: {ErrorMessage}", e.Message); - throw; - } - } - - private async Task CreateIndex(string indexName, IEnumerable columns, CancellationToken token) - { - _logger.LogDebug("Ensuring index {IndexName} on table {TableName} is created", indexName, _sqlTemplate.TableNameQualified); - await ExecuteNonQuery(Settings.SchemaCreationRetry, - @$"IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '{indexName}' AND object_id = OBJECT_ID('{_sqlTemplate.TableNameQualified}')) - BEGIN - CREATE NONCLUSTERED INDEX [{indexName}] ON {_sqlTemplate.TableNameQualified} - ( - {string.Join(",", columns.Select(c => $"{c} ASC"))} - ) - END", token: token); - } - - private async Task TryApplyMigration(string migrationId, string migrationSql, CancellationToken token) - { - var versionId = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - - _logger.LogTrace("Ensuring migration {MigrationId} is applied", migrationId); - var affected = await ExecuteNonQuery(Settings.SchemaCreationRetry, - @$"IF NOT EXISTS (SELECT * FROM {_sqlTemplate.MigrationsTableNameQualified} WHERE MigrationId = '{migrationId}') - BEGIN - INSERT INTO {_sqlTemplate.MigrationsTableNameQualified} (MigrationId, ProductVersion) VALUES ('{migrationId}', '{versionId}') - END", token: token); - - if (affected > 0) - { - if (migrationSql != null) - { - _logger.LogDebug("Executing migration {MigrationId}...", migrationId); - await ExecuteNonQuery(Settings.SchemaCreationRetry, migrationSql, token: token); - } - return true; - } - return false; - } - - private Task ExecuteNonQuery(SqlRetrySettings retrySettings, string sql, Action setParameters = null, CancellationToken token = default) => - SqlHelper.RetryIfTransientError(_logger, retrySettings, async () => - { - using var cmd = CreateCommand(); - cmd.CommandText = sql; - setParameters?.Invoke(cmd); - return await cmd.ExecuteNonQueryAsync(); - }, token); - public async virtual Task Save(OutboxMessage message, CancellationToken token) { await EnsureConnection(); // ToDo: Create command template - await ExecuteNonQuery(Settings.OperationRetry, _sqlTemplate.SqlOutboxMessageInsert, cmd => + await ExecuteNonQuery(Settings.SqlSettings.OperationRetry, _sqlTemplate.SqlOutboxMessageInsert, cmd => { cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = message.Id; cmd.Parameters.Add("@Timestamp", SqlDbType.DateTime2).Value = message.Timestamp; @@ -322,7 +100,7 @@ public async Task UpdateToSent(IReadOnlyCollection ids, CancellationToken await EnsureConnection(); - var affected = await ExecuteNonQuery(Settings.OperationRetry, + var affected = await ExecuteNonQuery(Settings.SqlSettings.OperationRetry, @$"UPDATE {_sqlTemplate.TableNameQualified} SET [DeliveryComplete] = 1 WHERE [Id] IN ({string.Join(",", ids.Select(id => string.Concat("'", id, "'")))})", token: token); @@ -337,7 +115,7 @@ public async Task TryToLock(string instanceId, DateTime expiresOn, Cancella await EnsureConnection(); // Extend the lease if still the owner of it, or claim the lease if another instace had possesion, but it expired (or message never was locked) - var affected = await ExecuteNonQuery(Settings.OperationRetry, _sqlTemplate.SqlOutboxMessageTryLockUpdate, cmd => + var affected = await ExecuteNonQuery(Settings.SqlSettings.OperationRetry, _sqlTemplate.SqlOutboxMessageTryLockUpdate, cmd => { cmd.Parameters.Add("@InstanceId", SqlDbType.NVarChar).Value = instanceId; cmd.Parameters.Add("@ExpiresOn", SqlDbType.DateTime2).Value = expiresOn; @@ -346,30 +124,15 @@ public async Task TryToLock(string instanceId, DateTime expiresOn, Cancella return affected; } - public async ValueTask DisposeAsync() - { - await DisposeAsyncCore(); - - GC.SuppressFinalize(this); - } - - protected async virtual ValueTask DisposeAsyncCore() - { - if (_transaction != null) - { - await RollbackTransaction(); - } - } - public async Task DeleteSent(DateTime olderThan, CancellationToken token) { await EnsureConnection(); - var affected = await ExecuteNonQuery(Settings.OperationRetry, _sqlTemplate.SqlOutboxMessageDeleteSent, cmd => + var affected = await ExecuteNonQuery(Settings.SqlSettings.OperationRetry, _sqlTemplate.SqlOutboxMessageDeleteSent, cmd => { cmd.Parameters.Add("@Timestamp", SqlDbType.DateTime2).Value = olderThan; }, token); - _logger.Log(affected > 0 ? LogLevel.Information : LogLevel.Debug, "Removed {MessageCount} sent messages from outbox table", affected); + Logger.Log(affected > 0 ? LogLevel.Information : LogLevel.Debug, "Removed {MessageCount} sent messages from outbox table", affected); } } diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs index a1d0747a..92c283aa 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs +++ b/src/SlimMessageBus.Host.Outbox.Sql/SqlOutboxTemplate.cs @@ -1,4 +1,5 @@ namespace SlimMessageBus.Host.Outbox.Sql; + public class SqlOutboxTemplate { public string TableNameQualified { get; } @@ -11,8 +12,8 @@ public class SqlOutboxTemplate public SqlOutboxTemplate(SqlOutboxSettings settings) { - TableNameQualified = $"[{settings.DatabaseSchemaName}].[{settings.DatabaseTableName}]"; - MigrationsTableNameQualified = $"[{settings.DatabaseSchemaName}].[{settings.DatabaseMigrationsTableName}]"; + TableNameQualified = $"[{settings.SqlSettings.DatabaseSchemaName}].[{settings.SqlSettings.DatabaseTableName}]"; + MigrationsTableNameQualified = $"[{settings.SqlSettings.DatabaseSchemaName}].[{settings.SqlSettings.DatabaseMigrationsTableName}]"; SqlOutboxMessageInsert = @$"INSERT INTO {TableNameQualified} ([Id], [Timestamp], [BusName], [MessageType], [MessagePayload], [Headers], [Path], [InstanceId], [LockInstanceId], [LockExpiresOn], [DeliveryAttempt], [DeliveryComplete]) diff --git a/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs b/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs index 89a785a8..70ed0ad5 100644 --- a/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs +++ b/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxForwardingPublishInterceptor.cs @@ -6,28 +6,25 @@ public abstract class OutboxForwardingPublishInterceptor { } -public class OutboxForwardingPublishInterceptor : OutboxForwardingPublishInterceptor, IPublishInterceptor where T : class +public class OutboxForwardingPublishInterceptor( + ILogger logger, + IOutboxRepository outboxRepository, + IInstanceIdProvider instanceIdProvider, + OutboxSettings outboxSettings) + : OutboxForwardingPublishInterceptor, IPublishInterceptor where T : class { static readonly internal string SkipOutboxHeader = "__SkipOutbox"; - private readonly ILogger _logger; - private readonly IOutboxRepository _outboxRepository; - private readonly IInstanceIdProvider _instanceIdProvider; - private readonly OutboxSettings _outboxSettings; - - public OutboxForwardingPublishInterceptor(ILogger logger, IOutboxRepository outboxRepository, IInstanceIdProvider instanceIdProvider, OutboxSettings outboxSettings) - { - _logger = logger; - _outboxRepository = outboxRepository; - _instanceIdProvider = instanceIdProvider; - _outboxSettings = outboxSettings; - } + private readonly ILogger _logger = logger; + private readonly IOutboxRepository _outboxRepository = outboxRepository; + private readonly IInstanceIdProvider _instanceIdProvider = instanceIdProvider; + private readonly OutboxSettings _outboxSettings = outboxSettings; public async Task OnHandle(T message, Func next, IProducerContext context) { - var bus = context.Bus as MessageBusBase; var skipOutbox = context.Headers != null && context.Headers.ContainsKey(SkipOutboxHeader); - if (bus is null || skipOutbox) + var busMaster = context.GetMasterMessageBus(); + if (busMaster == null || skipOutbox) { if (skipOutbox) { @@ -45,13 +42,13 @@ public async Task OnHandle(T message, Func next, IProducerContext context) _logger.LogDebug("Forwarding published message of type {MessageType} to the outbox", messageType.Name); // Take the proper serializer (meant for the bus) - var messagePayload = bus.Serializer?.Serialize(messageType, message) - ?? throw new PublishMessageBusException($"The {bus.Settings.Name} bus has no configured serializer, so it cannot be used with the outbox plugin"); + var messagePayload = busMaster.Serializer?.Serialize(messageType, message) + ?? throw new PublishMessageBusException($"The {busMaster.Name} bus has no configured serializer, so it cannot be used with the outbox plugin"); // Add message to the database, do not call next() var outboxMessage = new OutboxMessage { - BusName = bus.Settings.Name, + BusName = busMaster.Name, Headers = context.Headers, Path = context.Path, MessageType = messageType, diff --git a/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxSendingTask.cs b/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxSendingTask.cs index 6af2cfc2..290bd97b 100644 --- a/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxSendingTask.cs +++ b/src/SlimMessageBus.Host.Outbox/Interceptors/OutboxSendingTask.cs @@ -1,11 +1,16 @@ namespace SlimMessageBus.Host.Outbox; -public class OutboxSendingTask : IMessageBusLifecycleInterceptor, IAsyncDisposable +public class OutboxSendingTask( + ILoggerFactory loggerFactory, + OutboxSettings outboxSettings, + IServiceProvider serviceProvider, + IInstanceIdProvider instanceIdProvider) + : IMessageBusLifecycleInterceptor, IAsyncDisposable { - private readonly ILogger _logger; - private readonly OutboxSettings _outboxSettings; - private readonly IServiceProvider _serviceProvider; - private readonly IInstanceIdProvider _instanceIdProvider; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly OutboxSettings _outboxSettings = outboxSettings; + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly IInstanceIdProvider _instanceIdProvider = instanceIdProvider; private CancellationTokenSource _loopCts; private Task _loopTask; @@ -13,14 +18,6 @@ public class OutboxSendingTask : IMessageBusLifecycleInterceptor, IAsyncDisposab private DateTime? _cleanupNextRun; - public OutboxSendingTask(ILoggerFactory loggerFactory, OutboxSettings outboxSettings, IServiceProvider serviceProvider, IInstanceIdProvider instanceIdProvider) - { - _logger = loggerFactory.CreateLogger(); - _outboxSettings = outboxSettings; - _serviceProvider = serviceProvider; - _instanceIdProvider = instanceIdProvider; - } - private bool ShouldRunCleanup() { if (_outboxSettings.MessageCleanup?.Enabled == true) @@ -105,9 +102,10 @@ private async Task Run() var scope = _serviceProvider.CreateScope(); try { - var outboxRepository = scope.ServiceProvider.GetRequiredService(); + var outboxMigrationService = scope.ServiceProvider.GetRequiredService(); + await outboxMigrationService.Migrate(_loopCts.Token); - await outboxRepository.Initialize(_loopCts.Token); + var outboxRepository = scope.ServiceProvider.GetRequiredService(); var processedIds = new List(_outboxSettings.PollBatchSize); @@ -164,6 +162,7 @@ private async Task SendMessages(IServiceProvider serviceProvider, IOutboxR { var messageBus = serviceProvider.GetRequiredService(); var compositeMessageBus = messageBus as ICompositeMessageBus; + var messageBusTarget = messageBus as IMessageBusTarget; var idleRun = true; @@ -180,17 +179,22 @@ private async Task SendMessages(IServiceProvider serviceProvider, IOutboxR for (var i = 0; i < outboxMessages.Count && !ct.IsCancellationRequested; i++) { var outboxMessage = outboxMessages[i]; - - var now = DateTime.UtcNow; - if (now.Add(_outboxSettings.LockExpirationBuffer) > outboxMessage.LockExpiresOn) + + var now = DateTime.UtcNow; + if (now.Add(_outboxSettings.LockExpirationBuffer) > outboxMessage.LockExpiresOn) + { + _logger.LogDebug("Stopping the outbox message processing after {MessageCount} (out of {BatchCount}) because the message lock was close to expiration {LockBuffer}", i, _outboxSettings.PollBatchSize, _outboxSettings.LockExpirationBuffer); + hasMore = false; + break; + } + + var bus = GetBus(compositeMessageBus, messageBusTarget, outboxMessage.BusName); + if (bus == null) { - _logger.LogDebug("Stopping the outbox message processing after {MessageCount} (out of {BatchCount}) because the message lock was close to expiration {LockBuffer}", i, _outboxSettings.PollBatchSize, _outboxSettings.LockExpirationBuffer); - hasMore = false; - break; + _logger.LogWarning("Not able to find matching bus provider for the outbox message with Id {MessageId} of type {MessageType} to path {Path} using {BusName} bus. The message will be skipped.", outboxMessage.Id, outboxMessage.MessageType.Name, outboxMessage.Path, outboxMessage.BusName); + continue; } - var bus = (MessageBusBase)GetBus(compositeMessageBus, messageBus, outboxMessage.BusName); - _logger.LogDebug("Sending outbox message with Id {MessageId} of type {MessageType} to path {Path} using {BusName} bus", outboxMessage.Id, outboxMessage.MessageType.Name, outboxMessage.Path, outboxMessage.BusName); var message = bus.Serializer.Deserialize(outboxMessage.MessageType, outboxMessage.MessagePayload); @@ -200,7 +204,7 @@ private async Task SendMessages(IServiceProvider serviceProvider, IOutboxR if (!ct.IsCancellationRequested) { - await bus.ProducePublish(message, path: outboxMessage.Path, headers: headers, cancellationToken: ct, currentServiceProvider: serviceProvider); + await bus.ProducePublish(message, path: outboxMessage.Path, headers: headers, messageBusTarget, cancellationToken: ct); processedIds.Add(outboxMessage.Id); } @@ -223,12 +227,16 @@ private async Task SendMessages(IServiceProvider serviceProvider, IOutboxR return idleRun; } - private static IMessageBus GetBus(ICompositeMessageBus compositeMessageBus, IMessageBus messageBus, string name) + private static IMasterMessageBus GetBus(ICompositeMessageBus compositeMessageBus, IMessageBusTarget messageBusTarget, string name) { if (name != null && compositeMessageBus != null) { return compositeMessageBus.GetChildBus(name); } - return messageBus; + if (messageBusTarget != null) + { + return messageBusTarget.Target as IMasterMessageBus; + } + return null; } } diff --git a/src/SlimMessageBus.Host.Outbox/Interceptors/TransactionScopeConsumerInterceptor.cs b/src/SlimMessageBus.Host.Outbox/Interceptors/TransactionScopeConsumerInterceptor.cs index cef43e45..d895d549 100644 --- a/src/SlimMessageBus.Host.Outbox/Interceptors/TransactionScopeConsumerInterceptor.cs +++ b/src/SlimMessageBus.Host.Outbox/Interceptors/TransactionScopeConsumerInterceptor.cs @@ -10,16 +10,13 @@ public abstract class TransactionScopeConsumerInterceptor /// Wraps the consumer in an (conditionally). /// /// -public class TransactionScopeConsumerInterceptor : TransactionScopeConsumerInterceptor, IConsumerInterceptor where T : class +public class TransactionScopeConsumerInterceptor( + ILogger logger, + OutboxSettings settings) + : TransactionScopeConsumerInterceptor, IConsumerInterceptor where T : class { - private readonly ILogger _logger; - private readonly OutboxSettings _settings; - - public TransactionScopeConsumerInterceptor(ILogger logger, OutboxSettings settings) - { - _logger = logger; - _settings = settings; - } + private readonly ILogger _logger = logger; + private readonly OutboxSettings _settings = settings; public async Task OnHandle(T message, Func> next, IConsumerContext context) { diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMigrationService.cs b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMigrationService.cs new file mode 100644 index 00000000..e1ca97f4 --- /dev/null +++ b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxMigrationService.cs @@ -0,0 +1,10 @@ +namespace SlimMessageBus.Host.Outbox; + +public interface IOutboxMigrationService +{ + /// + /// Initializes the data schema for the outbox. Invoked once on bus start. + /// + /// + Task Migrate(CancellationToken token); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs index 7591bcc6..e153e071 100644 --- a/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs +++ b/src/SlimMessageBus.Host.Outbox/Repositories/IOutboxRepository.cs @@ -2,11 +2,6 @@ public interface IOutboxRepository { - /// - /// Initializes the data schema for the outbox. Invoked once on bus start. - /// - /// - Task Initialize(CancellationToken token); Task Save(OutboxMessage message, CancellationToken token); Task TryToLock(string instanceId, DateTime expiresOn, CancellationToken token); Task> FindNextToSend(string instanceId, CancellationToken token); diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProducerBuilderExtensions.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProducerBuilderExtensions.cs index 46353d72..0823f3dd 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProducerBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqProducerBuilderExtensions.cs @@ -14,7 +14,7 @@ public static class RabbitMqProducerBuilderExtensions /// /// /// - public static ProducerBuilder Exchange(this ProducerBuilder builder, string exchangeName, ExchangeType? exchangeType = null, bool? durable = null, bool? autoDelete = null, IDictionary? arguments = null) + public static ProducerBuilder Exchange(this ProducerBuilder builder, string exchangeName, ExchangeType? exchangeType = null, bool? durable = null, bool? autoDelete = null, IDictionary arguments = null) { if (string.IsNullOrEmpty(exchangeName)) throw new ArgumentNullException(nameof(exchangeName)); @@ -23,7 +23,7 @@ public static ProducerBuilder Exchange(this ProducerBuilder builder, st return builder; } - static internal void SetExchangeProperties(this HasProviderExtensions settings, ExchangeType? exchangeType = null, bool? durable = null, bool? autoDelete = null, IDictionary? arguments = null) + static internal void SetExchangeProperties(this HasProviderExtensions settings, ExchangeType? exchangeType = null, bool? durable = null, bool? autoDelete = null, IDictionary arguments = null) { if (exchangeType != null) { diff --git a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqRequestResponseBuilderExtensions.cs b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqRequestResponseBuilderExtensions.cs index 8500fdde..1b339c57 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqRequestResponseBuilderExtensions.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/Config/RabbitMqRequestResponseBuilderExtensions.cs @@ -13,7 +13,7 @@ public static class RabbitMqRequestResponseBuilderExtensions /// /// /// - public static RequestResponseBuilder ReplyToExchange(this RequestResponseBuilder builder, string exchangeName, ExchangeType? exchangeType = null, bool? durable = null, bool? autoDelete = null, IDictionary? arguments = null) + public static RequestResponseBuilder ReplyToExchange(this RequestResponseBuilder builder, string exchangeName, ExchangeType? exchangeType = null, bool? durable = null, bool? autoDelete = null, IDictionary arguments = null) { if (string.IsNullOrEmpty(exchangeName)) throw new ArgumentNullException(nameof(exchangeName)); diff --git a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs index 0bc899f5..537336d3 100644 --- a/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs +++ b/src/SlimMessageBus.Host.RabbitMQ/RabbitMqMessageBus.cs @@ -128,7 +128,7 @@ protected override async ValueTask DisposeAsyncCore() } } - protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { await EnsureInitFinished(); diff --git a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs index 76fd1462..170c1746 100644 --- a/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs +++ b/src/SlimMessageBus.Host.Redis/RedisMessageBus.cs @@ -147,23 +147,16 @@ void AddTopicConsumer(string topic, ISubscriber subscriber, IMessageProcessor messageHeaders = null, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { + if (message is null) throw new ArgumentNullException(nameof(message)); + if (messagePayload is null) throw new ArgumentNullException(nameof(messagePayload)); + var messageType = message.GetType(); // determine the SMB topic name if its a Azure SB queue or topic var kind = _kindMapping.GetKind(messageType, path); - return ProduceToTransport(messageType, message, path, messagePayload, messageHeaders, cancellationToken, kind); - } - - #endregion - - protected async virtual Task ProduceToTransport(Type messageType, object message, string path, byte[] messagePayload, IDictionary messageHeaders, CancellationToken cancellationToken, PathKind kind) - { - if (messageType is null) throw new ArgumentNullException(nameof(messageType)); - if (messagePayload is null) throw new ArgumentNullException(nameof(messagePayload)); - AssertActive(); var messageWithHeaders = new MessageWithHeaders(messagePayload, messageHeaders); @@ -181,4 +174,6 @@ protected async virtual Task ProduceToTransport(Type messageType, object message "Produced message {Message} of type {MessageType} to redis channel {PathKind} {Path} with result {RedisResult}", message, messageType, GetPathKindString(kind), path, result); } + + #endregion } \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Sql.Common/CommonSqlMigrationService.cs b/src/SlimMessageBus.Host.Sql.Common/CommonSqlMigrationService.cs new file mode 100644 index 00000000..09de7e86 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/CommonSqlMigrationService.cs @@ -0,0 +1,120 @@ +namespace SlimMessageBus.Host.Sql.Common; + +using System.Reflection; + +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; + +public abstract class CommonSqlMigrationService + where TRepository : CommonSqlRepository + where TSettings : ICommonSqlSettings +{ + protected ILogger Logger { get; } + protected TSettings Settings { get; } + protected TRepository Repository { get; } + + public CommonSqlMigrationService(ILogger logger, TRepository repository, TSettings settings) + { + Logger = logger; + Settings = settings; + Repository = repository; + } + + protected async Task TryApplyMigration(string migrationId, string migrationSql, CancellationToken token) + { + var versionId = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + var migrationsTableName = Repository.GetTableName(Settings.DatabaseMigrationsTableName); + + Logger.LogTrace("Ensuring migration {MigrationId} is applied", migrationId); + var affected = await Repository.ExecuteNonQuery(Settings.SchemaCreationRetry, + @$"IF NOT EXISTS (SELECT * FROM {migrationsTableName} WHERE MigrationId = '{migrationId}') + BEGIN + INSERT INTO {migrationsTableName} (MigrationId, ProductVersion) VALUES ('{migrationId}', '{versionId}') + END", token: token); + + if (affected > 0) + { + if (migrationSql != null) + { + Logger.LogDebug("Executing migration {MigrationId}...", migrationId); + await Repository.ExecuteNonQuery(Settings.SchemaCreationRetry, migrationSql, token: token); + } + return true; + } + return false; + } + + protected async Task CreateTable(string tableName, IEnumerable columns, CancellationToken token) + { + var qualifiedTableName = Repository.GetTableName(tableName); + + Logger.LogDebug("Ensuring table {TableName} is created", tableName); + await Repository.ExecuteNonQuery(Settings.SchemaCreationRetry, + @$"IF OBJECT_ID('{qualifiedTableName}') IS NULL + BEGIN + CREATE TABLE {qualifiedTableName} + ( + {string.Join(",", columns)} + ) + END", token: token); + } + + protected async Task CreateIndex(string indexName, string tableName, IEnumerable columns, CancellationToken token) + { + var qualifiedTableName = Repository.GetTableName(tableName); + + Logger.LogDebug("Ensuring index {IndexName} on table {TableName} is created", indexName, qualifiedTableName); + + await Repository.ExecuteNonQuery(Settings.SchemaCreationRetry, + @$"IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '{indexName}' AND object_id = OBJECT_ID('{qualifiedTableName}')) + BEGIN + CREATE NONCLUSTERED INDEX [{indexName}] ON {qualifiedTableName} + ( + {string.Join(",", columns.Select(c => $"{c} ASC"))} + ) + END", token: token); + } + + public async virtual Task Migrate(CancellationToken token) + { + await Repository.EnsureConnection(); + try + { + Logger.LogInformation("Database schema provisioning started..."); + + // Retry few times to create the schema - perhaps there are concurrently running other service process-es that attempt to do the same (distributed micro-service). + await SqlHelper.RetryIfError(Logger, Settings.SchemaCreationRetry, _ => true, async () => + { + await Repository.BeginTransaction(); + try + { + await CreateTable(Settings.DatabaseMigrationsTableName, new[] { + "MigrationId nvarchar(150) NOT NULL", + "ProductVersion nvarchar(32) NOT NULL", + $"CONSTRAINT [PK_{Settings.DatabaseMigrationsTableName}] PRIMARY KEY CLUSTERED ([MigrationId] ASC)" + }, + token); + + await OnMigrate(token); + + await Repository.CommitTransaction(); + return true; + } + catch (Exception) + { + await Repository.RollbackTransaction(); + throw; + } + }, token); + + Logger.LogInformation("Database schema provisioning finished"); + } + catch (SqlException e) + { + Logger.LogError(e, "Database schema provisioning enocuntered a non-recoverable SQL error: {ErrorMessage}", e.Message); + throw; + } + } + + protected abstract Task OnMigrate(CancellationToken token); +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs b/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs new file mode 100644 index 00000000..0c2b702c --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/CommonSqlRepository.cs @@ -0,0 +1,126 @@ +namespace SlimMessageBus.Host.Sql.Common; + +public abstract class CommonSqlRepository : IAsyncDisposable +{ + private readonly ICommonSqlSettings _settings; + private SqlTransaction _transaction; + + protected ILogger Logger { get; } + protected SqlConnection Connection { get; } + + public virtual SqlTransaction CurrentTransaction => _transaction; + + protected CommonSqlRepository(ILogger logger, ICommonSqlSettings settings, SqlConnection connection) + { + _settings = settings; + Logger = logger; + Connection = connection; + } + + #region IAsyncDisposable + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); + + GC.SuppressFinalize(this); + } + + protected async virtual ValueTask DisposeAsyncCore() + { + if (_transaction != null) + { + await RollbackTransaction(); + } + } + + #endregion + + public async Task EnsureConnection() + { + if (Connection.State != ConnectionState.Open) + { + await Connection.OpenAsync(); + } + } + + protected virtual SqlCommand CreateCommand() + { + var cmd = Connection.CreateCommand(); + cmd.Transaction = CurrentTransaction; + + if (_settings.CommandTimeout != null) + { + cmd.CommandTimeout = (int)_settings.CommandTimeout.Value.TotalSeconds; + } + + return cmd; + } + + public string GetTableName(string tableName) => $"[{_settings.DatabaseSchemaName}].[{tableName}]"; + + public Task ExecuteNonQuery(SqlRetrySettings retrySettings, string sql, Action setParameters = null, CancellationToken token = default) => + SqlHelper.RetryIfTransientError(Logger, retrySettings, async () => + { + using var cmd = CreateCommand(); + cmd.CommandText = sql; + setParameters?.Invoke(cmd); + return await cmd.ExecuteNonQueryAsync(); + }, token); + + public async virtual ValueTask BeginTransaction() + { + ValidateNoTransactionStarted(); +#if NETSTANDARD2_0 + _transaction = Connection.BeginTransaction(_settings.TransactionIsolationLevel); +#else + _transaction = (SqlTransaction)await Connection.BeginTransactionAsync(_settings.TransactionIsolationLevel); +#endif + } + + public async virtual ValueTask CommitTransaction() + { + ValidateTransactionStarted(); + +#if NETSTANDARD2_0 + _transaction.Commit(); + _transaction.Dispose(); +#else + await _transaction.CommitAsync(); + await _transaction.DisposeAsync(); +#endif + + _transaction = null; + } + + public async virtual ValueTask RollbackTransaction() + { + ValidateTransactionStarted(); + +#if NETSTANDARD2_0 + _transaction.Rollback(); + _transaction.Dispose(); +#else + await _transaction.RollbackAsync(); + await _transaction.DisposeAsync(); +#endif + + _transaction = null; + } + + protected void ValidateNoTransactionStarted() + { + if (CurrentTransaction != null) + { + throw new MessageBusException("Transaction is already in progress"); + } + } + + protected void ValidateTransactionStarted() + { + if (CurrentTransaction == null) + { + throw new MessageBusException("Transaction has not been started"); + } + } +} diff --git a/src/SlimMessageBus.Host.Sql.Common/CommonSqlSettings.cs b/src/SlimMessageBus.Host.Sql.Common/CommonSqlSettings.cs new file mode 100644 index 00000000..5d9df487 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/CommonSqlSettings.cs @@ -0,0 +1,35 @@ +namespace SlimMessageBus.Host.Sql.Common; + +using System.Data; + +public class CommonSqlSettings : ICommonSqlSettings +{ + public string DatabaseSchemaName { get; set; } = "dbo"; + public string DatabaseTableName { get; set; } = "Outbox"; + public string DatabaseMigrationsTableName { get; set; } = "__EFMigrationsHistory"; + public SqlDialect Dialect { get; set; } = SqlDialect.SqlServer; + + /// + /// Initializes the connection when set to a value. + /// + public TimeSpan? CommandTimeout { get; set; } + + /// + /// Desired of the transaction scope created by the consumers (when is enabled). + /// + public IsolationLevel TransactionIsolationLevel { get; set; } = IsolationLevel.RepeatableRead; + + public SqlRetrySettings SchemaCreationRetry { get; set; } = new() + { + RetryCount = 3, + RetryIntervalFactor = 1.2f, + RetryInterval = TimeSpan.FromSeconds(2), + }; + + public SqlRetrySettings OperationRetry { get; set; } = new() + { + RetryCount = 5, + RetryIntervalFactor = 1.5f, + RetryInterval = TimeSpan.FromSeconds(2), + }; +} diff --git a/src/SlimMessageBus.Host.Sql.Common/GlobalUsings.cs b/src/SlimMessageBus.Host.Sql.Common/GlobalUsings.cs new file mode 100644 index 00000000..6beb698d --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using System.Data; + +global using Microsoft.Data.SqlClient; +global using Microsoft.Extensions.Logging; diff --git a/src/SlimMessageBus.Host.Sql.Common/ICommonSqlSettings.cs b/src/SlimMessageBus.Host.Sql.Common/ICommonSqlSettings.cs new file mode 100644 index 00000000..75db52f7 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/ICommonSqlSettings.cs @@ -0,0 +1,24 @@ +namespace SlimMessageBus.Host.Sql.Common; + +using System.Data; + +public interface ICommonSqlSettings +{ + string DatabaseSchemaName { get; } + string DatabaseMigrationsTableName { get; } + string DatabaseTableName { get; } + SqlDialect Dialect { get; } + /// + /// Initializes the connection when set to a value. + /// + public TimeSpan? CommandTimeout { get; } + + SqlRetrySettings SchemaCreationRetry { get; } + + SqlRetrySettings OperationRetry { get; } + + /// + /// Desired of the transaction scope created by the consumers (when is enabled). + /// + IsolationLevel TransactionIsolationLevel { get; } +} diff --git a/src/SlimMessageBus.Host.Sql.Common/SlimMessageBus.Host.Sql.Common.csproj b/src/SlimMessageBus.Host.Sql.Common/SlimMessageBus.Host.Sql.Common.csproj new file mode 100644 index 00000000..06309c85 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/SlimMessageBus.Host.Sql.Common.csproj @@ -0,0 +1,20 @@ + + + + + + Common SQL logic for SlimMessageBus + SlimMessageBus SQL Common logic + icon.png + + + + + + + + + + + + diff --git a/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs b/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs new file mode 100644 index 00000000..1ebd0432 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql.Common/SqlDialect.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host.Sql.Common; + +public enum SqlDialect +{ + SqlServer = 1 + // ToDo: More to come +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Outbox.Sql/SqlHelper.cs b/src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs similarity index 95% rename from src/SlimMessageBus.Host.Outbox.Sql/SqlHelper.cs rename to src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs index 1ba1700f..e8961718 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/SqlHelper.cs +++ b/src/SlimMessageBus.Host.Sql.Common/SqlHelper.cs @@ -1,9 +1,10 @@ -namespace SlimMessageBus.Host.Outbox.Sql; +namespace SlimMessageBus.Host.Sql.Common; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; public static class SqlHelper + { private static readonly HashSet TransientErrorNumbers = new() { diff --git a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/RetrySettings.cs b/src/SlimMessageBus.Host.Sql.Common/SqlRetrySettings.cs similarity index 76% rename from src/SlimMessageBus.Host.Outbox.Sql/Configuration/RetrySettings.cs rename to src/SlimMessageBus.Host.Sql.Common/SqlRetrySettings.cs index 0b96f38b..6d57c23e 100644 --- a/src/SlimMessageBus.Host.Outbox.Sql/Configuration/RetrySettings.cs +++ b/src/SlimMessageBus.Host.Sql.Common/SqlRetrySettings.cs @@ -1,4 +1,4 @@ -namespace SlimMessageBus.Host.Outbox.Sql; +namespace SlimMessageBus.Host.Sql.Common; public class SqlRetrySettings { diff --git a/src/SlimMessageBus.Host.Sql/Configuration/SqlMessageBusBuilderExtensions.cs b/src/SlimMessageBus.Host.Sql/Configuration/SqlMessageBusBuilderExtensions.cs new file mode 100644 index 00000000..8d51df02 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/Configuration/SqlMessageBusBuilderExtensions.cs @@ -0,0 +1,37 @@ +namespace SlimMessageBus.Host.Sql; + +using Microsoft.Extensions.DependencyInjection; + +public static class SqlMessageBusBuilderExtensions +{ + public static MessageBusBuilder WithProviderSql(this MessageBusBuilder mbb, Action configure) + { + if (mbb == null) throw new ArgumentNullException(nameof(mbb)); + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + var providerSettings = new SqlMessageBusSettings(); + configure(providerSettings); + + mbb.PostConfigurationActions.Add(services => + { + services.TryAddSingleton(providerSettings); + + services.TryAddScoped(); + services.Replace(ServiceDescriptor.Scoped(svp => svp.GetRequiredService())); + + /* + services.Replace(ServiceDescriptor.Transient(svp => svp.GetRequiredService())); + + services.TryAddEnumerable(ServiceDescriptor.Transient(typeof(IConsumerInterceptor<>), typeof(SqlTransactionConsumerInterceptor<>))); + + services.TryAddScoped(); + services.Replace(ServiceDescriptor.Scoped(svp => svp.GetRequiredService())); + services.Replace(ServiceDescriptor.Scoped(svp => svp.GetRequiredService())); + + services.TryAddSingleton(); + */ + }); + + return mbb.WithProvider(settings => new SqlMessageBus(settings, providerSettings)); + } +} diff --git a/src/SlimMessageBus.Host.Sql/Configuration/SqlMessageBusSettings.cs b/src/SlimMessageBus.Host.Sql/Configuration/SqlMessageBusSettings.cs new file mode 100644 index 00000000..520efb11 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/Configuration/SqlMessageBusSettings.cs @@ -0,0 +1,5 @@ +namespace SlimMessageBus.Host.Sql; + +public class SqlMessageBusSettings : CommonSqlSettings +{ +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Sql/GlobalUsings.cs b/src/SlimMessageBus.Host.Sql/GlobalUsings.cs new file mode 100644 index 00000000..d9c841fc --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using System.Data; + +global using Microsoft.Data.SqlClient; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; + +global using SlimMessageBus.Host.Sql.Common; diff --git a/src/SlimMessageBus.Host.Sql/ISqlRepository.cs b/src/SlimMessageBus.Host.Sql/ISqlRepository.cs new file mode 100644 index 00000000..1220ddd6 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/ISqlRepository.cs @@ -0,0 +1,6 @@ +namespace SlimMessageBus.Host.Sql; + +public interface ISqlRepository +{ + +} diff --git a/src/SlimMessageBus.Host.Sql/SlimMessageBus.Host.Sql.csproj b/src/SlimMessageBus.Host.Sql/SlimMessageBus.Host.Sql.csproj new file mode 100644 index 00000000..a85047da --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/SlimMessageBus.Host.Sql.csproj @@ -0,0 +1,21 @@ + + + + + + Redis transport provider for SlimMessageBus + Redis transport provider SlimMessageBus MessageBus bus facade messaging client + icon.png + + + + + + + + + + + + + diff --git a/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs b/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs new file mode 100644 index 00000000..31e2baea --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/SqlMessageBus.cs @@ -0,0 +1,35 @@ +namespace SlimMessageBus.Host.Sql; + +using Microsoft.Extensions.DependencyInjection; + +public class SqlMessageBus : MessageBusBase +{ + public SqlMessageBus(MessageBusSettings settings, SqlMessageBusSettings providerSettings) + : base(settings, providerSettings) + { + } + + protected override void Build() + { + base.Build(); + + AddInit(ProvisionTopology()); + } + + public override async Task ProvisionTopology() + { + await base.ProvisionTopology(); + + using var scope = Settings.ServiceProvider.CreateScope(); + var sqlRepository = scope.ServiceProvider.GetService(); + var provisioningService = new SqlTopologyService(LoggerFactory.CreateLogger(), (SqlRepository)sqlRepository, ProviderSettings); + await provisioningService.Migrate(CancellationToken); // provisining happens asynchronously + } + + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) + { + var sqlRepository = targetBus.ServiceProvider.GetService(); + + // ToDo: Save to table + } +} \ No newline at end of file diff --git a/src/SlimMessageBus.Host.Sql/SqlRepository.cs b/src/SlimMessageBus.Host.Sql/SqlRepository.cs new file mode 100644 index 00000000..4bed89ee --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/SqlRepository.cs @@ -0,0 +1,9 @@ +namespace SlimMessageBus.Host.Sql; + +public class SqlRepository : CommonSqlRepository, ISqlRepository +{ + public SqlRepository(ILogger logger, SqlMessageBusSettings settings, SqlConnection connection) + : base(logger, settings, connection) + { + } +} diff --git a/src/SlimMessageBus.Host.Sql/SqlTemplate.cs b/src/SlimMessageBus.Host.Sql/SqlTemplate.cs new file mode 100644 index 00000000..f7e0443d --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/SqlTemplate.cs @@ -0,0 +1,13 @@ +namespace SlimMessageBus.Host.Sql; + +public class SqlTemplate +{ + public string TableNameQualified { get; } + public string MigrationsTableNameQualified { get; } + + public SqlTemplate(SqlMessageBusSettings settings) + { + TableNameQualified = $"[{settings.DatabaseSchemaName}].[{settings.DatabaseTableName}]"; + MigrationsTableNameQualified = $"[{settings.DatabaseSchemaName}].[{settings.DatabaseMigrationsTableName}]"; + } +} diff --git a/src/SlimMessageBus.Host.Sql/SqlTopologyService.cs b/src/SlimMessageBus.Host.Sql/SqlTopologyService.cs new file mode 100644 index 00000000..1fd56998 --- /dev/null +++ b/src/SlimMessageBus.Host.Sql/SqlTopologyService.cs @@ -0,0 +1,13 @@ +namespace SlimMessageBus.Host.Sql; + +public class SqlTopologyService : CommonSqlMigrationService +{ + public SqlTopologyService(ILogger logger, SqlRepository repository, SqlMessageBusSettings settings) + : base(logger, repository, settings) + { + } + + protected override async Task OnMigrate(CancellationToken token) + { + } +} diff --git a/src/SlimMessageBus.Host/AssemblyInfo.cs b/src/SlimMessageBus.Host/AssemblyInfo.cs deleted file mode 100644 index 057e60ee..00000000 --- a/src/SlimMessageBus.Host/AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// In SDK-style projects such as this one, several assembly attributes that were historically -// defined in this file are now automatically added during build and populated with -// values defined in project properties. For details of which attributes are included -// and how to customise this process see: https://aka.ms/assembly-info-properties - - -// Setting ComVisible to false makes the types in this assembly not visible to COM -// components. If you need to access a type in this assembly from COM, set the ComVisible -// attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM. - -[assembly: Guid("fa1c3d02-fc8e-41b9-bdbc-859a2cbda6cc")] - -[assembly: InternalsVisibleTo("SlimMessageBus.Host.Test")] \ No newline at end of file diff --git a/src/SlimMessageBus.Host/Consumer/Context/IConsumerContextExtensions.cs b/src/SlimMessageBus.Host/Consumer/Context/IConsumerContextExtensions.cs index f3baf7c7..037a8807 100644 --- a/src/SlimMessageBus.Host/Consumer/Context/IConsumerContextExtensions.cs +++ b/src/SlimMessageBus.Host/Consumer/Context/IConsumerContextExtensions.cs @@ -12,4 +12,10 @@ public static T GetPropertyOrDefault(this IConsumerContext context, string ke } return default; } + + public static IMasterMessageBus GetMasterMessageBus(this IConsumerContext context) + { + var busTarget = context.Bus as IMessageBusTarget; + return busTarget?.Target as IMasterMessageBus ?? context.Bus as IMasterMessageBus; + } } diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs index 0c2240f5..d8e780e9 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageHandler.cs @@ -89,6 +89,8 @@ public MessageHandler( } } + var messageBusTarget = new MessageBusProxy(MessageBus, messageScope.ServiceProvider); + Type consumerType = null; object consumerInstance = null; @@ -98,7 +100,7 @@ public MessageHandler( consumerInstance = messageScope.ServiceProvider.GetService(consumerType) ?? throw new ConfigurationMessageBusException($"Could not resolve consumer/handler type {consumerType} from the DI container. Please check that the configured type {consumerType} is registered within the DI container."); - var consumerContext = CreateConsumerContext(messageHeaders, consumerInvoker, transportMessage, consumerInstance, consumerContextProperties, cancellationToken); + var consumerContext = CreateConsumerContext(messageHeaders, consumerInvoker, transportMessage, consumerInstance, messageBusTarget, consumerContextProperties, cancellationToken); try { response = await DoHandleInternal(message, consumerInvoker, messageType, hasResponse, responseType, messageScope, consumerContext).ConfigureAwait(false); @@ -180,11 +182,12 @@ private object GetConsumerErrorHandler(Type messageType, Type consumerErrorHandl return messageScope.GetService(consumerErrorHandlerType); } - protected virtual ConsumerContext CreateConsumerContext(IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage, object consumerInstance, IDictionary consumerContextProperties, CancellationToken cancellationToken) + protected virtual ConsumerContext CreateConsumerContext(IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage, object consumerInstance, IMessageBus messageBus, IDictionary consumerContextProperties, CancellationToken cancellationToken) => new(consumerContextProperties) { Path = Path, Headers = messageHeaders, + Bus = messageBus, CancellationToken = cancellationToken, Consumer = consumerInstance, ConsumerInvoker = consumerInvoker diff --git a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs index 14792154..de3d62de 100644 --- a/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs +++ b/src/SlimMessageBus.Host/Consumer/MessageProcessors/MessageProcessor.cs @@ -54,10 +54,9 @@ public MessageProcessor( _shouldLogWhenUnrecognizedMessageType = consumerSettings.OfType().Any(x => x.UndeclaredMessageType.Log); } - protected override ConsumerContext CreateConsumerContext(IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage, object consumerInstance, IDictionary consumerContextProperties, CancellationToken cancellationToken) + protected override ConsumerContext CreateConsumerContext(IReadOnlyDictionary messageHeaders, IMessageTypeConsumerInvokerSettings consumerInvoker, object transportMessage, object consumerInstance, IMessageBus messageBus, IDictionary consumerContextProperties, CancellationToken cancellationToken) { - var context = base.CreateConsumerContext(messageHeaders, consumerInvoker, transportMessage, consumerInstance, consumerContextProperties, cancellationToken); - context.Bus = MessageBus; + var context = base.CreateConsumerContext(messageHeaders, consumerInvoker, transportMessage, consumerInstance, messageBus, consumerContextProperties, cancellationToken); _consumerContextInitializer?.Invoke((TTransportMessage)transportMessage, context); diff --git a/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs b/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs index 5c4b2cb5..690fcdda 100644 --- a/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs +++ b/src/SlimMessageBus.Host/Hybrid/HybridMessageBus.cs @@ -2,6 +2,8 @@ using System.Collections.Concurrent; +using SlimMessageBus.Host.Serialization; + public class HybridMessageBus : IMasterMessageBus, ICompositeMessageBus, IDisposable, IAsyncDisposable { private readonly ILogger _logger; @@ -14,8 +16,12 @@ public class HybridMessageBus : IMasterMessageBus, ICompositeMessageBus, IDispos public MessageBusSettings Settings { get; } public HybridMessageBusSettings ProviderSettings { get; } + public string Name => Settings.Name; + public bool IsStarted => _busByName.Values.All(x => x.IsStarted); + public IMessageSerializer Serializer => Settings.GetSerializer(Settings.ServiceProvider); + public HybridMessageBus(MessageBusSettings settings, HybridMessageBusSettings providerSettings, MessageBusBuilder mbb) { Settings = settings ?? throw new ArgumentNullException(nameof(settings)); @@ -28,7 +34,7 @@ public HybridMessageBus(MessageBusSettings settings, HybridMessageBusSettings pr _runtimeTypeCache = new RuntimeTypeCache(); - _busByName = new Dictionary(); + _busByName = []; foreach (var childBus in mbb.Children) { var bus = BuildBus(childBus.Value); @@ -133,22 +139,22 @@ protected virtual MessageBusBase[] Route(object message, string path) _logger.LogInformation("Could not find any bus that produces the message type: {MessageType}. Messages of that type will not be delivered to any child bus. Double check the message bus configuration.", messageType); } - return Array.Empty(); + return []; } #region Implementation of IMessageBusProducer - public Task ProduceSend(object request, TimeSpan? timeout, string path = null, IDictionary headers = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) + public Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) { var buses = Route(request, path); if (buses.Length > 0) { - return buses[0].ProduceSend(request, timeout, path, headers, currentServiceProvider, cancellationToken); + return buses[0].ProduceSend(request, path, headers, timeout, targetBus, cancellationToken); } return Task.FromResult(default); } - public async Task ProducePublish(object message, string path = null, IDictionary headers = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) + public async Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) { var buses = Route(message, path); if (buses.Length == 0) @@ -158,19 +164,19 @@ public async Task ProducePublish(object message, string path = null, IDictionary if (buses.Length == 1) { - await buses[0].ProducePublish(message, path, headers, currentServiceProvider, cancellationToken); + await buses[0].ProducePublish(message, path, headers, targetBus, cancellationToken); return; } if (ProviderSettings.PublishExecutionMode == PublishExecutionMode.Parallel) { - await Task.WhenAll(buses.Select(bus => bus.ProducePublish(message, path, headers, currentServiceProvider, cancellationToken))); + await Task.WhenAll(buses.Select(bus => bus.ProducePublish(message, path, headers, targetBus, cancellationToken))); return; } for (var i = 0; i < buses.Length; i++) { - await buses[i].ProducePublish(message, path, headers, currentServiceProvider, cancellationToken); + await buses[i].ProducePublish(message, path, headers, targetBus, cancellationToken); } } @@ -182,7 +188,7 @@ public Task ProvisionTopology() => #region ICompositeMessageBus - public IMessageBus GetChildBus(string name) + public IMasterMessageBus GetChildBus(string name) { if (_busByName.TryGetValue(name, out var bus)) { @@ -191,70 +197,7 @@ public IMessageBus GetChildBus(string name) return null; } - public IEnumerable GetChildBuses() => _busByName.Values; - - #endregion - - #region Implementation of IPublishBus - - public async Task Publish(TMessage message, string path = null, IDictionary headers = null, CancellationToken cancellationToken = default) - { - var buses = Route(message, path); - if (buses.Length == 0) - { - return; - } - if (buses.Length == 1) - { - await buses[0].Publish(message, path, headers, cancellationToken); - return; - } - - if (ProviderSettings.PublishExecutionMode == PublishExecutionMode.Parallel) - { - await Task.WhenAll(buses.Select(bus => bus.Publish(message, path, headers, cancellationToken))); - return; - } - - for (var i = 0; i < buses.Length; i++) - { - await buses[i].Publish(message, path, headers, cancellationToken); - } - } - - #endregion - - #region Implementation of IRequestResponseBus - - public Task Send(IRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - { - var buses = Route(request, path); - if (buses.Length > 0) - { - return buses[0].Send(request, path, headers, timeout, cancellationToken); - } - return Task.FromResult(default); - } - - public Task Send(IRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - { - var buses = Route(request, path); - if (buses.Length > 0) - { - return buses[0].Send(request, path, headers, timeout, cancellationToken); - } - return Task.CompletedTask; - } - - public Task Send(TRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - { - var buses = Route(request, path); - if (buses.Length > 0) - { - return buses[0].Send(request, path, headers, timeout, cancellationToken); - } - return Task.FromResult(default); - } + public IEnumerable GetChildBuses() => _busByName.Values; #endregion } diff --git a/src/SlimMessageBus.Host/ICompositeMessageBus.cs b/src/SlimMessageBus.Host/ICompositeMessageBus.cs index 4d851300..3ff7bd62 100644 --- a/src/SlimMessageBus.Host/ICompositeMessageBus.cs +++ b/src/SlimMessageBus.Host/ICompositeMessageBus.cs @@ -7,11 +7,11 @@ public interface ICompositeMessageBus /// /// /// - IMessageBus GetChildBus(string name); + IMasterMessageBus GetChildBus(string name); /// /// Get child buses /// /// - IEnumerable GetChildBuses(); + IEnumerable GetChildBuses(); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/IMasterMessageBus.cs b/src/SlimMessageBus.Host/IMasterMessageBus.cs index f0fdf40e..59fcf6b5 100644 --- a/src/SlimMessageBus.Host/IMasterMessageBus.cs +++ b/src/SlimMessageBus.Host/IMasterMessageBus.cs @@ -1,5 +1,6 @@ namespace SlimMessageBus.Host; -public interface IMasterMessageBus : IMessageBus, IMessageBusProducer, IConsumerControl, ITopologyControl +public interface IMasterMessageBus : IMessageBusProducer, IConsumerControl, ITopologyControl, IMessageBusProvider { + IMessageSerializer Serializer { get; } } diff --git a/src/SlimMessageBus.Host/IMessageBusProducer.cs b/src/SlimMessageBus.Host/IMessageBusProducer.cs index 3ff28f8e..71f9199a 100644 --- a/src/SlimMessageBus.Host/IMessageBusProducer.cs +++ b/src/SlimMessageBus.Host/IMessageBusProducer.cs @@ -2,6 +2,6 @@ public interface IMessageBusProducer { - Task ProducePublish(object message, string path = null, IDictionary headers = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default); - Task ProduceSend(object request, TimeSpan? timeout = null, string path = null, IDictionary headers = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default); + Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default); + Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/SlimMessageBus.Host/IMessageBusTarget.cs b/src/SlimMessageBus.Host/IMessageBusTarget.cs new file mode 100644 index 00000000..fd880f00 --- /dev/null +++ b/src/SlimMessageBus.Host/IMessageBusTarget.cs @@ -0,0 +1,7 @@ +namespace SlimMessageBus.Host; + +public interface IMessageBusTarget : IMessageBus +{ + IServiceProvider ServiceProvider { get; } + IMessageBusProducer Target { get; } +} diff --git a/src/SlimMessageBus.Host/MessageBusBase.cs b/src/SlimMessageBus.Host/MessageBusBase.cs index 8a2d7f5c..1b4c22e8 100644 --- a/src/SlimMessageBus.Host/MessageBusBase.cs +++ b/src/SlimMessageBus.Host/MessageBusBase.cs @@ -3,8 +3,8 @@ namespace SlimMessageBus.Host; using System.Globalization; using SlimMessageBus.Host.Consumer; -using SlimMessageBus.Host.Services; - +using SlimMessageBus.Host.Services; + public abstract class MessageBusBase : MessageBusBase where TProviderSettings : class { public TProviderSettings ProviderSettings { get; } @@ -13,15 +13,15 @@ protected MessageBusBase(MessageBusSettings settings, TProviderSettings provider { ProviderSettings = providerSettings ?? throw new ArgumentNullException(nameof(providerSettings)); } -} - +} + public abstract class MessageBusBase : IDisposable, IAsyncDisposable, IMasterMessageBus, IMessageScopeFactory, IMessageHeadersFactory, ICurrentTimeProvider, IResponseProducer, IResponseConsumer { private readonly ILogger _logger; private CancellationTokenSource _cancellationTokenSource = new(); private IMessageSerializer _serializer; - private readonly MessageHeaderService _headerService; - private readonly List _consumers = []; + private readonly MessageHeaderService _headerService; + private readonly List _consumers = []; /// /// Special market reference that signifies a dummy producer settings for response types. @@ -45,6 +45,11 @@ public virtual IMessageSerializer Serializer public IMessageTypeResolver MessageTypeResolver { get; } + /// + /// Default that corresponds to the root DI container, and pointing at self as the bus target. + /// + public virtual IMessageBusTarget MessageBusTarget { get; } + protected ProducerByMessageTypeCache ProducerSettingsByMessageType { get; private set; } protected IPendingRequestStore PendingRequestStore { get; set; } @@ -59,29 +64,29 @@ public virtual IMessageSerializer Serializer #endregion - private readonly object _initTaskLock = new(); - private Task _initTask = null; + private readonly object _initTaskLock = new(); + private Task _initTask = null; #region Start & Stop - - private readonly object _startLock = new(); + + private readonly object _startLock = new(); public bool IsStarted { get; private set; } protected bool IsStarting { get; private set; } protected bool IsStopping { get; private set; } - #endregion + #endregion + + public virtual string Name => Settings.Name ?? "Main"; - public virtual string Name => Settings.Name ?? "Main"; - public IReadOnlyCollection Consumers => _consumers; protected MessageBusBase(MessageBusSettings settings) { - if (settings is null) throw new ArgumentNullException(nameof(settings)); - if (settings.ServiceProvider is null) throw new ConfigurationMessageBusException($"The bus {Name} has no {nameof(settings.ServiceProvider)} configured"); - + if (settings is null) throw new ArgumentNullException(nameof(settings)); + if (settings.ServiceProvider is null) throw new ConfigurationMessageBusException($"The bus {Name} has no {nameof(settings.ServiceProvider)} configured"); + Settings = settings; // Try to resolve from DI, if also not available supress logging using the NullLoggerFactory @@ -92,52 +97,52 @@ protected MessageBusBase(MessageBusSettings settings) var messageTypeResolverType = settings.MessageTypeResolverType ?? typeof(IMessageTypeResolver); MessageTypeResolver = (IMessageTypeResolver)settings.ServiceProvider.GetService(messageTypeResolverType) ?? throw new ConfigurationMessageBusException($"The bus {Name} could not resolve the required type {messageTypeResolverType.Name} from {nameof(Settings.ServiceProvider)}"); - - _headerService = new MessageHeaderService(LoggerFactory.CreateLogger(), Settings, MessageTypeResolver); + + _headerService = new MessageHeaderService(LoggerFactory.CreateLogger(), Settings, MessageTypeResolver); RuntimeTypeCache = new RuntimeTypeCache(); + + MessageBusTarget = new MessageBusProxy(this, Settings.ServiceProvider); + } + + protected void AddInit(Task task) + { + lock (_initTaskLock) + { + var prevInitTask = _initTask; + _initTask = prevInitTask != null + ? prevInitTask.ContinueWith(_ => task) + : task; + } + } + + protected async Task EnsureInitFinished() + { + var initTask = _initTask; + if (initTask != null) + { + await initTask.ConfigureAwait(false); + + lock (_initTaskLock) + { + if (ReferenceEquals(_initTask, initTask)) + { + _initTask = null; + } + } + } } - - protected void AddInit(Task task) - { - lock (_initTaskLock) - { - var prevInitTask = _initTask; - _initTask = prevInitTask != null - ? prevInitTask.ContinueWith(_ => task) - : task; - } - } - - protected async Task EnsureInitFinished() - { - var initTask = _initTask; - if (initTask != null) - { - await initTask.ConfigureAwait(false); - - lock (_initTaskLock) - { - if (ReferenceEquals(_initTask, initTask)) - { - _initTask = null; - } - } - } - } - - protected virtual IMessageSerializer GetSerializer() => - (IMessageSerializer)Settings.ServiceProvider.GetService(Settings.SerializerType) - ?? throw new ConfigurationMessageBusException($"The bus {Name} could not resolve the required message serializer type {Settings.SerializerType.Name} from {nameof(Settings.ServiceProvider)}"); - - protected virtual IMessageBusSettingsValidationService ValidationService { get => new DefaultMessageBusSettingsValidationService(Settings); } + + protected virtual IMessageSerializer GetSerializer() => Settings.GetSerializer(Settings.ServiceProvider); + + protected virtual IMessageBusSettingsValidationService ValidationService { get => new DefaultMessageBusSettingsValidationService(Settings); } /// /// Called by the provider to initialize the bus. /// protected void OnBuildProvider() { - ValidationService.AssertSettings(); + ValidationService.AssertSettings(); Build(); @@ -156,7 +161,7 @@ protected void OnBuildProvider() } }); } - } + } protected virtual void Build() { @@ -185,8 +190,8 @@ private Dictionary BuildProducerByBaseMessageType() producerByBaseMessageType.TryAdd(consumerSettings.ResponseType, MarkerProducerSettingsForResponses); } return producerByBaseMessageType; - } - + } + private IEnumerable _lifecycleInterceptors; private async Task OnBusLifecycle(MessageBusLifecycleEventType eventType) @@ -196,92 +201,92 @@ private async Task OnBusLifecycle(MessageBusLifecycleEventType eventType) { foreach (var i in _lifecycleInterceptors) { - await i.OnBusLifecycle(eventType, this); + await i.OnBusLifecycle(eventType, MessageBusTarget); } } - } + } public async Task Start() - { - lock (_startLock) - { - if (IsStarting || IsStarted) - { - return; - } - IsStarting = true; - } - - try - { - await EnsureInitFinished(); - - _logger.LogInformation("Starting consumers for {BusName} bus...", Name); - await OnBusLifecycle(MessageBusLifecycleEventType.Starting).ConfigureAwait(false); - - await CreateConsumers(); - await OnStart().ConfigureAwait(false); - await Task.WhenAll(_consumers.Select(x => x.Start())).ConfigureAwait(false); - - await OnBusLifecycle(MessageBusLifecycleEventType.Started).ConfigureAwait(false); - _logger.LogInformation("Started consumers for {BusName} bus", Name); - - lock (_startLock) - { - IsStarted = true; - } - } - finally - { - lock (_startLock) - { - IsStarting = false; - } + { + lock (_startLock) + { + if (IsStarting || IsStarted) + { + return; + } + IsStarting = true; + } + + try + { + await EnsureInitFinished(); + + _logger.LogInformation("Starting consumers for {BusName} bus...", Name); + await OnBusLifecycle(MessageBusLifecycleEventType.Starting).ConfigureAwait(false); + + await CreateConsumers(); + await OnStart().ConfigureAwait(false); + await Task.WhenAll(_consumers.Select(x => x.Start())).ConfigureAwait(false); + + await OnBusLifecycle(MessageBusLifecycleEventType.Started).ConfigureAwait(false); + _logger.LogInformation("Started consumers for {BusName} bus", Name); + + lock (_startLock) + { + IsStarted = true; + } + } + finally + { + lock (_startLock) + { + IsStarting = false; + } } } public async Task Stop() - { - lock (_startLock) - { - if (IsStopping || !IsStarted) - { - return; - } - IsStopping = true; - } - - try - { - await EnsureInitFinished(); - - _logger.LogInformation("Stopping consumers for {BusName} bus...", Name); - await OnBusLifecycle(MessageBusLifecycleEventType.Stopping).ConfigureAwait(false); - - await Task.WhenAll(_consumers.Select(x => x.Stop())).ConfigureAwait(false); - await OnStop().ConfigureAwait(false); - await DestroyConsumers().ConfigureAwait(false); - - await OnBusLifecycle(MessageBusLifecycleEventType.Stopped).ConfigureAwait(false); - _logger.LogInformation("Stopped consumers for {BusName} bus", Name); - - lock (_startLock) - { - IsStarted = false; - } - } - finally - { - lock (_startLock) - { - IsStopping = false; - } + { + lock (_startLock) + { + if (IsStopping || !IsStarted) + { + return; + } + IsStopping = true; + } + + try + { + await EnsureInitFinished(); + + _logger.LogInformation("Stopping consumers for {BusName} bus...", Name); + await OnBusLifecycle(MessageBusLifecycleEventType.Stopping).ConfigureAwait(false); + + await Task.WhenAll(_consumers.Select(x => x.Stop())).ConfigureAwait(false); + await OnStop().ConfigureAwait(false); + await DestroyConsumers().ConfigureAwait(false); + + await OnBusLifecycle(MessageBusLifecycleEventType.Stopped).ConfigureAwait(false); + _logger.LogInformation("Stopped consumers for {BusName} bus", Name); + + lock (_startLock) + { + IsStarted = false; + } + } + finally + { + lock (_startLock) + { + IsStopping = false; + } } } protected internal virtual Task OnStart() => Task.CompletedTask; protected internal virtual Task OnStop() => Task.CompletedTask; - + protected void AssertActive() { if (IsDisposed) @@ -343,8 +348,8 @@ private async ValueTask DisposeAsyncInternal() /// protected async virtual ValueTask DisposeAsyncCore() { - await Stop().ConfigureAwait(false); - + await Stop().ConfigureAwait(false); + if (_cancellationTokenSource != null) { _cancellationTokenSource.Cancel(); @@ -357,28 +362,28 @@ protected async virtual ValueTask DisposeAsyncCore() PendingRequestManager.Dispose(); PendingRequestManager = null; } - } - - protected virtual Task CreateConsumers() - { - _logger.LogInformation("Creating consumers for {BusName} bus...", Name); - return Task.CompletedTask; - } - - protected async virtual Task DestroyConsumers() - { - _logger.LogInformation("Destroying consumers for {BusName} bus...", Name); - - foreach (var consumer in _consumers) - { - await consumer.DisposeSilently("Consumer", _logger).ConfigureAwait(false); - } - _consumers.Clear(); - } - + } + + protected virtual Task CreateConsumers() + { + _logger.LogInformation("Creating consumers for {BusName} bus...", Name); + return Task.CompletedTask; + } + + protected async virtual Task DestroyConsumers() + { + _logger.LogInformation("Destroying consumers for {BusName} bus...", Name); + + foreach (var consumer in _consumers) + { + await consumer.DisposeSilently("Consumer", _logger).ConfigureAwait(false); + } + _consumers.Clear(); + } + #endregion - - protected void AddConsumer(AbstractConsumer consumer) => _consumers.Add(consumer); + + protected void AddConsumer(AbstractConsumer consumer) => _consumers.Add(consumer); public virtual DateTimeOffset CurrentTime => DateTimeOffset.UtcNow; @@ -403,9 +408,9 @@ protected virtual string GetDefaultPath(Type messageType, ProducerSettings produ return path; } - protected abstract Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders = null, CancellationToken cancellationToken = default); + protected abstract Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default); - public virtual Task ProducePublish(object message, string path = null, IDictionary headers = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) + public virtual Task ProducePublish(object message, string path = null, IDictionary headers = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) { if (message == null) throw new ArgumentNullException(nameof(message)); AssertActive(); @@ -427,7 +432,7 @@ public virtual Task ProducePublish(object message, string path = null, IDictiona _headerService.AddMessageHeaders(messageHeaders, headers, message, producerSettings); } - var serviceProvider = currentServiceProvider ?? Settings.ServiceProvider; + var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; var producerInterceptors = RuntimeTypeCache.ProducerInterceptorType.ResolveAll(serviceProvider, messageType); var publishInterceptors = RuntimeTypeCache.PublishInterceptorType.ResolveAll(serviceProvider, messageType); @@ -438,23 +443,23 @@ public virtual Task ProducePublish(object message, string path = null, IDictiona Path = path, CancellationToken = cancellationToken, Headers = messageHeaders, - Bus = this, + Bus = new MessageBusProxy(this, serviceProvider), ProducerSettings = producerSettings }; - var pipeline = new PublishInterceptorPipeline(this, message, producerSettings, serviceProvider, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); + var pipeline = new PublishInterceptorPipeline(this, message, producerSettings, targetBus, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); return pipeline.Next(); } - return PublishInternal(message, path, messageHeaders, cancellationToken, producerSettings, currentServiceProvider); + return PublishInternal(message, path, messageHeaders, cancellationToken, producerSettings, targetBus); } - protected internal virtual Task PublishInternal(object message, string path, IDictionary messageHeaders, CancellationToken cancellationToken, ProducerSettings producerSettings, IServiceProvider currentServiceProvider) + protected internal virtual Task PublishInternal(object message, string path, IDictionary messageHeaders, CancellationToken cancellationToken, ProducerSettings producerSettings, IMessageBusTarget targetBus) { var payload = Serializer.Serialize(producerSettings.MessageType, message); _logger.LogDebug("Producing message {Message} of type {MessageType} to path {Path}", message, producerSettings.MessageType, path); - return ProduceToTransport(message, path, payload, messageHeaders, cancellationToken); + return ProduceToTransport(message, path, payload, messageHeaders, targetBus, cancellationToken); } /// @@ -472,7 +477,7 @@ protected virtual TimeSpan GetDefaultRequestTimeout(Type requestType, ProducerSe return timeout; } - public virtual Task ProduceSend(object request, TimeSpan? timeout = null, string path = null, IDictionary headers = null, IServiceProvider currentServiceProvider = null, CancellationToken cancellationToken = default) + public virtual Task ProduceSend(object request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, IMessageBusTarget targetBus = null, CancellationToken cancellationToken = default) { if (request == null) throw new ArgumentNullException(nameof(request)); AssertActive(); @@ -508,7 +513,7 @@ public virtual Task ProduceSend(object request, TimeSpan? requestHeaders.SetHeader(ReqRespMessageHeaders.Expires, expires); } - var serviceProvider = currentServiceProvider ?? Settings.ServiceProvider; + var serviceProvider = targetBus?.ServiceProvider ?? Settings.ServiceProvider; var producerInterceptors = RuntimeTypeCache.ProducerInterceptorType.ResolveAll(serviceProvider, requestType); var sendInterceptors = RuntimeTypeCache.SendInterceptorType.ResolveAll(serviceProvider, (requestType, responseType)); @@ -519,21 +524,21 @@ public virtual Task ProduceSend(object request, TimeSpan? Path = path, CancellationToken = cancellationToken, Headers = requestHeaders, - Bus = this, + Bus = new MessageBusProxy(this, serviceProvider), ProducerSettings = producerSettings, Created = created, Expires = expires, RequestId = requestId, }; - var pipeline = new SendInterceptorPipeline(this, request, producerSettings, serviceProvider, context, producerInterceptors: producerInterceptors, sendInterceptors: sendInterceptors); + var pipeline = new SendInterceptorPipeline(this, request, producerSettings, targetBus, context, producerInterceptors: producerInterceptors, sendInterceptors: sendInterceptors); return pipeline.Next(); } - return SendInternal(request, path, requestType, responseType, producerSettings, created, expires, requestId, requestHeaders, currentServiceProvider, cancellationToken); + return SendInternal(request, path, requestType, responseType, producerSettings, created, expires, requestId, requestHeaders, targetBus, cancellationToken); } - protected async internal virtual Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IServiceProvider currentServiceProvider, CancellationToken cancellationToken) + protected async internal virtual Task SendInternal(object request, string path, Type requestType, Type responseType, ProducerSettings producerSettings, DateTimeOffset created, DateTimeOffset expires, string requestId, IDictionary requestHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken) { // record the request state var requestState = new PendingRequestState(requestId, request, requestType, responseType, created, expires, cancellationToken); @@ -547,7 +552,7 @@ protected async internal virtual Task SendInternal SendInternal requestHeaders, string path, ProducerSettings producerSettings) + public virtual Task ProduceRequest(object request, IDictionary requestHeaders, string path, ProducerSettings producerSettings, IMessageBusTarget targetBus) { if (request == null) throw new ArgumentNullException(nameof(request)); if (producerSettings == null) throw new ArgumentNullException(nameof(producerSettings)); @@ -575,24 +580,24 @@ public virtual Task ProduceRequest(object request, IDictionary r _headerService.AddMessageTypeHeader(request, requestHeaders); } - return ProduceToTransport(request, path, requestPayload, requestHeaders); - } - - public virtual Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker) - { + return ProduceToTransport(request, path, requestPayload, requestHeaders, targetBus); + } + + public virtual Task ProduceResponse(string requestId, object request, IReadOnlyDictionary requestHeaders, object response, Exception responseException, IMessageTypeConsumerInvokerSettings consumerInvoker) + { if (requestHeaders == null) throw new ArgumentNullException(nameof(requestHeaders)); if (consumerInvoker == null) throw new ArgumentNullException(nameof(consumerInvoker)); - - var responseType = consumerInvoker.ParentSettings.ResponseType; - _logger.LogDebug("Sending the response {Response} of type {MessageType} for RequestId: {RequestId}...", response, responseType, requestId); - - var responseHeaders = CreateHeaders(); - responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); - if (responseException != null) - { - responseHeaders.SetHeader(ReqRespMessageHeaders.Error, responseException.Message); - } - + + var responseType = consumerInvoker.ParentSettings.ResponseType; + _logger.LogDebug("Sending the response {Response} of type {MessageType} for RequestId: {RequestId}...", response, responseType, requestId); + + var responseHeaders = CreateHeaders(); + responseHeaders.SetHeader(ReqRespMessageHeaders.RequestId, requestId); + if (responseException != null) + { + responseHeaders.SetHeader(ReqRespMessageHeaders.Error, responseException.Message); + } + if (!requestHeaders.TryGetHeader(ReqRespMessageHeaders.ReplyTo, out object replyTo)) { throw new MessageBusException($"The header {ReqRespMessageHeaders.ReplyTo} was missing on the message"); @@ -604,8 +609,8 @@ public virtual Task ProduceResponse(string requestId, object request, IReadOnlyD ? Serializer.Serialize(responseType, response) : null; - return ProduceToTransport(response, (string)replyTo, responsePayload, responseHeaders); - } + return ProduceToTransport(response, (string)replyTo, responsePayload, responseHeaders, null); + } /// /// Should be invoked by the concrete bus implementation whenever there is a message arrived on the reply to topic. @@ -705,28 +710,4 @@ public virtual IMessageScope CreateMessageScope(ConsumerSettings consumerSetting } public virtual Task ProvisionTopology() => Task.CompletedTask; - - #region Implementation of IMessageBus - - #region Implementation of IPublishBus - - public virtual Task Publish(TMessage message, string path = null, IDictionary headers = null, CancellationToken cancellationToken = default) - => ProducePublish(message, path, headers, currentServiceProvider: null, cancellationToken); - - #endregion - - #region Implementation of IRequestResponseBus - - public virtual Task Send(IRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => ProduceSend(request, timeout, path, headers, currentServiceProvider: null, cancellationToken); - - public Task Send(IRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => ProduceSend(request, timeout, path, headers, currentServiceProvider: null, cancellationToken); - - public Task Send(TRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => ProduceSend(request, timeout, path, headers, currentServiceProvider: null, cancellationToken); - - #endregion - - #endregion -} +} diff --git a/src/SlimMessageBus.Host/MessageBusProxy.cs b/src/SlimMessageBus.Host/MessageBusProxy.cs index f727148e..6883cfbe 100644 --- a/src/SlimMessageBus.Host/MessageBusProxy.cs +++ b/src/SlimMessageBus.Host/MessageBusProxy.cs @@ -1,41 +1,39 @@ namespace SlimMessageBus.Host; /// -/// Proxy to the that introduces its own for dependency lookup. +/// Proxy to the that introduces its own for dependency lookup. /// -public class MessageBusProxy : IMessageBus, ICompositeMessageBus +public class MessageBusProxy( + IMessageBusProducer target, + IServiceProvider serviceProvider) + : IMessageBusTarget, ICompositeMessageBus { /// /// The target of this proxy (the singleton master bus). /// - public IMessageBusProducer Target { get; } - public IServiceProvider ServiceProvider { get; } - - public MessageBusProxy(IMessageBusProducer target, IServiceProvider serviceProvider) - { - Target = target; - ServiceProvider = serviceProvider; - } + public IMessageBusProducer Target { get; } = target; + public IServiceProvider ServiceProvider { get; } = serviceProvider; + #region Implementation of IMessageBus - + #region Implementation of IPublishBus - + public Task Publish(TMessage message, string path = null, IDictionary headers = null, CancellationToken cancellationToken = default) - => Target.ProducePublish(message, path, headers, currentServiceProvider: ServiceProvider, cancellationToken); + => Target.ProducePublish(message, path, headers, targetBus: this, cancellationToken); #endregion #region Implementation of IRequestResponseBus public Task Send(IRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => Target.ProduceSend(request, timeout: timeout, path: path, headers: headers, currentServiceProvider: ServiceProvider, cancellationToken); + => Target.ProduceSend(request, path: path, headers: headers, timeout: timeout, targetBus: this, cancellationToken); public Task Send(TRequestMessage request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => Target.ProduceSend(request, timeout: timeout, path: path, headers: headers, currentServiceProvider: ServiceProvider, cancellationToken); + => Target.ProduceSend(request, path: path, headers: headers, timeout: timeout, targetBus: this, cancellationToken); public Task Send(IRequest request, string path = null, IDictionary headers = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => Target.ProduceSend(request, timeout: timeout, path: path, headers: headers, currentServiceProvider: ServiceProvider, cancellationToken); + => Target.ProduceSend(request, path: path, headers: headers, timeout: timeout, targetBus: this, cancellationToken); #endregion @@ -43,7 +41,7 @@ public Task Send(IRequest request, string path = null, IDictionary GetChildBuses() + public IEnumerable GetChildBuses() { if (Target is ICompositeMessageBus composite) { return composite.GetChildBuses(); } - return Enumerable.Empty(); + return Enumerable.Empty(); } #endregion diff --git a/src/SlimMessageBus.Host/MessageBusSettingsExtensions.cs b/src/SlimMessageBus.Host/MessageBusSettingsExtensions.cs new file mode 100644 index 00000000..36776d74 --- /dev/null +++ b/src/SlimMessageBus.Host/MessageBusSettingsExtensions.cs @@ -0,0 +1,8 @@ +namespace SlimMessageBus.Host; + +public static class MessageBusSettingsExtensions +{ + public static IMessageSerializer GetSerializer(this MessageBusSettings settings, IServiceProvider serviceProvider) => + (IMessageSerializer)serviceProvider.GetService(settings.SerializerType) + ?? throw new ConfigurationMessageBusException($"The bus {settings.Name} could not resolve the required message serializer type {settings.SerializerType.Name} from {nameof(serviceProvider)}"); +} diff --git a/src/SlimMessageBus.Host/Producer/IProducerContextExtensions.cs b/src/SlimMessageBus.Host/Producer/IProducerContextExtensions.cs new file mode 100644 index 00000000..f673ef3e --- /dev/null +++ b/src/SlimMessageBus.Host/Producer/IProducerContextExtensions.cs @@ -0,0 +1,10 @@ +namespace SlimMessageBus.Host; + +public static class IProducerContextExtensions +{ + public static IMasterMessageBus GetMasterMessageBus(this IProducerContext context) + { + var busTarget = context.Bus as IMessageBusTarget; + return busTarget?.Target as IMasterMessageBus ?? context.Bus as IMasterMessageBus; + } +} diff --git a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs index 23400a8f..7ca83079 100644 --- a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs +++ b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/ProducerInterceptorPipeline.cs @@ -6,7 +6,7 @@ abstract internal class ProducerInterceptorPipeline where TContext : P protected readonly object _message; protected readonly ProducerSettings _producerSettings; - protected readonly IServiceProvider _currentServiceProvider; + protected readonly IMessageBusTarget _targetBus; protected readonly TContext _context; protected readonly IEnumerable _producerInterceptors; @@ -16,13 +16,13 @@ abstract internal class ProducerInterceptorPipeline where TContext : P protected bool _targetVisited; - protected ProducerInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IServiceProvider currentServiceProvider, TContext context, IEnumerable producerInterceptors) + protected ProducerInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, TContext context, IEnumerable producerInterceptors) { _bus = bus; _message = message; _producerSettings = producerSettings; - _currentServiceProvider = currentServiceProvider; + _targetBus = targetBus; _context = context; _producerInterceptors = producerInterceptors; diff --git a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs index 2a6ddd19..89cf5502 100644 --- a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs +++ b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/PublishInterceptorPipeline.cs @@ -7,8 +7,8 @@ internal class PublishInterceptorPipeline : ProducerInterceptorPipeline _publishInterceptorsEnumerator; private bool _publishInterceptorsVisited = false; - public PublishInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IServiceProvider currentServiceProvider, PublishContext context, IEnumerable producerInterceptors, IEnumerable publishInterceptors) - : base(bus, message, producerSettings, currentServiceProvider, context, producerInterceptors) + public PublishInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, PublishContext context, IEnumerable producerInterceptors, IEnumerable publishInterceptors) + : base(bus, message, producerSettings, targetBus, context, producerInterceptors) { _publishInterceptors = publishInterceptors; _publishInterceptorFunc = bus.RuntimeTypeCache.PublishInterceptorType[message.GetType()]; @@ -42,7 +42,7 @@ public async Task Next() if (!_targetVisited) { _targetVisited = true; - await _bus.PublishInternal(_message, _context.Path, _context.Headers, _context.CancellationToken, _producerSettings, _currentServiceProvider); + await _bus.PublishInternal(_message, _context.Path, _context.Headers, _context.CancellationToken, _producerSettings, _targetBus); return null; } diff --git a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs index 66d40282..15f1c332 100644 --- a/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs +++ b/src/SlimMessageBus.Host/Producer/InterceptorPipelines/SendInterceptorPipeline.cs @@ -7,8 +7,8 @@ internal class SendInterceptorPipeline : ProducerInterceptorPipeline< private IEnumerator _sendInterceptorsEnumerator; private bool _sendInterceptorsVisited = false; - public SendInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IServiceProvider currentServiceProvider, SendContext context, IEnumerable producerInterceptors, IEnumerable sendInterceptors) - : base(bus, message, producerSettings, currentServiceProvider, context, producerInterceptors) + public SendInterceptorPipeline(MessageBusBase bus, object message, ProducerSettings producerSettings, IMessageBusTarget targetBus, SendContext context, IEnumerable producerInterceptors, IEnumerable sendInterceptors) + : base(bus, message, producerSettings, targetBus, context, producerInterceptors) { _sendInterceptors = sendInterceptors; _sendInterceptorFunc = bus.RuntimeTypeCache.SendInterceptorType[(message.GetType(), typeof(TResponse))]; @@ -45,7 +45,7 @@ public async Task Next() if (!_targetVisited) { _targetVisited = true; - var response = await _bus.SendInternal(_message, _context.Path, _message.GetType(), typeof(TResponse), _producerSettings, _context.Created, _context.Expires, _context.RequestId, _context.Headers, _currentServiceProvider, _context.CancellationToken); + var response = await _bus.SendInternal(_message, _context.Path, _message.GetType(), typeof(TResponse), _producerSettings, _context.Created, _context.Expires, _context.RequestId, _context.Headers, _targetBus, _context.CancellationToken); return response; } diff --git a/src/SlimMessageBus.sln b/src/SlimMessageBus.sln index 6d615ed9..885de7c5 100644 --- a/src/SlimMessageBus.sln +++ b/src/SlimMessageBus.sln @@ -230,6 +230,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.RabbitM EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlimMessageBus.Host.RabbitMQ.Test", "Tests\SlimMessageBus.Host.RabbitMQ.Test\SlimMessageBus.Host.RabbitMQ.Test.csproj", "{F5373E1D-A2B4-46CC-9B07-94F6655C8E29}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimMessageBus.Host.Sql", "SlimMessageBus.Host.Sql\SlimMessageBus.Host.Sql.csproj", "{5EED0E89-2475-40E0-81EF-0F05C9326612}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimMessageBus.Host.Sql.Common", "SlimMessageBus.Host.Sql.Common\SlimMessageBus.Host.Sql.Common.csproj", "{F19B7A21-7749-465A-8810-4C274A9E8956}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -710,6 +714,22 @@ Global {F5373E1D-A2B4-46CC-9B07-94F6655C8E29}.Release|Any CPU.Build.0 = Release|Any CPU {F5373E1D-A2B4-46CC-9B07-94F6655C8E29}.Release|x86.ActiveCfg = Release|Any CPU {F5373E1D-A2B4-46CC-9B07-94F6655C8E29}.Release|x86.Build.0 = Release|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Debug|x86.ActiveCfg = Debug|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Debug|x86.Build.0 = Debug|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Release|Any CPU.Build.0 = Release|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Release|x86.ActiveCfg = Release|Any CPU + {5EED0E89-2475-40E0-81EF-0F05C9326612}.Release|x86.Build.0 = Release|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Debug|x86.ActiveCfg = Debug|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Debug|x86.Build.0 = Debug|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Release|Any CPU.Build.0 = Release|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Release|x86.ActiveCfg = Release|Any CPU + {F19B7A21-7749-465A-8810-4C274A9E8956}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -781,6 +801,8 @@ Global {547C247F-0BAE-43D0-A5E7-FACC0B612B08} = {9F005B5C-A856-4351-8C0C-47A8B785C637} {65BD0C01-EFDC-4775-B0A8-CF13073D9650} = {9291D340-B4FA-44A3-8060-C14743FB1712} {F5373E1D-A2B4-46CC-9B07-94F6655C8E29} = {9F005B5C-A856-4351-8C0C-47A8B785C637} + {5EED0E89-2475-40E0-81EF-0F05C9326612} = {9291D340-B4FA-44A3-8060-C14743FB1712} + {F19B7A21-7749-465A-8810-4C274A9E8956} = {9291D340-B4FA-44A3-8060-C14743FB1712} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {435A0D65-610C-4B84-B1AA-2C7FBE72DB80} diff --git a/src/SlimMessageBus/IProducerContext.cs b/src/SlimMessageBus/IProducerContext.cs index 57e1ef91..9e085672 100644 --- a/src/SlimMessageBus/IProducerContext.cs +++ b/src/SlimMessageBus/IProducerContext.cs @@ -15,9 +15,10 @@ public interface IProducerContext /// CancellationToken CancellationToken { get; } /// - /// The bus on which the producer was executed. - /// - IMessageBus Bus { get; } + /// 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; } /// /// Additional transport provider specific features or user custom data. /// diff --git a/src/Tests/Host.Test.Properties.xml b/src/Tests/Host.Test.Properties.xml index a2879751..6a7dd624 100644 --- a/src/Tests/Host.Test.Properties.xml +++ b/src/Tests/Host.Test.Properties.xml @@ -10,9 +10,16 @@ - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.AsyncApi.Test/SlimMessageBus.Host.AsyncApi.Test.csproj b/src/Tests/SlimMessageBus.Host.AsyncApi.Test/SlimMessageBus.Host.AsyncApi.Test.csproj index 2bcbdd5c..2c6e3062 100644 --- a/src/Tests/SlimMessageBus.Host.AsyncApi.Test/SlimMessageBus.Host.AsyncApi.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.AsyncApi.Test/SlimMessageBus.Host.AsyncApi.Test.csproj @@ -1,25 +1,6 @@  - - net8.0 - enable - enable - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + diff --git a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs index 38d03f15..c18c51ce 100644 --- a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/EventHubMessageBusIt.cs @@ -171,16 +171,11 @@ public class PingMessage #endregion } -public class PingConsumer : IConsumer, IConsumerWithContext +public class PingConsumer(ILogger logger, ConcurrentBag messages) + : IConsumer, IConsumerWithContext { - private readonly ILogger _logger; - private readonly ConcurrentBag _messages; - - public PingConsumer(ILogger logger, ConcurrentBag messages) - { - _logger = logger; - _messages = messages; - } + private readonly ILogger _logger = logger; + private readonly ConcurrentBag _messages = messages; public IConsumerContext Context { get; set; } diff --git a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/SlimMessageBus.Host.AzureEventHub.Test.csproj b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/SlimMessageBus.Host.AzureEventHub.Test.csproj index e1172b1d..ff3ba5ff 100644 --- a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/SlimMessageBus.Host.AzureEventHub.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/SlimMessageBus.Host.AzureEventHub.Test.csproj @@ -3,23 +3,12 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - + - @@ -31,9 +20,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/appsettings.json index 9d90b92f..14424c15 100644 --- a/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.AzureEventHub.Test/appsettings.json @@ -1,7 +1,16 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "Azure": { "EventHub": "Endpoint=sb://slimmessagebus-ehn.servicebus.windows.net/;SharedAccessKeyName=application;SharedAccessKey={{azure_eventhub_key}}", "Storage": "DefaultEndpointsProtocol=https;AccountName=slimmessagebuscons;AccountKey={{azure_eventhub_storage_key}};EndpointSuffix=core.windows.net", "ContainerName": "leases" - } + } } diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs index fa011f9c..89cf1400 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusIt.cs @@ -254,13 +254,14 @@ public async Task FIFOUsingSessionsOnQueue() AddBusConfiguration(mbb => { - mbb.Produce(x => x.DefaultQueue(queue).WithModifier(MessageModifierWithSession)) - .Consume(x => x - .Queue(queue) - .WithConsumer() - .WithConsumer() - .Instances(concurrency) - .EnableSession(x => x.MaxConcurrentSessions(10).SessionIdleTimeout(TimeSpan.FromSeconds(5)))); + mbb + .Produce(x => x.DefaultQueue(queue).WithModifier(MessageModifierWithSession)) + .Consume(x => x + .Queue(queue) + .WithConsumer() + .WithConsumer() + .Instances(concurrency) + .EnableSession(x => x.MaxConcurrentSessions(10).SessionIdleTimeout(TimeSpan.FromSeconds(5)))); }); await BasicPubSub(concurrency, 1, 1, CheckMessagesWithinSameSessionAreInOrder); } @@ -317,7 +318,7 @@ public class PingConsumer : IConsumer, IConsumerWithContext private readonly ILogger _logger; private readonly TestEventCollector _messages; - public PingConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) + public PingConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) { _logger = logger; _messages = messages; @@ -346,7 +347,7 @@ public class PingDerivedConsumer : IConsumer, IConsumerWithC private readonly ILogger _logger; private readonly TestEventCollector _messages; - public PingDerivedConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) + public PingDerivedConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) { _logger = logger; _messages = messages; diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs index e053036f..b6d36696 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/ServiceBusMessageBusTests.cs @@ -82,8 +82,8 @@ public async Task WhenPublishGivenModifierConfiguredForMessageTypeThenModifierEx var m2 = new SomeMessage { Id = "2", Value = 3 }; // act - await ProviderBus.Value.Publish(m1); - await ProviderBus.Value.Publish(m2); + await ProviderBus.Value.ProducePublish(m1); + await ProviderBus.Value.ProducePublish(m2); // assert var topicClient = SenderMockByPath["default-topic"]; @@ -104,7 +104,7 @@ public async Task When_Publish_Given_ModifierConfiguredForMessageTypeThatThrowsE var m = new SomeMessage { Id = "1", Value = 10 }; // act - await ProviderBus.Value.Publish(m); + await ProviderBus.Value.ProducePublish(m); // assert SenderMockByPath["default-topic"].Verify(x => x.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); @@ -118,7 +118,7 @@ public void When_Create_Given_SameMessageTypeConfiguredTwiceForTopicAndForQueue_ BusBuilder.Produce(x => x.ToQueue()); // act - Func creation = () => BusBuilder.Build(); + Func creation = () => BusBuilder.Build(); // assert creation.Should().Throw() @@ -134,7 +134,7 @@ public void When_Create_Given_SameDefaultPathUsedForTopicAndForQueue_Then_Config BusBuilder.Produce(x => x.DefaultQueue(path)); // act - Func creation = () => BusBuilder.Build(); + Func creation = () => BusBuilder.Build(); // assert creation.Should().Throw() @@ -154,10 +154,10 @@ public async Task When_Publish_Then_TopicClientOrQueueClientIsCreatedForTopicNam var om2 = new OtherMessage { Id = "2" }; // act - await ProviderBus.Value.Publish(sm1, "some-topic"); - await ProviderBus.Value.Publish(sm2, "some-topic"); - await ProviderBus.Value.Publish(om1, "some-queue"); - await ProviderBus.Value.Publish(om2, "some-queue"); + await ProviderBus.Value.ProducePublish(sm1, "some-topic"); + await ProviderBus.Value.ProducePublish(sm2, "some-topic"); + await ProviderBus.Value.ProducePublish(om1, "some-queue"); + await ProviderBus.Value.ProducePublish(om2, "some-queue"); // assert SenderMockByPath.Should().HaveCount(2); @@ -166,12 +166,11 @@ public async Task When_Publish_Then_TopicClientOrQueueClientIsCreatedForTopicNam } } -public class WrappedProviderMessageBus : ServiceBusMessageBus +public class WrappedProviderMessageBus( + MessageBusSettings settings, + ServiceBusMessageBusSettings serviceBusSettings) + : ServiceBusMessageBus(settings, serviceBusSettings) { - public WrappedProviderMessageBus(MessageBusSettings settings, ServiceBusMessageBusSettings serviceBusSettings) - : base(settings, serviceBusSettings) - { - } } public class SomeMessage diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/SlimMessageBus.Host.AzureServiceBus.Test.csproj b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/SlimMessageBus.Host.AzureServiceBus.Test.csproj index 941de0cd..3ecf501c 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/SlimMessageBus.Host.AzureServiceBus.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/SlimMessageBus.Host.AzureServiceBus.Test.csproj @@ -3,21 +3,11 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + - @@ -29,9 +19,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/appsettings.json index cf18690e..da791c4b 100644 --- a/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.AzureServiceBus.Test/appsettings.json @@ -1,5 +1,14 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "Azure": { "ServiceBus": "Endpoint=sb://slimmessagebus-2.servicebus.windows.net/;SharedAccessKeyName=application;SharedAccessKey={{azure_servicebus_key}}" - } + } } diff --git a/src/Tests/SlimMessageBus.Host.Benchmark/SlimMessageBus.Host.Benchmark.csproj b/src/Tests/SlimMessageBus.Host.Benchmark/SlimMessageBus.Host.Benchmark.csproj index d7c673dd..1060b913 100644 --- a/src/Tests/SlimMessageBus.Host.Benchmark/SlimMessageBus.Host.Benchmark.csproj +++ b/src/Tests/SlimMessageBus.Host.Benchmark/SlimMessageBus.Host.Benchmark.csproj @@ -4,6 +4,7 @@ Exe + SlimMessageBus.Host.Benchmark.Program @@ -14,9 +15,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageBusBuilderTests.cs b/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageBusBuilderTests.cs index b295e533..9cea8f51 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageBusBuilderTests.cs +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/MessageBusBuilderTests.cs @@ -86,7 +86,7 @@ public void Given_OtherBuilder_When_CopyConstructorUsed_Then_AllStateIsCopied() { // arrange var subject = MessageBusBuilder.Create(); - subject.WithProvider(Mock.Of>()); + subject.WithProvider(Mock.Of>()); subject.AddChildBus("Bus1", mbb => { }); // act diff --git a/src/Tests/SlimMessageBus.Host.Configuration.Test/SlimMessageBus.Host.Configuration.Test.csproj b/src/Tests/SlimMessageBus.Host.Configuration.Test/SlimMessageBus.Host.Configuration.Test.csproj index 7ccce8b1..7f872f90 100644 --- a/src/Tests/SlimMessageBus.Host.Configuration.Test/SlimMessageBus.Host.Configuration.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Configuration.Test/SlimMessageBus.Host.Configuration.Test.csproj @@ -3,24 +3,9 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - + - - - - - - diff --git a/src/Tests/SlimMessageBus.Host.FluentValidation.Test/SlimMessageBus.Host.FluentValidation.Test.csproj b/src/Tests/SlimMessageBus.Host.FluentValidation.Test/SlimMessageBus.Host.FluentValidation.Test.csproj index 90efb405..db164188 100644 --- a/src/Tests/SlimMessageBus.Host.FluentValidation.Test/SlimMessageBus.Host.FluentValidation.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.FluentValidation.Test/SlimMessageBus.Host.FluentValidation.Test.csproj @@ -2,24 +2,10 @@ - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs index 255b5d3d..52720d77 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/HybridTests.cs @@ -76,13 +76,18 @@ private void SetupBus(MessageBusBuilder mbb, SerializerType serializerType) mbb.AddChildBus("AzureSB", (mbb) => { var topic = "integration-external-message"; - mbb.Produce(x => x.DefaultTopic(topic)); - mbb.Consume(x => x.Topic(topic).SubscriptionName("test").WithConsumer()); - mbb.WithProviderServiceBus(cfg => cfg.ConnectionString = Secrets.Service.PopulateSecrets(_configuration["Azure:ServiceBus"])); + mbb + .Produce(x => x.DefaultTopic(topic)) + .Consume(x => x.Topic(topic)) + .WithProviderServiceBus(cfg => + { + cfg.SubscriptionName("test"); + cfg.ConnectionString = Secrets.Service.PopulateSecrets(_configuration["Azure:ServiceBus"]); + }); }); } - public record EventMark(Guid CorrelationId, string Name); + public record EventMark(Guid CorrelationId, string Name, Type ContextMessageBusType); /// /// This test ensures that in a hybris bus setup External (Azure Service Bus) and Internal (Memory) the external message scope is carried over to memory bus, @@ -127,11 +132,23 @@ public async Task When_PublishToMemoryBus_Given_InsideConsumerWithMessageScope_T await store.WaitUntilArriving(newMessagesTimeout: 5, expectedCount: expectedStoreCount); store.Count.Should().Be(expectedStoreCount); + var grouping = store.GroupBy(x => x.CorrelationId, x => x.Name).ToDictionary(x => x.Key, x => x.ToList()); // all of the invocations should happen within the context of one unitOfWork = One CorrelationId = One Message Scope grouping.Count.Should().Be(2); + + // all the internal messages should be processed by Memory bus + store + .Where(x => x.Name == nameof(InternalMessageConsumer) || x.Name == nameof(InternalMessageConsumerInterceptor) || x.Name == nameof(InternalMessageProducerInterceptor) || x.Name == nameof(InternalMessagePublishInterceptor)) + .Should().AllSatisfy(x => x.ContextMessageBusType.Should().Be(typeof(MemoryMessageBus))); + + // all the external messages should be processed by Azure Service Bus + store + .Where(x => x.Name == nameof(ExternalMessageConsumer) || x.Name == nameof(ExternalMessageConsumerInterceptor)) + .Should().AllSatisfy(x => x.ContextMessageBusType.Should().Be(typeof(ServiceBusMessageBus))); + // in this order var eventsThatHappenedWhenExternalWasPublished = grouping.Values.SingleOrDefault(x => x.Count == 2); eventsThatHappenedWhenExternalWasPublished.Should().NotBeNull(); @@ -166,24 +183,15 @@ public class UnitOfWork public Task Commit() => Task.CompletedTask; } - public class ExternalMessageConsumer : IConsumer + public class ExternalMessageConsumer(IMessageBus bus, UnitOfWork unitOfWork, List store) : IConsumer, IConsumerWithContext { - private readonly IMessageBus bus; - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public ExternalMessageConsumer(IMessageBus bus, UnitOfWork unitOfWork, List store) - { - this.bus = bus; - this.unitOfWork = unitOfWork; - this.store = store; - } + public IConsumerContext Context { get; set; } public async Task OnHandle(ExternalMessage message) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessageConsumer))); + store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessageConsumer), GetMessageBusTarget(Context))); } // some processing @@ -195,25 +203,17 @@ public async Task OnHandle(ExternalMessage message) } } - public class InternalMessageConsumer : IConsumer + public class InternalMessageConsumer(UnitOfWork unitOfWork, List store) : IConsumer, IConsumerWithContext { - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public InternalMessageConsumer(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } + public IConsumerContext Context { get; set; } public Task OnHandle(InternalMessage message) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessageConsumer))); + store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessageConsumer), GetMessageBusTarget(Context))); } // some processing - return Task.CompletedTask; } } @@ -222,129 +222,92 @@ public record ExternalMessage(Guid CustomerId); public record InternalMessage(Guid CustomerId); - public class InternalMessageProducerInterceptor : IProducerInterceptor + public class InternalMessageProducerInterceptor(UnitOfWork unitOfWork, List store) : IProducerInterceptor { - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public InternalMessageProducerInterceptor(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } - public Task OnHandle(InternalMessage message, Func> next, IProducerContext context) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessageProducerInterceptor))); + store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessageProducerInterceptor), GetMessageBusTarget(context))); } return next(); } } - public class InternalMessagePublishInterceptor : IPublishInterceptor + private static Type GetMessageBusTarget(IConsumerContext context) { - private readonly UnitOfWork unitOfWork; - private readonly List store; + var messageBusTarget = context.Bus is IMessageBusTarget busTarget + ? busTarget.Target + : null; - public InternalMessagePublishInterceptor(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } + return messageBusTarget?.GetType(); + } + + private static Type GetMessageBusTarget(IProducerContext context) + { + var messageBusTarget = context.Bus is IMessageBusTarget busTarget + ? busTarget.Target + : null; + return messageBusTarget?.GetType(); + } + + public class InternalMessagePublishInterceptor(UnitOfWork unitOfWork, List store) : IPublishInterceptor + { public Task OnHandle(InternalMessage message, Func next, IProducerContext context) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessagePublishInterceptor))); + store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessagePublishInterceptor), GetMessageBusTarget(context))); } return next(); } } - public class ExternalMessageProducerInterceptor : IProducerInterceptor + public class ExternalMessageProducerInterceptor(UnitOfWork unitOfWork, List store) : IProducerInterceptor { - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public ExternalMessageProducerInterceptor(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } - public Task OnHandle(ExternalMessage message, Func> next, IProducerContext context) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessageProducerInterceptor))); + store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessageProducerInterceptor), GetMessageBusTarget(context))); } - return next(); } } - public class ExternalMessagePublishInterceptor : IPublishInterceptor + public class ExternalMessagePublishInterceptor(UnitOfWork unitOfWork, List store) : IPublishInterceptor { - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public ExternalMessagePublishInterceptor(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } - public Task OnHandle(ExternalMessage message, Func next, IProducerContext context) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessagePublishInterceptor))); + store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessagePublishInterceptor), GetMessageBusTarget(context))); } return next(); } } - public class InternalMessageConsumerInterceptor : IConsumerInterceptor + public class InternalMessageConsumerInterceptor(UnitOfWork unitOfWork, List store) : IConsumerInterceptor { - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public InternalMessageConsumerInterceptor(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } - public Task OnHandle(InternalMessage message, Func> next, IConsumerContext context) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessageConsumerInterceptor))); + store.Add(new(unitOfWork.CorrelationId, nameof(InternalMessageConsumerInterceptor), GetMessageBusTarget(context))); } return next(); } } - public class ExternalMessageConsumerInterceptor : IConsumerInterceptor + public class ExternalMessageConsumerInterceptor(UnitOfWork unitOfWork, List store) : IConsumerInterceptor { - private readonly UnitOfWork unitOfWork; - private readonly List store; - - public ExternalMessageConsumerInterceptor(UnitOfWork unitOfWork, List store) - { - this.unitOfWork = unitOfWork; - this.store = store; - } - public Task OnHandle(ExternalMessage message, Func> next, IConsumerContext context) { lock (store) { - store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessageConsumerInterceptor))); + store.Add(new(unitOfWork.CorrelationId, nameof(ExternalMessageConsumerInterceptor), GetMessageBusTarget(context))); } return next(); } diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/SlimMessageBus.Host.Integration.Test.csproj b/src/Tests/SlimMessageBus.Host.Integration.Test/SlimMessageBus.Host.Integration.Test.csproj index db1d9286..dab44e8b 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/SlimMessageBus.Host.Integration.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/SlimMessageBus.Host.Integration.Test.csproj @@ -3,26 +3,12 @@ - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + - @@ -31,9 +17,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Integration.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Integration.Test/appsettings.json index cf18690e..da791c4b 100644 --- a/src/Tests/SlimMessageBus.Host.Integration.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.Integration.Test/appsettings.json @@ -1,5 +1,14 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "Azure": { "ServiceBus": "Endpoint=sb://slimmessagebus-2.servicebus.windows.net/;SharedAccessKeyName=application;SharedAccessKey={{azure_servicebus_key}}" - } + } } diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs index 9a3449e9..56cfff8f 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/KafkaMessageBusIt.cs @@ -229,16 +229,11 @@ private class PingMessage record struct ConsumedMessage(PingMessage Message, int Partition); - private class PingConsumer : IConsumer, IConsumerWithContext + private class PingConsumer(ILogger logger, TestEventCollector messages) + : IConsumer, IConsumerWithContext { - private readonly ILogger _logger; - private readonly TestEventCollector _messages; - - public PingConsumer(ILogger logger, TestEventCollector messages) - { - _logger = logger; - _messages = messages; - } + private readonly ILogger _logger = logger; + private readonly TestEventCollector _messages = messages; public IConsumerContext Context { get; set; } diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/SlimMessageBus.Host.Kafka.Test.csproj b/src/Tests/SlimMessageBus.Host.Kafka.Test/SlimMessageBus.Host.Kafka.Test.csproj index 1888692f..83dccebd 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/SlimMessageBus.Host.Kafka.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/SlimMessageBus.Host.Kafka.Test.csproj @@ -3,25 +3,12 @@ - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - + - + - @@ -34,11 +21,6 @@ PreserveNewest - - - - - - + diff --git a/src/Tests/SlimMessageBus.Host.Kafka.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Kafka.Test/appsettings.json index f4354676..c60b16c5 100644 --- a/src/Tests/SlimMessageBus.Host.Kafka.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.Kafka.Test/appsettings.json @@ -1,4 +1,13 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "Kafka": { "Brokers_": "localhost:9092", "Brokers": "moped.srvs.cloudkafka.com:9094", diff --git a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj index be607026..0e7f1dd3 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj +++ b/src/Tests/SlimMessageBus.Host.Memory.Benchmark/SlimMessageBus.Host.Memory.Benchmark.csproj @@ -4,6 +4,7 @@ Exe + SlimMessageBus.Host.Memory.Benchmark.Program @@ -15,9 +16,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs index aa3d0b17..b205cf75 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/MemoryMessageBusTests.cs @@ -99,7 +99,7 @@ public async Task When_Publish_Given_MessageSerializationSetting_Then_DeliversMe var m = new SomeMessageA(Guid.NewGuid()); // act - await _subject.Value.Publish(m); + await _subject.Value.ProducePublish(m); // assert if (enableMessageSerialization) @@ -148,7 +148,7 @@ public async Task When_Publish_Given_PerMessageScopeEnabled_Then_TheScopeIsCreat _providerSettings.EnableMessageSerialization = false; // act - await _subject.Value.Publish(m); + await _subject.Value.ProducePublish(m); // assert _serviceProviderMock.ScopeFactoryMock.Verify(x => x.CreateScope(), Times.Once); @@ -196,7 +196,7 @@ public async Task When_Publish_Given_PerMessageScopeDisabled_Then_TheScopeIsNotC var m = new SomeMessageA(Guid.NewGuid()); // act - await _subject.Value.Publish(m); + await _subject.Value.ProducePublish(m); // assert _serviceProviderMock.ProviderMock.Verify(x => x.GetService(typeof(ILoggerFactory)), Times.Once); @@ -216,8 +216,8 @@ public async Task When_Publish_Given_PerMessageScopeDisabled_Then_TheScopeIsNotC } [Theory] - [InlineData(new object[] { true })] - [InlineData(new object[] { false })] + [InlineData([true])] + [InlineData([false])] public async Task When_ProducePublish_Given_PerMessageScopeDisabledOrEnabled_And_OutterBusCreatedMesssageScope_Then_TheScopeIsNotCreated_And_ConsumerObtainedFromCurrentMessageScope(bool isMessageScopeEnabled) { // arrange @@ -248,7 +248,7 @@ public async Task When_ProducePublish_Given_PerMessageScopeDisabledOrEnabled_And MessageScope.Current = null; // act - await _subject.Value.ProducePublish(m, path: null, headers: null, currentServiceProvider: currentServiceProviderMock.ProviderMock.Object, default); + await _subject.Value.ProducePublish(m, path: null, headers: null, new MessageBusProxy(_subject.Value, currentServiceProviderMock.ProviderMock.Object), default); // assert @@ -302,7 +302,7 @@ public async Task When_Publish_Given_TwoConsumersOnSameTopic_Then_BothAreInvoked var m = new SomeMessageA(Guid.NewGuid()); // act - await _subject.Value.Publish(m); + await _subject.Value.ProducePublish(m); // assert @@ -348,7 +348,7 @@ public async Task When_Send_Given_AConsumersAndHandlerOnSameTopic_Then_BothAreIn _builder.Handle(x => x.Topic(topic).WithHandler()); // act - var response = await _subject.Value.Send(m); + var response = await _subject.Value.ProduceSend(m); // assert response.Should().NotBeNull(); @@ -409,7 +409,7 @@ public async Task When_Publish_Given_AConsumersThatThrowsException_Then_Exceptio _builder.Consume(x => x.Topic(topic)); // act - var act = () => _subject.Value.Publish(m); + var act = () => _subject.Value.ProducePublish(m); // assert if (errorHandlerRegistered && errorHandlerHandlesError) @@ -457,7 +457,7 @@ public async Task When_Send_Given_AHandlerThatThrowsException_Then_ExceptionIsBu _builder.Handle(x => x.Topic(topic)); // act - var act = () => _subject.Value.Send(m); + var act = () => _subject.Value.ProduceSend(m); // assert if (errorHandlerRegistered && errorHandlerHandlesError) @@ -480,6 +480,7 @@ public class SomeMessageAConsumer : IConsumer, IDisposable public virtual void Dispose() { // Needed to check disposing + GC.SuppressFinalize(this); } public virtual Task OnHandle(SomeMessageA messageA) => Task.CompletedTask; diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/SlimMessageBus.Host.Memory.Test.csproj b/src/Tests/SlimMessageBus.Host.Memory.Test/SlimMessageBus.Host.Memory.Test.csproj index ad4f0674..6fb9d793 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/SlimMessageBus.Host.Memory.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/SlimMessageBus.Host.Memory.Test.csproj @@ -3,20 +3,10 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - - - + - @@ -25,9 +15,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Memory.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Memory.Test/appsettings.json index 0f530c14..4893337d 100644 --- a/src/Tests/SlimMessageBus.Host.Memory.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.Memory.Test/appsettings.json @@ -1,2 +1,11 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + } } diff --git a/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs index 3887fdfb..8188de2e 100644 --- a/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Mqtt.Test/MqttMessageBusIt.cs @@ -178,16 +178,11 @@ private async Task WaitUntilConnected() private record PingMessage(int Counter, Guid Value); - private class PingConsumer : IConsumer, IConsumerWithContext + private class PingConsumer(ILogger logger, TestEventCollector messages) + : IConsumer, IConsumerWithContext { - private readonly ILogger _logger; - private readonly TestEventCollector _messages; - - public PingConsumer(ILogger logger, TestEventCollector messages) - { - _logger = logger; - _messages = messages; - } + private readonly ILogger _logger = logger; + private readonly TestEventCollector _messages = messages; public IConsumerContext Context { get; set; } diff --git a/src/Tests/SlimMessageBus.Host.Mqtt.Test/SlimMessageBus.Host.Mqtt.Test.csproj b/src/Tests/SlimMessageBus.Host.Mqtt.Test/SlimMessageBus.Host.Mqtt.Test.csproj index c78eb1c4..c38bd7ba 100644 --- a/src/Tests/SlimMessageBus.Host.Mqtt.Test/SlimMessageBus.Host.Mqtt.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Mqtt.Test/SlimMessageBus.Host.Mqtt.Test.csproj @@ -3,20 +3,10 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + - @@ -25,9 +15,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Mqtt.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Mqtt.Test/appsettings.json index 18390aa1..1dfefb19 100644 --- a/src/Tests/SlimMessageBus.Host.Mqtt.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.Mqtt.Test/appsettings.json @@ -1,4 +1,13 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "Mqtt": { "Server": "55b1ca64de1f4929ac020eb3045170ae.s2.eu.hivemq.cloud", "Port": "8883", diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs index 00517b96..9f099c1e 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs +++ b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxTests.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using SecretStore; @@ -20,19 +21,15 @@ using SlimMessageBus.Host.Test.Common.IntegrationTest; [Trait("Category", "Integration")] -public class OutboxTests : BaseIntegrationTest +public class OutboxTests(ITestOutputHelper testOutputHelper) : BaseIntegrationTest(testOutputHelper) { private TransactionType _testParamTransactionType; private BusType _testParamBusType; - public OutboxTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - } - public enum TransactionType { SqlTransaction, - TrnasactionScope + TarnsactionScope } public enum BusType @@ -54,7 +51,7 @@ protected override void SetupServices(ServiceCollection services, IConfiguration { mbb.UseSqlTransaction(); // Consumers/Handlers will be wrapped in a SqlTransaction } - if (_testParamTransactionType == TransactionType.TrnasactionScope) + if (_testParamTransactionType == TransactionType.TarnsactionScope) { mbb.UseTransactionScope(); // Consumers/Handlers will be wrapped in a TransactionScope } @@ -98,7 +95,8 @@ protected override void SetupServices(ServiceCollection services, IConfiguration topic = "tests.outbox/customer-events"; } - mbb.Produce(x => x.DefaultTopic(topic)) + mbb + .Produce(x => x.DefaultTopic(topic)) .Consume(x => x .Topic(topic) .WithConsumer() @@ -114,7 +112,7 @@ protected override void SetupServices(ServiceCollection services, IConfiguration opts.PollIdleSleep = TimeSpan.FromSeconds(0.5); opts.MessageCleanup.Interval = TimeSpan.FromSeconds(10); opts.MessageCleanup.Age = TimeSpan.FromMinutes(1); - opts.DatabaseTableName = "IntTest_Outbox"; + opts.SqlSettings.DatabaseTableName = "IntTest_Outbox"; }); }); @@ -134,9 +132,9 @@ private async Task PerformDbOperation(Func action) public const string InvalidLastname = "Exception"; [Theory] - [InlineData(new object[] { TransactionType.SqlTransaction, BusType.AzureSB })] - [InlineData(new object[] { TransactionType.TrnasactionScope, BusType.AzureSB })] - [InlineData(new object[] { TransactionType.SqlTransaction, BusType.Kafka })] + [InlineData([TransactionType.SqlTransaction, BusType.AzureSB])] + [InlineData([TransactionType.TarnsactionScope, BusType.AzureSB])] + [InlineData([TransactionType.SqlTransaction, BusType.Kafka])] public async Task Given_CommandHandlerInTransaction_When_ExceptionThrownDuringHandlingRaisedAtTheEnd_Then_TransactionIsRolledBack_And_NoDataSaved_And_NoEventRaised(TransactionType transactionType, BusType busType) { // arrange @@ -180,8 +178,9 @@ await PerformDbOperation(async context => { var res = await bus.Send(cmd); } - catch + catch (Exception ex) { + Logger.LogInformation("Exception occured while handling cmd {Command}: {Message}", cmd, ex.Message); } } } @@ -217,7 +216,7 @@ private static void AddKafkaSsl(string username, string password, ClientConfig c public record CreateCustomerCommand(string Firstname, string Lastname) : IRequest; -public record CreateCustomerCommandHandler(IMessageBus Bus, CustomerContext CustomerContext) : IRequestHandler +public class CreateCustomerCommandHandler(IMessageBus Bus, CustomerContext CustomerContext) : IRequestHandler { public async Task OnHandle(CreateCustomerCommand request) { @@ -231,7 +230,7 @@ public async Task OnHandle(CreateCustomerCommand request) await Bus.Publish(new CustomerCreatedEvent(customer.Id, customer.Firstname, customer.Lastname), headers: new Dictionary { ["CustomerId"] = customer.Id }); // Simulate some variable processing time - await Task.Delay(Random.Shared.Next(10, 500)); + await Task.Delay(Random.Shared.Next(10, 250)); if (request.Lastname == OutboxTests.InvalidLastname) { @@ -245,7 +244,7 @@ public async Task OnHandle(CreateCustomerCommand request) public record CustomerCreatedEvent(Guid Id, string Firstname, string Lastname); -public record CustomerCreatedEventConsumer(TestEventCollector Store) : IConsumer, IConsumerWithContext +public class CustomerCreatedEventConsumer(TestEventCollector Store) : IConsumer, IConsumerWithContext { public IConsumerContext Context { get; set; } @@ -257,7 +256,6 @@ public Task OnHandle(CustomerCreatedEvent message) { Store.Add(message); } - } return Task.CompletedTask; } diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj index 5e4a8d67..d723074e 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/SlimMessageBus.Host.Outbox.DbContext.Test.csproj @@ -3,23 +3,7 @@ - - Always - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,15 +11,18 @@ + - + + Always + Always true @@ -43,9 +30,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/appsettings.json index ee68bd3d..42028e4b 100644 --- a/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/appsettings.json @@ -1,10 +1,13 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "MinimumLevel": { "Default": "Information", - "SlimMessageBus": "Information", - "SlimMessageBus.Host.Outbox": "Trace", - "Microsoft": "Warning" + "Override": { + "SlimMessageBus": "Information", + "SlimMessageBus.Host.Outbox": "Debug", + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } } }, "Kafka": { diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs index 6b48bce0..279a0cef 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/IntegrationTests/RabbitMqMessageBusIt.cs @@ -272,7 +272,7 @@ public class PingConsumer : IConsumer, IConsumerWithContext private readonly ILogger _logger; private readonly TestEventCollector _messages; - public PingConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) + public PingConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) { _logger = logger; _messages = messages; @@ -298,7 +298,7 @@ public class PingDerivedConsumer : IConsumer, IConsumerWithC private readonly ILogger _logger; private readonly TestEventCollector _messages; - public PingDerivedConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) + public PingDerivedConsumer(ILogger logger, TestEventCollector messages, TestMetric testMetric) { _logger = logger; _messages = messages; diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/SlimMessageBus.Host.RabbitMQ.Test.csproj b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/SlimMessageBus.Host.RabbitMQ.Test.csproj index 5c2143fc..2fbdaa81 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/SlimMessageBus.Host.RabbitMQ.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/SlimMessageBus.Host.RabbitMQ.Test.csproj @@ -2,16 +2,6 @@ - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - @@ -29,9 +19,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/appsettings.json index 8150c8a6..cf9c08d5 100644 --- a/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.RabbitMQ.Test/appsettings.json @@ -1,6 +1,15 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "RabbitMQ": { "ConnectionString": "amqps://ljvttaao:{{rabbitmq_password}}@goose.rmq2.cloudamqp.com/ljvttaao" - } + } } diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs index 597851db..d750adde 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusIt.cs @@ -241,16 +241,11 @@ private async Task BasicReqResp() private record PingMessage(int Counter, Guid Value); - private class PingConsumer : IConsumer, IConsumerWithContext + private class PingConsumer(ILogger logger, TestEventCollector messages) + : IConsumer, IConsumerWithContext { - private readonly ILogger _logger; - private readonly TestEventCollector _messages; - - public PingConsumer(ILogger logger, TestEventCollector messages) - { - _logger = logger; - _messages = messages; - } + private readonly ILogger _logger = logger; + private readonly TestEventCollector _messages = messages; public IConsumerContext Context { get; set; } diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs index b5f518f7..d3faafc9 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/RedisMessageBusTest.cs @@ -86,8 +86,8 @@ public async Task When_Publish_Given_QueueAndTopic_Then_RoutesToRespectiveChanne var payloadB = _messageSerializerMock.Object.Serialize(typeof(SomeMessageB), mB); // act - await _subject.Value.Publish(mA); - await _subject.Value.Publish(mB); + await _subject.Value.ProducePublish(mA); + await _subject.Value.ProducePublish(mB); // assert _databaseMock.Verify(x => x.PublishAsync(It.Is(channel => channel == topicA), It.Is(x => StructuralComparisons.StructuralEqualityComparer.Equals(UnwrapPayload(x).Payload, payloadA)), It.IsAny()), Times.Once); @@ -120,7 +120,7 @@ protected bool Equals(SomeMessageA other) public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((SomeMessageA)obj); @@ -147,7 +147,7 @@ protected bool Equals(SomeMessageB other) public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((SomeMessageB)obj); @@ -166,6 +166,7 @@ public class SomeMessageAConsumer : IConsumer, IDisposable public virtual void Dispose() { // Needed to check disposing + GC.SuppressFinalize(this); } #region Implementation of IConsumer diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/SlimMessageBus.Host.Redis.Test.csproj b/src/Tests/SlimMessageBus.Host.Redis.Test/SlimMessageBus.Host.Redis.Test.csproj index f553d6d9..1fd74cdd 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/SlimMessageBus.Host.Redis.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/SlimMessageBus.Host.Redis.Test.csproj @@ -2,16 +2,6 @@ - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - @@ -25,9 +15,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Redis.Test/appsettings.json b/src/Tests/SlimMessageBus.Host.Redis.Test/appsettings.json index 891ca603..3213a29d 100644 --- a/src/Tests/SlimMessageBus.Host.Redis.Test/appsettings.json +++ b/src/Tests/SlimMessageBus.Host.Redis.Test/appsettings.json @@ -1,5 +1,14 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "SlimMessageBus": "Information", + "Microsoft": "Warning" + } + } + }, "Redis": { "ConnectionString": "redis-16821.c56.east-us.azure.cloud.redislabs.com:16821,password={{redis_password}}" - } + } } diff --git a/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj b/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj index 3e675f38..6deddd01 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj +++ b/src/Tests/SlimMessageBus.Host.Serialization.Benchmark/SlimMessageBus.Host.Serialization.Benchmark.csproj @@ -4,6 +4,7 @@ Exe + SlimMessageBus.Host.Serialization.Benchmark.Program @@ -17,9 +18,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj b/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj index d8be059e..4032e98a 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test/SlimMessageBus.Host.Serialization.GoogleProtobuf.Test.csproj @@ -2,34 +2,22 @@ - + - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - all - runtime; build; native; contentfiles; analyzers - - - + - - - + + + - - - - - - - - + + + diff --git a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SlimMessageBus.Host.Serialization.Json.Test.csproj b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SlimMessageBus.Host.Serialization.Json.Test.csproj index 5711c17d..990f80f0 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SlimMessageBus.Host.Serialization.Json.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Serialization.Json.Test/SlimMessageBus.Host.Serialization.Json.Test.csproj @@ -2,23 +2,8 @@ - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SlimMessageBus.Host.Serialization.SystemTextJson.Test.csproj b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SlimMessageBus.Host.Serialization.SystemTextJson.Test.csproj index 9c17a876..64416abe 100644 --- a/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SlimMessageBus.Host.Serialization.SystemTextJson.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/SlimMessageBus.Host.Serialization.SystemTextJson.Test.csproj @@ -2,23 +2,8 @@ - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs index 9701c456..00435556 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/IntegrationTest/BaseIntegrationTest.cs @@ -4,7 +4,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +using Serilog; +using Serilog.Extensions.Logging; using SlimMessageBus.Host; @@ -20,31 +23,37 @@ /// public abstract class BaseIntegrationTest : IAsyncLifetime { - private Lazy _serviceProvider; + private readonly Lazy _serviceProvider; private Action messageBusBuilderAction = (mbb) => { }; - protected ILoggerFactory LoggerFactory { get; } - protected ILogger Logger { get; } + private ILogger? _logger; + protected ILogger Logger => _logger ??= ServiceProvider.GetRequiredService>(); + protected IConfigurationRoot Configuration { get; } protected ServiceProvider ServiceProvider => _serviceProvider.Value; protected BaseIntegrationTest(ITestOutputHelper testOutputHelper) { - LoggerFactory = new XunitLoggerFactory(testOutputHelper); - Logger = LoggerFactory.CreateLogger(); + // Creating a `LoggerProviderCollection` lets Serilog optionally write + // events through other dynamically-added MEL ILoggerProviders. + var providers = new LoggerProviderCollection(); Configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + Log.Logger = new LoggerConfiguration() + //.WriteTo.Providers(providers) + .WriteTo.TestOutput(testOutputHelper, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}") + .ReadFrom.Configuration(Configuration) + .CreateLogger(); + Secrets.Load(@"..\..\..\..\..\secrets.txt"); _serviceProvider = new Lazy(() => { var services = new ServiceCollection(); - services.AddSingleton(Configuration); - services.AddSingleton(LoggerFactory); - services.Add(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(XunitLogger<>))); - services.Add(ServiceDescriptor.Singleton(typeof(ILogger), typeof(XunitLogger))); + services.AddSingleton(Configuration); + services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); services.AddSingleton(); diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj b/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj index 2a78c393..ec164621 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj +++ b/src/Tests/SlimMessageBus.Host.Test.Common/SlimMessageBus.Host.Test.Common.csproj @@ -9,9 +9,11 @@ - + + + @@ -19,9 +21,4 @@ - - - - - diff --git a/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs b/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs index dce980a2..40729f8b 100644 --- a/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs +++ b/src/Tests/SlimMessageBus.Host.Test.Common/XunitLoggerFactory.cs @@ -1,13 +1,11 @@ namespace SlimMessageBus.Host.Test.Common; -public class XunitLoggerFactory : ILoggerFactory +public class XunitLoggerFactory(ITestOutputHelper output) : ILoggerFactory { - private readonly ITestOutputHelper _output; + private readonly ITestOutputHelper _output = output; public ITestOutputHelper Output => _output; - public XunitLoggerFactory(ITestOutputHelper output) => _output = output; - public void AddProvider(ILoggerProvider provider) { } diff --git a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs index 02c87c99..b8f68ad7 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Consumer/MessageBusMock.cs @@ -74,6 +74,7 @@ void SetupDependencyResolver(Mock mock) where T : class, IServiceProvider BusMock = new Mock(mbSettings); BusMock.SetupGet(x => x.Settings).Returns(mbSettings); BusMock.SetupGet(x => x.Serializer).CallBase(); + BusMock.SetupGet(x => x.MessageBusTarget).CallBase(); BusMock.SetupGet(x => x.CurrentTime).Returns(() => CurrentTime); BusMock.Setup(x => x.CreateHeaders()).CallBase(); BusMock.Setup(x => x.CreateMessageScope(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).CallBase(); diff --git a/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ServiceCollectionExtensionsTest.cs b/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ServiceCollectionExtensionsTest.cs index 9935a72c..748fc8d8 100644 --- a/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ServiceCollectionExtensionsTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/DependencyResolver/ServiceCollectionExtensionsTest.cs @@ -16,7 +16,7 @@ public ServiceCollectionExtensionsTest() public void When_AddSlimMessageBus_Given_ModularizedSettingsWhichAddTheSameChildBus_Then_ChildBusSettingIsContinued() { // arrange - var mockBus = new Mock(); + var mockBus = new Mock(); _services.AddSlimMessageBus(mbb => { @@ -54,7 +54,7 @@ public void When_AddSlimMessageBus_Given_ModularizedSettingsWhichAddTheSameChild public void When_AddSlimMessageBus_Given_ModularizedSettings_Then_AllOfTheModulesAreConcatenated() { // arrange - var mockBus = new Mock(); + var mockBus = new Mock(); _services.AddSlimMessageBus(); diff --git a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs index 852f1e94..5ddf3fe9 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Hybrid/HybridMessageBusTest.cs @@ -50,8 +50,8 @@ public HybridMessageBusTest() _bus1Mock = new Mock(new[] { mbs }); _bus1Mock.SetupGet(x => x.Settings).Returns(mbs); - _bus1Mock.Setup(x => x.Publish(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).Returns(Task.CompletedTask); - _bus1Mock.Setup(x => x.Publish(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).Returns(Task.CompletedTask); + _bus1Mock.Setup(x => x.ProducePublish(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + _bus1Mock.Setup(x => x.ProducePublish(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); return _bus1Mock.Object; }); @@ -65,8 +65,8 @@ public HybridMessageBusTest() _bus2Mock = new Mock(new[] { mbs }); _bus2Mock.SetupGet(x => x.Settings).Returns(mbs); - _bus2Mock.Setup(x => x.Publish(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).Returns(Task.CompletedTask); - _bus2Mock.Setup(x => x.Send(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), default)).Returns(Task.FromResult(new SomeResponse())); + _bus2Mock.Setup(x => x.ProducePublish(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + _bus2Mock.Setup(x => x.ProduceSend(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), default)).Returns(Task.FromResult(new SomeResponse())); return _bus2Mock.Object; }); @@ -160,7 +160,7 @@ public async Task Given_UndeclareMessageType_When_Publish_Then_FollowsSettingsMo var message = new object(); // act - Func act = () => _subject.Value.Publish(message); + Func act = () => _subject.Value.ProducePublish(message); // assert if (mode == UndeclaredMessageTypeMode.RaiseException) @@ -198,7 +198,7 @@ public async Task Given_UndeclaredRequestType_When_Send_Then_FollowsSettingsMode var message = new SomeUndeclaredRequest(); // act - Func> act = () => _subject.Value.Send(message); + Func> act = () => _subject.Value.ProduceSend(message); // assert if (mode == UndeclaredMessageTypeMode.RaiseException) @@ -237,7 +237,7 @@ public async Task Given_UndeclaredRequestTypeWithoutResponse_When_Send_Then_Foll var message = new SomeUndeclaredRequestWithoutResponse(); // act - Func act = () => _subject.Value.Send(message); + Func act = () => _subject.Value.ProduceSend(message); // assert if (mode == UndeclaredMessageTypeMode.RaiseException) @@ -270,16 +270,16 @@ public async Task Given_DeclaredMessageTypeWithTwoBuses_When_Publish_Then_Routes var someMessage = new SomeMessage(); // act - await _subject.Value.Publish(someMessage); + await _subject.Value.ProducePublish(someMessage); // assert _bus1Mock.VerifyGet(x => x.Settings); - _bus1Mock.Verify(x => x.Publish(someMessage, null, null, It.IsAny())); + _bus1Mock.Verify(x => x.ProducePublish(someMessage, null, null, null, It.IsAny())); _bus1Mock.VerifyNoOtherCalls(); _bus2Mock.VerifyGet(x => x.Settings); - _bus2Mock.Verify(x => x.Publish(someMessage, null, null, It.IsAny())); + _bus2Mock.Verify(x => x.ProducePublish(someMessage, null, null, null, It.IsAny())); _bus2Mock.VerifyNoOtherCalls(); } @@ -290,12 +290,12 @@ public async Task Given_DeclaredMessageTypeWithOneBus_When_Publish_Then_RoutesTo var anotherMessage = new AnotherMessage(); // act - await _subject.Value.Publish(anotherMessage); + await _subject.Value.ProducePublish(anotherMessage); // assert _bus1Mock.VerifyGet(x => x.Settings); - _bus1Mock.Verify(x => x.Publish(anotherMessage, null, null, It.IsAny())); + _bus1Mock.Verify(x => x.ProducePublish(anotherMessage, null, null, null, It.IsAny())); _bus1Mock.VerifyNoOtherCalls(); _bus2Mock.VerifyGet(x => x.Settings); @@ -311,26 +311,18 @@ public async Task Given_DeclaredMessageTypeAndItsAncestors_When_Publish_Then_Rou var someDerivedOfDerivedMessage = new SomeDerivedOfDerivedMessage(); // act - await _subject.Value.Publish(someMessage); - await _subject.Value.Publish(someDerivedMessage); - await _subject.Value.Publish(someDerivedMessage); - await _subject.Value.Publish(someDerivedMessage); - await _subject.Value.Publish(someDerivedOfDerivedMessage); - await _subject.Value.Publish(someDerivedOfDerivedMessage); - await _subject.Value.Publish(someDerivedOfDerivedMessage); + await _subject.Value.ProducePublish(someMessage); + await _subject.Value.ProducePublish(someDerivedMessage); + await _subject.Value.ProducePublish(someDerivedOfDerivedMessage); // assert // note: Moq does not match exact generic types but with match with assignment compatibility // - cannot count the exact times a specific generic method ws executed // see https://stackoverflow.com/a/54721582 - _bus1Mock.Verify(x => x.Publish(someMessage, null, null, It.IsAny())); - _bus1Mock.Verify(x => x.Publish(someDerivedMessage, null, null, It.IsAny())); - _bus1Mock.Verify(x => x.Publish(someDerivedMessage, null, null, It.IsAny())); - _bus1Mock.Verify(x => x.Publish(someDerivedMessage, null, null, It.IsAny())); - _bus1Mock.Verify(x => x.Publish(someDerivedOfDerivedMessage, null, null, It.IsAny())); - _bus1Mock.Verify(x => x.Publish(someDerivedOfDerivedMessage, null, null, It.IsAny())); - _bus1Mock.Verify(x => x.Publish(someDerivedOfDerivedMessage, null, null, It.IsAny())); + _bus1Mock.Verify(x => x.ProducePublish(someMessage, null, null, null, It.IsAny())); + _bus1Mock.Verify(x => x.ProducePublish(someDerivedMessage, null, null, null, It.IsAny())); + _bus1Mock.Verify(x => x.ProducePublish(someDerivedOfDerivedMessage, null, null, null, It.IsAny())); _bus1Mock.VerifyGet(x => x.Settings); _bus1Mock.VerifyNoOtherCalls(); } @@ -343,12 +335,12 @@ public async Task Given_DeclaredRequestMessageTypeAndItsAncestors_When_Send_Then var someDerivedRequest = new SomeDerivedRequest(); // act - await _subject.Value.Send(someRequest); - await _subject.Value.Send(someDerivedRequest); + await _subject.Value.ProduceSend(someRequest); + await _subject.Value.ProduceSend(someDerivedRequest); // assert - _bus2Mock.Verify(x => x.Send(someRequest, null, null, null, default), Times.Once); - _bus2Mock.Verify(x => x.Send(someDerivedRequest, null, null, null, default), Times.Once); + _bus2Mock.Verify(x => x.ProduceSend(someRequest, null, null, null, null, default), Times.Once); + _bus2Mock.Verify(x => x.ProduceSend(someDerivedRequest, null, null, null, null, default), Times.Once); } [Fact] diff --git a/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs b/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs index dc2deeba..1db90b8c 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Interceptors/PublishInterceptorPipelineTests.cs @@ -1,16 +1,16 @@ namespace SlimMessageBus.Host.Test; - -using SlimMessageBus.Host.Interceptor; - -public class PublishInterceptorPipelineTests -{ - private readonly MessageBusMock _busMock; - - public PublishInterceptorPipelineTests() - { - _busMock = new MessageBusMock(); - } - + +using SlimMessageBus.Host.Interceptor; + +public class PublishInterceptorPipelineTests +{ + private readonly MessageBusMock _busMock; + + public PublishInterceptorPipelineTests() + { + _busMock = new MessageBusMock(); + } + [Theory] [InlineData(false, false)] [InlineData(true, false)] @@ -19,8 +19,8 @@ public async Task When_Next_Then_InterceptorIsCalledAndTargetIsCalled(bool produ { // arrange var message = new SomeMessage(); - var topic = "topic1"; - + var topic = "topic1"; + var producerInterceptorMock = new Mock>(); producerInterceptorMock .Setup(x => x.OnHandle(message, It.IsAny>>(), It.IsAny())) @@ -31,44 +31,44 @@ public async Task When_Next_Then_InterceptorIsCalledAndTargetIsCalled(bool produ var publishInterceptorMock = new Mock>(); publishInterceptorMock .Setup(x => x.OnHandle(message, It.IsAny>(), It.IsAny())) - .Returns((SomeMessage message, Func next, IProducerContext context) => next()); - - var publishInterceptors = publishInterceptorRegisterd ? new[] { publishInterceptorMock.Object } : null; - - var producerSettings = new ProducerBuilder(new ProducerSettings()) - .DefaultTopic(topic) - .Settings; - - var context = new PublishContext - { - Path = topic, - Headers = new Dictionary(), - }; - - _busMock.BusMock - .Setup(x => x.PublishInternal(message, context.Path, context.Headers, context.CancellationToken, producerSettings, _busMock.ServiceProviderMock.Object)) - .Returns(() => Task.FromResult(null)); - - var subject = new PublishInterceptorPipeline(_busMock.Bus, message, producerSettings, _busMock.ServiceProviderMock.Object, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); - - // act - var result = await subject.Next(); + .Returns((SomeMessage message, Func next, IProducerContext context) => next()); + + var publishInterceptors = publishInterceptorRegisterd ? new[] { publishInterceptorMock.Object } : null; + + var producerSettings = new ProducerBuilder(new ProducerSettings()) + .DefaultTopic(topic) + .Settings; + + var context = new PublishContext + { + Path = topic, + Headers = new Dictionary(), + }; + + _busMock.BusMock + .Setup(x => x.PublishInternal(message, context.Path, context.Headers, context.CancellationToken, producerSettings, _busMock.Bus.MessageBusTarget)) + .Returns(() => Task.FromResult(null)); + + var subject = new PublishInterceptorPipeline(_busMock.Bus, message, producerSettings, _busMock.Bus.MessageBusTarget, context, producerInterceptors: producerInterceptors, publishInterceptors: publishInterceptors); + + // act + var result = await subject.Next(); // assert - result.Should().BeNull(); - - if (producerInterceptorRegisterd) - { - producerInterceptorMock.Verify(x => x.OnHandle(message, It.IsAny>>(), It.IsAny()), Times.Once); + result.Should().BeNull(); + + if (producerInterceptorRegisterd) + { + producerInterceptorMock.Verify(x => x.OnHandle(message, It.IsAny>>(), It.IsAny()), Times.Once); } - producerInterceptorMock.VerifyNoOtherCalls(); - - if (publishInterceptorRegisterd) - { - publishInterceptorMock.Verify(x => x.OnHandle(message, It.IsAny>(), It.IsAny()), Times.Once); + producerInterceptorMock.VerifyNoOtherCalls(); + + if (publishInterceptorRegisterd) + { + publishInterceptorMock.Verify(x => x.OnHandle(message, It.IsAny>(), It.IsAny()), Times.Once); } - publishInterceptorMock.VerifyNoOtherCalls(); - - _busMock.BusMock.Verify(x => x.PublishInternal(message, context.Path, context.Headers, context.CancellationToken, producerSettings, _busMock.ServiceProviderMock.Object), Times.Once); - } + publishInterceptorMock.VerifyNoOtherCalls(); + + _busMock.BusMock.Verify(x => x.PublishInternal(message, context.Path, context.Headers, context.CancellationToken, producerSettings, _busMock.Bus.MessageBusTarget), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/SlimMessageBus.Host.Test/Interceptors/SendInterceptorPipelineTests.cs b/src/Tests/SlimMessageBus.Host.Test/Interceptors/SendInterceptorPipelineTests.cs index 1656240e..f1c71807 100644 --- a/src/Tests/SlimMessageBus.Host.Test/Interceptors/SendInterceptorPipelineTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/Interceptors/SendInterceptorPipelineTests.cs @@ -1,16 +1,16 @@ namespace SlimMessageBus.Host.Test; - -using SlimMessageBus.Host.Interceptor; - -public class SendInterceptorPipelineTests -{ - private readonly MessageBusMock _busMock; - - public SendInterceptorPipelineTests() - { - _busMock = new MessageBusMock(); - } - + +using SlimMessageBus.Host.Interceptor; + +public class SendInterceptorPipelineTests +{ + private readonly MessageBusMock _busMock; + + public SendInterceptorPipelineTests() + { + _busMock = new MessageBusMock(); + } + [Theory] [InlineData(false, false)] [InlineData(true, false)] @@ -20,8 +20,8 @@ public async Task When_Next_Then_InterceptorIsCalledAndTargetIsCalled(bool produ // arrange var request = new SomeRequest(); var response = new SomeResponse(); - var topic = "topic1"; - + var topic = "topic1"; + var producerInterceptorMock = new Mock>(); producerInterceptorMock .Setup(x => x.OnHandle(request, It.IsAny>>(), It.IsAny())) @@ -32,44 +32,44 @@ public async Task When_Next_Then_InterceptorIsCalledAndTargetIsCalled(bool produ var sendInterceptorMock = new Mock>(); sendInterceptorMock .Setup(x => x.OnHandle(request, It.IsAny>>(), It.IsAny())) - .Returns((SomeRequest message, Func> next, IProducerContext context) => next()); - - var sendInterceptors = sendInterceptorRegisterd ? new[] { sendInterceptorMock.Object } : null; - - var producerSettings = new ProducerBuilder(new ProducerSettings()) - .DefaultTopic(topic) - .Settings; - - var context = new SendContext - { - Path = topic, - Headers = new Dictionary(), - }; - - _busMock.BusMock - .Setup(x => x.SendInternal(request, context.Path, request.GetType(), typeof(SomeResponse), producerSettings, context.Created, context.Expires, context.RequestId, context.Headers, _busMock.ServiceProviderMock.Object, context.CancellationToken)) - .Returns(() => Task.FromResult(response)); - - var subject = new SendInterceptorPipeline(_busMock.Bus, request, producerSettings, _busMock.ServiceProviderMock.Object, context, producerInterceptors: producerInterceptors, sendInterceptors: sendInterceptors); - - // act - var result = await subject.Next(); + .Returns((SomeRequest message, Func> next, IProducerContext context) => next()); + + var sendInterceptors = sendInterceptorRegisterd ? new[] { sendInterceptorMock.Object } : null; + + var producerSettings = new ProducerBuilder(new ProducerSettings()) + .DefaultTopic(topic) + .Settings; + + var context = new SendContext + { + Path = topic, + Headers = new Dictionary(), + }; + + _busMock.BusMock + .Setup(x => x.SendInternal(request, context.Path, request.GetType(), typeof(SomeResponse), producerSettings, context.Created, context.Expires, context.RequestId, context.Headers, _busMock.Bus.MessageBusTarget, context.CancellationToken)) + .Returns(() => Task.FromResult(response)); + + var subject = new SendInterceptorPipeline(_busMock.Bus, request, producerSettings, _busMock.Bus.MessageBusTarget, context, producerInterceptors: producerInterceptors, sendInterceptors: sendInterceptors); + + // act + var result = await subject.Next(); // assert - result.Should().BeSameAs(response); - - if (producerInterceptorRegisterd) - { - producerInterceptorMock.Verify(x => x.OnHandle(request, It.IsAny>>(), It.IsAny()), Times.Once); + result.Should().BeSameAs(response); + + if (producerInterceptorRegisterd) + { + producerInterceptorMock.Verify(x => x.OnHandle(request, It.IsAny>>(), It.IsAny()), Times.Once); } - producerInterceptorMock.VerifyNoOtherCalls(); - - if (sendInterceptorRegisterd) - { - sendInterceptorMock.Verify(x => x.OnHandle(request, It.IsAny>>(), It.IsAny()), Times.Once); + producerInterceptorMock.VerifyNoOtherCalls(); + + if (sendInterceptorRegisterd) + { + sendInterceptorMock.Verify(x => x.OnHandle(request, It.IsAny>>(), It.IsAny()), Times.Once); } - sendInterceptorMock.VerifyNoOtherCalls(); - - _busMock.BusMock.Verify(x => x.SendInternal(request, context.Path, request.GetType(), typeof(SomeResponse), producerSettings, context.Created, context.Expires, context.RequestId, context.Headers, _busMock.ServiceProviderMock.Object, context.CancellationToken), Times.Once); - } -} + sendInterceptorMock.VerifyNoOtherCalls(); + + _busMock.BusMock.Verify(x => x.SendInternal(request, context.Path, request.GetType(), typeof(SomeResponse), producerSettings, context.Created, context.Expires, context.RequestId, context.Headers, _busMock.Bus.MessageBusTarget, context.CancellationToken), Times.Once); + } +} diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs index 9919249e..f15dcac9 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusBaseTests.cs @@ -116,8 +116,8 @@ public async Task When_NoTimeoutProvided_Then_TakesDefaultTimeoutForRequestTypeA var rb = new RequestB(); // act - var raTask = Bus.Send(ra); - var rbTask = Bus.Send(rb); + var raTask = Bus.ProduceSend(ra); + var rbTask = Bus.ProduceSend(rb); // after 10 seconds _timeNow = _timeZero.AddSeconds(TimeoutFor5 + 1); @@ -156,13 +156,14 @@ public async Task When_ResponseArrives_Then_ResolvesPendingRequestAsync() }; // act - var rTask = Bus.Send(r); + var rTask = Bus.ProduceSend(r); await WaitForTasks(2000, rTask); Bus.TriggerPendingRequestCleanup(); // assert rTask.IsCompleted.Should().BeTrue("Response should be completed"); - r.Id.Should().Be(rTask.Result.Id); + var response = await rTask; + response.Id.Should().Be(r.Id); Bus.PendingRequestsCount.Should().Be(0, "There should be no pending requests"); } @@ -190,9 +191,9 @@ public async Task When_ResponseArrivesTooLate_Then_ExpiresPendingRequestAsync() }; // act - var r1Task = Bus.Send(r1); - var r2Task = Bus.Send(r2, timeout: TimeSpan.FromSeconds(1)); - var r3Task = Bus.Send(r3); + var r1Task = Bus.ProduceSend(r1); + var r2Task = Bus.ProduceSend(r2, timeout: TimeSpan.FromSeconds(1)); + var r3Task = Bus.ProduceSend(r3); // 2 seconds later _timeNow = _timeZero.AddSeconds(2); @@ -218,8 +219,8 @@ public async Task When_CancellationTokenCancelled_Then_CancellsPendingRequest() using var cts2 = new CancellationTokenSource(); cts2.Cancel(); - var r1Task = Bus.Send(r1, cancellationToken: cts1.Token); - var r2Task = Bus.Send(r2, cancellationToken: cts2.Token); + var r1Task = Bus.ProduceSend(r1, cancellationToken: cts1.Token); + var r2Task = Bus.ProduceSend(r2, cancellationToken: cts2.Token); // act Bus.TriggerPendingRequestCleanup(); @@ -249,7 +250,7 @@ public async Task When_Produce_DerivedMessage_Given_OnlyBaseMessageConfigured_Th { // arrange var messageSerializerMock = new Mock(); - messageSerializerMock.Setup(x => x.Serialize(It.IsAny(), It.IsAny())).Returns(Array.Empty()); + messageSerializerMock.Setup(x => x.Serialize(It.IsAny(), It.IsAny())).Returns([]); var someMessageTopic = "some-messages"; @@ -263,9 +264,9 @@ public async Task When_Produce_DerivedMessage_Given_OnlyBaseMessageConfigured_Th var m3 = new SomeDerived2Message(); // act - await Bus.Publish(m1); - await Bus.Publish(m2); - await Bus.Publish(m3); + await Bus.ProducePublish(m1); + await Bus.ProducePublish(m2); + await Bus.ProducePublish(m3); // assert _producedMessages.Count.Should().Be(3); @@ -292,7 +293,7 @@ public async Task When_Produce_DerivedMessage_Given_OnlyBaseMessageConfigured_Th var m = new SomeDerivedMessage(); // act - await Bus.Publish(m); + await Bus.ProducePublish(m); // assert _producedMessages.Count.Should().Be(1); @@ -319,20 +320,20 @@ public async Task When_Publish_DerivedMessage_Given_DeriveMessageConfigured_Then if (caseId == 1) { // act - await Bus.Publish(m); + await Bus.ProducePublish(m); } if (caseId == 2) { // act - await Bus.Publish(m); + await Bus.ProducePublish(m); } if (caseId == 3) { // act - await Bus.Publish(m); + await Bus.ProducePublish(m); } // assert @@ -398,8 +399,8 @@ public async Task When_Publish_Given_Disposed_Then_ThrowsException() Bus.Dispose(); // act - Func act = async () => await Bus.Publish(new SomeMessage()); - Func actWithTopic = async () => await Bus.Publish(new SomeMessage(), "some-topic"); + Func act = async () => await Bus.ProducePublish(new SomeMessage()); + Func actWithTopic = async () => await Bus.ProducePublish(new SomeMessage(), "some-topic"); // assert await act.Should().ThrowAsync(); @@ -413,8 +414,8 @@ public async Task When_Send_Given_Disposed_Then_ThrowsException() Bus.Dispose(); // act - Func act = async () => await Bus.Send(new SomeRequest()); - Func actWithTopic = async () => await Bus.Send(new SomeRequest(), "some-topic"); + Func act = async () => await Bus.ProduceSend(new SomeRequest()); + Func actWithTopic = async () => await Bus.ProduceSend(new SomeRequest(), "some-topic"); // assert await act.Should().ThrowAsync(); @@ -465,7 +466,7 @@ public async Task When_Publish_Given_InterceptorsInDI_Then_InterceptorInfluenceI } // act - await Bus.Publish(m); + await Bus.ProducePublish(m); // assert @@ -556,7 +557,7 @@ public async Task When_Send_Given_InterceptorsInDI_Then_InterceptorInfluenceIfTh } // act - var response = await Bus.Send(request); + var response = await Bus.ProduceSend(request); // assert diff --git a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs index ac842641..b0f45386 100644 --- a/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs +++ b/src/Tests/SlimMessageBus.Host.Test/MessageBusTested.cs @@ -35,7 +35,7 @@ protected internal override Task OnStop() return base.OnStop(); } - protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, CancellationToken cancellationToken = default) + protected override async Task ProduceToTransport(object message, string path, byte[] messagePayload, IDictionary messageHeaders, IMessageBusTarget targetBus, CancellationToken cancellationToken = default) { var messageType = message.GetType(); OnProduced(messageType, path, message); diff --git a/src/Tests/SlimMessageBus.Host.Test/SlimMessageBus.Host.Test.csproj b/src/Tests/SlimMessageBus.Host.Test/SlimMessageBus.Host.Test.csproj index eee0ccd9..881dd88c 100644 --- a/src/Tests/SlimMessageBus.Host.Test/SlimMessageBus.Host.Test.csproj +++ b/src/Tests/SlimMessageBus.Host.Test/SlimMessageBus.Host.Test.csproj @@ -2,18 +2,6 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Tests/SlimMessageBus.Test/SlimMessageBus.Test.csproj b/src/Tests/SlimMessageBus.Test/SlimMessageBus.Test.csproj index 21902a85..b9e6a91a 100644 --- a/src/Tests/SlimMessageBus.Test/SlimMessageBus.Test.csproj +++ b/src/Tests/SlimMessageBus.Test/SlimMessageBus.Test.csproj @@ -2,24 +2,9 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - -