Files
pos-system/microservices/.agent/skills/api-aggregation/references/REFERENCE.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

552 lines
17 KiB
Markdown

# API Aggregation & BFF - Reference Examples
## Complete Implementation Examples
### 1. YARP Advanced Configuration
```csharp
/// <summary>
/// EN: Advanced YARP configuration with transforms and load balancing.
/// VI: Cấu hình YARP nâng cao với transforms và load balancing.
/// </summary>
// Program.cs
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(transforms =>
{
// EN: Add correlation ID to all requests
transforms.AddRequestTransform(ctx =>
{
var correlationId = ctx.HttpContext.Request.Headers["X-Correlation-Id"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
ctx.ProxyRequest.Headers.TryAddWithoutValidation(
"X-Correlation-Id", correlationId);
return ValueTask.CompletedTask;
});
// EN: Add timing header to responses
transforms.AddResponseTransform(ctx =>
{
ctx.HttpContext.Response.Headers["X-Processed-At"] =
DateTime.UtcNow.ToString("O");
return ValueTask.CompletedTask;
});
});
// EN: Add health checks for backends
builder.Services.AddHealthChecks()
.AddUrlGroup(new Uri("http://user-service:5001/health"), name: "user-service")
.AddUrlGroup(new Uri("http://order-service:5002/health"), name: "order-service");
var app = builder.Build();
// EN: Add authentication before proxy
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.UseSessionAffinity();
proxyPipeline.UseLoadBalancing();
proxyPipeline.UsePassiveHealthChecks();
});
```
### 2. Complete Service Clients with Resilience
```csharp
/// <summary>
/// EN: HTTP client configuration with resilience.
/// VI: Cấu hình HTTP client với resilience.
/// </summary>
public static class HttpClientExtensions
{
public static IServiceCollection AddServiceClients(
this IServiceCollection services,
IConfiguration configuration)
{
// EN: Product Service Client
services.AddHttpClient<IProductServiceClient, ProductServiceClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Products:BaseUrl"]!);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler();
// EN: Review Service Client
services.AddHttpClient<IReviewServiceClient, ReviewServiceClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Reviews:BaseUrl"]!);
})
.AddStandardResilienceHandler();
// EN: Inventory Service Client
services.AddHttpClient<IInventoryServiceClient, InventoryServiceClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Inventory:BaseUrl"]!);
})
.AddStandardResilienceHandler();
return services;
}
}
/// <summary>
/// EN: Product service client implementation.
/// VI: Triển khai client cho product service.
/// </summary>
public interface IProductServiceClient
{
Task<ProductDto?> GetProductAsync(Guid productId, CancellationToken ct = default);
Task<PagedResult<ProductDto>> GetProductsAsync(int page, int pageSize, CancellationToken ct = default);
}
public class ProductServiceClient : IProductServiceClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<ProductServiceClient> _logger;
public ProductServiceClient(
HttpClient httpClient,
ILogger<ProductServiceClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<ProductDto?> GetProductAsync(Guid productId, CancellationToken ct = default)
{
try
{
var response = await _httpClient.GetAsync($"/api/products/{productId}", ct);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ProductDto>(ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to get product {ProductId}", productId);
throw;
}
}
public async Task<PagedResult<ProductDto>> GetProductsAsync(
int page,
int pageSize,
CancellationToken ct = default)
{
var response = await _httpClient.GetAsync(
$"/api/products?page={page}&pageSize={pageSize}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PagedResult<ProductDto>>(ct)
?? new PagedResult<ProductDto>();
}
}
```
### 3. Aggregation with Timeout and Fallback
```csharp
/// <summary>
/// EN: Aggregator with timeout and fallback handling.
/// VI: Aggregator với xử lý timeout và fallback.
/// </summary>
public class ResilientAggregatorService
{
private readonly IProductServiceClient _productClient;
private readonly IReviewServiceClient _reviewClient;
private readonly IInventoryServiceClient _inventoryClient;
private readonly IDistributedCache _cache;
private readonly ILogger<ResilientAggregatorService> _logger;
private static readonly TimeSpan AggregationTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
public ResilientAggregatorService(
IProductServiceClient productClient,
IReviewServiceClient reviewClient,
IInventoryServiceClient inventoryClient,
IDistributedCache cache,
ILogger<ResilientAggregatorService> logger)
{
_productClient = productClient;
_reviewClient = reviewClient;
_inventoryClient = inventoryClient;
_cache = cache;
_logger = logger;
}
public async Task<ProductDetailsDto?> GetProductDetailsAsync(
Guid productId,
CancellationToken ct = default)
{
// EN: Try cache first
var cacheKey = $"product-details:{productId}";
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (!string.IsNullOrEmpty(cached))
{
return JsonSerializer.Deserialize<ProductDetailsDto>(cached);
}
// EN: Create timeout cancellation token
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(AggregationTimeout);
// EN: Fetch product (required)
var product = await _productClient.GetProductAsync(productId, timeoutCts.Token);
if (product == null)
return null;
// EN: Fetch optional data with fallbacks
var reviewsTask = GetReviewsSafeAsync(productId, timeoutCts.Token);
var stockTask = GetStockSafeAsync(productId, timeoutCts.Token);
await Task.WhenAll(reviewsTask, stockTask);
var reviews = await reviewsTask;
var stock = await stockTask;
var result = new ProductDetailsDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
ImageUrl = product.ImageUrl,
// EN: Reviews with fallback
AverageRating = reviews?.AverageRating ?? 0,
ReviewCount = reviews?.TotalCount ?? 0,
TopReviews = reviews?.Items.Take(3).ToList() ?? new List<ReviewDto>(),
ReviewsAvailable = reviews != null,
// EN: Stock with fallback
InStock = stock?.AvailableQuantity > 0 ?? true, // Assume in stock if unknown
AvailableQuantity = stock?.AvailableQuantity,
StockAvailable = stock != null
};
// EN: Cache the result
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = CacheDuration
},
ct);
return result;
}
private async Task<ReviewSummaryDto?> GetReviewsSafeAsync(
Guid productId,
CancellationToken ct)
{
try
{
return await _reviewClient.GetReviewSummaryAsync(productId, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to get reviews for product {ProductId}", productId);
return null;
}
}
private async Task<StockDto?> GetStockSafeAsync(
Guid productId,
CancellationToken ct)
{
try
{
return await _inventoryClient.GetStockAsync(productId, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to get stock for product {ProductId}", productId);
return null;
}
}
}
```
### 4. Complete BFF Project Structure
```csharp
/// <summary>
/// EN: Mobile BFF project structure and implementation.
/// VI: Cấu trúc và triển khai Mobile BFF project.
/// </summary>
// MobileBff/Program.cs
var builder = WebApplication.CreateBuilder(args);
// EN: Add services
builder.Services.AddControllers();
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddServiceClients(builder.Configuration);
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
// EN: Add authentication
builder.Services.AddJwtAuthentication(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
// MobileBff/Features/Home/GetHomeScreenQuery.cs
public record GetHomeScreenQuery : IRequest<HomeScreenDto>
{
public string UserId { get; init; } = default!;
}
public record HomeScreenDto
{
public UserProfileSummaryDto? Profile { get; init; }
public List<FeaturedProductDto> FeaturedProducts { get; init; } = new();
public List<CategoryDto> Categories { get; init; } = new();
public List<PromotionDto> ActivePromotions { get; init; } = new();
public int CartItemCount { get; init; }
public int UnreadNotifications { get; init; }
}
public class GetHomeScreenQueryHandler : IRequestHandler<GetHomeScreenQuery, HomeScreenDto>
{
private readonly IUserServiceClient _userClient;
private readonly IProductServiceClient _productClient;
private readonly ICategoryServiceClient _categoryClient;
private readonly IPromotionServiceClient _promotionClient;
private readonly ICartServiceClient _cartClient;
private readonly INotificationServiceClient _notificationClient;
private readonly ILogger<GetHomeScreenQueryHandler> _logger;
public GetHomeScreenQueryHandler(
IUserServiceClient userClient,
IProductServiceClient productClient,
ICategoryServiceClient categoryClient,
IPromotionServiceClient promotionClient,
ICartServiceClient cartClient,
INotificationServiceClient notificationClient,
ILogger<GetHomeScreenQueryHandler> logger)
{
_userClient = userClient;
_productClient = productClient;
_categoryClient = categoryClient;
_promotionClient = promotionClient;
_cartClient = cartClient;
_notificationClient = notificationClient;
_logger = logger;
}
public async Task<HomeScreenDto> Handle(
GetHomeScreenQuery request,
CancellationToken ct)
{
// EN: All calls in parallel with safe wrappers
var profileTask = SafeCallAsync(() =>
_userClient.GetProfileSummaryAsync(request.UserId, ct));
var featuredTask = SafeCallAsync(() =>
_productClient.GetFeaturedAsync(6, ct));
var categoriesTask = SafeCallAsync(() =>
_categoryClient.GetTopCategoriesAsync(8, ct));
var promotionsTask = SafeCallAsync(() =>
_promotionClient.GetActiveAsync(ct));
var cartCountTask = SafeCallAsync(() =>
_cartClient.GetItemCountAsync(request.UserId, ct));
var notificationCountTask = SafeCallAsync(() =>
_notificationClient.GetUnreadCountAsync(request.UserId, ct));
await Task.WhenAll(
profileTask, featuredTask, categoriesTask,
promotionsTask, cartCountTask, notificationCountTask);
return new HomeScreenDto
{
Profile = await profileTask,
FeaturedProducts = (await featuredTask)?.ToList() ?? new(),
Categories = (await categoriesTask)?.ToList() ?? new(),
ActivePromotions = (await promotionsTask)?.ToList() ?? new(),
CartItemCount = await cartCountTask ?? 0,
UnreadNotifications = await notificationCountTask ?? 0
};
}
private async Task<T?> SafeCallAsync<T>(Func<Task<T>> call)
{
try
{
return await call();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Service call failed in home screen aggregation");
return default;
}
}
}
```
### 5. GraphQL BFF with DataLoaders
```csharp
/// <summary>
/// EN: GraphQL BFF with efficient data loading.
/// VI: GraphQL BFF với data loading hiệu quả.
/// </summary>
// GraphQL/Query.cs
public class Query
{
[UseProjection]
[UseFiltering]
[UseSorting]
public async Task<IQueryable<Product>> GetProducts(
[Service] IProductRepository repository) =>
repository.GetAll();
public async Task<Product?> GetProduct(
[ID] Guid id,
ProductByIdDataLoader loader) =>
await loader.LoadAsync(id);
}
// GraphQL/DataLoaders/ProductByIdDataLoader.cs
public class ProductByIdDataLoader : BatchDataLoader<Guid, Product>
{
private readonly IProductServiceClient _client;
public ProductByIdDataLoader(
IProductServiceClient client,
IBatchScheduler batchScheduler,
DataLoaderOptions? options = null)
: base(batchScheduler, options)
{
_client = client;
}
protected override async Task<IReadOnlyDictionary<Guid, Product>> LoadBatchAsync(
IReadOnlyList<Guid> keys,
CancellationToken ct)
{
// EN: Batch load all requested products
var products = await _client.GetProductsByIdsAsync(keys, ct);
return products.ToDictionary(p => p.Id);
}
}
// GraphQL/Types/ProductType.cs
public class ProductType : ObjectType<Product>
{
protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
{
descriptor.Field(p => p.Id).Type<NonNullType<IdType>>();
descriptor.Field(p => p.Name).Type<NonNullType<StringType>>();
descriptor.Field(p => p.Price).Type<NonNullType<DecimalType>>();
// EN: Reviews field with data loader
descriptor
.Field("reviews")
.Type<NonNullType<ListType<NonNullType<ReviewType>>>>()
.Resolve(async ctx =>
{
var loader = ctx.DataLoader<ReviewsByProductIdDataLoader>();
return await loader.LoadAsync(ctx.Parent<Product>().Id);
});
// EN: Stock field with data loader
descriptor
.Field("stock")
.Type<StockType>()
.Resolve(async ctx =>
{
var loader = ctx.DataLoader<StockByProductIdDataLoader>();
return await loader.LoadAsync(ctx.Parent<Product>().Id);
});
// EN: Related products
descriptor
.Field("relatedProducts")
.Type<ListType<ProductType>>()
.Argument("limit", a => a.Type<IntType>().DefaultValue(4))
.Resolve(async ctx =>
{
var product = ctx.Parent<Product>();
var limit = ctx.ArgumentValue<int>("limit");
var loader = ctx.DataLoader<RelatedProductsDataLoader>();
var related = await loader.LoadAsync(product.CategoryId);
return related.Where(p => p.Id != product.Id).Take(limit);
});
}
}
```
## Docker Compose for API Gateway
```yaml
version: '3.8'
services:
api-gateway:
build:
context: ./src/ApiGateway
ports:
- "5000:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ReverseProxy__Clusters__users-cluster__Destinations__user-service__Address=http://user-service:80
- ReverseProxy__Clusters__orders-cluster__Destinations__order-service__Address=http://order-service:80
depends_on:
- user-service
- order-service
mobile-bff:
build:
context: ./src/MobileBff
ports:
- "5001:80"
environment:
- Services__Products__BaseUrl=http://product-service:80
- Services__Reviews__BaseUrl=http://review-service:80
- ConnectionStrings__Redis=redis:6379
web-bff:
build:
context: ./src/WebBff
ports:
- "5002:80"
user-service:
build:
context: ./src/UserService
expose:
- "80"
order-service:
build:
context: ./src/OrderService
expose:
- "80"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
```