478 lines
16 KiB
Markdown
478 lines
16 KiB
Markdown
---
|
|
name: api-aggregation
|
|
description: API Gateway Aggregation và Backend for Frontend (BFF) patterns. Use for response composition, request routing, và client-specific APIs.
|
|
compatibility: ".NET 10+, YARP, Ocelot, GraphQL"
|
|
metadata:
|
|
author: Velik Ho
|
|
version: "1.0"
|
|
---
|
|
|
|
# API Aggregation & BFF / API Aggregation và BFF Pattern
|
|
|
|
Patterns cho API Gateway aggregation và Backend for Frontend trong microservices.
|
|
|
|
## When to Use This Skill / Khi Nào Sử Dụng
|
|
|
|
Use this skill when:
|
|
- Aggregating responses from multiple services / Tổng hợp responses từ nhiều services
|
|
- Creating client-specific APIs / Tạo APIs riêng cho từng client
|
|
- Reducing chatty client-server communication / Giảm giao tiếp "chatty"
|
|
- Implementing API composition / Triển khai API composition
|
|
|
|
## Core Concepts / Khái Niệm Cốt Lõi
|
|
|
|
### Gateway Patterns Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ GATEWAY PATTERNS │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ GATEWAY ROUTING (Reverse Proxy) │ │
|
|
│ │ Client ──▶ Gateway ──▶ Service A │ │
|
|
│ │ (Route) Service B │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ GATEWAY AGGREGATION │ │
|
|
│ │ Client ──▶ Gateway ──┬─▶ Service A ─┐ │ │
|
|
│ │ └─▶ Service B ─┼─▶ Combined │ │
|
|
│ │ └─▶ Service C ─┘ Response │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ BACKEND FOR FRONTEND (BFF) │ │
|
|
│ │ Mobile ──▶ Mobile BFF ──▶ Services │ │
|
|
│ │ Web ──▶ Web BFF ──▶ Services │ │
|
|
│ │ IoT ──▶ IoT BFF ──▶ Services │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### When to Use What / Khi Nào Dùng Gì
|
|
|
|
| Pattern | Use Case | Example |
|
|
|---------|----------|---------|
|
|
| **Gateway Routing** | Simple proxy | Route `/api/users/*` to User Service |
|
|
| **Gateway Aggregation** | Combine multiple calls | Product page = Product + Reviews + Stock |
|
|
| **BFF** | Client-specific needs | Mobile needs different data than Web |
|
|
|
|
### Benefits and Trade-offs
|
|
|
|
| Benefit | Trade-off |
|
|
|---------|-----------|
|
|
| Reduces client complexity | Single point of failure |
|
|
| Optimizes for client needs | Additional latency hop |
|
|
| Enables caching | Gateway becomes bottleneck |
|
|
| Centralizes cross-cutting concerns | More complexity to maintain |
|
|
|
|
## Key Patterns / Mẫu Chính
|
|
|
|
### YARP Reverse Proxy Configuration
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Configure YARP reverse proxy for routing.
|
|
/// VI: Cấu hình YARP reverse proxy cho routing.
|
|
/// </summary>
|
|
|
|
// Program.cs
|
|
builder.Services.AddReverseProxy()
|
|
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
|
|
|
var app = builder.Build();
|
|
app.MapReverseProxy();
|
|
app.Run();
|
|
|
|
// appsettings.json
|
|
{
|
|
"ReverseProxy": {
|
|
"Routes": {
|
|
"users-route": {
|
|
"ClusterId": "users-cluster",
|
|
"Match": {
|
|
"Path": "/api/users/{**catch-all}"
|
|
},
|
|
"Transforms": [
|
|
{ "PathRemovePrefix": "/api/users" }
|
|
]
|
|
},
|
|
"orders-route": {
|
|
"ClusterId": "orders-cluster",
|
|
"Match": {
|
|
"Path": "/api/orders/{**catch-all}"
|
|
}
|
|
}
|
|
},
|
|
"Clusters": {
|
|
"users-cluster": {
|
|
"Destinations": {
|
|
"user-service": {
|
|
"Address": "http://user-service:5001"
|
|
}
|
|
}
|
|
},
|
|
"orders-cluster": {
|
|
"Destinations": {
|
|
"order-service": {
|
|
"Address": "http://order-service:5002"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### API Aggregation Controller
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Aggregation controller combining multiple service responses.
|
|
/// VI: Controller aggregation kết hợp responses từ nhiều services.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/v1/aggregator")]
|
|
public class AggregatorController : ControllerBase
|
|
{
|
|
private readonly IProductServiceClient _productClient;
|
|
private readonly IReviewServiceClient _reviewClient;
|
|
private readonly IInventoryServiceClient _inventoryClient;
|
|
private readonly ILogger<AggregatorController> _logger;
|
|
|
|
public AggregatorController(
|
|
IProductServiceClient productClient,
|
|
IReviewServiceClient reviewClient,
|
|
IInventoryServiceClient inventoryClient,
|
|
ILogger<AggregatorController> logger)
|
|
{
|
|
_productClient = productClient;
|
|
_reviewClient = reviewClient;
|
|
_inventoryClient = inventoryClient;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get product details with reviews and stock info.
|
|
/// VI: Lấy chi tiết sản phẩm với reviews và thông tin tồn kho.
|
|
/// </summary>
|
|
[HttpGet("products/{productId}")]
|
|
public async Task<ActionResult<ProductDetailsDto>> GetProductDetails(
|
|
Guid productId,
|
|
CancellationToken ct)
|
|
{
|
|
// EN: Make parallel calls to services
|
|
// VI: Gọi song song đến các services
|
|
var productTask = _productClient.GetProductAsync(productId, ct);
|
|
var reviewsTask = _reviewClient.GetReviewsAsync(productId, ct);
|
|
var stockTask = _inventoryClient.GetStockAsync(productId, ct);
|
|
|
|
await Task.WhenAll(productTask, reviewsTask, stockTask);
|
|
|
|
var product = await productTask;
|
|
if (product == null)
|
|
return NotFound();
|
|
|
|
var reviews = await reviewsTask;
|
|
var stock = await stockTask;
|
|
|
|
// EN: Aggregate responses
|
|
// VI: Tổng hợp responses
|
|
return Ok(new ProductDetailsDto
|
|
{
|
|
Id = product.Id,
|
|
Name = product.Name,
|
|
Description = product.Description,
|
|
Price = product.Price,
|
|
ImageUrl = product.ImageUrl,
|
|
AverageRating = reviews.AverageRating,
|
|
ReviewCount = reviews.TotalCount,
|
|
TopReviews = reviews.Items.Take(3).ToList(),
|
|
InStock = stock.AvailableQuantity > 0,
|
|
AvailableQuantity = stock.AvailableQuantity
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get user dashboard data from multiple services.
|
|
/// VI: Lấy dữ liệu dashboard user từ nhiều services.
|
|
/// </summary>
|
|
[HttpGet("dashboard")]
|
|
[Authorize]
|
|
public async Task<ActionResult<DashboardDto>> GetDashboard(CancellationToken ct)
|
|
{
|
|
var userId = User.GetUserId();
|
|
|
|
var profileTask = _userClient.GetProfileAsync(userId, ct);
|
|
var ordersTask = _orderClient.GetRecentOrdersAsync(userId, 5, ct);
|
|
var notificationsTask = _notificationClient.GetUnreadCountAsync(userId, ct);
|
|
var walletTask = _walletClient.GetBalanceAsync(userId, ct);
|
|
|
|
await Task.WhenAll(profileTask, ordersTask, notificationsTask, walletTask);
|
|
|
|
return Ok(new DashboardDto
|
|
{
|
|
Profile = await profileTask,
|
|
RecentOrders = await ordersTask,
|
|
UnreadNotifications = await notificationsTask,
|
|
WalletBalance = await walletTask
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### BFF for Mobile
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Mobile BFF with optimized responses.
|
|
/// VI: Mobile BFF với responses được tối ưu.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("mobile/api/v1")]
|
|
public class MobileBffController : ControllerBase
|
|
{
|
|
private readonly IMediator _mediator;
|
|
|
|
public MobileBffController(IMediator mediator)
|
|
{
|
|
_mediator = mediator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Mobile-optimized product list.
|
|
/// VI: Danh sách sản phẩm tối ưu cho mobile.
|
|
/// </summary>
|
|
[HttpGet("products")]
|
|
public async Task<ActionResult<MobileProductListDto>> GetProducts(
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 10,
|
|
CancellationToken ct = default)
|
|
{
|
|
// EN: Mobile gets smaller images and fewer fields
|
|
// VI: Mobile nhận images nhỏ hơn và ít fields hơn
|
|
var result = await _mediator.Send(new GetMobileProductsQuery
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
ImageSize = "thumbnail" // Only thumbnails for mobile
|
|
}, ct);
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Mobile-optimized checkout.
|
|
/// VI: Checkout tối ưu cho mobile.
|
|
/// </summary>
|
|
[HttpPost("checkout")]
|
|
[Authorize]
|
|
public async Task<ActionResult<MobileCheckoutResultDto>> Checkout(
|
|
MobileCheckoutRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
// EN: Single endpoint handles entire checkout for mobile
|
|
// VI: Một endpoint xử lý toàn bộ checkout cho mobile
|
|
var result = await _mediator.Send(new MobileCheckoutCommand
|
|
{
|
|
UserId = User.GetUserId(),
|
|
CartId = request.CartId,
|
|
PaymentMethodId = request.PaymentMethodId,
|
|
ShippingAddressId = request.ShippingAddressId,
|
|
UseWalletBalance = request.UseWalletBalance
|
|
}, ct);
|
|
|
|
return Ok(result);
|
|
}
|
|
}
|
|
```
|
|
|
|
### GraphQL BFF
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: GraphQL query for flexible aggregation.
|
|
/// VI: GraphQL query cho aggregation linh hoạt.
|
|
/// </summary>
|
|
public class Query
|
|
{
|
|
public async Task<ProductType?> GetProduct(
|
|
[ID] Guid productId,
|
|
ProductDataLoader productLoader,
|
|
ReviewDataLoader reviewLoader,
|
|
StockDataLoader stockLoader)
|
|
{
|
|
return await productLoader.LoadAsync(productId);
|
|
}
|
|
}
|
|
|
|
public class ProductType : ObjectType<Product>
|
|
{
|
|
protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
|
|
{
|
|
descriptor.Field(p => p.Id).ID();
|
|
|
|
descriptor.Field(p => p.Name);
|
|
descriptor.Field(p => p.Price);
|
|
|
|
// EN: Lazy load reviews only when requested
|
|
// VI: Lazy load reviews chỉ khi được yêu cầu
|
|
descriptor
|
|
.Field("reviews")
|
|
.ResolveWith<ProductResolvers>(r => r.GetReviews(default!, default!))
|
|
.Type<ListType<ReviewType>>();
|
|
|
|
// EN: Lazy load stock only when requested
|
|
// VI: Lazy load stock chỉ khi được yêu cầu
|
|
descriptor
|
|
.Field("stock")
|
|
.ResolveWith<ProductResolvers>(r => r.GetStock(default!, default!))
|
|
.Type<StockType>();
|
|
}
|
|
}
|
|
|
|
public class ProductResolvers
|
|
{
|
|
public async Task<IEnumerable<Review>> GetReviews(
|
|
[Parent] Product product,
|
|
ReviewDataLoader loader)
|
|
{
|
|
return await loader.LoadAsync(product.Id);
|
|
}
|
|
|
|
public async Task<Stock?> GetStock(
|
|
[Parent] Product product,
|
|
StockDataLoader loader)
|
|
{
|
|
return await loader.LoadAsync(product.Id);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Mistakes / Lỗi Thường Gặp
|
|
|
|
### 1. Sequential Instead of Parallel Calls
|
|
|
|
```csharp
|
|
// ❌ BAD: Sequential calls
|
|
public async Task<ProductDetailsDto> GetProductDetails(Guid productId)
|
|
{
|
|
var product = await _productClient.GetAsync(productId);
|
|
var reviews = await _reviewClient.GetAsync(productId); // Waits for product
|
|
var stock = await _stockClient.GetAsync(productId); // Waits for reviews
|
|
// Total time = product + reviews + stock
|
|
}
|
|
|
|
// ✅ GOOD: Parallel calls
|
|
public async Task<ProductDetailsDto> GetProductDetails(Guid productId)
|
|
{
|
|
var productTask = _productClient.GetAsync(productId);
|
|
var reviewsTask = _reviewClient.GetAsync(productId);
|
|
var stockTask = _stockClient.GetAsync(productId);
|
|
|
|
await Task.WhenAll(productTask, reviewsTask, stockTask);
|
|
// Total time = max(product, reviews, stock)
|
|
}
|
|
```
|
|
|
|
### 2. No Fallback on Partial Failure
|
|
|
|
```csharp
|
|
// ❌ BAD: Fails entirely if one service fails
|
|
public async Task<DashboardDto> GetDashboard()
|
|
{
|
|
var profile = await _userClient.GetProfileAsync(userId);
|
|
var orders = await _orderClient.GetOrdersAsync(userId); // If this fails, whole request fails
|
|
var notifications = await _notificationClient.GetAsync(userId);
|
|
}
|
|
|
|
// ✅ GOOD: Graceful degradation
|
|
public async Task<DashboardDto> GetDashboard()
|
|
{
|
|
var profileTask = _userClient.GetProfileAsync(userId);
|
|
var ordersTask = GetOrdersSafeAsync(userId);
|
|
var notificationsTask = GetNotificationsSafeAsync(userId);
|
|
|
|
await Task.WhenAll(profileTask, ordersTask, notificationsTask);
|
|
|
|
return new DashboardDto
|
|
{
|
|
Profile = await profileTask,
|
|
Orders = await ordersTask ?? Array.Empty<Order>(), // Empty if failed
|
|
Notifications = await notificationsTask ?? 0 // Zero if failed
|
|
};
|
|
}
|
|
|
|
private async Task<IEnumerable<Order>?> GetOrdersSafeAsync(string userId)
|
|
{
|
|
try
|
|
{
|
|
return await _orderClient.GetOrdersAsync(userId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to get orders for user {UserId}", userId);
|
|
return null; // Return null, not throw
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Missing Caching
|
|
|
|
```csharp
|
|
// ❌ BAD: No caching, repeated calls
|
|
public async Task<ProductDetailsDto> GetProductDetails(Guid productId)
|
|
{
|
|
var product = await _productClient.GetAsync(productId); // Called every time
|
|
return MapToDto(product);
|
|
}
|
|
|
|
// ✅ GOOD: With caching
|
|
public async Task<ProductDetailsDto> GetProductDetails(Guid productId)
|
|
{
|
|
var cacheKey = $"product:{productId}";
|
|
|
|
var cached = await _cache.GetAsync<ProductDetailsDto>(cacheKey);
|
|
if (cached != null)
|
|
return cached;
|
|
|
|
var product = await _productClient.GetAsync(productId);
|
|
var dto = MapToDto(product);
|
|
|
|
await _cache.SetAsync(cacheKey, dto, TimeSpan.FromMinutes(5));
|
|
|
|
return dto;
|
|
}
|
|
```
|
|
|
|
## Quick Reference / Tham Chiếu Nhanh
|
|
|
|
### Pattern Selection Guide
|
|
|
|
| Need | Pattern | Implementation |
|
|
|------|---------|----------------|
|
|
| Route requests | Gateway Routing | YARP, Ocelot |
|
|
| Combine data | Gateway Aggregation | Custom controller |
|
|
| Client-specific API | BFF | Separate project per client |
|
|
| Flexible queries | GraphQL | HotChocolate |
|
|
|
|
### Performance Checklist
|
|
|
|
| Optimization | Applied? |
|
|
|-------------|----------|
|
|
| Parallel service calls | ✅ |
|
|
| Response caching | ✅ |
|
|
| Graceful degradation | ✅ |
|
|
| Connection pooling | ✅ |
|
|
| Timeout configuration | ✅ |
|
|
|
|
## Resources / Tài Nguyên
|
|
|
|
- [Detailed Examples](./references/REFERENCE.md) - Full code examples
|
|
- [API Design](../api-design/SKILL.md) - REST API patterns
|
|
- [Redis Caching](../redis-caching/SKILL.md) - Caching strategies
|
|
- [Error Handling](../error-handling-patterns/SKILL.md) - Resilience patterns
|
|
- [Inter-service Communication](../inter-service-communication/SKILL.md) - HTTP clients
|