Skip to content

Commit d033b19

Browse files
Copilotpm7yclaude
authored
fix!: Send Service Bus events as single objects instead of arrays (#261)
* Initial plan * 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> * 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> * deps: Nuget upgrade # Conflicts: # src/Directory.Packages.props * 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 <noreply@anthropic.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pm7y <3075792+pm7y@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bae6b1b commit d033b19

8 files changed

Lines changed: 78 additions & 11 deletions

File tree

.githooks/commit-msg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ if echo "$commit_msg" | grep -qE "^Merge "; then
1515
exit 0
1616
fi
1717

18-
# Conventional commit pattern: type(optional scope): description
18+
# Conventional commit pattern: type(optional scope)(optional !): description
1919
# Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, deps
20-
pattern="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|deps)(\(.+\))?: .+"
20+
# The optional ! before the colon indicates a breaking change.
21+
pattern="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|deps)(\(.+\))?!?: .+"
2122

2223
if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then
2324
echo ""
2425
echo "ERROR: Commit message does not follow Conventional Commits format."
2526
echo ""
2627
echo "Expected format: <type>: <description>"
2728
echo " <type>(<scope>): <description>"
29+
echo " <type>!: <description>"
30+
echo " <type>(<scope>)!: <description>"
2831
echo ""
2932
echo "Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, deps"
3033
echo ""
@@ -33,8 +36,11 @@ if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then
3336
echo " fix: resolve validation error"
3437
echo " docs: update README"
3538
echo " chore(deps): update dependencies"
39+
echo " fix!: change Service Bus message format"
40+
echo " feat(api)!: remove deprecated endpoint"
3641
echo ""
3742
echo "Note: There must be a space after the colon."
43+
echo " Use ! before the colon to indicate a breaking change."
3844
echo ""
3945
echo "Your commit message was:"
4046
echo " $(head -1 "$commit_msg_file")"

src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ EventSchema schema
184184
}
185185

186186
[Fact]
187-
public void GivenEventGridFormatter_WhenSerializing_ThenReturnsJson()
187+
public void GivenEventGridFormatter_WhenSerializing_ThenReturnsJsonArray()
188188
{
189189
var formatter = _formatterFactory.GetFormatter(EventSchema.EventGridSchema);
190190
var evt = CreateTestEvent();
@@ -193,10 +193,12 @@ public void GivenEventGridFormatter_WhenSerializing_ThenReturnsJson()
193193

194194
json.ShouldNotBeNullOrEmpty();
195195
json.ShouldContain("test-event-id");
196+
json.TrimStart().ShouldStartWith("[");
197+
json.TrimEnd().ShouldEndWith("]");
196198
}
197199

198200
[Fact]
199-
public void GivenCloudEventFormatter_WhenSerializing_ThenReturnsJson()
201+
public void GivenCloudEventFormatter_WhenSerializing_ThenReturnsJsonArray()
200202
{
201203
var formatter = _formatterFactory.GetFormatter(EventSchema.CloudEventV1_0);
202204
var evt = CreateTestEvent();
@@ -205,6 +207,8 @@ public void GivenCloudEventFormatter_WhenSerializing_ThenReturnsJson()
205207

206208
json.ShouldNotBeNullOrEmpty();
207209
json.ShouldContain("test-event-id");
210+
json.TrimStart().ShouldStartWith("[");
211+
json.TrimEnd().ShouldEndWith("]");
208212
}
209213

210214
[Fact]
@@ -248,4 +252,34 @@ public void GivenPropertyResolver_WhenResolvingDynamicProperty_ThenReturnsEventV
248252

249253
resolved["Subject"].ShouldBe("test/subject");
250254
}
255+
256+
[Fact]
257+
public void GivenCloudEventFormatter_WhenSerializingSingle_ThenReturnsJsonWithoutArray()
258+
{
259+
var formatter = _formatterFactory.GetFormatter(EventSchema.CloudEventV1_0);
260+
var evt = CreateTestEvent();
261+
262+
var json = formatter.SerializeSingle(evt);
263+
264+
json.ShouldNotBeNullOrEmpty();
265+
json.ShouldContain("test-event-id");
266+
// Verify it's NOT an array (doesn't start with '[')
267+
json.TrimStart().ShouldStartWith("{");
268+
json.TrimEnd().ShouldEndWith("}");
269+
}
270+
271+
[Fact]
272+
public void GivenEventGridFormatter_WhenSerializingSingle_ThenReturnsJsonWithoutArray()
273+
{
274+
var formatter = _formatterFactory.GetFormatter(EventSchema.EventGridSchema);
275+
var evt = CreateTestEvent();
276+
277+
var json = formatter.SerializeSingle(evt);
278+
279+
json.ShouldNotBeNullOrEmpty();
280+
json.ShouldContain("test-event-id");
281+
// Verify it's NOT an array (doesn't start with '[')
282+
json.TrimStart().ShouldStartWith("{");
283+
json.TrimEnd().ShouldEndWith("}");
284+
}
251285
}

src/AzureEventGridSimulator/Domain/Services/CloudEventSchemaFormatter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ public string Serialize(SimulatorEvent evt)
3333
return JsonSerializer.Serialize(new[] { cloudEvent }, _serializerOptions);
3434
}
3535

36+
/// <inheritdoc />
37+
public string SerializeSingle(SimulatorEvent evt)
38+
{
39+
var cloudEvent = ConvertToCloudEvent(evt);
40+
return JsonSerializer.Serialize(cloudEvent, _serializerOptions);
41+
}
42+
3643
/// <inheritdoc />
3744
public string SerializeArray(IEnumerable<SimulatorEvent> events)
3845
{

src/AzureEventGridSimulator/Domain/Services/Delivery/ServiceBusEventDeliveryService.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ CancellationToken cancellationToken
5858
subscription.DeliverySchema ?? delivery.Topic.OutputSchema ?? delivery.InputSchema;
5959
var formatter = formatterFactory.GetFormatter(deliverySchema);
6060

61-
// Serialize the event
62-
var json = formatter.Serialize(delivery.Event);
61+
// Serialize as a single event (matches Azure Event Grid to Service Bus behavior)
62+
var json = formatter.SerializeSingle(delivery.Event);
6363

6464
// Get or create the sender
6565
var sender = GetOrCreateSender(subscription);
@@ -171,8 +171,8 @@ EventSchema inputSchema
171171
var deliverySchema = subscription.DeliverySchema ?? topic.OutputSchema ?? inputSchema;
172172
var formatter = formatterFactory.GetFormatter(deliverySchema);
173173

174-
// Serialize the event
175-
var json = formatter.Serialize(evt);
174+
// Serialize as a single event (matches Azure Event Grid to Service Bus behavior)
175+
var json = formatter.SerializeSingle(evt);
176176

177177
// Get or create the sender
178178
var sender = GetOrCreateSender(subscription);

src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ public string Serialize(SimulatorEvent evt)
2121
return JsonSerializer.Serialize(new[] { eventGridEvent });
2222
}
2323

24+
/// <inheritdoc />
25+
public string SerializeSingle(SimulatorEvent evt)
26+
{
27+
var eventGridEvent = ConvertToEventGridEvent(evt);
28+
return JsonSerializer.Serialize(eventGridEvent);
29+
}
30+
2431
/// <inheritdoc />
2532
public string SerializeArray(IEnumerable<SimulatorEvent> events)
2633
{

src/AzureEventGridSimulator/Domain/Services/IEventSchemaFormatter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public interface IEventSchemaFormatter
1919

2020
/// <summary>
2121
/// Serializes an event to JSON for delivery.
22+
/// By default, wraps single events in an array for Azure Event Grid compatibility.
2223
/// </summary>
2324
/// <param name="evt">
2425
/// The event to serialize.
@@ -28,6 +29,18 @@ public interface IEventSchemaFormatter
2829
/// </returns>
2930
string Serialize(SimulatorEvent evt);
3031

32+
/// <summary>
33+
/// Serializes a single event to JSON without array wrapper.
34+
/// Used for Service Bus delivery which doesn't use array format.
35+
/// </summary>
36+
/// <param name="evt">
37+
/// The event to serialize.
38+
/// </param>
39+
/// <returns>
40+
/// The JSON representation of the single event.
41+
/// </returns>
42+
string SerializeSingle(SimulatorEvent evt);
43+
3144
/// <summary>
3245
/// Serializes multiple events to JSON for delivery.
3346
/// </summary>

src/Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
3535
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
3636
<!-- Analyzers -->
37-
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.302" />
37+
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.1" />
3838
<!-- Conformance Testing CLI -->
3939
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
4040
<!-- Testing -->
@@ -44,6 +44,6 @@
4444
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
4545
<PackageVersion Include="xunit" Version="2.9.3" />
4646
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
47-
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
47+
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
4848
</ItemGroup>
4949
</Project>

wiki

Submodule wiki updated from 8638568 to bf6f87f

0 commit comments

Comments
 (0)