Skip to content

Commit c164b62

Browse files
authored
feat: Add Event Hub Subscriber support (#227)
* feat: Add Event Hub Subscriber support
1 parent 805594a commit c164b62

23 files changed

Lines changed: 1408 additions & 21 deletions

AGENTS.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Azure Event Grid Simulator is a local development simulator that provides HTTPS
1414
**Key Features:**
1515

1616
- Multi-topic support with individual HTTPS endpoints
17-
- HTTP webhook, Azure Service Bus, and Storage Queue subscriber delivery
17+
- HTTP webhook, Azure Service Bus, Storage Queue, and Event Hub subscriber delivery
1818
- Azure Event Grid-compatible retry with exponential backoff
1919
- Dead-letter support with local JSON file output
2020
- Event filtering (subject-based and advanced)
@@ -50,7 +50,7 @@ dotnet dev-certs https --trust
5050
The simulator uses `appsettings.json` for topic and subscriber configuration. Key settings:
5151

5252
- **Topics**: Each topic requires `name`, `port`, and optional `key` for authentication
53-
- **Subscribers**: Supports HTTP webhooks, Azure Service Bus (queues/topics), and Storage Queues
53+
- **Subscribers**: Supports HTTP webhooks, Azure Service Bus (queues/topics), Storage Queues, and Event Hubs
5454
- **Filtering**: Event type, subject-based, and advanced filtering per subscriber
5555
- **Retry Policy**: Configurable per subscriber (maxDeliveryAttempts, eventTimeToLiveInMinutes, enabled)
5656
- **Dead-Letter**: Configurable per subscriber (enabled, folderPath for JSON output)
@@ -127,7 +127,7 @@ docker-compose -f /src/AzureEventGridSimulator/docker-compose.yml down
127127
│ │ ├── Commands/ # Command handlers
128128
│ │ ├── Entities/ # Domain models (including PendingDelivery, DeadLetterEvent)
129129
│ │ └── Services/ # Domain services
130-
│ │ ├── Delivery/ # Event delivery services (HTTP, ServiceBus, StorageQueue)
130+
│ │ ├── Delivery/ # Event delivery services (HTTP, ServiceBus, StorageQueue, EventHub)
131131
│ │ └── Retry/ # Retry infrastructure (queue, scheduler, dead-letter)
132132
│ ├── Infrastructure/ # Cross-cutting concerns
133133
│ │ ├── Extensions/ # Extension methods
@@ -259,13 +259,34 @@ Edit `appsettings.json` or `docker/appsettings.docker.json`:
259259
"endpoint": "https://example.com/webhook",
260260
"disableValidation": false
261261
}
262+
],
263+
"eventHub": [
264+
{
265+
"name": "MyEventHubSubscriber",
266+
"connectionString": "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=abc123",
267+
"eventHubName": "my-event-hub",
268+
"deliverySchema": "CloudEventV1_0",
269+
"properties": {
270+
"Label": { "type": "dynamic", "value": "Subject" },
271+
"Region": { "type": "static", "value": "west-us" }
272+
}
273+
}
262274
]
263275
}
264276
}
265277
]
266278
}
267279
```
268280

281+
Event Hub subscribers support:
282+
- **connectionString**: Full connection string (or use namespace/sharedAccessKeyName/sharedAccessKey separately)
283+
- **eventHubName**: Target Event Hub name (required)
284+
- **deliverySchema**: Output schema (EventGridSchema or CloudEventV1_0)
285+
- **properties**: Custom properties added to EventData (static or dynamic values from event fields)
286+
- **filter**: Subject-based and advanced event filtering
287+
- **retryPolicy**: Retry configuration (maxDeliveryAttempts, eventTimeToLiveInMinutes)
288+
- **deadLetter**: Dead-letter configuration for failed deliveries
289+
269290
### Testing Event Publishing
270291

271292
```bash

README.md

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ of [Azure Event Grid](https://azure.microsoft.com/en-au/services/event-grid/) to
1212
with the `Microsoft.Azure.EventGrid` client library. Both the `EventGrid` schema and the `CloudEvents v1.0` schema are
1313
supported.
1414

15-
> **Note:** This simulator is intended for **local development and testing only**. It is not designed for production use. For production workloads, use the official [Azure Event Grid](https://azure.microsoft.com/en-au/services/event-grid/) service.
15+
> **Note:** This simulator is intended for **local development and testing only**. It is not designed for production
16+
> use. For production workloads, use the
17+
> official [Azure Event Grid](https://azure.microsoft.com/en-au/services/event-grid/) service.
1618
1719
## Installation
1820

@@ -98,11 +100,15 @@ An example of one topic with one subscriber is shown below.
98100
| `serviceBusSharedAccessKeyName` | (Optional) Default shared access key name for Service Bus. |
99101
| `serviceBusSharedAccessKey` | (Optional) Default shared access key for Service Bus. |
100102
| `storageQueueConnectionString` | (Optional) Default Storage Queue connection string for all Storage Queue subscribers in this topic. Subscribers can override with their own. |
103+
| `eventHubConnectionString` | (Optional) Default Event Hub connection string for all Event Hub subscribers in this topic. Subscribers can override with their own. |
104+
| `eventHubNamespace` | (Optional) Default Event Hub namespace (without `.servicebus.windows.net`). Use with `eventHubSharedAccessKeyName` and `eventHubSharedAccessKey`. |
105+
| `eventHubSharedAccessKeyName` | (Optional) Default shared access key name for Event Hub. |
106+
| `eventHubSharedAccessKey` | (Optional) Default shared access key for Event Hub. |
101107

102108
### Subscriber Settings
103109

104-
The simulator supports three subscriber types: **HTTP webhooks**, **Azure Service Bus** (queues and topics), and **Azure
105-
Storage Queues**.
110+
The simulator supports four subscriber types: **HTTP webhooks**, **Azure Service Bus** (queues and topics), **Azure
111+
Storage Queues**, and **Azure Event Hubs**.
106112

107113
#### Grouped Format (Recommended)
108114

@@ -128,6 +134,13 @@ Storage Queues**.
128134
"connectionString": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net",
129135
"queueName": "my-queue"
130136
}
137+
],
138+
"eventHub": [
139+
{
140+
"name": "EventHubSubscription",
141+
"connectionString": "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=...",
142+
"eventHubName": "my-event-hub"
143+
}
131144
]
132145
}
133146
}
@@ -214,6 +227,31 @@ You can add custom application properties to Service Bus messages using static o
214227
| `retryPolicy` | (Optional) Retry policy settings. See [Retry & Dead-Letter](#retry--dead-letter) section. |
215228
| `deadLetter` | (Optional) Dead-letter settings. See [Retry & Dead-Letter](#retry--dead-letter) section. |
216229

230+
#### Event Hub Subscriber Settings
231+
232+
| Setting | Description |
233+
|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
234+
| `name` | The name of the subscriber. |
235+
| `connectionString` | The Event Hub connection string. Can be omitted if `eventHubConnectionString` is set at the topic level. |
236+
| `namespace` | The Event Hub namespace (without `.servicebus.windows.net` suffix). Alternative to `connectionString`. Can inherit from topic-level settings. |
237+
| `sharedAccessKeyName` | The shared access key name (e.g., `RootManageSharedAccessKey`). Used with `namespace`. |
238+
| `sharedAccessKey` | The shared access key. Used with `namespace`. |
239+
| `eventHubName` | The name of the Event Hub to send events to. (Required) |
240+
| `deliverySchema` | (Optional) Override the delivery schema. Values: `EventGridSchema` or `CloudEventV1_0`. |
241+
| `properties` | (Optional) Custom delivery properties to add to Event Hub messages. See Service Bus Delivery Properties above for format. |
242+
| `filter` | (Optional) Event filtering configuration. See [Filtering Events](#filtering-events) section. |
243+
| `retryPolicy` | (Optional) Retry policy settings. See [Retry & Dead-Letter](#retry--dead-letter) section. |
244+
| `deadLetter` | (Optional) Dead-letter settings. See [Retry & Dead-Letter](#retry--dead-letter) section. |
245+
246+
Event Hub messages include standard Event Grid headers as properties:
247+
248+
- `aeg-event-type`: Always "Notification"
249+
- `aeg-subscription-name`: The subscriber name (uppercase)
250+
- `aeg-delivery-count`: The delivery attempt number
251+
- `aeg-output-event-id`: The event ID
252+
- `aeg-data-version`: The event data version (EventGrid schema only)
253+
- `aeg-metadata-version`: Always "1" (EventGrid schema only)
254+
217255
#### Complete Example
218256

219257
```json
@@ -225,6 +263,7 @@ You can add custom application properties to Service Bus messages using static o
225263
"key": "TheLocal+DevelopmentKey=",
226264
"serviceBusConnectionString": "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...",
227265
"storageQueueConnectionString": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=...;EndpointSuffix=core.windows.net",
266+
"eventHubConnectionString": "Endpoint=sb://my-eventhub-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...",
228267
"subscribers": {
229268
"http": [
230269
{
@@ -252,14 +291,26 @@ You can add custom application properties to Service Bus messages using static o
252291
"name": "OrdersStorageQueueSubscription",
253292
"queueName": "orders-archive"
254293
}
294+
],
295+
"eventHub": [
296+
{
297+
"name": "OrdersEventHubSubscription",
298+
"eventHubName": "orders-events",
299+
"deliverySchema": "CloudEventV1_0",
300+
"properties": {
301+
"OrderId": { "type": "dynamic", "value": "data.orderId" },
302+
"Region": { "type": "static", "value": "west-us" }
303+
}
304+
}
255305
]
256306
}
257307
}
258308
]
259309
}
260310
```
261311

262-
Note: The `serviceBus` and `storageQueue` subscribers above inherit their connection strings from the topic-level
312+
Note: The `serviceBus`, `storageQueue`, and `eventHub` subscribers above inherit their connection strings from the
313+
topic-level
263314
settings. Subscribers can override these by specifying their own `connectionString`.
264315

265316
### App Settings
@@ -576,9 +627,13 @@ The Docker Compose setup includes:
576627

577628
- **Azure Event Grid Simulator** - The main simulator
578629
- **Azure Service Bus Emulator** - For testing Service Bus subscribers locally
630+
- **Azure Event Hubs Emulator** - For testing Event Hub subscribers locally
631+
- **Azurite** - Azure Storage emulator (required by Event Hubs emulator, also for Storage Queue subscribers)
579632
- **SQL Server** - Required by the Service Bus emulator
633+
- **Seq** - Structured log viewer (accessible at http://localhost:8081)
580634

581-
See `docker/appsettings.docker.json` for an example configuration with both HTTP and Service Bus subscribers.
635+
See `docker/appsettings.docker.json` for an example configuration with HTTP, Service Bus, Storage Queue, and Event Hub
636+
subscribers.
582637

583638
## Using the Simulator
584639

@@ -775,5 +830,4 @@ Some features that could be added if there was a need for them:
775830

776831
- Certificate configuration in `appsettings.json`.
777832
- Subscriber token auth.
778-
- Azure Event Hub subscriber support.
779833
- Web-based console for admin stats etc.

docker/appsettings.docker.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@
6868
"connectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://azurite:10001/devstoreaccount1;",
6969
"queueName": "eventgrid-events"
7070
}
71+
],
72+
"eventHub": [
73+
{
74+
"name": "EventHubSubscription",
75+
"connectionString": "Endpoint=sb://eventhubs-emulator;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
76+
"eventHubName": "eventgrid-events",
77+
"properties": {
78+
"EventType": {
79+
"type": "dynamic",
80+
"value": "EventType"
81+
},
82+
"Source": {
83+
"type": "static",
84+
"value": "AzureEventGridSimulator"
85+
}
86+
}
87+
}
7188
]
7289
}
7390
}

docker/docker-compose.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323

2424
depends_on:
2525
- servicebus-emulator
26+
- eventhubs-emulator
2627
- azurite
2728
- seq
2829

@@ -85,6 +86,27 @@ services:
8586
aliases:
8687
- servicebus-emulator
8788

89+
# Azure Event Hubs Emulator
90+
# See: https://learn.microsoft.com/en-us/azure/event-hubs/test-locally-with-event-hub-emulator
91+
eventhubs-emulator:
92+
container_name: eventhubs-emulator
93+
image: mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest
94+
pull_policy: always
95+
volumes:
96+
- ./eventhub-config.json:/Eventhubs_Emulator/ConfigFiles/Config.json
97+
ports:
98+
- "5673:5672" # AMQP port (5673 external to avoid conflict with Service Bus)
99+
environment:
100+
BLOB_SERVER: azurite
101+
METADATA_SERVER: azurite
102+
ACCEPT_EULA: "Y"
103+
depends_on:
104+
- azurite
105+
networks:
106+
aegs-network:
107+
aliases:
108+
- eventhubs-emulator
109+
88110
mssql:
89111
container_name: mssql
90112
image: mcr.microsoft.com/mssql/server:2022-latest

docker/eventhub-config.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"UserConfig": {
3+
"NamespaceConfig": [
4+
{
5+
"Type": "EventHub",
6+
"Name": "ehemulatorns",
7+
"Entities": [
8+
{
9+
"Name": "eventgrid-events",
10+
"PartitionCount": "2",
11+
"ConsumerGroups": [
12+
{
13+
"Name": "default"
14+
}
15+
]
16+
}
17+
]
18+
}
19+
],
20+
"LoggingConfig": {
21+
"Type": "File"
22+
}
23+
}
24+
}

docker/rebuild.ps1

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Rebuilds and restarts the Azure Event Grid Simulator Docker Compose stack.
5+
6+
.DESCRIPTION
7+
Stops all containers, rebuilds images with no cache, and starts the stack in detached mode.
8+
9+
.EXAMPLE
10+
./rebuild.ps1
11+
#>
12+
13+
$ErrorActionPreference = "Stop"
14+
15+
Push-Location $PSScriptRoot
16+
17+
try {
18+
Write-Host "Stopping containers..." -ForegroundColor Yellow
19+
docker-compose down
20+
21+
Write-Host "Rebuilding images (no cache)..." -ForegroundColor Yellow
22+
docker-compose build --no-cache
23+
24+
Write-Host "Starting containers..." -ForegroundColor Green
25+
docker-compose up -d
26+
27+
Write-Host "Done. Use 'docker-compose logs -f' to view logs." -ForegroundColor Cyan
28+
}
29+
finally {
30+
Pop-Location
31+
}

src/AzureEventGridSimulator.Tests/UnitTests/Configuration/ConfigurationLoadingTests.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Text;
12
using System.Text.Json;
23
using AzureEventGridSimulator.Infrastructure.Settings;
34
using Shouldly;
@@ -9,7 +10,53 @@ namespace AzureEventGridSimulator.Tests.UnitTests.Configuration;
910
public class ConfigurationLoadingTests
1011
{
1112
[Fact]
12-
public void TestConfigurationLoad()
13+
public void IConfigurationBind_ShouldLoadEventHubSubscribers()
14+
{
15+
const string json = """
16+
{
17+
"topics": [{
18+
"name": "TestTopic",
19+
"port": 60101,
20+
"key": "TheLocal+DevelopmentKey=",
21+
"subscribers": {
22+
"http": [{
23+
"name": "HttpSubscriber",
24+
"endpoint": "https://example.com/webhook",
25+
"disableValidation": true
26+
}],
27+
"eventHub": [{
28+
"name": "EventHubSubscriber",
29+
"connectionString": "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=testkey",
30+
"eventHubName": "test-hub"
31+
}]
32+
}
33+
}]
34+
}
35+
""";
36+
37+
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
38+
var configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();
39+
40+
var settings = new SimulatorSettings();
41+
configuration.Bind(settings);
42+
43+
settings.ShouldNotBeNull();
44+
settings.Topics.ShouldNotBeNull();
45+
settings.Topics.Length.ShouldBe(1);
46+
47+
var topic = settings.Topics.First();
48+
topic.Subscribers.HttpSubscribers.Count().ShouldBe(1);
49+
topic.Subscribers.HttpSubscribers.First().Name.ShouldBe("HttpSubscriber");
50+
51+
topic.Subscribers.EventHubSubscribers.Count().ShouldBe(1);
52+
var eventHubSubscriber = topic.Subscribers.EventHubSubscribers.First();
53+
eventHubSubscriber.Name.ShouldBe("EventHubSubscriber");
54+
eventHubSubscriber.EventHubName.ShouldBe("test-hub");
55+
eventHubSubscriber.ConnectionString.ShouldContain("sb://test.servicebus.windows.net");
56+
}
57+
58+
[Fact]
59+
public void JsonDeserialize_LegacyFormat_ShouldLoadHttpSubscribers()
1360
{
1461
// This test uses the legacy format (array of subscribers) to verify backwards compatibility
1562
const string json = """

0 commit comments

Comments
 (0)