Ở 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à:
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:
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:
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:
POST /orders
API xử lý:
Create Order
↓
Save Database
↓
Publish OrderCreated
Sau đó Event Grid route event tới Azure Function.
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:
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:
az login
Tạo resource group:
az group create \
--name event-grid-demo-rg \
--location southeastasia
Đăng ký Event Grid provider nếu subscription chưa có:
az provider register \
--namespace Microsoft.EventGrid
Kiểm tra:
az provider show \
--namespace Microsoft.EventGrid \
--query registrationState
Khi kết quả là:
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:
order-events
Tạo topic:
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:
CloudEvents 1.0
làm event schema.
Architecture hiện tại:
Order API
↓
order-events
Chưa có consumer.
4. Lấy Topic Endpoint
Chạy:
az eventgrid topic show \
--name order-events \
--resource-group event-grid-demo-rg \
--query endpoint \
--output tsv
Kết quả sẽ tương tự:
https://order-events.
<region>-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:
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:
appsettings.json
↓
commit Event Grid key
↓
Git
Secret nên được quản lý bằng:
Azure Key Vault
Environment variables
Managed Identity
6. Tạo ASP.NET Core API
Tạo project:
dotnet new webapi \
-n Order.Api
Di chuyển vào project:
cd Order.Api
Cài Azure Event Grid SDK:
dotnet add package Azure.Messaging.EventGrid
7. Configuration
Ví dụ appsettings.json:
{
"EventGrid": {
"Endpoint": "https://order-events.
<region>-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:
EventGrid__Key
8. Tạo Event Grid Publisher
Tạo interface:
public interface IEventPublisher
{
Task PublishAsync
<T>(
string eventType,
string subject,
T data,
CancellationToken cancellationToken = default);
}
Implementation:
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
<T>(
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:
builder.Services
.AddSingleton<IEventPublisher, EventGridPublisher>();
9. Tạo Order API
Ví dụ request:
public sealed record CreateOrderRequest(
Guid CustomerId,
decimal Amount);
Endpoint:
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:
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:
{
"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:
id
source
type
subject
time
Business payload:
data
Điểm này rất quan trọng.
Event metadata phục vụ:
routing
filtering
traceability
event identification
Trong khi:
data
chứa business information.
11. Tạo Azure Function Consumer
Tạo Function App project theo isolated worker model.
Ví dụ:
func init Order.EventHandlers \
--worker-runtime dotnet-isolated
Tạo function:
cd Order.EventHandlers
Cài Event Grid extension nếu project chưa có:
dotnet add package \
Microsoft.Azure.Functions.Worker.Extensions.EventGrid
12. OrderCreated Handler
Azure Function:
using Azure.Messaging;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Order.EventHandlers;
public sealed class OrderCreatedHandler
{
private readonly ILogger
<OrderCreatedHandler> _logger;
public OrderCreatedHandler(
ILogger
<OrderCreatedHandler> 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:
order.created
Function được trigger.
13. Deploy Azure Function
Sau khi Function được deploy, chúng ta sẽ có Azure Function resource:
Order.EventHandlers
và function:
OrderCreatedHandler
Architecture lúc này:
Order API
↓
Event Grid Topic
↓
???
↓
Azure Function
Chúng ta vẫn thiếu một thành phần:
Event Subscription
14. Event Subscription là gì?
Event Subscription nối:
Topic
với:
Consumer
Ví dụ:
order-events
↓
Event Subscription
↓
OrderCreatedHandler
Publisher không biết:
OrderCreatedHandler
tồn tại.
Subscriber cũng không cần biết:
Order API
được implement như thế nào.
15. Tạo Event Subscription
Lấy Function resource ID.
Conceptually:
/subscriptions/{subscriptionId}
/resourceGroups/event-grid-demo-rg
/providers/Microsoft.Web/sites/{function-app}
/functions/OrderCreatedHandler
Sau đó:
az eventgrid topic event-subscription create \
--name order-created-handler \
--resource-group event-grid-demo-rg \
--topic-name order-events \
--endpoint-type azurefunction \
--endpoint "
<FUNCTION_RESOURCE_ID>" \
--event-delivery-schema cloudeventschemav1_0
Bây giờ:
Order API
↓
Event Grid Topic
↓
Event Subscription
↓
Azure Function
Flow E2E đã hoàn chỉnh.
16. Test
Call:
POST /orders
Content-Type: application/json
Body:
{
"customerId": "ee325c11-7253-4475-b174-c34626c94045",
"amount": 150
}
Order API:
Create Order
↓
Publish order.created
Event Grid:
Receive
↓
Route
Azure Function:
Receive Event
↓
Process
Logs có thể hiển thị:
Received Event Grid event
EventId: ...
Type: order.created
Subject: /orders/...
17. Thêm Event Filtering
Giả sử topic có nhiều event:
order.created
order.paid
order.completed
order.cancelled
Nhưng Azure Function của chúng ta chỉ xử lý:
order.created
Không nên gửi toàn bộ event xuống Function rồi:
if (cloudEvent.Type != "order.created")
{
return;
}
Nếu broker đã hỗ trợ filtering, chúng ta nên filter ngay từ subscription.
Ví dụ:
az eventgrid topic event-subscription create \
--name order-created-handler \
--resource-group event-grid-demo-rg \
--topic-name order-events \
--endpoint-type azurefunction \
--endpoint "
<FUNCTION_RESOURCE_ID>" \
--included-event-types order.created \
--event-delivery-schema cloudeventschemav1_0
Flow:
order.created
↓
Event Grid
↓
MATCH
↓
OrderCreatedHandler
Nhưng:
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:
/orders/123
hoặc:
/orders/456
Subscription còn có thể filter theo subject.
Ví dụ:
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:
order.created
nhưng database tạm thời unavailable:
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:
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:
Maximum delivery attempts
Event TTL
Ví dụ:
az eventgrid topic event-subscription create \
--name order-created-handler \
--resource-group event-grid-demo-rg \
--topic-name order-events \
--endpoint-type azurefunction \
--endpoint "
<FUNCTION_RESOURCE_ID>" \
--included-event-types order.created \
--max-delivery-attempts 10 \
--event-ttl 120
Concept:
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:
Event Grid
↓
Retry
↓
Retry
↓
Retry exhausted
↓
Dead Letter
Dead Letter có thể được cấu hình tới Azure Storage.
Flow:
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à:
Dead Letter
↓
Alert
↓
Investigate
↓
Fix root cause
↓
Replay
22. Tạo Storage cho Dead Letter
Ví dụ:
az storage account create \
--name eventgriddlstg123 \
--resource-group event-grid-demo-rg \
--location southeastasia \
--sku Standard_LRS
Tạo container:
az storage container create \
--name event-grid-deadletter \
--account-name eventgriddlstg123
Sau đó cấu hình dead-letter endpoint ở Event Subscription.
Concept resource path:
/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:
One event
=
exactly one execution
Distributed messaging thường phải chấp nhận khả năng:
Event A
↓
Consumer
Event A
↓
Consumer again
Ví dụ:
OrderCreated
consumer tạo invoice.
Lần đầu:
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:
Create Invoice
lần nữa:
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:
processed_events
Schema concept:
CREATE TABLE processed_events
(
event_id VARCHAR(128) PRIMARY KEY,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Consumer:
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ụ:
BEGIN
Sau đó:
INSERT processed_event
CREATE invoice
Cuối cùng:
COMMIT
Concept:
Receive OrderCreated
↓
BEGIN TRANSACTION
↓
Insert ProcessedEvent(EventId)
↓
Create Invoice
↓
COMMIT
Duplicate:
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ú ý:
Save Order
và:
Publish Event Grid Event
không nằm trong cùng database transaction.
Nếu code:
await db.SaveChangesAsync();
await eventPublisher.PublishAsync(...);
có thể xảy ra:
Database COMMIT
↓
Application crash
↓
Event never published
Kết quả:
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à:
Order API
↓
DB Transaction
├── INSERT Order
└── INSERT Outbox
↓
COMMIT
Sau đó:
Outbox Worker
↓
Event Grid
Full flow:
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:
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ư:
EventId
EventType
TraceId
CorrelationId
AggregateId
Subject
Ví dụ:
{
"eventId": "evt-100",
"eventType": "order.created",
"correlationId": "corr-001",
"aggregateId": "order-123"
}
Consumer log:
_logger.LogInformation(
"Processing event {EventId}, Type {EventType}, Subject {Subject}",
cloudEvent.Id,
cloudEvent.Type,
cloudEvent.Subject);
Structured log giúp query theo:
EventId
OrderId
CorrelationId
TraceId
29. Distributed Tracing
Full production flow có thể là:
HTTP Request
↓
Order API
↓
Outbox
↓
Event Grid
↓
Azure Function
Nếu sử dụng OpenTelemetry:
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:
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:
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:
orders_created
orders_failed
notifications_sent
Nếu:
dead_letter_count > 0
hoặc:
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ể:
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:
ngrok
Cloudflare Tunnel
hoặc deploy development Azure Function.
Một workflow đơn giản:
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:
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:
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
No Order
No Event
An toàn.
DB commit nhưng application crash trước publish
Order exists
Event missing
Giải quyết bằng:
Transactional Outbox
Event Grid deliver duplicate
Giải quyết bằng:
Idempotent Consumer
Inbox
Unique Constraint
Consumer temporary unavailable
Giải quyết bằng:
Retry
Consumer unavailable quá lâu
Giải quyết bằng:
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à:
Producer
→ Broker
→ Consumer
Mà thực tế phải nghĩ:
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:
Something happened
và nhiều independent consumers muốn react.
Ví dụ:
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:
GrantReward
và operation này phải:
đượ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:
Azure Service Bus
Mental model:
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:
ASP.NET Core
↓
CloudEvent
↓
Event Grid Topic
↓
Event Subscription
↓
Azure Function
Sau đó đưa nó gần hơn với production bằng:
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:
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:
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:
"Biết dùng Event Grid"
và:
"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.