Ở 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.