Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
namespace Paramore.Brighter.MessagingGateway.AzureServiceBus
using System;
using System.Linq;
using Paramore.Brighter.JsonConverters;

namespace Paramore.Brighter.MessagingGateway.AzureServiceBus
{
internal static class ASBConstants
{
Expand Down Expand Up @@ -26,5 +30,29 @@ internal static class ASBConstants

public static readonly string[] ReservedHeaders = [LockTokenHeaderBagKey, SequenceNumberBagKey, MessageTypeHeaderBagKey, HandledCountHeaderBagKey, ReplyToHeaderBagKey, SessionIdKey
];

/// <summary>
/// Does a header bag <paramref name="key"/> refer to the reserved key <paramref name="reservedKey"/>?
/// Brighter's Outbox serializes the bag with <see cref="JsonSerialisationOptions.Options"/>, whose
/// <see cref="System.Text.Json.JsonSerializerOptions.PropertyNamingPolicy"/> rewrites bag keys — so a key
/// written as "SessionId" comes back transformed (for example "sessionId" or "session_id") depending on the
/// configured policy. We therefore match both the raw key and the key as the active naming policy would
/// render it, case-insensitively, so the lookup stays correct whatever policy the user configures.
/// </summary>
public static bool IsBagKey(string key, string reservedKey)
{
if (string.Equals(key, reservedKey, StringComparison.OrdinalIgnoreCase))
return true;

var namingPolicy = JsonSerialisationOptions.Options.PropertyNamingPolicy;
return namingPolicy is not null
&& string.Equals(key, namingPolicy.ConvertName(reservedKey), StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Is a header bag <paramref name="key"/> one of the <see cref="ReservedHeaders"/>, accounting for any
/// naming-policy transformation applied during an Outbox round-trip? See <see cref="IsBagKey"/>.
/// </summary>
public static bool IsReservedHeader(string key) => ReservedHeaders.Any(reserved => IsBagKey(key, reserved));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,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 Outbox serializes bag keys with the configured JsonNamingPolicy, so a key written as
//"SessionId" returns transformed (e.g. "sessionId" or "session_id") after a round-trip. Resolve the
//SessionId using the same policy so we stay correct whatever policy is configured (see ASBConstants.IsBagKey).
var sessionId = message.Header.Bag
.FirstOrDefault(h => ASBConstants.IsBagKey(h.Key, ASBConstants.SessionIdKey))
.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.IsReservedHeader(h.Key)
&& !MessageHeader.IsLocalHeader(h.Key)))
{
azureServiceBusMessage.ApplicationProperties[header.Key] = header.Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using Paramore.Brighter.JsonConverters;
using Paramore.Brighter.MessagingGateway.AzureServiceBus;
using Xunit;

namespace Paramore.Brighter.AzureServiceBus.Tests.MessagingGateway;

// These tests mutate the global JsonSerialisationOptions.Options, so they must not run in
// parallel with any other test that relies on the serialization policy. Grouping them in a
// single, parallelism-disabled collection isolates that shared-state change.
[CollectionDefinition("SerializationPolicy", DisableParallelization = true)]
public class SerializationPolicyCollection;

[Trait("Category", "ASB")]
[Collection("SerializationPolicy")]
public class AzureServiceBusMessagePublisherSessionIdTests
{
public static IEnumerable<object?[]> NamingPolicies =>
[
// the default: a bag key written as "SessionId" round-trips as "sessionId"
[JsonNamingPolicy.CamelCase],
// a user-overridden policy: "SessionId" round-trips as "session_id" — the case a
// case-insensitive-only fix cannot resolve
[JsonNamingPolicy.SnakeCaseLower],
// no policy: the key stays "SessionId"
[null],
];

[Theory]
[MemberData(nameof(NamingPolicies))]
public void When_the_session_id_bag_key_round_trips_under_the_configured_policy_should_set_the_session_id(
JsonNamingPolicy? policy)
{
// Brighter's Outbox serializes the header Bag with JsonSerialisationOptions.Options, and its
// PropertyNamingPolicy rewrites bag keys on the way out. A key written as "SessionId" therefore
// comes back transformed by whatever policy is configured. The publisher must resolve the
// SessionId regardless of the policy — and the reserved key must never leak onto the wire.
var original = JsonSerialisationOptions.Options;
try
{
JsonSerialisationOptions.Options = new JsonSerializerOptions(original) { PropertyNamingPolicy = policy };

const string sessionIdKey = "SessionId"; // the reserved bag key as written by application code
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;

// round-trip the Bag exactly as an Outbox does, so the key is mangled by the real policy
var json = JsonSerializer.Serialize(header.Bag, JsonSerialisationOptions.Options);
header.Bag = JsonSerializer.Deserialize<Dictionary<string, object>>(json, JsonSerialisationOptions.Options)!;

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, whatever casing the policy gave it, does not leak into ApplicationProperties
var roundTrippedKey = policy?.ConvertName(sessionIdKey) ?? sessionIdKey;
Assert.False(asbMessage.ApplicationProperties.ContainsKey(roundTrippedKey));
}
finally
{
JsonSerialisationOptions.Options = original;
}
}
}
Loading