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

17 KiB

API Aggregation & BFF - Reference Examples

Complete Implementation Examples

1. YARP Advanced Configuration

/// <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

/// <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

/// <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

/// <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

/// <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

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"