From f43b58760b1a5918426c32bb328db6731384e9bb Mon Sep 17 00:00:00 2001
From: alexey-troshkin-xpress <alexey.troshkin@xpress.com.ph>
Date: Fri, 21 Nov 2025 01:46:54 +0300
Subject: [PATCH 1/8] added ability to specify SessionId and ReplyToSessionId
 for the producer, along with support for ServiceBusSessionProcessor for the
 consumer

---
 .../Producers/ServiceBusMessageBuilder.cs     | 10 ++-
 .../Producers/ServiceBusProduceOptions.cs     | 10 +++
 .../Subscriptions/ServiceBusSubscription.cs   | 66 +++++++++++++++++--
 .../ServiceBusSubscriptionOptions.cs          | 41 ++++++++++++
 4 files changed, 122 insertions(+), 5 deletions(-)

diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusMessageBuilder.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusMessageBuilder.cs
index 171d8ba56..0264d9be6 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusMessageBuilder.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusMessageBuilder.cs
@@ -35,9 +35,17 @@ internal ServiceBusMessage CreateServiceBusMessage(ProducedMessage message) {
             TimeToLive = options?.TimeToLive ?? TimeSpan.MaxValue,
             CorrelationId = message.Metadata?.GetCorrelationId(),
             To = metadata?.GetValueOrDefault(attributes.To, options?.To)?.ToString(),
-            ReplyTo = metadata?.GetValueOrDefault(attributes.ReplyTo, options?.ReplyTo)?.ToString()
+            ReplyTo = metadata?.GetValueOrDefault(attributes.ReplyTo, options?.ReplyTo)?.ToString(),
+            ReplyToSessionId = options?.ReplyToSessionId,
         };
 
+        // We set the SessionId only when a value is present because
+        // it overrides the PartitionKey, even if the SessionId is null.
+
+        if (options?.SessionId is not null) {
+            serviceBusMessage.SessionId = options.SessionId;
+        }
+
         var reservedAttributes = attributes.ReservedNames();
 
         foreach (var property in GetCustomApplicationProperties(message, messageType, reservedAttributes)) {
diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusProduceOptions.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusProduceOptions.cs
index f82d8be16..119ea98d5 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusProduceOptions.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Producers/ServiceBusProduceOptions.cs
@@ -22,6 +22,16 @@ public class ServiceBusProduceOptions {
     /// </summary>
     public string? ReplyTo { get; set; }
 
+    /// <summary>
+    /// Session ID to guarantee ordering on session-enabled entities.
+    /// </summary>
+    public string? SessionId { get; init; }
+
+    /// <summary>
+    /// The reply-to session ID attribute name for request-reply over sessions.
+    /// </summary>
+    public string? ReplyToSessionId { get; init; }
+
     /// <summary>
     /// Gets or sets the time interval after which the message expires.
     /// </summary>
diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
index 3cc8c1c22..41bb3386f 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
@@ -15,6 +15,7 @@ public class ServiceBusSubscription : EventSubscription<ServiceBusSubscriptionOp
     readonly ServiceBusClient                  _client;
     readonly Func<ProcessErrorEventArgs, Task> _defaultErrorHandler;
     ServiceBusProcessor?                       _processor;
+    ServiceBusSessionProcessor?                _sessionProcessor;
 
     /// <summary>
     /// Initializes a new instance of the <see cref="ServiceBusSubscription"/> class.
@@ -37,12 +38,21 @@ public ServiceBusSubscription(ServiceBusClient client, ServiceBusSubscriptionOpt
     /// <returns></returns>
     /// <exception cref="InvalidOperationException"></exception>
     protected override ValueTask Subscribe(CancellationToken cancellationToken) {
-        _processor = Options.QueueOrTopic.MakeProcessor(_client, Options);
+        if (Options.SessionProcessorOptions is not null) {
+            _sessionProcessor = Options.QueueOrTopic.MakeSessionProcessor(_client, Options);
 
-        _processor.ProcessMessageAsync += HandleMessage;
-        _processor.ProcessErrorAsync   += _defaultErrorHandler;
+            _sessionProcessor.ProcessMessageAsync += HandleSessionMessage;
+            _sessionProcessor.ProcessErrorAsync   += _defaultErrorHandler;
 
-        return new(_processor.StartProcessingAsync(cancellationToken));
+            return new ValueTask(_sessionProcessor.StartProcessingAsync(cancellationToken));
+        } else {
+            _processor = Options.QueueOrTopic.MakeProcessor(_client, Options);
+
+            _processor.ProcessMessageAsync += HandleMessage;
+            _processor.ProcessErrorAsync   += _defaultErrorHandler;
+
+            return new(_processor.StartProcessingAsync(cancellationToken));
+        }
 
         async Task HandleMessage(ProcessMessageEventArgs arg) {
             var ct = arg.CancellationToken;
@@ -91,6 +101,54 @@ async Task HandleMessage(ProcessMessageEventArgs arg) {
                 Log.ErrorLog?.Log(ex, "Error processing message: {MessageId}", msg.MessageId);
             }
         }
+
+        async Task HandleSessionMessage(ProcessSessionMessageEventArgs arg) {
+            var ct = arg.CancellationToken;
+
+            if (ct.IsCancellationRequested) return;
+
+            var msg = arg.Message;
+
+            var eventType = msg.ApplicationProperties[Options.AttributeNames.MessageType].ToString()
+             ?? throw new InvalidOperationException("Event type is missing in message properties");
+            var contentType = msg.ContentType;
+
+            // Should this be a stream name? or topic or something
+            var streamName = msg.ApplicationProperties[Options.AttributeNames.StreamName].ToString()
+             ?? throw new InvalidOperationException("Stream name is missing in message properties");
+
+            Logger.Current = Log;
+
+            var evt = DeserializeData(contentType, eventType, msg.Body, streamName);
+
+            var applicationProperties = msg.ApplicationProperties.Concat(MessageProperties(msg));
+
+            var ctx = new MessageConsumeContext(
+                msg.MessageId,
+                eventType,
+                contentType,
+                streamName,
+                0,
+                0,
+                0,
+                Sequence++,
+                msg.EnqueuedTime.UtcDateTime,
+                evt,
+                AsMeta(applicationProperties),
+                SubscriptionId,
+                ct
+            );
+
+            try {
+                await Handler(ctx).NoContext();
+                await arg.CompleteMessageAsync(msg, ct).NoContext();
+            } catch (Exception ex) {
+                // Abandoning the message will make it available for reprocessing, or dead letter it?
+                await arg.AbandonMessageAsync(msg, null, ct).NoContext();
+                await _defaultErrorHandler(new(ex, ServiceBusErrorSource.Abandon, arg.FullyQualifiedNamespace, arg.EntityPath, arg.Identifier, arg.CancellationToken)).NoContext();
+                Log.ErrorLog?.Log(ex, "Error processing message: {MessageId}", msg.MessageId);
+            }
+        }
     }
 
     IEnumerable<KeyValuePair<string, object>> MessageProperties(ServiceBusReceivedMessage msg) {
diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscriptionOptions.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscriptionOptions.cs
index 6ce02918c..2659ca533 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscriptionOptions.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscriptionOptions.cs
@@ -20,6 +20,12 @@ public record ServiceBusSubscriptionOptions : SubscriptionOptions {
     /// </summary>
     public ServiceBusProcessorOptions ProcessorOptions { get; set; } = new();
 
+    /// <summary>
+    /// Gets or sets the options for the session Service Bus processor.
+    /// If these options are specified, they take priority over <see cref="ProcessorOptions"/>
+    /// </summary>
+    public ServiceBusSessionProcessorOptions? SessionProcessorOptions { get; set; }
+
     /// <summary>
     /// Gets the message attributes for Service Bus messages.
     /// </summary>
@@ -42,6 +48,14 @@ public interface IQueueOrTopic {
     /// <param name="options">The subscription options.</param>
     /// <returns>A configured <see cref="ServiceBusProcessor"/> instance.</returns>
     ServiceBusProcessor MakeProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options);
+
+    /// <summary>
+    /// Creates a <see cref="ServiceBusSessionProcessor"/> for the specfiied client and options.
+    /// </summary>
+    /// <param name="client">The Service Bus client.</param>
+    /// <param name="options">The subscription options.</param>
+    /// <returns>A configured <see cref="ServiceBusSessionProcessor"/> instance.</returns>
+    ServiceBusSessionProcessor MakeSessionProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options);
 }
 
 /// <summary>
@@ -56,6 +70,15 @@ public record Queue(string Name) : IQueueOrTopic {
     /// <returns>A configured <see cref="ServiceBusProcessor"/> for the queue.</returns>
     public ServiceBusProcessor MakeProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options) 
         => client.CreateProcessor(Name, options.ProcessorOptions);
+
+    /// <summary>
+    /// Creates a <see cref="ServiceBusSessionProcessor"/> for the queue.
+    /// </summary>
+    /// <param name="client">The Service Bus client.</param>
+    /// <param name="options">The subscription options.</param>
+    /// <returns>A configured <see cref="ServiceBusSessionProcessor"/> for the queue.</returns>
+    public ServiceBusSessionProcessor MakeSessionProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options)
+        => client.CreateSessionProcessor(Name, options.SessionProcessorOptions);
 }
 
 /// <summary>
@@ -70,6 +93,15 @@ public record Topic(string Name) : IQueueOrTopic {
     /// <returns>A configured <see cref="ServiceBusProcessor"/> for the topic.</returns>
     public ServiceBusProcessor MakeProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options) 
         => client.CreateProcessor(Name, options.SubscriptionId, options.ProcessorOptions);
+
+    /// <summary>
+    /// Creates a <see cref="ServiceBusSessionProcessor"/> for the topic and subscription ID from options.
+    /// </summary>
+    /// <param name="client">The Service Bus client.</param>
+    /// <param name="options">The subscription options.</param>
+    /// <returns>A configured <see cref="ServiceBusSessionProcessor"/> for the topic.</returns>
+    public ServiceBusSessionProcessor MakeSessionProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options)
+        => client.CreateSessionProcessor(Name, options.SubscriptionId, options.SessionProcessorOptions);
 }
 
 /// <summary>
@@ -84,4 +116,13 @@ public record TopicAndSubscription(string Name, string Subscription) : IQueueOrT
     /// <returns>A configured <see cref="ServiceBusProcessor"/> for the topic and subscription.</returns>
     public ServiceBusProcessor MakeProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options)
         => client.CreateProcessor(Name, Subscription, options.ProcessorOptions);
+
+    /// <summary>
+    /// Creates a <see cref="ServiceBusSessionProcessor"/> for the topic and specified subscription.
+    /// </summary>
+    /// <param name="client">The Service Bus client.</param>
+    /// <param name="options">The subscription options.</param>
+    /// <returns>A configured <see cref="ServiceBusSessionProcessor"/> for the topic and subscription.</returns>
+    public ServiceBusSessionProcessor MakeSessionProcessor(ServiceBusClient client, ServiceBusSubscriptionOptions options)
+        => client.CreateSessionProcessor(Name, Subscription, options.SessionProcessorOptions);
 }

From adb53f42258260932de3fc7c76b6896a60744c05 Mon Sep 17 00:00:00 2001
From: alexey-troshkin-xpress <alexey.troshkin@xpress.com.ph>
Date: Fri, 21 Nov 2025 01:59:18 +0300
Subject: [PATCH 2/8] handle unsubscribe for session processor

---
 .../Subscriptions/ServiceBusSubscription.cs               | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
index 41bb3386f..441f47e00 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
@@ -187,7 +187,11 @@ async Task DefaultErrorHandler(ProcessErrorEventArgs arg) {
     /// <param name="cancellationToken"></param>
     /// <returns></returns>
     protected override async ValueTask Unsubscribe(CancellationToken cancellationToken) {
-        if (_processor == null) return;
-        await _processor.StopProcessingAsync(cancellationToken).NoContext();
+        if (_sessionProcessor is not null) {
+            await _sessionProcessor.StopProcessingAsync(cancellationToken).NoContext();
+        }
+        else if (_processor is not null) {
+            await _processor.StopProcessingAsync(cancellationToken).NoContext();
+        }
     }
 }

From 191b92069d5510b8fc4cc1b1c2c8aab6a051241a Mon Sep 17 00:00:00 2001
From: Simon Cropp <simon.cropp@gmail.com>
Date: Sun, 23 Nov 2025 01:28:23 +1100
Subject: [PATCH 3/8] make verified file paths shorter (#472)

---
 ...eCommandsTests.MapContractExplicitly.verified.txt} |  0
 ...ts.MapContractExplicitlyWithoutRoute.verified.txt} |  0
 ...xplicitlyWithoutRouteWithGenericAttr.verified.txt} |  0
 .../AggregateCommandsTests.cs                         | 11 ++++++-----
 4 files changed, 6 insertions(+), 5 deletions(-)
 rename src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/{AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt => AggregateCommandsTests.MapContractExplicitly.verified.txt} (100%)
 rename src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/{AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt => AggregateCommandsTests.MapContractExplicitlyWithoutRoute.verified.txt} (100%)
 rename src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/{AggregateCommandsTests.MapAggregateContractToCommandExplicitly_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt => AggregateCommandsTests.MapContractExplicitlyWithoutRouteWithGenericAttr.verified.txt} (100%)

diff --git a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapContractExplicitly.verified.txt
similarity index 100%
rename from src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt
rename to src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapContractExplicitly.verified.txt
diff --git a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapContractExplicitlyWithoutRoute.verified.txt
similarity index 100%
rename from src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt
rename to src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapContractExplicitlyWithoutRoute.verified.txt
diff --git a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapAggregateContractToCommandExplicitly_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapContractExplicitlyWithoutRouteWithGenericAttr.verified.txt
similarity index 100%
rename from src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapAggregateContractToCommandExplicitly_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt
rename to src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapContractExplicitlyWithoutRouteWithGenericAttr.verified.txt
diff --git a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs
index 646dca2fe..bd02fa94b 100644
--- a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs
+++ b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs
@@ -9,7 +9,7 @@ namespace Eventuous.Tests.Extensions.AspNetCore;
 [ClassDataSource<WebApplicationFactory<Program>>]
 public class AggregateCommandsTests(WebApplicationFactory<Program> factory) : TestBaseWithLogs {
     [Test]
-    public async Task MapAggregateContractToCommandExplicitly() {
+    public async Task MapContractExplicitly() {
         var fixture = new ServerFixture(
             factory,
             _ => { },
@@ -22,7 +22,7 @@ public async Task MapAggregateContractToCommandExplicitly() {
     }
 
     [Test]
-    public async Task MapAggregateContractToCommandExplicitlyWithoutRoute() {
+    public async Task MapContractExplicitlyWithoutRoute() {
         var fixture = new ServerFixture(
             factory,
             _ => { },
@@ -35,7 +35,7 @@ public async Task MapAggregateContractToCommandExplicitlyWithoutRoute() {
     }
 
     [Test]
-    public async Task MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr() {
+    public async Task MapContractExplicitlyWithoutRouteWithGenericAttr() {
         var fixture = new ServerFixture(
             factory,
             _ => { },
@@ -48,7 +48,7 @@ public async Task MapAggregateContractToCommandExplicitlyWithoutRouteWithGeneric
     }
 
     [Test]
-    public void MapAggregateContractToCommandExplicitlyWithoutRouteWithWrongGenericAttr() {
+    public void MapContractExplicitlyWithoutRouteWithWrongGenericAttr() {
         Assert.Throws<InvalidOperationException>(Act);
 
         return;
@@ -92,6 +92,7 @@ static async Task Execute(ServerFixture fixture, string route) {
         );
         var content = await fixture.ExecuteRequest<ImportBookingHttp, BookingState>(import, route, bookRoom.BookingId);
 
-        await VerifyJson(content);
+        await VerifyJson(content)
+            .IgnoreParameters();
     }
 }

From eb1f925b1b3d3a1d37d80860778602d081c0d993 Mon Sep 17 00:00:00 2001
From: Alexey Zimarev <alex@zimarev.com>
Date: Sat, 22 Nov 2025 15:38:57 +0100
Subject: [PATCH 4/8] Refactor one more test for shorter name

---
 ...> AggregateCommandsTests.MapEnrichedCommand.verified.txt} | 0
 .../AggregateCommandsTests.cs                                | 5 ++---
 2 files changed, 2 insertions(+), 3 deletions(-)
 rename src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/{AggregateCommandsTests.MapEnrichedCommand_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt => AggregateCommandsTests.MapEnrichedCommand.verified.txt} (100%)

diff --git a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapEnrichedCommand_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapEnrichedCommand.verified.txt
similarity index 100%
rename from src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapEnrichedCommand_factory=Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1[Program].verified.txt
rename to src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.MapEnrichedCommand.verified.txt
diff --git a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs
index bd02fa94b..a0c7449bc 100644
--- a/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs
+++ b/src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/AggregateCommandsTests.cs
@@ -77,7 +77,7 @@ public async Task MapEnrichedCommand() {
         );
         var cmd     = ServerFixture.GetBookRoom();
         var content = await fixture.ExecuteRequest<BookRoom, BookingState>(cmd, "book", cmd.BookingId);
-        await VerifyJson(content);
+        await VerifyJson(content).IgnoreParameters();
     }
 
     static async Task Execute(ServerFixture fixture, string route) {
@@ -92,7 +92,6 @@ static async Task Execute(ServerFixture fixture, string route) {
         );
         var content = await fixture.ExecuteRequest<ImportBookingHttp, BookingState>(import, route, bookRoom.BookingId);
 
-        await VerifyJson(content)
-            .IgnoreParameters();
+        await VerifyJson(content).IgnoreParameters();
     }
 }

From f832640b912cdfc5e4102c302c639cf19c9244ae Mon Sep 17 00:00:00 2001
From: Alexey Zimarev <alex@zimarev.com>
Date: Sat, 22 Nov 2025 16:04:11 +0100
Subject: [PATCH 5/8] * Update TestContainers for the emulator * Remove
 duplicate code * Check if properties exist and add a fall back

---
 Directory.Packages.props                      |   2 +-
 .../Subscriptions/ServiceBusSubscription.cs   | 118 ++++++++----------
 2 files changed, 50 insertions(+), 70 deletions(-)

diff --git a/Directory.Packages.props b/Directory.Packages.props
index dfcedb826..6621085a7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -15,7 +15,7 @@
     <MicrosoftExtensionsVer>9.0.10</MicrosoftExtensionsVer>
   </PropertyGroup>
   <PropertyGroup Label="Testcontainers version">
-    <TestcontainersVersion>4.6.0</TestcontainersVersion>
+    <TestcontainersVersion>4.8.1</TestcontainersVersion>
   </PropertyGroup>
   <PropertyGroup>
     <NpgsqlVersion>9.0.3</NpgsqlVersion>
diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
index 441f47e00..bd9a44e2a 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
@@ -44,83 +44,65 @@ protected override ValueTask Subscribe(CancellationToken cancellationToken) {
             _sessionProcessor.ProcessMessageAsync += HandleSessionMessage;
             _sessionProcessor.ProcessErrorAsync   += _defaultErrorHandler;
 
-            return new ValueTask(_sessionProcessor.StartProcessingAsync(cancellationToken));
-        } else {
-            _processor = Options.QueueOrTopic.MakeProcessor(_client, Options);
-
-            _processor.ProcessMessageAsync += HandleMessage;
-            _processor.ProcessErrorAsync   += _defaultErrorHandler;
-
-            return new(_processor.StartProcessingAsync(cancellationToken));
+            return new(_sessionProcessor.StartProcessingAsync(cancellationToken));
         }
 
-        async Task HandleMessage(ProcessMessageEventArgs arg) {
-            var ct = arg.CancellationToken;
-
-            if (ct.IsCancellationRequested) return;
-
-            var msg = arg.Message;
+        _processor = Options.QueueOrTopic.MakeProcessor(_client, Options);
 
-            var eventType = msg.ApplicationProperties[Options.AttributeNames.MessageType].ToString()
-             ?? throw new InvalidOperationException("Event type is missing in message properties");
-            var contentType = msg.ContentType;
-
-            // Should this be a stream name? or topic or something
-            var streamName = msg.ApplicationProperties[Options.AttributeNames.StreamName].ToString()
-             ?? throw new InvalidOperationException("Stream name is missing in message properties");
-
-            Logger.Current = Log;
+        _processor.ProcessMessageAsync += HandleMessage;
+        _processor.ProcessErrorAsync   += _defaultErrorHandler;
 
-            var evt = DeserializeData(contentType, eventType, msg.Body, streamName);
+        return new(_processor.StartProcessingAsync(cancellationToken));
 
-            var applicationProperties = msg.ApplicationProperties.Concat(MessageProperties(msg));
-
-            var ctx = new MessageConsumeContext(
-                msg.MessageId,
-                eventType,
-                contentType,
-                streamName,
-                0,
-                0,
-                0,
-                Sequence++,
-                msg.EnqueuedTime.UtcDateTime,
-                evt,
-                AsMeta(applicationProperties),
-                SubscriptionId,
-                ct
+        Task HandleMessage(ProcessMessageEventArgs arg)
+            => ProcessMessageAsync(
+                arg.Message,
+                arg.CancellationToken,
+                msg => arg.CompleteMessageAsync(msg, arg.CancellationToken),
+                msg => arg.AbandonMessageAsync(msg, null, arg.CancellationToken),
+                arg.FullyQualifiedNamespace,
+                arg.EntityPath,
+                arg.Identifier
             );
 
-            try {
-                await Handler(ctx).NoContext();
-                await arg.CompleteMessageAsync(msg, ct).NoContext();
-            } catch (Exception ex) {
-                // Abandoning the message will make it available for reprocessing, or dead letter it?
-                await arg.AbandonMessageAsync(msg, null, ct).NoContext(); 
-                await _defaultErrorHandler(new(ex, ServiceBusErrorSource.Abandon, arg.FullyQualifiedNamespace, arg.EntityPath, arg.Identifier, arg.CancellationToken)).NoContext();
-                Log.ErrorLog?.Log(ex, "Error processing message: {MessageId}", msg.MessageId);
-            }
-        }
-
-        async Task HandleSessionMessage(ProcessSessionMessageEventArgs arg) {
-            var ct = arg.CancellationToken;
+        Task HandleSessionMessage(ProcessSessionMessageEventArgs arg)
+            => ProcessMessageAsync(
+                arg.Message,
+                arg.CancellationToken,
+                msg => arg.CompleteMessageAsync(msg, arg.CancellationToken),
+                msg => arg.AbandonMessageAsync(msg, null, arg.CancellationToken),
+                arg.FullyQualifiedNamespace,
+                arg.EntityPath,
+                arg.Identifier
+            );
 
+        async Task ProcessMessageAsync(
+                ServiceBusReceivedMessage             msg,
+                CancellationToken                     ct,
+                Func<ServiceBusReceivedMessage, Task> completeMessage,
+                Func<ServiceBusReceivedMessage, Task> abandonMessage,
+                string                                fullyQualifiedNamespace,
+                string                                entityPath,
+                string                                identifier
+            ) {
             if (ct.IsCancellationRequested) return;
 
-            var msg = arg.Message;
-
-            var eventType = msg.ApplicationProperties[Options.AttributeNames.MessageType].ToString()
-             ?? throw new InvalidOperationException("Event type is missing in message properties");
+            var eventType = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.MessageType, out var messageType)
+                ? messageType.ToString()
+                : msg.Subject) ?? throw new InvalidOperationException("Message type is missing in message properties");
             var contentType = msg.ContentType;
 
             // Should this be a stream name? or topic or something
-            var streamName = msg.ApplicationProperties[Options.AttributeNames.StreamName].ToString()
-             ?? throw new InvalidOperationException("Stream name is missing in message properties");
+            var streamName = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.StreamName, out var stream)
+                ? stream.ToString()
+                : Options.QueueOrTopic switch {
+                    Queue queue => queue.Name,
+                    Topic topic => topic.Name,
+                    _           => null
+                }) ?? throw new InvalidOperationException("Stream name is missing in message properties");
 
             Logger.Current = Log;
-
-            var evt = DeserializeData(contentType, eventType, msg.Body, streamName);
-
+            var evt                   = DeserializeData(contentType, eventType, msg.Body, streamName);
             var applicationProperties = msg.ApplicationProperties.Concat(MessageProperties(msg));
 
             var ctx = new MessageConsumeContext(
@@ -141,11 +123,11 @@ async Task HandleSessionMessage(ProcessSessionMessageEventArgs arg) {
 
             try {
                 await Handler(ctx).NoContext();
-                await arg.CompleteMessageAsync(msg, ct).NoContext();
+                await completeMessage(msg).NoContext();
             } catch (Exception ex) {
                 // Abandoning the message will make it available for reprocessing, or dead letter it?
-                await arg.AbandonMessageAsync(msg, null, ct).NoContext();
-                await _defaultErrorHandler(new(ex, ServiceBusErrorSource.Abandon, arg.FullyQualifiedNamespace, arg.EntityPath, arg.Identifier, arg.CancellationToken)).NoContext();
+                await abandonMessage(msg).NoContext();
+                await _defaultErrorHandler(new(ex, ServiceBusErrorSource.Abandon, fullyQualifiedNamespace, entityPath, identifier, ct)).NoContext();
                 Log.ErrorLog?.Log(ex, "Error processing message: {MessageId}", msg.MessageId);
             }
         }
@@ -173,12 +155,10 @@ IEnumerable<KeyValuePair<string, object>> MessageProperties(ServiceBusReceivedMe
     static Metadata AsMeta(IEnumerable<KeyValuePair<string, object>> applicationProperties) =>
         new(applicationProperties.ToDictionary(pair => pair.Key, object? (pair) => pair.Value));
 
-    async Task DefaultErrorHandler(ProcessErrorEventArgs arg) {
-        // Log the error
+    Task DefaultErrorHandler(ProcessErrorEventArgs arg) {
         Log.ErrorLog?.Log(arg.Exception, "Error processing message: {Identifier}", arg.Identifier);
 
-        // Optionally, you can handle the error further, e.g., by sending to a dead-letter queue
-        await Task.CompletedTask;
+        return Task.CompletedTask;
     }
 
     /// <summary>

From 35baa5a6f07cb3cc8e2602bc98ee23f0783fd98c Mon Sep 17 00:00:00 2001
From: Alexey Zimarev <alex@zimarev.com>
Date: Sat, 22 Nov 2025 16:16:10 +0100
Subject: [PATCH 6/8] Replace with composition

---
 .../Subscriptions/ServiceBusSubscription.cs   | 234 ++++++++++--------
 1 file changed, 132 insertions(+), 102 deletions(-)

diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
index bd9a44e2a..5799d91c1 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
@@ -12,10 +12,8 @@ namespace Eventuous.Azure.ServiceBus.Subscriptions;
 /// Represents a Service Bus subscription that processes messages from a queue or topic.
 /// </summary>
 public class ServiceBusSubscription : EventSubscription<ServiceBusSubscriptionOptions> {
-    readonly ServiceBusClient                  _client;
     readonly Func<ProcessErrorEventArgs, Task> _defaultErrorHandler;
-    ServiceBusProcessor?                       _processor;
-    ServiceBusSessionProcessor?                _sessionProcessor;
+    readonly IServiceBusProcessorStrategy      _processorStrategy;
 
     /// <summary>
     /// Initializes a new instance of the <see cref="ServiceBusSubscription"/> class.
@@ -27,8 +25,11 @@ public class ServiceBusSubscription : EventSubscription<ServiceBusSubscriptionOp
     /// <param name="eventSerializer">Event serializer (optional)</param>
     public ServiceBusSubscription(ServiceBusClient client, ServiceBusSubscriptionOptions options, ConsumePipe consumePipe, ILoggerFactory? loggerFactory, IEventSerializer? eventSerializer) :
         base(options, consumePipe, loggerFactory, eventSerializer) {
-        _client              = client;
         _defaultErrorHandler = Options.ErrorHandler ?? DefaultErrorHandler;
+
+        _processorStrategy = Options.SessionProcessorOptions is not null
+            ? new SessionProcessorStrategy(client, Options, HandleSessionMessage, _defaultErrorHandler)
+            : new StandardProcessorStrategy(client, Options, HandleMessage, _defaultErrorHandler);
     }
 
     /// <summary>
@@ -37,99 +38,84 @@ public ServiceBusSubscription(ServiceBusClient client, ServiceBusSubscriptionOpt
     /// <param name="cancellationToken"></param>
     /// <returns></returns>
     /// <exception cref="InvalidOperationException"></exception>
-    protected override ValueTask Subscribe(CancellationToken cancellationToken) {
-        if (Options.SessionProcessorOptions is not null) {
-            _sessionProcessor = Options.QueueOrTopic.MakeSessionProcessor(_client, Options);
-
-            _sessionProcessor.ProcessMessageAsync += HandleSessionMessage;
-            _sessionProcessor.ProcessErrorAsync   += _defaultErrorHandler;
-
-            return new(_sessionProcessor.StartProcessingAsync(cancellationToken));
-        }
-
-        _processor = Options.QueueOrTopic.MakeProcessor(_client, Options);
-
-        _processor.ProcessMessageAsync += HandleMessage;
-        _processor.ProcessErrorAsync   += _defaultErrorHandler;
-
-        return new(_processor.StartProcessingAsync(cancellationToken));
-
-        Task HandleMessage(ProcessMessageEventArgs arg)
-            => ProcessMessageAsync(
-                arg.Message,
-                arg.CancellationToken,
-                msg => arg.CompleteMessageAsync(msg, arg.CancellationToken),
-                msg => arg.AbandonMessageAsync(msg, null, arg.CancellationToken),
-                arg.FullyQualifiedNamespace,
-                arg.EntityPath,
-                arg.Identifier
-            );
-
-        Task HandleSessionMessage(ProcessSessionMessageEventArgs arg)
-            => ProcessMessageAsync(
-                arg.Message,
-                arg.CancellationToken,
-                msg => arg.CompleteMessageAsync(msg, arg.CancellationToken),
-                msg => arg.AbandonMessageAsync(msg, null, arg.CancellationToken),
-                arg.FullyQualifiedNamespace,
-                arg.EntityPath,
-                arg.Identifier
-            );
-
-        async Task ProcessMessageAsync(
-                ServiceBusReceivedMessage             msg,
-                CancellationToken                     ct,
-                Func<ServiceBusReceivedMessage, Task> completeMessage,
-                Func<ServiceBusReceivedMessage, Task> abandonMessage,
-                string                                fullyQualifiedNamespace,
-                string                                entityPath,
-                string                                identifier
-            ) {
-            if (ct.IsCancellationRequested) return;
-
-            var eventType = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.MessageType, out var messageType)
-                ? messageType.ToString()
-                : msg.Subject) ?? throw new InvalidOperationException("Message type is missing in message properties");
-            var contentType = msg.ContentType;
-
-            // Should this be a stream name? or topic or something
-            var streamName = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.StreamName, out var stream)
-                ? stream.ToString()
-                : Options.QueueOrTopic switch {
-                    Queue queue => queue.Name,
-                    Topic topic => topic.Name,
-                    _           => null
-                }) ?? throw new InvalidOperationException("Stream name is missing in message properties");
-
-            Logger.Current = Log;
-            var evt                   = DeserializeData(contentType, eventType, msg.Body, streamName);
-            var applicationProperties = msg.ApplicationProperties.Concat(MessageProperties(msg));
-
-            var ctx = new MessageConsumeContext(
-                msg.MessageId,
-                eventType,
-                contentType,
-                streamName,
-                0,
-                0,
-                0,
-                Sequence++,
-                msg.EnqueuedTime.UtcDateTime,
-                evt,
-                AsMeta(applicationProperties),
-                SubscriptionId,
-                ct
-            );
-
-            try {
-                await Handler(ctx).NoContext();
-                await completeMessage(msg).NoContext();
-            } catch (Exception ex) {
-                // Abandoning the message will make it available for reprocessing, or dead letter it?
-                await abandonMessage(msg).NoContext();
-                await _defaultErrorHandler(new(ex, ServiceBusErrorSource.Abandon, fullyQualifiedNamespace, entityPath, identifier, ct)).NoContext();
-                Log.ErrorLog?.Log(ex, "Error processing message: {MessageId}", msg.MessageId);
-            }
+    protected override ValueTask Subscribe(CancellationToken cancellationToken)
+        => _processorStrategy.Start(cancellationToken);
+
+    Task HandleMessage(ProcessMessageEventArgs arg)
+        => ProcessMessageAsync(
+            arg.Message,
+            msg => arg.CompleteMessageAsync(msg, arg.CancellationToken),
+            msg => arg.AbandonMessageAsync(msg, null, arg.CancellationToken),
+            arg.FullyQualifiedNamespace,
+            arg.EntityPath,
+            arg.Identifier,
+            arg.CancellationToken
+        );
+
+    Task HandleSessionMessage(ProcessSessionMessageEventArgs arg)
+        => ProcessMessageAsync(
+            arg.Message,
+            msg => arg.CompleteMessageAsync(msg, arg.CancellationToken),
+            msg => arg.AbandonMessageAsync(msg, null, arg.CancellationToken),
+            arg.FullyQualifiedNamespace,
+            arg.EntityPath,
+            arg.Identifier,
+            arg.CancellationToken
+        );
+
+    async Task ProcessMessageAsync(
+            ServiceBusReceivedMessage             msg,
+            Func<ServiceBusReceivedMessage, Task> completeMessage,
+            Func<ServiceBusReceivedMessage, Task> abandonMessage,
+            string                                fullyQualifiedNamespace,
+            string                                entityPath,
+            string                                identifier,
+            CancellationToken                     ct
+        ) {
+        if (ct.IsCancellationRequested) return;
+
+        var eventType = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.MessageType, out var messageType)
+            ? messageType.ToString()
+            : msg.Subject) ?? throw new InvalidOperationException("Message type is missing in message properties");
+        var contentType = msg.ContentType;
+
+        // Should this be a stream name? or topic or something
+        var streamName = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.StreamName, out var stream)
+            ? stream.ToString()
+            : Options.QueueOrTopic switch {
+                Queue queue => queue.Name,
+                Topic topic => topic.Name,
+                _           => null
+            }) ?? throw new InvalidOperationException("Stream name is missing in message properties");
+
+        Logger.Current = Log;
+        var evt                   = DeserializeData(contentType, eventType, msg.Body, streamName);
+        var applicationProperties = msg.ApplicationProperties.Concat(MessageProperties(msg));
+
+        var ctx = new MessageConsumeContext(
+            msg.MessageId,
+            eventType,
+            contentType,
+            streamName,
+            0,
+            0,
+            0,
+            Sequence++,
+            msg.EnqueuedTime.UtcDateTime,
+            evt,
+            AsMeta(applicationProperties),
+            SubscriptionId,
+            ct
+        );
+
+        try {
+            await Handler(ctx).NoContext();
+            await completeMessage(msg).NoContext();
+        } catch (Exception ex) {
+            // Abandoning the message will make it available for reprocessing, or dead letter it?
+            await abandonMessage(msg).NoContext();
+            await _defaultErrorHandler(new(ex, ServiceBusErrorSource.Abandon, fullyQualifiedNamespace, entityPath, identifier, ct)).NoContext();
+            Log.ErrorLog?.Log(ex, "Error processing message: {MessageId}", msg.MessageId);
         }
     }
 
@@ -166,12 +152,56 @@ Task DefaultErrorHandler(ProcessErrorEventArgs arg) {
     /// </summary>
     /// <param name="cancellationToken"></param>
     /// <returns></returns>
-    protected override async ValueTask Unsubscribe(CancellationToken cancellationToken) {
-        if (_sessionProcessor is not null) {
-            await _sessionProcessor.StopProcessingAsync(cancellationToken).NoContext();
+    protected override ValueTask Unsubscribe(CancellationToken cancellationToken) => _processorStrategy.Stop(cancellationToken);
+
+    interface IServiceBusProcessorStrategy {
+        ValueTask Start(CancellationToken cancellationToken);
+        ValueTask Stop(CancellationToken  cancellationToken);
+    }
+
+    sealed class StandardProcessorStrategy(
+            ServiceBusClient                    client,
+            ServiceBusSubscriptionOptions       options,
+            Func<ProcessMessageEventArgs, Task> handleMessage,
+            Func<ProcessErrorEventArgs, Task>   handleError
+        )
+        : IServiceBusProcessorStrategy {
+        ServiceBusProcessor? _processor;
+
+        public ValueTask Start(CancellationToken cancellationToken) {
+            _processor                     =  options.QueueOrTopic.MakeProcessor(client, options);
+            _processor.ProcessMessageAsync += handleMessage;
+            _processor.ProcessErrorAsync   += handleError;
+
+            return new(_processor.StartProcessingAsync(cancellationToken));
         }
-        else if (_processor is not null) {
-            await _processor.StopProcessingAsync(cancellationToken).NoContext();
+
+        public ValueTask Stop(CancellationToken cancellationToken)
+            => _processor is not null
+                ? new(_processor.StopProcessingAsync(cancellationToken))
+                : ValueTask.CompletedTask;
+    }
+
+    sealed class SessionProcessorStrategy(
+            ServiceBusClient                           client,
+            ServiceBusSubscriptionOptions              options,
+            Func<ProcessSessionMessageEventArgs, Task> handleSessionMessage,
+            Func<ProcessErrorEventArgs, Task>          handleError
+        )
+        : IServiceBusProcessorStrategy {
+        ServiceBusSessionProcessor? _sessionProcessor;
+
+        public ValueTask Start(CancellationToken cancellationToken) {
+            _sessionProcessor                     =  options.QueueOrTopic.MakeSessionProcessor(client, options);
+            _sessionProcessor.ProcessMessageAsync += handleSessionMessage;
+            _sessionProcessor.ProcessErrorAsync   += handleError;
+
+            return new(_sessionProcessor.StartProcessingAsync(cancellationToken));
         }
+
+        public ValueTask Stop(CancellationToken cancellationToken)
+            => _sessionProcessor is not null
+                ? new(_sessionProcessor.StopProcessingAsync(cancellationToken))
+                : ValueTask.CompletedTask;
     }
 }

From 5b1331d1505dc2d1afd487045cdc661b53b14e09 Mon Sep 17 00:00:00 2001
From: Alexey Zimarev <alex@zimarev.com>
Date: Sat, 22 Nov 2025 16:50:35 +0100
Subject: [PATCH 7/8] Update
 src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs

Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com>
---
 .../Subscriptions/ServiceBusSubscription.cs                     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
index 5799d91c1..141abfc3d 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
@@ -74,7 +74,7 @@ CancellationToken                     ct
         ) {
         if (ct.IsCancellationRequested) return;
 
-        var eventType = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.MessageType, out var messageType)
+        var eventType = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.MessageType, out var messageType) && messageType is not null
             ? messageType.ToString()
             : msg.Subject) ?? throw new InvalidOperationException("Message type is missing in message properties");
         var contentType = msg.ContentType;

From c08bd14a0cf12aee3d514e4649d4a7cb65f922e3 Mon Sep 17 00:00:00 2001
From: Alexey Zimarev <alex@zimarev.com>
Date: Sat, 22 Nov 2025 16:51:17 +0100
Subject: [PATCH 8/8] Update
 src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs

Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com>
---
 .../Subscriptions/ServiceBusSubscription.cs                     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
index 141abfc3d..daa30259a 100644
--- a/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
+++ b/src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs
@@ -80,7 +80,7 @@ CancellationToken                     ct
         var contentType = msg.ContentType;
 
         // Should this be a stream name? or topic or something
-        var streamName = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.StreamName, out var stream)
+        var streamName = (msg.ApplicationProperties.TryGetValue(Options.AttributeNames.StreamName, out var stream) && stream is not null
             ? stream.ToString()
             : Options.QueueOrTopic switch {
                 Queue queue => queue.Name,
