Azure Event Grid Custom Topics
Publishing Application Events to Event Grid
Introduction
Custom Event Grid topics allow you to publish your own application events to Event Grid, enabling event-driven architectures where your applications can emit events and have multiple subscribers react to them. This is perfect for building loosely coupled, scalable systems where different parts of your application can communicate through events.
This comprehensive guide covers:
- Creating topics — Setting up custom Event Grid topics
- Publishing events — Sending events from your applications
- Subscribing — Creating event subscriptions with filtering
- Event schema — Understanding event structure
- Best practices — Designing effective event systems
Creating Custom Topics
Azure Portal
- Search for "Event Grid Topics" in Azure Portal
- Click + Create
- Configure:
- Subscription: Your Azure subscription
- Resource Group: Existing or new resource group
- Topic Name: Unique name (e.g.,
orders-events) - Region: Choose region
- Schema: CloudEvents or Event Grid schema
Azure CLI
# Create Event Grid topic
az eventgrid topic create \
--name orders-events \
--resource-group my-rg \
--location eastus \
--sku Standard \
--input-schema CloudEventSchemaV1_0
# Verify creation
az eventgrid topic show \
--name orders-events \
--resource-group my-rg \
--query "endpoint"
ARM Template
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"resources": [
{
"type": "Microsoft.EventGrid/topics",
"apiVersion": "2022-06-15",
"name": "orders-events",
"location": "eastus",
"properties": {
"inputSchema": "CloudEventSchemaV1_0",
"publicNetworkAccess": "Enabled"
},
"sku": {
"name": "Standard"
}
}
]
}
Get Access Keys
# Get topic keys
az eventgrid topic key show \
--name orders-events \
--resource-group my-rg \
--query "key1"
Publishing Events
.NET SDK
using Azure.Messaging.EventGrid;
// Create publisher client
var topicCredential = new AzureKeyCredential("your-topic-key");
var publisher = new EventGridPublisherClient(
new Uri("https://orders-events.eastus-1.eventgrid.azure.net"),
topicCredential);
// Create event
var orderCreatedEvent = new EventGridEvent(
subject: "orders/12345",
eventType: "Order.Created",
dataVersion: "1.0",
data: new
{
orderId = "12345",
customerId = "cust-001",
customerName = "John Doe",
totalAmount = 99.99,
items = new[] { new { productId = "prod-1", quantity = 2 } }
});
// Send event
await publisher.SendEventAsync(orderCreatedEvent);
// Or send multiple events
var events = new EventGridEvent[]
{
orderCreatedEvent,
new EventGridEvent(
subject: "orders/12345",
eventType: "Order.Shipped",
dataVersion: "1.0",
data: new { trackingNumber = "TRACK123" })
};
await publisher.SendEventsAsync(events);
Publishing with CloudEvents
using CloudNative.CloudEvents;
// Create CloudEvent
var cloudEvent = new CloudEvent(
source: new Uri("https://myapp.com/orders"),
type: "Order.Created",
dataContentType: "application/json",
data: new { orderId = "12345" }
);
// Send via Event Grid
var cloudEventFormatter = new CloudNative.CloudEvents.Formatters.JSON.CloudEventFormatter();
var binaryData = cloudEventFormatter.EncodeStructuredEvent(cloudEvent);
// Use REST API
var client = new HttpClient();
client.DefaultRequestHeaders.Add("aeg-sas-key", "your-topic-key");
await client.PostAsync(
"https://orders-events.eastus-1.eventgrid.azure.net/api/events",
new ByteArrayContent(binaryData));
REST API
TOPIC_KEY="your-topic-key"
TOPIC_URL="https://orders-events.eastus-1.eventgrid.azure.net/api/events"
curl -X POST $TOPIC_URL \
-H "Content-Type: application/json" \
-H "aeg-sas-key: $TOPIC_KEY" \
-d '[
{
"id": "12345-6789",
"subject": "orders/12345",
"eventType": "Order.Created",
"eventTime": "2024-01-15T10:30:00Z",
"data": {
"orderId": "12345",
"customerId": "cust-001"
},
"dataVersion": "1.0"
}
]'
Subscribing to Events
Create Subscription
# Webhook subscription
az eventgrid event-subscription create \
--name process-orders \
--topic-name orders-events \
--resource-group my-rg \
--endpoint-type webhook \
--endpoint https://myfunction.azurewebsites.net/api/handler
# Azure Function subscription
az eventgrid event-subscription create \
--name orders-processor \
--topic-name orders-events \
--resource-group my-rg \
--endpoint-type azurefunction \
--endpoint /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Web/sites/my-function-app/functions/orderHandler
# Service Bus Topic subscription
az eventgrid event-subscription create \
--name send-to-service-bus \
--topic-name orders-events \
--resource-group my-rg \
--endpoint-type servicebusqueuetopic \
--endpoint /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/my-namespace/topics/orders-events
Filter Events
{
"filters": {
"subjectBeginsWith": "orders/",
"subjectEndsWith": "",
"includedEventTypes": [
"Order.Created",
"Order.Updated",
"Order.Shipped"
],
"advancedFilters": [
{
"key": "data.totalAmount",
"operatorType": "GreaterThan",
"value": 100
}
]
}
}
Subject-Based Filtering
# Subscribe only to specific subject pattern
az eventgrid event-subscription create \
--name process-orders \
--topic-name orders-events \
--resource-group my-rg \
--endpoint-type webhook \
--endpoint https://... \
--filter-include-subject-pattern "orders/*"
Event Structure
Event Grid Event Schema
{
"id": "12345-abcde",
"subject": "orders/12345",
"eventType": "Order.Created",
"eventTime": "2024-01-15T10:30:00Z",
"data": {
"orderId": "12345",
"customerId": "cust-001",
"totalAmount": 99.99,
"items": [
{ "productId": "prod-1", "quantity": 2, "price": 29.99 },
{ "productId": "prod-2", "quantity": 1, "price": 39.99 }
]
},
"dataVersion": "1.0",
"metadataVersion": "1"
}
Subject Design Patterns
┌─────────────────────────────────────────────────────────────────────┐
│ SUBJECT NAMING CONVENTIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Pattern 1: Resource Type + ID │
│ ───────────────────────────────────────────────────────────────── │
│ orders/12345 → Order 12345 events │
│ customers/cust-001 → Customer events │
│ products/prod-456 → Product events │
│ │
│ Pattern 2: Resource Type + Action + ID │
│ ───────────────────────────────────────────────────────────────── │
│ orders/12345/created → Order created event │
│ orders/12345/shipped → Order shipped event │
│ orders/12345/cancelled → Order cancelled event │
│ │
│ Pattern 3: Business Domain │
│ ───────────────────────────────────────────────────────────────── │
│ orders/new → All new orders │
│ orders/updated → All updated orders │
│ inventory/low-stock → Low stock alerts │
│ │
└─────────────────────────────────────────────────────────────────────┘
Complete Example: Order System
Publisher: Order Service
public class OrderService
{
private readonly EventGridPublisherClient _publisher;
public async Task PublishOrderCreatedAsync(Order order)
{
var evt = new EventGridEvent(
subject: $"orders/{order.Id}",
eventType: "Order.Created",
dataVersion: "1.0",
data: new
{
orderId = order.Id,
customerId = order.CustomerId,
customerName = order.CustomerName,
totalAmount = order.Total,
itemCount = order.Items.Count,
orderDate = order.CreatedAt
});
await _publisher.SendEventAsync(evt);
}
public async Task PublishOrderShippedAsync(Order order, string trackingNumber)
{
var evt = new EventGridEvent(
subject: $"orders/{order.Id}/shipped",
eventType: "Order.Shipped",
dataVersion: "1.0",
data: new
{
orderId = order.Id,
trackingNumber = trackingNumber,
shippedAt = DateTime.UtcNow,
carrier = "FedEx"
});
await _publisher.SendEventAsync(evt);
}
}
Subscriber 1: Email Notifications
[FunctionName("OrderEmailNotifications")]
public async Task Run([EventGridTrigger] EventGridEvent evt, ILogger log)
{
if (evt.EventType == "Order.Created")
{
var data = evt.Data.ToObjectFromJson<OrderCreatedData>();
await _emailService.SendOrderConfirmationAsync(data);
}
else if (evt.EventType == "Order.Shipped")
{
var data = evt.Data.ToObjectFromJson<OrderShippedData>();
await _emailService.SendShippingNotificationAsync(data);
}
}
Subscriber 2: Analytics
[FunctionName("OrderAnalytics")]
public async Task Run([EventGridTrigger] EventGridEvent evt, ILogger log)
{
var data = evt.Data.ToObjectFromJson<dynamic>();
// Store in data warehouse
await _analyticsService.RecordOrderEventAsync(new AnalyticsEvent
{
EventType = evt.EventType,
CustomerId = data.customerId,
Amount = data.totalAmount,
Timestamp = evt.EventTime
});
}
Subscriber 3: Inventory
[FunctionName("OrderInventory")]
public async Task Run([EventGridTrigger] EventGridEvent evt, ILogger log)
{
if (evt.EventType == "Order.Created")
{
var data = evt.Data.ToObjectFromJson<OrderCreatedData>();
// Reserve inventory for each item
foreach (var item in data.Items)
{
await _inventoryService.ReserveAsync(item.ProductId, item.Quantity);
}
}
}
Best Practices
| Practice | Description |
|---|---|
| Use CloudEvents schema | Industry-standard format |
| Design subject hierarchy | Enable filtering at multiple levels |
| Version your data | Include dataVersion in events |
| Implement idempotency | Handle duplicate events gracefully |
| Monitor topic health | Track publish/delivery metrics |
Security
# Enable managed identity on topic
az eventgrid topic update \
--name orders-events \
--resource-group my-rg \
--identity systemassigned
# Use Azure AD authentication
az eventgrid topic update \
--name orders-events \
--resource-group my-rg \
--public-network-access Enabled
Related Topics
- Event Grid Concepts — Event Grid fundamentals
- Blob Events — Storage events
- Logic Apps Events — Logic Apps triggers
Azure Integration Hub - Intermediate Level