Ở bài trước, chúng ta đã tìm hiểu Azure Event Grid là gì và khi nào nên sử dụng nó.

Mental model đơn giản nhất là:

“`text
Something happened
↓
Event Grid
↓
Notify interested subscribers
“`

Nhưng hiểu concept thôi vẫn chưa đủ.

Trong bài này chúng ta sẽ triển khai một flow hoàn chỉnh:

“`text
ASP.NET Core Order API
↓
OrderCreated
↓
Azure Event Grid Topic
↓
Event Subscription
↓
Azure Function
↓
Process event
“`

Sau đó chúng ta sẽ mở rộng thêm:

“`text
Filtering
Retry
Dead Letter
Idempotency
Observability
“`

Mục tiêu là hiểu Event Grid hoạt động như thế nào từ lúc application publish một event cho đến khi consumer xử lý nó trong production.

—

# 1. Architecture

Giả sử chúng ta có một Order API.

Khi user tạo order:

“`text
POST /orders
“`

API xử lý:

“`text
Create Order
↓
Save Database
↓
Publish OrderCreated
“`

Sau đó Event Grid route event tới Azure Function.

“`text
Azure

┌─────────────────────────────┐
│ ASP.NET Core Order API │
│ │
│ POST /orders │
│ ↓ │
│ Save Order │
│ ↓ │
│ Publish OrderCreated │
└─────────────┬───────────────┘
│
▼
┌───────────────────┐
│ Event Grid Topic │
└─────────┬─────────┘
│
▼
┌────────────────────┐
│ Event Subscription │
│ │
│ Filter: │
│ order.created │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Azure Function │
│ │
│ OrderCreated │
│ Handler │
└────────────────────┘
“`

Điểm quan trọng:

> Order API không gọi Azure Function trực tiếp.

Order API chỉ publish:

“`text
OrderCreated
“`

Event Grid chịu trách nhiệm route event đến subscriber phù hợp.

Đây chính là loose coupling.

—

# 2. Chuẩn bị Azure Resource Group

Đầu tiên login Azure CLI:

“`bash
az login
“`

Tạo resource group:

“`bash
az group create \
–name event-grid-demo-rg \
–location southeastasia
“`

Đăng ký Event Grid provider nếu subscription chưa có:

“`bash
az provider register \
–namespace Microsoft.EventGrid
“`

Kiểm tra:

“`bash
az provider show \
–namespace Microsoft.EventGrid \
–query registrationState
“`

Khi kết quả là:

“`text
Registered
“`

chúng ta có thể tiếp tục.

—

# 3. Tạo Event Grid Topic

Chúng ta sẽ sử dụng một custom topic:

“`text
order-events
“`

Tạo topic:

“`bash
az eventgrid topic create \
–name order-events \
–resource-group event-grid-demo-rg \
–location southeastasia \
–input-schema cloudeventschemav1_0
“`

Ở đây chúng ta chọn:

“`text
CloudEvents 1.0
“`

làm event schema.

Architecture hiện tại:

“`text
Order API
↓
order-events
“`

Chưa có consumer.

—

# 4. Lấy Topic Endpoint

Chạy:

“`bash
az eventgrid topic show \
–name order-events \
–resource-group event-grid-demo-rg \
–query endpoint \
–output tsv
“`

Kết quả sẽ tương tự:

“`text
https://order-events.-1.eventgrid.azure.net/api/events
“`

Application sẽ publish event tới endpoint này.

—

# 5. Authentication

Để demo đơn giản, chúng ta có thể sử dụng Event Grid Topic Key.

Lấy key:

“`bash
az eventgrid topic key list \
–name order-events \
–resource-group event-grid-demo-rg
“`

Production nên cân nhắc Managed Identity / Azure AD authentication thay vì hard-code secret trong application.

Không nên:

“`text
appsettings.json
↓
commit Event Grid key
↓
Git
“`

Secret nên được quản lý bằng:

“`text
Azure Key Vault

Environment variables

Managed Identity
“`

—

# 6. Tạo ASP.NET Core API

Tạo project:

“`bash
dotnet new webapi \
-n Order.Api
“`

Di chuyển vào project:

“`bash
cd Order.Api
“`

Cài Azure Event Grid SDK:

“`bash
dotnet add package Azure.Messaging.EventGrid
“`

—

# 7. Configuration

Ví dụ `appsettings.json`:

“`json
{
“EventGrid”: {
“Endpoint”: “https://order-events.-1.eventgrid.azure.net/api/events”,
“Key”: “”
}
}
“`

Không nên đặt key production trực tiếp ở đây.

Có thể inject bằng environment variable:

“`text
EventGrid__Key
“`

—

# 8. Tạo Event Grid Publisher

Tạo interface:

“`csharp
public interface IEventPublisher
{
Task PublishAsync(
string eventType,
string subject,
T data,
CancellationToken cancellationToken = default);
}
“`

Implementation:

“`csharp
using Azure;
using Azure.Messaging;
using Azure.Messaging.EventGrid;

public sealed class EventGridPublisher : IEventPublisher
{
private readonly EventGridPublisherClient _client;

public EventGridPublisher(IConfiguration configuration)
{
var endpoint =
configuration[“EventGrid:Endpoint”]
?? throw new InvalidOperationException(
“Event Grid endpoint is missing.”);

var key =
configuration[“EventGrid:Key”]
?? throw new InvalidOperationException(
“Event Grid key is missing.”);

_client = new EventGridPublisherClient(
new Uri(endpoint),
new AzureKeyCredential(key));
}

public async Task PublishAsync(
string eventType,
string subject,
T data,
CancellationToken cancellationToken = default)
{
var cloudEvent = new CloudEvent(
source: “/orders”,
type: eventType,
jsonSerializableData: data)
{
Subject = subject
};

await _client.SendEventAsync(
cloudEvent,
cancellationToken);
}
}
“`

Đăng ký DI:

“`csharp
builder.Services
.AddSingleton();
“`

—

# 9. Tạo Order API

Ví dụ request:

“`csharp
public sealed record CreateOrderRequest(
Guid CustomerId,
decimal Amount);
“`

Endpoint:

“`csharp
app.MapPost(
“/orders”,
async (
CreateOrderRequest request,
IEventPublisher publisher,
CancellationToken cancellationToken) =>
{
var orderId = Guid.NewGuid();

// Demo:
// Save order into database here.

await publisher.PublishAsync(
eventType: “order.created”,
subject: $”/orders/{orderId}”,
data: new
{
OrderId = orderId,
request.CustomerId,
request.Amount,
CreatedAt = DateTimeOffset.UtcNow
},
cancellationToken);

return Results.Ok(new
{
OrderId = orderId
});
});
“`

Flow hiện tại:

“`text
POST /orders
↓
Generate OrderId
↓
Save Order
↓
CloudEvent
↓
Event Grid
“`

—

# 10. CloudEvent trông như thế nào?

Event chúng ta publish conceptually sẽ giống:

“`json
{
“specversion”: “1.0”,
“id”: “generated-event-id”,
“source”: “/orders”,
“type”: “order.created”,
“subject”: “/orders/f643…”,
“time”: “2026-09-11T03:00:00Z”,
“data”: {
“orderId”: “f643…”,
“customerId”: “82ce…”,
“amount”: 150,
“createdAt”: “2026-09-11T03:00:00Z”
}
}
“`

Có thể chia thành hai phần.

Event metadata:

“`text
id
source
type
subject
time
“`

Business payload:

“`text
data
“`

Điểm này rất quan trọng.

Event metadata phục vụ:

“`text
routing
filtering
traceability
event identification
“`

Trong khi:

“`text
data
“`

chứa business information.

—

# 11. Tạo Azure Function Consumer

Tạo Function App project theo isolated worker model.

Ví dụ:

“`bash
func init Order.EventHandlers \
–worker-runtime dotnet-isolated
“`

Tạo function:

“`bash
cd Order.EventHandlers
“`

Cài Event Grid extension nếu project chưa có:

“`bash
dotnet add package \
Microsoft.Azure.Functions.Worker.Extensions.EventGrid
“`

—

# 12. OrderCreated Handler

Azure Function:

“`csharp
using Azure.Messaging;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace Order.EventHandlers;

public sealed class OrderCreatedHandler
{
private readonly ILogger _logger;

public OrderCreatedHandler(
ILogger logger)
{
_logger = logger;
}

[Function(“OrderCreatedHandler”)]
public void Run(
[EventGridTrigger] CloudEvent cloudEvent)
{
_logger.LogInformation(
“Received Event Grid event {EventId}, Type {EventType}, Subject {Subject}”,
cloudEvent.Id,
cloudEvent.Type,
cloudEvent.Subject);

_logger.LogInformation(
“Event data: {EventData}”,
cloudEvent.Data?.ToString());
}
}
“`

Khi Event Grid gửi:

“`text
order.created
“`

Function được trigger.

—

# 13. Deploy Azure Function

Sau khi Function được deploy, chúng ta sẽ có Azure Function resource:

“`text
Order.EventHandlers
“`

và function:

“`text
OrderCreatedHandler
“`

Architecture lúc này:

“`text
Order API
↓
Event Grid Topic
↓

???

↓
Azure Function
“`

Chúng ta vẫn thiếu một thành phần:

“`text
Event Subscription
“`

—

# 14. Event Subscription là gì?

Event Subscription nối:

“`text
Topic
“`

với:

“`text
Consumer
“`

Ví dụ:

“`text
order-events
↓
Event Subscription
↓
OrderCreatedHandler
“`

Publisher không biết:

“`text
OrderCreatedHandler
“`

tồn tại.

Subscriber cũng không cần biết:

“`text
Order API
“`

được implement như thế nào.

—

# 15. Tạo Event Subscription

Lấy Function resource ID.

Conceptually:

“`text
/subscriptions/{subscriptionId}
/resourceGroups/event-grid-demo-rg
/providers/Microsoft.Web/sites/{function-app}
/functions/OrderCreatedHandler
“`

Sau đó:

“`bash
az eventgrid topic event-subscription create \
–name order-created-handler \
–resource-group event-grid-demo-rg \
–topic-name order-events \
–endpoint-type azurefunction \
–endpoint “” \
–event-delivery-schema cloudeventschemav1_0
“`

Bây giờ:

“`text
Order API
↓
Event Grid Topic
↓
Event Subscription
↓
Azure Function
“`

Flow E2E đã hoàn chỉnh.

—

# 16. Test

Call:

“`http
POST /orders
Content-Type: application/json
“`

Body:

“`json
{
“customerId”: “ee325c11-7253-4475-b174-c34626c94045”,
“amount”: 150
}
“`

Order API:

“`text
Create Order
↓
Publish order.created
“`

Event Grid:

“`text
Receive
↓
Route
“`

Azure Function:

“`text
Receive Event
↓
Process
“`

Logs có thể hiển thị:

“`text
Received Event Grid event
EventId: …
Type: order.created
Subject: /orders/…
“`

—

# 17. Thêm Event Filtering

Giả sử topic có nhiều event:

“`text
order.created

order.paid

order.completed

order.cancelled
“`

Nhưng Azure Function của chúng ta chỉ xử lý:

“`text
order.created
“`

Không nên gửi toàn bộ event xuống Function rồi:

“`csharp
if (cloudEvent.Type != “order.created”)
{
return;
}
“`

Nếu broker đã hỗ trợ filtering, chúng ta nên filter ngay từ subscription.

Ví dụ:

“`bash
az eventgrid topic event-subscription create \
–name order-created-handler \
–resource-group event-grid-demo-rg \
–topic-name order-events \
–endpoint-type azurefunction \
–endpoint “” \
–included-event-types order.created \
–event-delivery-schema cloudeventschemav1_0
“`

Flow:

“`text
order.created
↓
Event Grid
↓
MATCH
↓
OrderCreatedHandler
“`

Nhưng:

“`text
order.cancelled
↓
Event Grid
↓
NO MATCH
“`

Function không cần nhận event không liên quan.

—

# 18. Subject Filtering

Giả sử subject:

“`text
/orders/123
“`

hoặc:

“`text
/orders/456
“`

Subscription còn có thể filter theo subject.

Ví dụ:

“`text
subject begins with
/orders/
“`

Filtering rất hữu ích khi một topic chứa nhiều nhóm event có cấu trúc rõ ràng.

—

# 19. Failure Scenario

Giả sử Function nhận được:

“`text
order.created
“`

nhưng database tạm thời unavailable:

“`text
Event Grid
↓
Azure Function
↓
Database
↓
Timeout
“`

Consumer processing thất bại.

Event Grid không đơn giản bỏ event ngay.

Nó có retry mechanism cho delivery failure.

Concept:

“`text
Delivery attempt #1
↓
Failure
↓
Retry
↓
Delivery attempt #2
↓
Failure
↓
Retry
“`

Đây là lý do Event Grid phù hợp hơn việc Order API tự fire một HTTP request rồi quên nó.

—

# 20. Configure Retry Policy

Event Subscription cho phép cấu hình:

“`text
Maximum delivery attempts

Event TTL
“`

Ví dụ:

“`bash
az eventgrid topic event-subscription create \
–name order-created-handler \
–resource-group event-grid-demo-rg \
–topic-name order-events \
–endpoint-type azurefunction \
–endpoint “” \
–included-event-types order.created \
–max-delivery-attempts 10 \
–event-ttl 120
“`

Concept:

“`text
Event
↓
Retry until:

Max attempts reached

OR

Event TTL expired
“`

—

# 21. Dead Letter

Retry không thể kéo dài mãi.

Nếu consumer không thể nhận event sau retry policy:

“`text
Event Grid
↓
Retry
↓
Retry
↓
Retry exhausted
↓
Dead Letter
“`

Dead Letter có thể được cấu hình tới Azure Storage.

Flow:

“`text
Event Grid
↓
Delivery failure
↓
Retry
↓
Still failed
↓
Azure Blob Storage
“`

Điểm quan trọng:

> Dead Letter không có nghĩa sự cố đã được xử lý.

Production flow nên là:

“`text
Dead Letter
↓
Alert
↓
Investigate
↓
Fix root cause
↓
Replay
“`

—

# 22. Tạo Storage cho Dead Letter

Ví dụ:

“`bash
az storage account create \
–name eventgriddlstg123 \
–resource-group event-grid-demo-rg \
–location southeastasia \
–sku Standard_LRS
“`

Tạo container:

“`bash
az storage container create \
–name event-grid-deadletter \
–account-name eventgriddlstg123
“`

Sau đó cấu hình dead-letter endpoint ở Event Subscription.

Concept resource path:

“`text
/subscriptions/{subscription}
/resourceGroups/{resource-group}
/providers/Microsoft.Storage
/storageAccounts/{account}
/blobServices/default
/containers/{container}
“`

—

# 23. At-Least-Once và Duplicate Event

Đây là phần quan trọng nhất khi đưa Event Grid vào production.

Không nên thiết kế consumer với assumption:

“`text
One event
=
exactly one execution
“`

Distributed messaging thường phải chấp nhận khả năng:

“`text
Event A
↓
Consumer

Event A
↓
Consumer again
“`

Ví dụ:

“`text
OrderCreated
“`

consumer tạo invoice.

Lần đầu:

“`text
Create Invoice
↓
DB COMMIT
↓
Consumer crash trước khi delivery được xác nhận
“`

Event có thể được delivery lại.

Nếu consumer làm:

“`text
Create Invoice
“`

lần nữa:

“`text
Invoice #1
Invoice #2
“`

Đây là duplicate side effect.

—

# 24. Idempotent Consumer

Consumer phải có khả năng xử lý cùng event nhiều lần mà business result vẫn giống nhau.

Một cách đơn giản:

“`text
processed_events
“`

Schema concept:

“`sql
CREATE TABLE processed_events
(
event_id VARCHAR(128) PRIMARY KEY,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
“`

Consumer:

“`text
Receive event
↓
Check EventId
↓
Already processed?
├── YES
│ ↓
│ Ignore
│
└── NO
↓
Process
↓
Save EventId
“`

—

# 25. Inbox Pattern

Một cách an toàn hơn là ghi processed event và business operation trong cùng database transaction.

Ví dụ:

“`text
BEGIN
“`

Sau đó:

“`text
INSERT processed_event

CREATE invoice
“`

Cuối cùng:

“`text
COMMIT
“`

Concept:

“`text
Receive OrderCreated
↓
BEGIN TRANSACTION
↓
Insert ProcessedEvent(EventId)
↓
Create Invoice
↓
COMMIT
“`

Duplicate:

“`text
Receive same EventId
↓
INSERT ProcessedEvent
↓
Already exists
↓
Skip safely
“`

Đây chính là nền tảng của idempotent event consumer.

—

# 26. Event Grid và Database Transaction

Một điểm khác cần chú ý:

“`text
Save Order
“`

và:

“`text
Publish Event Grid Event
“`

không nằm trong cùng database transaction.

Nếu code:

“`csharp
await db.SaveChangesAsync();

await eventPublisher.PublishAsync(…);
“`

có thể xảy ra:

“`text
Database COMMIT
↓
Application crash
↓
Event never published
“`

Kết quả:

“`text
Order exists

BUT

OrderCreated event missing
“`

Đây là **dual-write problem**.

—

# 27. Production Solution: Transactional Outbox

Nếu `OrderCreated` là business event quan trọng, production architecture tốt hơn là:

“`text
Order API
↓
DB Transaction
├── INSERT Order
└── INSERT Outbox
↓
COMMIT
“`

Sau đó:

“`text
Outbox Worker
↓
Event Grid
“`

Full flow:

“`text
POST /orders
↓
BEGIN
↓
INSERT Order
↓
INSERT Outbox Event
↓
COMMIT
↓
Return HTTP 200

async

Outbox Worker
↓
Read Pending Event
↓
Publish Event Grid
↓
Mark Published
“`

Điều này giúp tránh tình trạng:

“`text
Business data committed

BUT

event disappeared
“`

—

# 28. Observability

Khi event-driven system lớn dần, troubleshooting trở nên khó nếu không có traceability.

Mỗi event nên có những thông tin như:

“`text
EventId

EventType

TraceId

CorrelationId

AggregateId

Subject
“`

Ví dụ:

“`json
{
“eventId”: “evt-100”,
“eventType”: “order.created”,
“correlationId”: “corr-001”,
“aggregateId”: “order-123”
}
“`

Consumer log:

“`csharp
_logger.LogInformation(
“Processing event {EventId}, Type {EventType}, Subject {Subject}”,
cloudEvent.Id,
cloudEvent.Type,
cloudEvent.Subject);
“`

Structured log giúp query theo:

“`text
EventId

OrderId

CorrelationId

TraceId
“`

—

# 29. Distributed Tracing

Full production flow có thể là:

“`text
HTTP Request
↓
Order API
↓
Outbox
↓
Event Grid
↓
Azure Function
“`

Nếu sử dụng OpenTelemetry:

“`text
Trace ABC123

POST /orders
│
├── PostgreSQL
│
├── Outbox
│
└── Event Publish
↓
Azure Function
“`

Trace context cần được propagate xuyên qua async boundary nếu muốn reconstruct E2E flow.

Observability lúc đó gồm:

“`text
Metrics
→ Có vấn đề không?

Trace
→ Lỗi nằm ở đâu?

Logs
→ Tại sao lỗi?

Profile
→ Code nào đang gây performance problem?
“`

—

# 30. Metrics nên monitor

Một event-driven system nên có ít nhất:

“`text
events_published_total

event_publish_failures

event_processing_success

event_processing_failure

event_processing_duration

dead_letter_count
“`

Ngoài technical metrics, có thể thêm business metrics:

“`text
orders_created

orders_failed

notifications_sent
“`

Nếu:

“`text
dead_letter_count > 0
“`

hoặc:

“`text
consumer_failure_rate
“`

tăng đột biến thì nên alert.

—

# 31. Local Development

Một điểm cần hiểu:

Event Grid bản thân nó là Azure managed service.

Application local có thể:

“`text
Local ASP.NET Core
↓ Internet
Azure Event Grid
“`

Nhưng consumer local sẽ khó hơn nếu Event Grid cần push HTTP event tới machine của developer.

Có thể sử dụng:

“`text
ngrok
Cloudflare Tunnel
“`

hoặc deploy development Azure Function.

Một workflow đơn giản:

“`text
Local API
↓
Azure Event Grid Dev Topic
↓
Azure Function Dev
“`

—

# 32. Production Architecture hoàn chỉnh

Sau khi thêm reliability và observability, architecture có thể trở thành:

“`text
POST /orders
↓
Order API
↓
DB Transaction
┌─────┴─────┐
│ │
Order Outbox
│ │
└─────┬─────┘
↓
COMMIT

Outbox Worker
↓
Azure Event Grid
↓
Event Subscription
↓
Azure Function
↓
Idempotency / Inbox
↓
Business Processing
“`

Bao quanh flow:

“`text
Metrics
Logs
Tracing
Alerts
Dead Letter Monitoring
“`

—

# 33. Những Failure Scenario cần nghĩ tới

Khi thiết kế production Event Grid flow, nên hỏi:

### Producer fail trước DB commit

“`text
No Order
No Event
“`

An toàn.

### DB commit nhưng application crash trước publish

“`text
Order exists
Event missing
“`

Giải quyết bằng:

“`text
Transactional Outbox
“`

### Event Grid deliver duplicate

Giải quyết bằng:

“`text
Idempotent Consumer
Inbox
Unique Constraint
“`

### Consumer temporary unavailable

Giải quyết bằng:

“`text
Retry
“`

### Consumer unavailable quá lâu

Giải quyết bằng:

“`text
Dead Letter
Alert
Replay
“`

### Event arrives out of expected business order

Không assume broker sẽ sửa business ordering.

Consumer vẫn phải validate state transition.

—

# 34. Một nguyên tắc quan trọng

Một production event-driven architecture không chỉ là:

“`text
Producer
→ Broker
→ Consumer
“`

Mà thực tế phải nghĩ:

“`text
Producer
↓
Transactional Outbox
↓
Broker
↓
Retry
↓
Consumer
↓
Idempotency
↓
Business Transaction
↓
Observability
↓
Dead Letter / Recovery
“`

Đây mới là phần khó của distributed systems.

—

# 35. Khi nào Event Grid phù hợp?

Event Grid đặc biệt phù hợp khi:

“`text
Something happened
“`

và nhiều independent consumers muốn react.

Ví dụ:

“`text
OrderCreated
↓
Event Grid
├── Analytics
├── Notification
└── Audit
“`

Producer không quan tâm subscriber nào tồn tại.

—

# 36. Khi nào nên nghĩ tới Service Bus?

Giả sử requirement thay đổi thành:

“`text
GrantReward
“`

và operation này phải:

“`text
được xử lý chắc chắn

retry có kiểm soát

có DLQ

có ordering requirement

có business workflow rõ ràng
“`

thì câu hỏi tiếp theo là:

> Event Grid có còn là lựa chọn tốt nhất không?

Đây là lúc chúng ta cần tìm hiểu:

“`text
Azure Service Bus
“`

Mental model:

“`text
Event Grid
→ Something happened.

Service Bus
→ Please do this.
“`

—

# Kết luận

Trong bài này chúng ta đã triển khai một flow Azure Event Grid hoàn chỉnh:

“`text
ASP.NET Core
↓
CloudEvent
↓
Event Grid Topic
↓
Event Subscription
↓
Azure Function
“`

Sau đó đưa nó gần hơn với production bằng:

“`text
Filtering

Retry

Dead Letter

Idempotency

Transactional Outbox

Observability
“`

Phần quan trọng nhất không phải là vài dòng Azure CLI hay SDK code.

Điểm cần hiểu là lifecycle của một event:

“`text
Business transaction
↓
Publish
↓
Route
↓
Deliver
↓
Process
↓
Retry if failed
↓
Dead Letter if unrecoverable
↓
Monitor and recover
“`

Một event-driven system đáng tin cậy cần trả lời được:

“`text
Nếu publish fail thì sao?

Nếu consumer fail thì sao?

Nếu event duplicate thì sao?

Nếu event bị mất thì sao?

Nếu consumer xử lý xong rồi crash thì sao?

Làm sao trace một event end-to-end?

Làm sao replay event an toàn?
“`

Đó chính là ranh giới giữa:

“`text
“Biết dùng Event Grid”
“`

và:

“`text
“Biết thiết kế Event Grid cho production”
“`

Ở bài tiếp theo, chúng ta sẽ đi sâu vào **Azure Service Bus** và xem tại sao Queue, Topic, PeekLock, Retry, Dead Letter, Sessions và Duplicate Detection lại phù hợp hơn với những business workflow cần reliability cao.