# 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"
```