# API Aggregation & BFF - Reference Examples ## Complete Implementation Examples ### 1. YARP Advanced Configuration ```csharp /// /// EN: Advanced YARP configuration with transforms and load balancing. /// VI: Cấu hình YARP nâng cao với transforms và load balancing. /// // 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 /// /// EN: HTTP client configuration with resilience. /// VI: Cấu hình HTTP client với resilience. /// public static class HttpClientExtensions { public static IServiceCollection AddServiceClients( this IServiceCollection services, IConfiguration configuration) { // EN: Product Service Client services.AddHttpClient(client => { client.BaseAddress = new Uri(configuration["Services:Products:BaseUrl"]!); client.DefaultRequestHeaders.Add("Accept", "application/json"); }) .AddStandardResilienceHandler(); // EN: Review Service Client services.AddHttpClient(client => { client.BaseAddress = new Uri(configuration["Services:Reviews:BaseUrl"]!); }) .AddStandardResilienceHandler(); // EN: Inventory Service Client services.AddHttpClient(client => { client.BaseAddress = new Uri(configuration["Services:Inventory:BaseUrl"]!); }) .AddStandardResilienceHandler(); return services; } } /// /// EN: Product service client implementation. /// VI: Triển khai client cho product service. /// public interface IProductServiceClient { Task GetProductAsync(Guid productId, CancellationToken ct = default); Task> GetProductsAsync(int page, int pageSize, CancellationToken ct = default); } public class ProductServiceClient : IProductServiceClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; public ProductServiceClient( HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } public async Task 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(ct); } catch (HttpRequestException ex) { _logger.LogError(ex, "Failed to get product {ProductId}", productId); throw; } } public async Task> 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>(ct) ?? new PagedResult(); } } ``` ### 3. Aggregation with Timeout and Fallback ```csharp /// /// EN: Aggregator with timeout and fallback handling. /// VI: Aggregator với xử lý timeout và fallback. /// public class ResilientAggregatorService { private readonly IProductServiceClient _productClient; private readonly IReviewServiceClient _reviewClient; private readonly IInventoryServiceClient _inventoryClient; private readonly IDistributedCache _cache; private readonly ILogger _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 logger) { _productClient = productClient; _reviewClient = reviewClient; _inventoryClient = inventoryClient; _cache = cache; _logger = logger; } public async Task 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(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(), 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 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 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 /// /// EN: Mobile BFF project structure and implementation. /// VI: Cấu trúc và triển khai Mobile BFF project. /// // 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 { public string UserId { get; init; } = default!; } public record HomeScreenDto { public UserProfileSummaryDto? Profile { get; init; } public List FeaturedProducts { get; init; } = new(); public List Categories { get; init; } = new(); public List ActivePromotions { get; init; } = new(); public int CartItemCount { get; init; } public int UnreadNotifications { get; init; } } public class GetHomeScreenQueryHandler : IRequestHandler { 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 _logger; public GetHomeScreenQueryHandler( IUserServiceClient userClient, IProductServiceClient productClient, ICategoryServiceClient categoryClient, IPromotionServiceClient promotionClient, ICartServiceClient cartClient, INotificationServiceClient notificationClient, ILogger logger) { _userClient = userClient; _productClient = productClient; _categoryClient = categoryClient; _promotionClient = promotionClient; _cartClient = cartClient; _notificationClient = notificationClient; _logger = logger; } public async Task 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 SafeCallAsync(Func> 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 /// /// EN: GraphQL BFF with efficient data loading. /// VI: GraphQL BFF với data loading hiệu quả. /// // GraphQL/Query.cs public class Query { [UseProjection] [UseFiltering] [UseSorting] public async Task> GetProducts( [Service] IProductRepository repository) => repository.GetAll(); public async Task GetProduct( [ID] Guid id, ProductByIdDataLoader loader) => await loader.LoadAsync(id); } // GraphQL/DataLoaders/ProductByIdDataLoader.cs public class ProductByIdDataLoader : BatchDataLoader { private readonly IProductServiceClient _client; public ProductByIdDataLoader( IProductServiceClient client, IBatchScheduler batchScheduler, DataLoaderOptions? options = null) : base(batchScheduler, options) { _client = client; } protected override async Task> LoadBatchAsync( IReadOnlyList 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 { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Field(p => p.Id).Type>(); descriptor.Field(p => p.Name).Type>(); descriptor.Field(p => p.Price).Type>(); // EN: Reviews field with data loader descriptor .Field("reviews") .Type>>>() .Resolve(async ctx => { var loader = ctx.DataLoader(); return await loader.LoadAsync(ctx.Parent().Id); }); // EN: Stock field with data loader descriptor .Field("stock") .Type() .Resolve(async ctx => { var loader = ctx.DataLoader(); return await loader.LoadAsync(ctx.Parent().Id); }); // EN: Related products descriptor .Field("relatedProducts") .Type>() .Argument("limit", a => a.Type().DefaultValue(4)) .Resolve(async ctx => { var product = ctx.Parent(); var limit = ctx.ArgumentValue("limit"); var loader = ctx.DataLoader(); 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" ```