From ab8a3587a28076987422d4fcb68e077e9760d8d2 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 25 Jun 2026 15:47:34 +0100 Subject: [PATCH] fix: case-insensitive SessionId and reserved-header handling for ASB (#4054) Brighter's JSON serialization camelCases header bag keys, so a "SessionId" key returns as "sessionId" after an Outbox round-trip. The publisher's exact-match lookup missed it (session never set) and the case-sensitive reserved-header filter let it leak into ApplicationProperties. Both lookups are now OrdinalIgnoreCase. Co-Authored-By: Claude Opus 4.8 --- .../AzureServiceBusMessagePublisher.cs | 15 +++++--- ..._Converting_A_Message_With_A_Session_Id.cs | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_Converting_A_Message_With_A_Session_Id.cs diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessagePublisher.cs index 54699eb392..cf92fd64cc 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessagePublisher.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Mime; using Azure.Messaging.ServiceBus; using Paramore.Brighter.Extensions; @@ -59,11 +60,17 @@ private static void AddBrighterHeaders(Message message, ServiceBusMessage azureS azureServiceBusMessage.CorrelationId = message.Header.CorrelationId; if (!string.IsNullOrEmpty(message.Header.ReplyTo!)) azureServiceBusMessage.ReplyTo = message.Header.ReplyTo?.Value; - if (message.Header.Bag.TryGetValue(ASBConstants.SessionIdKey, out object? value)) - azureServiceBusMessage.SessionId = value.ToString(); + //Brighter's JSON serialization camelCases bag keys (JsonNamingPolicy.CamelCase), so a key + //written as "SessionId" returns as "sessionId" after a round-trip (e.g. via an Outbox). + //Resolve the SessionId regardless of casing. + var sessionId = message.Header.Bag + .FirstOrDefault(h => string.Equals(h.Key, ASBConstants.SessionIdKey, StringComparison.OrdinalIgnoreCase)) + .Value; + if (sessionId is not null) + azureServiceBusMessage.SessionId = sessionId.ToString(); foreach (var header in message.Header.Bag.Where(h => - !ASBConstants.ReservedHeaders.Contains(h.Key) + !ASBConstants.ReservedHeaders.Contains(h.Key, StringComparer.OrdinalIgnoreCase) && !MessageHeader.IsLocalHeader(h.Key))) { azureServiceBusMessage.ApplicationProperties[header.Key] = header.Value; diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_Converting_A_Message_With_A_Session_Id.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_Converting_A_Message_With_A_Session_Id.cs new file mode 100644 index 0000000000..7d18f14fcd --- /dev/null +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_Converting_A_Message_With_A_Session_Id.cs @@ -0,0 +1,35 @@ +using System; +using Paramore.Brighter.MessagingGateway.AzureServiceBus; +using Xunit; + +namespace Paramore.Brighter.AzureServiceBus.Tests.MessagingGateway; + +[Trait("Category", "ASB")] +public class AzureServiceBusMessagePublisherSessionIdTests +{ + [Theory] + [InlineData("SessionId")] // as written by application code + [InlineData("sessionId")] // as it returns from a camelCasing serialization round-trip (e.g. via the Outbox) + public void When_Converting_A_Message_With_A_SessionId_Bag_Key_Of_Any_Casing_The_SessionId_Is_Set(string sessionIdKey) + { + // Brighter's JSON serialization uses JsonNamingPolicy.CamelCase, so a bag key written as + // "SessionId" comes back as "sessionId" once the message round-trips through serialization + // (for example when stored in and read back from an Outbox). The publisher must resolve the + // SessionId regardless of the key's casing — and the reserved key must not leak onto the wire. + const string expectedSessionId = "order-42"; + var header = new MessageHeader( + messageId: Guid.NewGuid().ToString(), + topic: new RoutingKey("test.topic"), + messageType: MessageType.MT_COMMAND); + header.Bag[sessionIdKey] = expectedSessionId; + + var message = new Message(header, new MessageBody("body")); + + var asbMessage = AzureServiceBusMessagePublisher.ConvertToServiceBusMessage(message); + + // the session id is set on the outgoing message... + Assert.Equal(expectedSessionId, asbMessage.SessionId); + // ...and the reserved header does not leak into ApplicationProperties + Assert.False(asbMessage.ApplicationProperties.ContainsKey(sessionIdKey)); + } +}