-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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.
- Overview
- Architecture
- Getting Started
- Core Concepts
- Nested API Calls and Parent-Child Relationships
- Implementation Guide
- Advanced Features
- Configuration
- Testing
- Performance Considerations
- Best Practices
- Troubleshooting
- API Reference
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.
- 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
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:
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"]
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.
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
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
- .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
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client │───▶│ ApiAggregator │───▶│ External APIs │
│ │ │ │ │ │
│ Request Context │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ API Names[] │ │ │ ApiBuilder │ │ │ │ CustomerAPI │ │
└─────────────────┘ │ │ ApiExecutor │ │ │ │ OrderAPI │ │
│ │ ContractBld │ │ │ │ ContactAPI │ │
│ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Aggregated │
│ Response │
└─────────────────┘
- The main orchestrator that coordinates the entire aggregation process.
Responsible for building the list of APIs to execute based on the request context and configured mappings.
- Manages the execution flow of APIs, handling dependencies and nested calls.
- Transforms API results into the final aggregated contract using configured transformers.
- Executes the actual HTTP requests using HttpClient and manages concurrent operations.
Install the latest version via NuGet Package Manager:
NuGet\Install-Package ApiAggregator
Or via .NET CLI:
dotnet add package ApiAggregator
// 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 });
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; }
}
}
Web APIs represent individual HTTP endpoints. They extend WebApi<TResult>
where TResult
implements IApiResult
.
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" }
};
}
}
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
}
}
}
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();
}
}
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.
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
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() }
};
}
}
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}");
}
}
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}");
}
}
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];
}
}
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
};
}
}
}
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);
}
}
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();
}
}
// 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)
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; }
}
// 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}");
}
}
// 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);
}
}
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();
}
}
services.AddLogging();
services.AddHttpClient();
services.UseApiAggregator()
.AddApiAggregate<Customer>(new CustomerAggregate());
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);
}
}
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
}
}
}
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>();
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
}
}
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");
}
}
services.AddHttpClient("resilient")
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
services.AddLogging(config =>
{
config.AddConsole();
config.SetMinimumLevel(LogLevel.Information);
});
[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)));
}
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"));
}
}
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)
The framework supports up to 5 levels of API nesting.
- Use
[CacheResult]
for frequently accessed data - Cache is scoped to individual requests
- Cached results are available to all transformers in the same request
- Use
CollectionResult<T>
for large datasets - Implement proper disposal patterns in custom components
- Consider pagination for large result sets
- 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
- 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
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
}
}
}
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>());
}
}
}
- Always validate input in request contexts
- Use secure HTTP headers
- Implement authentication/authorization in APIs
- Sanitize logged data to avoid sensitive information exposure
- Use dependency injection for all components
- Keep transformers simple and focused
- Document complex business logic
- Use meaningful variable names and comments
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
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
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
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
- Enable detailed logging:
services.AddLogging(config => config.SetMinimumLevel(LogLevel.Debug));
- 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");
}
}
- 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);
}
}
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"]
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();
}
}
Main interface for data aggregation.
public interface IApiAggregator<TContract> where TContract : IContract
{
TContract GetData(IRequestContext context);
}
Marker interface for aggregated contracts.
public interface IContract { }
Context for aggregation requests.
public interface IRequestContext : IApiResultCache
{
string[] Names { get; }
}
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);
}
Interface for result transformers.
public interface IResultTransformer
{
void Transform(IApiResult apiResult, IContract contract);
}
- Base class for API aggregate definitions.
- Base class for web API implementations.
- Base class for typed result transformers.
- Base class for request contexts.
- Base class for API results.
- Fluent API for creating aggregate mappings.
- Helper class for creating API names.
- Utility for array operations in transformers.
- Default implementation of
IApiNameMatcher
.
- Marks API results for caching within request scope.