From e29a40046c61dff45dea115582179b654b9488a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:06:14 +0000 Subject: [PATCH 1/5] Initial plan From 81e57cfbe37e9079adfbd323a7c972257ed7470e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:10:48 +0000 Subject: [PATCH 2/5] feat: Add singleEventDelivery configuration for ServiceBus subscribers - Add SerializeSingle method to IEventSchemaFormatter to serialize single events without array wrapper - Implement SerializeSingle in CloudEventSchemaFormatter and EventGridSchemaFormatter - Add singleEventDelivery property to ServiceBusSubscriberSettings - Update ServiceBusEventDeliveryService to use single event serialization when configured - Add unit tests for single event serialization Co-authored-by: pm7y <3075792+pm7y@users.noreply.github.com> --- .../ServiceBusEventDeliveryServiceTests.cs | 53 +++++++++++++++++++ .../Services/CloudEventSchemaFormatter.cs | 7 +++ .../ServiceBusEventDeliveryService.cs | 12 +++-- .../Services/EventGridSchemaFormatter.cs | 7 +++ .../Domain/Services/IEventSchemaFormatter.cs | 13 +++++ .../ServiceBusSubscriberSettings.cs | 8 +++ 6 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs index b7c65ff..4c026e0 100644 --- a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs +++ b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs @@ -248,4 +248,57 @@ public void GivenPropertyResolver_WhenResolvingDynamicProperty_ThenReturnsEventV resolved["Subject"].ShouldBe("test/subject"); } + + [Fact] + public void GivenCloudEventFormatter_WhenSerializingSingle_ThenReturnsJsonWithoutArray() + { + var formatter = _formatterFactory.GetFormatter(EventSchema.CloudEventV1_0); + var evt = CreateTestEvent(); + + var json = formatter.SerializeSingle(evt); + + json.ShouldNotBeNullOrEmpty(); + json.ShouldContain("test-event-id"); + // Verify it's NOT an array (doesn't start with '[') + json.TrimStart().ShouldStartWith("{"); + json.TrimEnd().ShouldEndWith("}"); + } + + [Fact] + public void GivenEventGridFormatter_WhenSerializingSingle_ThenReturnsJsonWithoutArray() + { + var formatter = _formatterFactory.GetFormatter(EventSchema.EventGridSchema); + var evt = CreateTestEvent(); + + var json = formatter.SerializeSingle(evt); + + json.ShouldNotBeNullOrEmpty(); + json.ShouldContain("test-event-id"); + // Verify it's NOT an array (doesn't start with '[') + json.TrimStart().ShouldStartWith("{"); + json.TrimEnd().ShouldEndWith("}"); + } + + [Fact] + public void GivenSubscriptionWithSingleEventDelivery_WhenConfigured_ThenPropertyIsSet() + { + var subscription = new ServiceBusSubscriberSettings + { + Name = "TestSubscriber", + ConnectionString = + "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=abc123", + Queue = "my-queue", + SingleEventDelivery = true, + }; + + subscription.SingleEventDelivery.ShouldBe(true); + } + + [Fact] + public void GivenSubscriptionWithoutSingleEventDelivery_WhenConfigured_ThenPropertyIsNull() + { + var subscription = CreateValidQueueSettings(); + + subscription.SingleEventDelivery.ShouldBeNull(); + } } diff --git a/src/AzureEventGridSimulator/Domain/Services/CloudEventSchemaFormatter.cs b/src/AzureEventGridSimulator/Domain/Services/CloudEventSchemaFormatter.cs index 3c54575..1974df9 100644 --- a/src/AzureEventGridSimulator/Domain/Services/CloudEventSchemaFormatter.cs +++ b/src/AzureEventGridSimulator/Domain/Services/CloudEventSchemaFormatter.cs @@ -33,6 +33,13 @@ public string Serialize(SimulatorEvent evt) return JsonSerializer.Serialize(new[] { cloudEvent }, _serializerOptions); } + /// + public string SerializeSingle(SimulatorEvent evt) + { + var cloudEvent = ConvertToCloudEvent(evt); + return JsonSerializer.Serialize(cloudEvent, _serializerOptions); + } + /// public string SerializeArray(IEnumerable events) { diff --git a/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs b/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs index 31481fd..b4fa30c 100644 --- a/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs +++ b/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs @@ -58,8 +58,10 @@ CancellationToken cancellationToken subscription.DeliverySchema ?? delivery.Topic.OutputSchema ?? delivery.InputSchema; var formatter = formatterFactory.GetFormatter(deliverySchema); - // Serialize the event - var json = formatter.Serialize(delivery.Event); + // Serialize the event (use single event format if configured) + var json = subscription.SingleEventDelivery == true + ? formatter.SerializeSingle(delivery.Event) + : formatter.Serialize(delivery.Event); // Get or create the sender var sender = GetOrCreateSender(subscription); @@ -171,8 +173,10 @@ EventSchema inputSchema var deliverySchema = subscription.DeliverySchema ?? topic.OutputSchema ?? inputSchema; var formatter = formatterFactory.GetFormatter(deliverySchema); - // Serialize the event - var json = formatter.Serialize(evt); + // Serialize the event (use single event format if configured) + var json = subscription.SingleEventDelivery == true + ? formatter.SerializeSingle(evt) + : formatter.Serialize(evt); // Get or create the sender var sender = GetOrCreateSender(subscription); diff --git a/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs b/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs index f3377f7..8ac4272 100644 --- a/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs +++ b/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs @@ -21,6 +21,13 @@ public string Serialize(SimulatorEvent evt) return JsonSerializer.Serialize(new[] { eventGridEvent }); } + /// + public string SerializeSingle(SimulatorEvent evt) + { + var eventGridEvent = ConvertToEventGridEvent(evt); + return JsonSerializer.Serialize(eventGridEvent); + } + /// public string SerializeArray(IEnumerable events) { diff --git a/src/AzureEventGridSimulator/Domain/Services/IEventSchemaFormatter.cs b/src/AzureEventGridSimulator/Domain/Services/IEventSchemaFormatter.cs index 7bb4e30..3cbf2ff 100644 --- a/src/AzureEventGridSimulator/Domain/Services/IEventSchemaFormatter.cs +++ b/src/AzureEventGridSimulator/Domain/Services/IEventSchemaFormatter.cs @@ -19,6 +19,7 @@ public interface IEventSchemaFormatter /// /// Serializes an event to JSON for delivery. + /// By default, wraps single events in an array for Azure Event Grid compatibility. /// /// /// The event to serialize. @@ -28,6 +29,18 @@ public interface IEventSchemaFormatter /// string Serialize(SimulatorEvent evt); + /// + /// Serializes a single event to JSON without array wrapper. + /// Used for Service Bus delivery which doesn't use array format. + /// + /// + /// The event to serialize. + /// + /// + /// The JSON representation of the single event. + /// + string SerializeSingle(SimulatorEvent evt); + /// /// Serializes multiple events to JSON for delivery. /// diff --git a/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs b/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs index 776a195..8eb7820 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs @@ -130,6 +130,14 @@ public string? EffectiveConnectionString [JsonConverter(typeof(JsonStringEnumConverter))] public EventSchema? DeliverySchema { get; init; } + /// + /// Gets or sets whether to send events as single objects without array wrapper. + /// When true, events are sent as individual objects (Azure behavior for Service Bus). + /// When false or null, events are sent in an array with a single event (default for backward compatibility). + /// + [JsonPropertyName("singleEventDelivery")] + public bool? SingleEventDelivery { get; init; } + /// /// Gets or sets the retry policy for this subscriber. /// If null, default Azure Event Grid retry behavior is used (enabled with 30 attempts, 24h TTL). From bed9a1e97718110153268ca5bafddad8b5bfa126 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:12:36 +0000 Subject: [PATCH 3/5] test: Add test for explicit false value of singleEventDelivery Address code review feedback by adding test for SingleEventDelivery = false Co-authored-by: pm7y <3075792+pm7y@users.noreply.github.com> --- .../ServiceBusEventDeliveryServiceTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs index 4c026e0..5e13ba0 100644 --- a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs +++ b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs @@ -301,4 +301,19 @@ public void GivenSubscriptionWithoutSingleEventDelivery_WhenConfigured_ThenPrope subscription.SingleEventDelivery.ShouldBeNull(); } + + [Fact] + public void GivenSubscriptionWithSingleEventDeliveryFalse_WhenConfigured_ThenPropertyIsFalse() + { + var subscription = new ServiceBusSubscriberSettings + { + Name = "TestSubscriber", + ConnectionString = + "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=abc123", + Queue = "my-queue", + SingleEventDelivery = false, + }; + + subscription.SingleEventDelivery.ShouldBe(false); + } } From d12b0009f0835e4c6d76ddf64e3c619defb8a266 Mon Sep 17 00:00:00 2001 From: Paul Mcilreavy <3075792+pm7y@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:02:23 +1000 Subject: [PATCH 4/5] deps: Nuget upgrade # Conflicts: # src/Directory.Packages.props --- .../Delivery/ServiceBusEventDeliveryService.cs | 14 ++++++++------ src/Directory.Packages.props | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs b/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs index b4fa30c..b656d35 100644 --- a/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs +++ b/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs @@ -59,9 +59,10 @@ CancellationToken cancellationToken var formatter = formatterFactory.GetFormatter(deliverySchema); // Serialize the event (use single event format if configured) - var json = subscription.SingleEventDelivery == true - ? formatter.SerializeSingle(delivery.Event) - : formatter.Serialize(delivery.Event); + var json = + subscription.SingleEventDelivery == true + ? formatter.SerializeSingle(delivery.Event) + : formatter.Serialize(delivery.Event); // Get or create the sender var sender = GetOrCreateSender(subscription); @@ -174,9 +175,10 @@ EventSchema inputSchema var formatter = formatterFactory.GetFormatter(deliverySchema); // Serialize the event (use single event format if configured) - var json = subscription.SingleEventDelivery == true - ? formatter.SerializeSingle(evt) - : formatter.Serialize(evt); + var json = + subscription.SingleEventDelivery == true + ? formatter.SerializeSingle(evt) + : formatter.Serialize(evt); // Get or create the sender var sender = GetOrCreateSender(subscription); diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index cc396f7..89b5caf 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -34,7 +34,7 @@ - + @@ -44,6 +44,6 @@ - + From b9d01564246c782e73841c344adf7698ead7375f Mon Sep 17 00:00:00 2001 From: Paul Mcilreavy <3075792+pm7y@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:20:29 +1000 Subject: [PATCH 5/5] fix!: Send Service Bus events as single objects instead of arrays Service Bus delivery now always serializes events as single JSON objects without array wrapping, matching Azure Event Grid's actual behavior. Previously events were wrapped in a single-element array which forced consumers to add unnecessary array-handling logic. Removes the singleEventDelivery configuration option in favour of always using the correct Azure behavior. Also updates the commit-msg hook to support the ! breaking change indicator per the Conventional Commits spec. BREAKING CHANGE: Service Bus message bodies are now single JSON objects instead of single-element arrays. Consumers that parse the array format will need to be updated. Co-Authored-By: Claude Opus 4.6 --- .githooks/commit-msg | 10 +++- .../ServiceBusEventDeliveryServiceTests.cs | 46 +++---------------- .../ServiceBusEventDeliveryService.cs | 14 ++---- .../ServiceBusSubscriberSettings.cs | 8 ---- wiki | 2 +- 5 files changed, 19 insertions(+), 61 deletions(-) diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 8d68861..d4cdbe2 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -15,9 +15,10 @@ if echo "$commit_msg" | grep -qE "^Merge "; then exit 0 fi -# Conventional commit pattern: type(optional scope): description +# Conventional commit pattern: type(optional scope)(optional !): description # Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, deps -pattern="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|deps)(\(.+\))?: .+" +# The optional ! before the colon indicates a breaking change. +pattern="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|deps)(\(.+\))?!?: .+" if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then echo "" @@ -25,6 +26,8 @@ if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then echo "" echo "Expected format: : " echo " (): " + echo " !: " + echo " ()!: " echo "" echo "Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, deps" echo "" @@ -33,8 +36,11 @@ if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then echo " fix: resolve validation error" echo " docs: update README" echo " chore(deps): update dependencies" + echo " fix!: change Service Bus message format" + echo " feat(api)!: remove deprecated endpoint" echo "" echo "Note: There must be a space after the colon." + echo " Use ! before the colon to indicate a breaking change." echo "" echo "Your commit message was:" echo " $(head -1 "$commit_msg_file")" diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs index 5e13ba0..a0af4bc 100644 --- a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs +++ b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs @@ -184,7 +184,7 @@ EventSchema schema } [Fact] - public void GivenEventGridFormatter_WhenSerializing_ThenReturnsJson() + public void GivenEventGridFormatter_WhenSerializing_ThenReturnsJsonArray() { var formatter = _formatterFactory.GetFormatter(EventSchema.EventGridSchema); var evt = CreateTestEvent(); @@ -193,10 +193,12 @@ public void GivenEventGridFormatter_WhenSerializing_ThenReturnsJson() json.ShouldNotBeNullOrEmpty(); json.ShouldContain("test-event-id"); + json.TrimStart().ShouldStartWith("["); + json.TrimEnd().ShouldEndWith("]"); } [Fact] - public void GivenCloudEventFormatter_WhenSerializing_ThenReturnsJson() + public void GivenCloudEventFormatter_WhenSerializing_ThenReturnsJsonArray() { var formatter = _formatterFactory.GetFormatter(EventSchema.CloudEventV1_0); var evt = CreateTestEvent(); @@ -205,6 +207,8 @@ public void GivenCloudEventFormatter_WhenSerializing_ThenReturnsJson() json.ShouldNotBeNullOrEmpty(); json.ShouldContain("test-event-id"); + json.TrimStart().ShouldStartWith("["); + json.TrimEnd().ShouldEndWith("]"); } [Fact] @@ -278,42 +282,4 @@ public void GivenEventGridFormatter_WhenSerializingSingle_ThenReturnsJsonWithout json.TrimStart().ShouldStartWith("{"); json.TrimEnd().ShouldEndWith("}"); } - - [Fact] - public void GivenSubscriptionWithSingleEventDelivery_WhenConfigured_ThenPropertyIsSet() - { - var subscription = new ServiceBusSubscriberSettings - { - Name = "TestSubscriber", - ConnectionString = - "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=abc123", - Queue = "my-queue", - SingleEventDelivery = true, - }; - - subscription.SingleEventDelivery.ShouldBe(true); - } - - [Fact] - public void GivenSubscriptionWithoutSingleEventDelivery_WhenConfigured_ThenPropertyIsNull() - { - var subscription = CreateValidQueueSettings(); - - subscription.SingleEventDelivery.ShouldBeNull(); - } - - [Fact] - public void GivenSubscriptionWithSingleEventDeliveryFalse_WhenConfigured_ThenPropertyIsFalse() - { - var subscription = new ServiceBusSubscriberSettings - { - Name = "TestSubscriber", - ConnectionString = - "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=abc123", - Queue = "my-queue", - SingleEventDelivery = false, - }; - - subscription.SingleEventDelivery.ShouldBe(false); - } } diff --git a/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs b/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs index b656d35..15e81f6 100644 --- a/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs +++ b/src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs @@ -58,11 +58,8 @@ CancellationToken cancellationToken subscription.DeliverySchema ?? delivery.Topic.OutputSchema ?? delivery.InputSchema; var formatter = formatterFactory.GetFormatter(deliverySchema); - // Serialize the event (use single event format if configured) - var json = - subscription.SingleEventDelivery == true - ? formatter.SerializeSingle(delivery.Event) - : formatter.Serialize(delivery.Event); + // Serialize as a single event (matches Azure Event Grid to Service Bus behavior) + var json = formatter.SerializeSingle(delivery.Event); // Get or create the sender var sender = GetOrCreateSender(subscription); @@ -174,11 +171,8 @@ EventSchema inputSchema var deliverySchema = subscription.DeliverySchema ?? topic.OutputSchema ?? inputSchema; var formatter = formatterFactory.GetFormatter(deliverySchema); - // Serialize the event (use single event format if configured) - var json = - subscription.SingleEventDelivery == true - ? formatter.SerializeSingle(evt) - : formatter.Serialize(evt); + // Serialize as a single event (matches Azure Event Grid to Service Bus behavior) + var json = formatter.SerializeSingle(evt); // Get or create the sender var sender = GetOrCreateSender(subscription); diff --git a/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs b/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs index 8eb7820..776a195 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/ServiceBusSubscriberSettings.cs @@ -130,14 +130,6 @@ public string? EffectiveConnectionString [JsonConverter(typeof(JsonStringEnumConverter))] public EventSchema? DeliverySchema { get; init; } - /// - /// Gets or sets whether to send events as single objects without array wrapper. - /// When true, events are sent as individual objects (Azure behavior for Service Bus). - /// When false or null, events are sent in an array with a single event (default for backward compatibility). - /// - [JsonPropertyName("singleEventDelivery")] - public bool? SingleEventDelivery { get; init; } - /// /// Gets or sets the retry policy for this subscriber. /// If null, default Azure Event Grid retry behavior is used (enabled with 30 attempts, 24h TTL). diff --git a/wiki b/wiki index 8638568..bf6f87f 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 8638568a117a71446e81eb7a927be343c0f0f963 +Subproject commit bf6f87fc527d6c9c6bc1db5cd61bd0b443cd83cf