Skip to content
Najaf Shaikh edited this page Aug 12, 2025 · 7 revisions

ApiAggregator - Complete Developer Guide

This comprehensive documentation provides detailed information about the ApiAggregator framework, with special emphasis on nested API calls and parent-child relationships. The composition model allows you to build flexible, reusable APIs that can be combined in different ways to serve various client needs without modifying the underlying individual APIs.

Table of Contents

  1. Overview
  2. Architecture
  3. Getting Started
  4. Core Concepts
  5. Nested API Calls and Parent-Child Relationships
  6. Implementation Guide
  7. Advanced Features
  8. Configuration
  9. Testing
  10. Performance Considerations
  11. Best Practices
  12. Troubleshooting
  13. API Reference

Overview

ApiAggregator is a powerful .NET utility designed to combine multiple API requests into a single aggregated response. It provides a flexible framework for building composite APIs that can fetch data from various sources and transform them into a unified contract.

Key Features

  • Multi-API Aggregation: Combine responses from multiple APIs into a single result
  • Hierarchical Dependencies: Support for nested API calls with parent-child relationships
  • Selective Data Retrieval: Fetch only the data you need based on configurable API names
  • Result Transformation: Map API responses to your desired contract structure
  • Caching Support: Built-in caching mechanism for performance optimization
  • Header Management: Custom request/response header handling
  • Dependency Injection: Full support for .NET DI container
  • Circuit Breaker Ready: Compatible with resilience patterns

Composition Model Benefits

ApiAggregator follows a powerful composition model where individual APIs are composed together to create bespoke, larger responses without requiring changes to the underlying individual APIs. This approach provides several critical advantages:

1. API Reusability and Microservices Compatibility

Instead of modifying existing APIs to support different response formats, you can compose them in various combinations:

// Same CustomerApi can be used in different compositions
// Composition 1: Basic customer info
Names = ["customer"]

// Composition 2: Customer with communication only
Names = ["customer.communication"]  

// Composition 3: Complete customer profile
Names = ["customer.orders.items", "customer.communication", "customer.account"]

2. Reduced API Surface Proliferation

Without ApiAggregator, you might need multiple specialized endpoints:

  • /customers/{id}/basic
  • /customers/{id}/with-orders
  • /customers/{id}/with-communication
  • /customers/{id}/complete-profile

With ApiAggregator, you have one flexible endpoint that can serve all these scenarios by composing existing APIs dynamically.

3. Backend for Frontend (BFF) Pattern

Perfect for building BFF layers where different client applications need different data shapes from the same underlying services:

// Mobile app - lightweight data
Names = ["customer", "customer.orders"]

// Web dashboard - comprehensive data  
Names = ["customer.orders.items", "customer.communication", "customer.account.payments"]

// Admin panel - everything
Names = null // Fetches complete aggregated response

4. Evolutionary Architecture

As your system evolves, you can add new APIs to the composition without breaking existing consumers or modifying working APIs:

// Original composition
.Map<CustomerApi, CustomerTransform>(With.Name("customer"))
.Map<OrdersApi, OrdersTransform>(With.Name("customer.orders"))

// Evolution - add new APIs without touching existing ones
.Map<CustomerApi, CustomerTransform>(With.Name("customer"))
.Map<OrdersApi, OrdersTransform>(With.Name("customer.orders"))
.Map<LoyaltyApi, LoyaltyTransform>(With.Name("customer.loyalty"))        // NEW
.Map<PreferencesApi, PreferencesTransform>(With.Name("customer.preferences")) // NEW

Supported Frameworks

  • .NET 9.0
  • .NET 8.0
  • .NET 6.0
  • .NET 5.0
  • .NET Standard 2.1
  • .NET Standard 2.0
  • .NET Framework 4.6.2

Architecture

High-Level Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Client        │───▶│  ApiAggregator  │───▶│  External APIs  │
│                 │    │                 │    │                 │
│ Request Context │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ API Names[]     │    │ │ ApiBuilder  │ │    │ │ CustomerAPI │ │
└─────────────────┘    │ │ ApiExecutor │ │    │ │ OrderAPI    │ │
                       │ │ ContractBld │ │    │ │ ContactAPI  │ │
                       │ └─────────────┘ │    │ └─────────────┘ │
                       └─────────────────┘    └─────────────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Aggregated      │
                       │ Response        │
                       └─────────────────┘

Core Components

1. ApiAggregator<T>

  • The main orchestrator that coordinates the entire aggregation process.

2. ApiBuilder<T>

Responsible for building the list of APIs to execute based on the request context and configured mappings.

3. ApiExecutor

  • Manages the execution flow of APIs, handling dependencies and nested calls.

4. ContractBuilder<T>

  • Transforms API results into the final aggregated contract using configured transformers.

5. ApiEngine

  • Executes the actual HTTP requests using HttpClient and manages concurrent operations.

Getting Started

Installation

Install the latest version via NuGet Package Manager:

NuGet\Install-Package ApiAggregator

Or via .NET CLI:

dotnet add package ApiAggregator

Quick Start Example

// 1. Define your aggregated contract
public class Customer : IContract
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Code { get; set; }
    public Contacts Communication { get; set; }
    public Order[] Orders { get; set; }
}

// 2. Create API classes
public class CustomerApi : WebApi<CustomerResult>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var customerContext = (CustomerContext)context;
        return new Uri($"https://api.example.com/customers/{customerContext.CustomerId}");
    }
}

// 3. Create transformers
public class CustomerTransform : ResultTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult apiResult, Customer contract)
    {
        contract.Id = apiResult.Id;
        contract.Name = apiResult.Name;
        contract.Code = apiResult.Code;
    }
}

// 4. Define API aggregate
public class CustomerAggregate : ApiAggregate<Customer>
{
    public override IEnumerable<Mapping<Customer, IApiResult>> Construct()
    {
        return CreateAggregate.For<Customer>()
            .Map<CustomerApi, CustomerTransform>(With.Name("customer"))
            .Create();
    }
}

// 5. Configure DI
services.UseApiAggregator()
        .AddApiAggregate<Customer>(new CustomerAggregate());

// 6. Use the aggregator
var aggregator = serviceProvider.GetService<IApiAggregator<Customer>>();
var result = aggregator.GetData(new CustomerContext { CustomerId = 123 });

Core Concepts

1. Contracts (IContract)

Contracts define the shape of your aggregated response. They must implement the IContract interface.

public class Customer : IContract
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Contacts Communication { get; set; }
    
    public class Contacts
    {
        public string Email { get; set; }
        public string Phone { get; set; }
    }
}

2. Web APIs (WebApi<T>)

Web APIs represent individual HTTP endpoints. They extend WebApi<TResult> where TResult implements IApiResult.

Basic API Example

public class CustomerApi : WebApi<CustomerResult>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var customerContext = (CustomerContext)context;
        return new Uri($"https://api.example.com/customers/{customerContext.CustomerId}");
    }
    
    protected override IDictionary<string, string> GetRequestHeaders()
    {
        return new Dictionary<string, string>
        {
            { "Authorization", "Bearer token" },
            { "x-api-version", "v2" }
        };
    }
}

3. Result Transformers

Transformers map API results to your contract structure.

public class CustomerTransform : ResultTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult apiResult, Customer contract)
    {
        contract.Id = apiResult.Id;
        contract.Name = apiResult.Name;
        contract.Code = apiResult.Code;
        
        // Access request context if needed
        if (Context.Cache.ContainsKey("someKey"))
        {
            // Use cached data
        }
    }
}

4. API Aggregates

Aggregates define the mapping between APIs and transformers.

public class CustomerAggregate : ApiAggregate<Customer>
{
    public override IEnumerable<Mapping<Customer, IApiResult>> Construct()
    {
        return CreateAggregate.For<Customer>()
            .Map<CustomerApi, CustomerTransform>(With.Name("customer"))
            .Create();
    }
}

Nested API Calls and Parent-Child Relationships

Understanding Hierarchical Execution

ApiAggregator's most powerful feature is its ability to handle complex parent-child API relationships where the output of parent APIs serves as input to child APIs. This creates a data pipeline that can build sophisticated, nested data structures.

Execution Flow Overview

Request Context: { Names: ["customer.orders.items"], CustomerId: 123 }

Step 1: ApiBuilder Analysis
├── Identifies required APIs based on names
├── Builds dependency tree: Customer → Orders → OrderItems  
└── Creates execution plan with proper parent-child relationships

Step 2: Level 1 Execution (Root APIs)
├── CustomerApi.ResolveApiParameter(context, null)
│   └── GetUrl() called with context only, parentApiResult = null
├── CustomerApi.Run() executes HTTP call
└── Returns CustomerResult { Id: 123, Name: "John", Code: "ABC" }

Step 3: Level 2 Execution (Child APIs)  
├── OrdersApi.ResolveApiParameter(context, CustomerResult)
│   └── GetUrl() called with CustomerResult from Step 2
├── URL built: "/customers/123/orders"
├── OrdersApi.Run() executes HTTP call
└── Returns CollectionResult<OrderResult> with 2 orders

Step 4: Level 3 Execution (Grandchild APIs)
├── OrderItemsApi.ResolveApiParameter(context, CollectionResult<OrderResult>)
│   └── GetUrl() extracts order IDs [1001, 1002] from collection
├── URL built: "/order-items?orderIds=1001,1002"  
├── OrderItemsApi.Run() executes HTTP call
└── Returns CollectionResult<OrderItemResult> with 5 items

Step 5: Contract Building (Transformers execute in same order)
├── CustomerTransform: Builds basic customer data
├── OrdersTransform: Adds orders array to customer
└── OrderItemsTransform: Populates items within each order

Parent-Child API Examples

Level 1: Root API (Parent)

public class CustomerApi : WebApi<CustomerResult>
{
    public CustomerApi() : base("https://api.example.com")
    {
    }

    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        // parentApiResult is ALWAYS null for root-level APIs
        var customerContext = (CustomerContext)context;
        return new Uri($"/api/customers/{customerContext.CustomerId}");
    }

    protected override IDictionary<string, string> GetRequestHeaders()
    {
        return new Dictionary<string, string>
        {
            { "x-correlation-id", Guid.NewGuid().ToString() }
        };
    }
}

Level 2: Child API

public class OrdersApi : WebApi<CollectionResult<OrderResult>>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        // parentApiResult contains CustomerResult from CustomerApi
        var customer = (CustomerResult)parentApiResult;
        var customerContext = (CustomerContext)context;
        
        // Build URL using data from both parent and context
        return new Uri($"/api/customers/{customer.Id}/orders?branch={customerContext.BranchCode}");
    }
}

Level 3: Grandchild API

public class OrderItemsApi : WebApi<CollectionResult<OrderItemResult>>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        // parentApiResult is CollectionResult<OrderResult> from OrdersApi
        var orders = (CollectionResult<OrderResult>)parentApiResult;
        var customerContext = (CustomerContext)context;
        
        if (!orders.Any())
            return null; // Skip execution if no orders
            
        // Extract order IDs for batch query
        var orderIds = orders.Select(o => o.OrderId).ToArray();
        var orderIdsQuery = string.Join(",", orderIds);
        
        return new Uri($"/api/customers/{customerContext.CustomerId}/order-items?orderIds={orderIdsQuery}");
    }
}

Hierarchical Transformers

Level 1 Transformer: Foundation Builder

public class CustomerTransform : ResultTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult apiResult, Customer contract)
    {
        // Build the foundation of our contract
        contract.Id = apiResult.Id;
        contract.Name = apiResult.Name;
        contract.Code = apiResult.Code;
        
        // Initialize collections that child transformers will populate
        contract.Orders = new Order[0];
    }
}

Level 2 Transformer: Building on Parent Data

public class OrdersTransform : ResultTransformer<CollectionResult<OrderResult>, Customer>
{
    public override void Transform(CollectionResult<OrderResult> collectionResult, Customer contract)
    {
        if (collectionResult == null || !collectionResult.Any())
        {
            contract.Orders = new Order[0];
            return;
        }
        
        // The customer basic info is already populated by CustomerTransform
        contract.Orders = new Order[collectionResult.Count];
        
        for (var index = 0; index < collectionResult.Count; index++)
        {
            var orderResult = collectionResult[index];
            contract.Orders[index] = new Order
            {
                OrderId = orderResult.OrderId,
                OrderNo = orderResult.OrderNo,
                Date = orderResult.Date,
                CustomerId = contract.Id, // Use data from parent transformer
                Items = new OrderItem[0] // Will be populated by OrderItemsTransform
            };
        }
    }
}

Level 3 Transformer: Complex Nested Building

public class OrderItemsTransform : ResultTransformer<CollectionResult<OrderItemResult>, Customer>
{
    public override void Transform(CollectionResult<OrderItemResult> collectionResult, Customer customer)
    {
        if (collectionResult == null || !collectionResult.Any() || customer.Orders == null)
            return;
            
        // Group items by order ID for efficient processing
        var itemsByOrder = collectionResult.GroupBy(item => item.OrderId);
        
        foreach (var orderGroup in itemsByOrder)
        {
            // Find the corresponding order in our contract
            var order = customer.Orders.FirstOrDefault(o => o.OrderId == orderGroup.Key);
            if (order == null) continue;
            
            // Populate the items array
            var items = orderGroup.ToList();
            order.Items = new OrderItem[items.Count];
            
            for (int i = 0; i < items.Count; i++)
            {
                var itemResult = items[i];
                order.Items[i] = new OrderItem
                {
                    ItemId = itemResult.ItemId,
                    Name = itemResult.Name,
                    Cost = itemResult.Cost,
                    OrderId = itemResult.OrderId,
                    // Calculate derived fields using parent data
                    CustomerName = customer.Name, // From root level
                    OrderDate = order.Date        // From parent level
                };
            }
            
            // Calculate order totals now that we have items
            order.TotalAmount = order.Items.Sum(item => item.Cost);
            order.ItemCount = order.Items.Length;
        }
        
        // Update customer-level aggregates
        customer.TotalOrderValue = customer.Orders.Sum(o => o.TotalAmount);
        customer.TotalItemCount = customer.Orders.Sum(o => o.ItemCount);
    }
}

Hierarchical Aggregate Definition

public class CustomerAggregate : ApiAggregate<Customer>
{
    public override IEnumerable<Mapping<Customer, IApiResult>> Construct()
    {
        return CreateAggregate.For<Customer>()
            .Map<CustomerApi, CustomerTransform>(With.Name("customer"),           // Level 1
                customer => customer.Dependents
                    .Map<CommunicationApi, CommunicationTransform>(                // Level 2
                        With.Name("customer.communication"))
                    .Map<OrdersApi, OrdersTransform>(                              // Level 2
                        With.Name("customer.orders"),
                        orders => orders.Dependents
                            .Map<OrderItemsApi, OrderItemsTransform>(              // Level 3
                                With.Name("customer.orders.items")))
            ).Create();
    }
}

Selective Execution with Auto-Parent Inclusion

// Request: ["customer.orders.items"]
// Actual Execution:
// 1. CustomerApi (auto-included as parent)
// 2. OrdersApi (auto-included as parent) 
// 3. OrderItemsApi (explicitly requested)

// Request: ["customer.communication", "customer.orders.items"]  
// Actual Execution:
// 1. CustomerApi (shared parent, executed once)
// 2. CommunicationApi (explicitly requested)
// 3. OrdersApi (auto-included for items)
// 4. OrderItemsApi (explicitly requested)

Implementation Guide

Step-by-Step Implementation

Step 1: Create API Result Classes

public class CustomerResult : ApiResult
{
    public int Id { get; set; }
    public string Code { get; set; }
    public string Name { get; set; }
}

public class CommunicationResult : ApiResult
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

public class OrderResult : ApiResult
{
    public int OrderId { get; set; }
    public string OrderNo { get; set; }
    public DateTime Date { get; set; }
}

Step 2: Implement Web APIs with Parent-Child Dependencies

// Level 1: Root API - Uses only context
public class CustomerApi : WebApi<CustomerResult>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var customerContext = (CustomerContext)context;
        return new Uri($"/api/customers/{customerContext.CustomerId}");
    }
}

// Level 2: Child API - Uses parent result + context  
public class OrdersApi : WebApi<CollectionResult<OrderResult>>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var customer = (CustomerResult)parentApiResult;
        return new Uri($"/api/customers/{customer.Id}/orders");
    }
}

// Level 3: Grandchild API - Uses grandparent context + immediate parent result
public class OrderItemsApi : WebApi<CollectionResult<OrderItemResult>>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var orders = (CollectionResult<OrderResult>)parentApiResult;
        var customerContext = (CustomerContext)context;
        
        if (!orders.Any()) return null;
            
        var orderIds = orders.Select(o => o.OrderId).ToArray();
        var orderIdsQuery = string.Join(",", orderIds);
        
        return new Uri($"/api/customers/{customerContext.CustomerId}/order-items?orderIds={orderIdsQuery}");
    }
}

Step 3: Create Hierarchical Result Transformers

// Level 1 Transformer: Foundation builder
public class CustomerTransform : ResultTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult apiResult, Customer contract)
    {
        contract.Id = apiResult.Id;
        contract.Name = apiResult.Name;
        contract.Code = apiResult.Code;
        contract.Orders = new Order[0]; // Initialize for child transformers
    }
}

// Level 2 Transformer: Orders branch  
public class OrdersTransform : ResultTransformer<CollectionResult<OrderResult>, Customer>
{
    public override void Transform(CollectionResult<OrderResult> collectionResult, Customer contract)
    {
        if (collectionResult == null || !collectionResult.Any())
        {
            contract.Orders = new Order[0];
            return;
        }
        
        contract.Orders = collectionResult.Select(orderResult => new Order
        {
            OrderId = orderResult.OrderId,
            OrderNo = orderResult.OrderNo,
            Date = orderResult.Date,
            CustomerId = contract.Id, // Link back to customer data
            Items = new OrderItem[0]  // Will be populated by OrderItemsTransform
        }).ToArray();
        
        contract.OrderCount = contract.Orders.Length;
    }
}

// Level 3 Transformer: Order items
public class OrderItemsTransform : ResultTransformer<CollectionResult<OrderItemResult>, Customer>
{
    public override void Transform(CollectionResult<OrderItemResult> collectionResult, Customer customer)
    {
        if (collectionResult == null || !collectionResult.Any() || customer.Orders == null)
            return;
            
        var itemsByOrder = collectionResult.GroupBy(item => item.OrderId);
        
        foreach (var order in customer.Orders)
        {
            var orderItems = itemsByOrder.FirstOrDefault(g => g.Key == order.OrderId)?.ToList() ?? new List<OrderItemResult>();
            
            order.Items = orderItems.Select(itemResult => new OrderItem
            {
                ItemId = itemResult.ItemId,
                Name = itemResult.Name,
                Cost = itemResult.Cost,
                OrderId = order.OrderId,
                CustomerName = customer.Name  // Enrich with root-level data
            }).ToArray();
            
            order.TotalAmount = order.Items.Sum(item => item.Cost);
        }
        
        customer.TotalOrderValue = customer.Orders.Sum(o => o.TotalAmount);
    }
}

Step 4: Configure Complete Hierarchical Aggregate

public class CustomerAggregate : ApiAggregate<Customer>
{
    public override IEnumerable<Mapping<Customer, IApiResult>> Construct()
    {
        return CreateAggregate.For<Customer>()
            .Map<CustomerApi, CustomerTransform>(With.Name("customer"), 
                customer => customer.Dependents
                    .Map<CommunicationApi, CommunicationTransform>(
                        With.Name("customer.communication"))
                    .Map<OrdersApi, OrdersTransform>(With.Name("customer.orders"),
                        orders => orders.Dependents
                            .Map<OrderItemsApi, OrderItemsTransform>(
                                With.Name("customer.orders.items")))
            ).Create();
    }
}

Step 5: Configure Dependency Injection

services.AddLogging();
services.AddHttpClient();

services.UseApiAggregator()
        .AddApiAggregate<Customer>(new CustomerAggregate());

Step 6: Usage in Controllers

public class CustomerController : ControllerBase
{
    private readonly IApiAggregator<Customer> _aggregator;
    
    public CustomerController(IApiAggregator<Customer> aggregator)
    {
        _aggregator = aggregator;
    }
    
    [HttpGet("{customerId}")]
    public Customer GetCustomer(int customerId, [FromQuery] string[] includes = null)
    {
        var context = new CustomerContext 
        { 
            CustomerId = customerId,
            Names = includes // e.g., ["customer.orders.items", "customer.communication"]
        };
        
        return _aggregator.GetData(context);
    }
    
    // Get customer with recent orders and their items
    [HttpGet("{customerId}/recent-activity")]
    public Customer GetCustomerRecentActivity(int customerId)
    {
        var context = new CustomerContext 
        { 
            CustomerId = customerId,
            Names = new[] { "customer.orders.items" }, // Auto-includes customer, orders
            BranchCode = "MAIN"
        };
        
        return _aggregator.GetData(context);
    }
}

Advanced Features

1. Caching Support

Use the [CacheResult] attribute to cache API results:

[CacheResult]
public class CommunicationResult : ApiResult
{
    // This result will be cached and available to other transformers
}

public class SomeTransformer : ResultTransformer<SomeResult, Customer>
{
    public override void Transform(SomeResult apiResult, Customer contract)
    {
        // Access cached communication data
        if (Context.Cache.TryGetValue(nameof(CommunicationResult), out var cached))
        {
            var communication = (CommunicationResult)cached;
            // Use cached data
        }
    }
}

2. Custom API Name Matching

Implement custom matching logic:

public class CustomMatcher : IApiNameMatcher
{
    public bool IsMatch(string inputName, IApiNames configuredNames)
    {
        return configuredNames.Names.Any(name => 
            inputName.StartsWith(name, StringComparison.OrdinalIgnoreCase));
    }
}

services.AddTransient<IApiNameMatcher, CustomMatcher>();

3. Conditional API Execution

public class ConditionalApi : WebApi<SomeResult>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var parent = (ParentResult)parentApiResult;
        
        // Only execute if parent has specific data
        if (parent.HasChildData)
            return new Uri($"/parent/{parent.Id}/children");
        
        return null; // Skip execution
    }
}

4. Collection Results

Handle API responses that return collections:

public class OrdersApi : WebApi<CollectionResult<OrderResult>>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var customer = (CustomerResult)parentApiResult;
        return new Uri($"/customers/{customer.Id}/orders");
    }
}

Configuration

HTTP Client Configuration

services.AddHttpClient("resilient")
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .WaitAndRetryAsync(3, retryAttempt =>
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

Logging Configuration

services.AddLogging(config =>
{
    config.AddConsole();
    config.SetMinimumLevel(LogLevel.Information);
});

Testing

Unit Testing Parent-Child Relationships

[Test]
public void TestApiBuilderForCorrectParentApiListWithNestedChildren()
{
    var context = new CustomerContext { Names = new[] { "customer.orders.items" } };

    var result = _apiBuilder.Build(context);

    Assert.That(result.Apis.Count, Is.EqualTo(1));
    
    var parentApi = result.Apis.First();
    Assert.That(parentApi.GetType(), Is.EqualTo(typeof(CustomerApi)));
    Assert.That(parentApi.Children.Count, Is.EqualTo(1));

    var ordersApi = parentApi.Children.First();
    Assert.That(ordersApi.GetType(), Is.EqualTo(typeof(OrdersApi)));
    Assert.That(ordersApi.Children.Count, Is.EqualTo(1));

    var itemsApi = ordersApi.Children.First();
    Assert.That(itemsApi.GetType(), Is.EqualTo(typeof(OrderItemsApi)));
}

Integration Testing with WireMock

public class HierarchicalApiE2ETest : BaseE2ETest
{
    [Test]
    public void TestCompleteHierarchicalFlow()
    {
        // Setup mock responses at each level
        StubApi("/api/customers/1000", new { Id = 1000, Name = "John McKinsey" });
        StubApi("/api/customers/1000/orders", new[] { 
            new { OrderId = 1234, OrderNo = "ORD-1234", Date = "2024-01-01" }
        });
        StubApi("/api/customers/1000/order-items?orderIds=1234", new[] { 
            new { OrderId = 1234, ItemId = 2244, Name = "Laptop", Cost = 1200.00m }
        });

        var customer = apiAggregator.GetData(new CustomerContext
        {
            CustomerId = 1000,
            Names = new[] { "customer.orders.items" }
        });

        // Verify complete hierarchical structure
        Assert.That(customer.Id, Is.EqualTo(1000));
        Assert.That(customer.Orders.Length, Is.EqualTo(1));
        Assert.That(customer.Orders[0].Items.Length, Is.EqualTo(1));
        Assert.That(customer.Orders[0].Items[0].CustomerName, Is.EqualTo("John McKinsey"));
    }
}

Performance Considerations

1. Concurrent Execution

APIs at the same level execute concurrently:

// Level 2 APIs execute in parallel when they share the same parent
// Customer (Level 1) → [Orders, Communication, Account] (Level 2 - Parallel)

2. Nesting Depth Limitation

The framework supports up to 5 levels of API nesting.

3. Caching Strategy

  • Use [CacheResult] for frequently accessed data
  • Cache is scoped to individual requests
  • Cached results are available to all transformers in the same request

4. Memory Management

  • Use CollectionResult<T> for large datasets
  • Implement proper disposal patterns in custom components
  • Consider pagination for large result sets

Best Practices

1. API Design

  • Keep API names hierarchical and intuitive (customer.orders.items)
  • Use meaningful result type names
  • Implement proper error handling in APIs
  • Return null from GetUrl() to skip API execution when conditions aren't met

2. Performance

  • Cache expensive or frequently accessed data
  • Use selective data retrieval to minimize API calls
  • Implement timeout policies for external APIs
  • Monitor and log API execution times

3. Error Handling

public class RobustApi : WebApi<SomeResult>
{
    public override async Task<IApiResult> Run(IHttpClientFactory factory, ILogger logger)
    {
        try
        {
            return await base.Run(factory, logger);
        }
        catch (HttpRequestException ex)
        {
            logger.LogWarning(ex, "API call failed, returning default result");
            return new SomeResult(); // Return default or null
        }
    }
}

4. Hierarchical Error Handling

public class ResilientOrderItemsApi : WebApi<CollectionResult<OrderItemResult>>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        var orders = (CollectionResult<OrderResult>)parentApiResult;
        
        // If parent API failed or returned empty, gracefully handle
        if (orders == null || !orders.Any())
        {
            logger?.LogWarning("No orders found, skipping order items API");
            return null;
        }
        
        return new Uri($"/order-items?orderIds={string.Join(",", orders.Select(o => o.OrderId))}");
    }
    
    public override async Task<IApiResult> Run(IHttpClientFactory factory, ILogger logger)
    {
        try
        {
            return await base.Run(factory, logger);
        }
        catch (HttpRequestException ex)
        {
            logger?.LogWarning(ex, "Order items API failed, returning empty collection");
            return new CollectionResult<OrderItemResult>(new List<OrderItemResult>());
        }
    }
}

5. Security

  • Always validate input in request contexts
  • Use secure HTTP headers
  • Implement authentication/authorization in APIs
  • Sanitize logged data to avoid sensitive information exposure

6. Maintainability

  • Use dependency injection for all components
  • Keep transformers simple and focused
  • Document complex business logic
  • Use meaningful variable names and comments

Troubleshooting

Common Issues

1. API Not Executing

Problem: API is not being called
Solution:

  • Verify API name matches the configured mapping
  • Check IApiNameMatcher implementation
  • Ensure API is included in request context Names array

2. Null Parent Results

Problem: Child APIs receive null parent results
Solution:

  • Check parent API's GetUrl() implementation returns valid URI
  • Verify parent API HTTP response is successful
  • Ensure parent result type matches child API expectations

3. Transformation Not Working

Problem: Data not appearing in final contract
Solution:

  • Verify transformer implements correct result type
  • Check if API result type matches transformer's expected type
  • Ensure transformer is registered in aggregate mapping

4. Hierarchical Structure Issues

Problem: Child APIs not executing in proper order
Solution:

  • Check aggregate mapping defines correct parent-child relationships
  • Verify API names follow dot-separated convention
  • Ensure parent APIs return expected result types

Debugging Tips

  1. Enable detailed logging:
services.AddLogging(config => config.SetMinimumLevel(LogLevel.Debug));
  1. Validate hierarchical configuration:
public void ValidateHierarchicalConfiguration()
{
    var aggregate = serviceProvider.GetService<IApiAggregate<Customer>>();
    var mappings = aggregate.Mappings.ToList();
    
    // Check for orphaned child APIs
    var childApis = mappings.Where(m => m.DependentOn != null);
    foreach (var child in childApis)
    {
        var parentExists = mappings.Any(m => m.Api.GetType() == child.DependentOn.GetType());
        if (!parentExists)
            throw new InvalidOperationException($"Child API {child.Api.GetType().Name} has no matching parent");
    }
}
  1. Monitor execution order:
public class DebuggingApi : WebApi<SomeResult>
{
    protected override Uri GetUrl(IRequestContext context, IApiResult parentApiResult)
    {
        logger?.LogInformation($"Executing {GetType().Name}, Parent: {parentApiResult?.GetType().Name ?? "None"}");
        return base.GetUrl(context, parentApiResult);
    }
}

Real-World Examples

E-Commerce Customer Profile

public class ECommerceAggregate : ApiAggregate<CustomerProfile>
{
    public override IEnumerable<Mapping<CustomerProfile, IApiResult>> Construct()
    {
        return CreateAggregate.For<CustomerProfile>()
            .Map<CustomerApi, CustomerTransform>(With.Name("customer"),
                customer => customer.Dependents
                    .Map<OrderHistoryApi, OrderHistoryTransform>(With.Name("customer.orders"),
                        orders => orders.Dependents
                            .Map<OrderDetailsApi, OrderDetailsTransform>(With.Name("customer.orders.details"),
                                details => details.Dependents
                                    .Map<ProductInfoApi, ProductInfoTransform>(With.Name("customer.orders.details.products"))
                                    .Map<ReviewsApi, ReviewsTransform>(With.Name("customer.orders.details.reviews"))))
                    .Map<WishlistApi, WishlistTransform>(With.Name("customer.wishlist"))
                    .Map<RecommendationsApi, RecommendationsTransform>(With.Name("customer.recommendations"))
            ).Create();
    }
}

// Usage examples:
// Basic profile: ["customer"]
// With order history: ["customer.orders"]  
// Complete profile: ["customer.orders.details.products", "customer.wishlist", "customer.recommendations"]

Financial Services Account

public class FinancialAccountAggregate : ApiAggregate<AccountOverview>
{
    public override IEnumerable<Mapping<AccountOverview, IApiResult>> Construct()
    {
        return CreateAggregate.For<AccountOverview>()
            .Map<AccountHolderApi, AccountHolderTransform>(With.Name("account"),
                account => account.Dependents
                    .Map<AccountsApi, AccountsTransform>(With.Name("account.accounts"),
                        accounts => accounts.Dependents
                            .Map<TransactionsApi, TransactionsTransform>(With.Name("account.accounts.transactions"),
                                transactions => transactions.Dependents
                                    .Map<TransactionDetailsApi, TransactionDetailsTransform>(With.Name("account.accounts.transactions.details"))
                                    .Map<MerchantInfoApi, MerchantInfoTransform>(With.Name("account.accounts.transactions.merchants")))
                            .Map<BalanceHistoryApi, BalanceHistoryTransform>(With.Name("account.accounts.balances")))
                    .Map<InvestmentsApi, InvestmentsTransform>(With.Name("account.investments"))
                    .Map<CreditScoreApi, CreditScoreTransform>(With.Name("account.credit"))
            ).Create();
    }
}

API Reference

Core Interfaces

IApiAggregator<TContract>

Main interface for data aggregation.

public interface IApiAggregator<TContract> where TContract : IContract
{
    TContract GetData(IRequestContext context);
}

IContract

Marker interface for aggregated contracts.

public interface IContract { }

IRequestContext

Context for aggregation requests.

public interface IRequestContext : IApiResultCache
{
    string[] Names { get; }
}

IWebApi

Interface for web API implementations.

public interface IWebApi
{
    List<IWebApi> Children { get; set; }
    Type ResultType { get; }
    bool IsContextResolved();
    Task<IApiResult> Run(IHttpClientFactory httpClientFactory, ILogger logger = null);
    void ResolveApiParameter(IRequestContext context, IApiResult parentApiResult = null);
}

IResultTransformer

Interface for result transformers.

public interface IResultTransformer
{
    void Transform(IApiResult apiResult, IContract contract);
}

Base Classes

ApiAggregate<TContract>

  • Base class for API aggregate definitions.

WebApi<TResult>

  • Base class for web API implementations.

ResultTransformer<TApiResult, TContract>

  • Base class for typed result transformers.

RequestContext

  • Base class for request contexts.

ApiResult

  • Base class for API results.

Utility Classes

CreateAggregate

  • Fluent API for creating aggregate mappings.

With

  • Helper class for creating API names.

ArrayUtil

  • Utility for array operations in transformers.

StringContainsMatcher

  • Default implementation of IApiNameMatcher.

Attributes

[CacheResult]

  • Marks API results for caching within request scope.

Clone this wiki locally