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

  1. Search for "Event Grid Topics" in Azure Portal
  2. Click + Create
  3. 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

PracticeDescription
Use CloudEvents schemaIndustry-standard format
Design subject hierarchyEnable filtering at multiple levels
Version your dataInclude dataVersion in events
Implement idempotencyHandle duplicate events gracefully
Monitor topic healthTrack 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


Azure Integration Hub - Intermediate Level