Kiến trúc xây trên nền tảng sự kiện (events) thay vì gọi trực tiếp. Blueprint này dùng stack: .NET 9 + Next.js + Azure Service Bus + Azure Functions + SQL Server + Azure Entra ID (SSO).
Tổng Quan
Event Flow Chi Tiết
Khi Nào Dùng / Không Dùng
| ✅ Dùng khi | ❌ Không dùng khi |
|---|---|
| Workflows nhiều bước, nhiều side effects | Simple request-response CRUD |
| Cần decouple producer ↔ consumer | Mọi thứ cần response NGAY LẬP TỨC |
| Scale consumers ĐỘC LẬP (email vs inventory) | Team nhỏ, không có infra cho message broker |
| Eventual consistency chấp nhận được | Strong consistency BẮT BUỘC mọi lúc |
| Serverless workload (pay-per-execution) | Luôn cần compute 24/7 |
Dự án hay dùng: eCommerce (đặt hàng → xử lý kho → gửi mail), IoT telemetry, Logistics tracking, Fintech (fraud detection pipeline).
Project Structure (Full Stack)
eventdriven-app/
├── frontend/
│ └── next-app/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── orders/
│ │ │ └── page.tsx
│ │ └── api/
│ │ └── auth/[...nextauth]/route.ts
│ ├── lib/
│ │ ├── auth.ts ← MSAL.js config (Azure Entra ID)
│ │ └── api-client.ts ← Fetch wrapper with Bearer Token
│ ├── next.config.js
│ └── package.json
│
├── backend/
│ ├── src/
│ │ ├── OrderApi/ ← .NET 9 Minimal API
│ │ │ ├── Program.cs
│ │ │ ├── Features/
│ │ │ │ ├── Orders/
│ │ │ │ │ ├── CreateOrder.cs
│ │ │ │ │ └── GetOrders.cs
│ │ │ │ └── Products/
│ │ │ ├── Infrastructure/
│ │ │ │ ├── AppDbContext.cs
│ │ │ │ ├── Outbox/
│ │ │ │ │ ├── OutboxMessage.cs
│ │ │ │ │ └── OutboxProcessor.cs ← Background Service
│ │ │ │ └── Auth/
│ │ │ │ └── EntraIdExtensions.cs
│ │ │ ├── Dockerfile
│ │ │ └── appsettings.json
│ │ │
│ │ └── Functions/ ← Azure Functions (Isolated Worker)
│ │ ├── InventoryProcessor.cs
│ │ ├── EmailNotifier.cs
│ │ ├── AnalyticsIngestion.cs
│ │ ├── Program.cs
│ │ └── host.json
│ │
│ └── shared/
│ └── Contracts/
│ ├── Events/
│ │ ├── OrderCreatedEvent.cs
│ │ └── OrderCancelledEvent.cs
│ └── DTOs/
│ └── OrderDto.cs
│
├── infra/
│ ├── bicep/
│ │ ├── main.bicep
│ │ ├── servicebus.bicep
│ │ └── function.bicep
│ └── docker-compose.yml
│
└── eventdriven-app.sln
NuGet & NPM Checklist
Backend (.NET 9)
xml<!-- Order API -->
<PackageReference Include="Microsoft.Identity.Web" Version="3.*" /> <!-- Entra ID -->
<PackageReference Include="MassTransit.Azure.ServiceBus.Core" Version="8.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.*" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.*" />
<PackageReference Include="Serilog.AspNetCore" Version="8.*" />
<!-- Azure Functions (Isolated) -->
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.ServiceBus" Version="5.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.*" />
Frontend (Next.js)
json{
"dependencies": {
"next": "14.*",
"@azure/msal-browser": "^3.0",
"@azure/msal-react": "^2.0",
"next-auth": "^5.0"
}
}
Code Blueprint - Key Components
1. Azure Entra ID Setup (.NET API)
csharp// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
// appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-api-client-id",
"Audience": "api://your-api-client-id"
}
}
2. Next.js MSAL Integration
typescript// lib/auth.ts
import { PublicClientApplication } from "@azure/msal-browser";
export const msalConfig = {
auth: {
clientId: process.env.NEXT_PUBLIC_AZURE_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_TENANT_ID}`,
redirectUri: "http://localhost:3000",
},
};
export const loginRequest = {
scopes: ["api://your-api-client-id/.default"],
};
// lib/api-client.ts
export async function fetchWithAuth(url: string, msalInstance: PublicClientApplication) {
const account = msalInstance.getActiveAccount();
const token = await msalInstance.acquireTokenSilent({
...loginRequest,
account: account!,
});
return fetch(url, {
headers: { Authorization: `Bearer ${token.accessToken}` },
});
}
3. Outbox Pattern (Reliable Event Publishing)
csharp// Features/Orders/CreateOrder.cs
app.MapPost("/api/orders", async (CreateOrderRequest req, AppDbContext db) =>
{
await using var tx = await db.Database.BeginTransactionAsync();
var order = new Order { /* ... */ };
db.Orders.Add(order);
// Save event to Outbox (SAME transaction!)
db.OutboxMessages.Add(new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(OrderCreatedEvent),
Payload = JsonSerializer.Serialize(new OrderCreatedEvent(order.Id, order.ProductId, order.Quantity)),
CreatedAt = DateTime.UtcNow
});
await db.SaveChangesAsync();
await tx.CommitAsync();
return Results.Accepted($"/api/orders/{order.Id}");
});
4. Azure Function Consumer
csharp// Functions/InventoryProcessor.cs
public class InventoryProcessor(IDbContextFactory<AppDbContext> dbFactory)
{
[Function(nameof(ProcessOrderCreated))]
public async Task ProcessOrderCreated(
[ServiceBusTrigger("order-events", "inventory-processor",
Connection = "ServiceBusConnection")]
OrderCreatedEvent evt,
FunctionContext context)
{
await using var db = await dbFactory.CreateDbContextAsync();
var product = await db.Products.FindAsync(evt.ProductId);
if (product is null || product.Stock < evt.Quantity)
{
// Publish compensation event
throw new InvalidOperationException("Insufficient stock - will retry/DLQ");
}
product.Stock -= evt.Quantity;
await db.SaveChangesAsync();
}
}
5. Azure Service Bus Topic/Subscription
Infrastructure as Code (Bicep)
bicep// infra/bicep/servicebus.bicep resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = { name: 'myapp-sb-${environment}' location: location sku: { name: 'Standard', tier: 'Standard' } } resource topic 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = { parent: serviceBusNamespace name: 'order-events' properties: { maxSizeInMegabytes: 1024 } } resource inventorySub 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = { parent: topic name: 'inventory-processor' properties: { maxDeliveryCount: 3 deadLetteringOnMessageExpiration: true } }