DynamoDB Multi-Entity Queries in C#
Query multiple related entity types in a single DynamoDB request using FluentDynamoDB's ToCompoundEntityAsync() method. This approach is essential for single-table design, where related entities (like orders with line items) share the same partition key and can be retrieved together efficiently.
Why Use Compound Entities?
Single-table design stores multiple entity types in one table, differentiated by sort key patterns:
PK: CUSTOMER#123 SK: ORDER#001 → Order header
PK: CUSTOMER#123 SK: ORDER#001#LINE#001 → Line item 1
PK: CUSTOMER#123 SK: ORDER#001#LINE#002 → Line item 2
PK: CUSTOMER#123 SK: ORDER#001#PAYMENT → Payment info
Benefits of ToCompoundEntityAsync():
- Single query: Retrieve parent and all related entities in one DynamoDB request
- Automatic mapping: Related entities are automatically populated based on
[RelatedEntity]patterns - Type safety: Full IntelliSense support for parent and child entities
- Reduced latency: No multiple round trips to fetch related data
- Lambda/Fluent
- String Formatted
- Manual Builder
Query with Related Entities
// Retrieve order with all line items, payment, and shipment
var order = await ecommerceTable.Query()
.Where<Order>(x => x.CustomerId == "customer123"
&& x.OrderId.StartsWith("ORDER#order456"))
.ToCompoundEntityAsync();
// Access populated related entities
Console.WriteLine($"Order: {order.OrderId}");
Console.WriteLine($"Lines: {order.Lines?.Count ?? 0}");
Console.WriteLine($"Payment: {order.Payment?.Method ?? "None"}");
foreach (var line in order.Lines ?? Enumerable.Empty<OrderLine>())
{
Console.WriteLine($" - {line.ProductName}: {line.Quantity} x ${line.Price}");
}
Query Invoice with Items and Status History
// Retrieve invoice with all items and status history
var invoice = await invoiceTable.Query()
.Where<Invoice>(x => x.InvoiceId == "INVOICE#invoice123")
.ToCompoundEntityAsync();
// Access nested collections
foreach (var item in invoice.Items ?? Enumerable.Empty<InvoiceItem>())
{
Console.WriteLine($"Item: {item.Description} - ${item.UnitPrice * item.Quantity}");
}
foreach (var status in invoice.StatusHistory ?? Enumerable.Empty<InvoiceStatus>())
{
Console.WriteLine($"Status: {status.Status} at {status.Timestamp}");
}
Query with Sort Key Range
// Get all orders for a customer with their related entities
var orders = await ecommerceTable.Query()
.Where<Order>(x => x.CustomerId == "customer123"
&& x.OrderId.StartsWith("ORDER#"))
.ToListAsync();
// Get a specific order with all related data
var completeOrder = await ecommerceTable.Query()
.Where<Order>(x => x.CustomerId == "customer123"
&& x.OrderId.Between("ORDER#order456", "ORDER#order456#~"))
.ToCompoundEntityAsync();
Query with Related Entities
// Retrieve order with all line items, payment, and shipment
var order = await ecommerceTable.Query()
.Where<Order>($"{Order.Fields.CustomerId} = {{0}} AND begins_with({Order.Fields.OrderId}, {{1}})",
"customer123", "ORDER#order456")
.ToCompoundEntityAsync();
// Access populated related entities
Console.WriteLine($"Order: {order.OrderId}");
Console.WriteLine($"Lines: {order.Lines?.Count ?? 0}");
Console.WriteLine($"Payment: {order.Payment?.Method ?? "None"}");
foreach (var line in order.Lines ?? Enumerable.Empty<OrderLine>())
{
Console.WriteLine($" - {line.ProductName}: {line.Quantity} x ${line.Price}");
}
Query Invoice with Items and Status History
// Retrieve invoice with all items and status history
var invoice = await invoiceTable.Query()
.Where<Invoice>($"{Invoice.Fields.InvoiceId} = {{0}}", "INVOICE#invoice123")
.ToCompoundEntityAsync();
// Access nested collections
foreach (var item in invoice.Items ?? Enumerable.Empty<InvoiceItem>())
{
Console.WriteLine($"Item: {item.Description} - ${item.UnitPrice * item.Quantity}");
}
foreach (var status in invoice.StatusHistory ?? Enumerable.Empty<InvoiceStatus>())
{
Console.WriteLine($"Status: {status.Status} at {status.Timestamp}");
}
Query with Sort Key Range
// Get all orders for a customer with their related entities
var orders = await ecommerceTable.Query()
.Where<Order>($"{Order.Fields.CustomerId} = {{0}} AND begins_with({Order.Fields.OrderId}, {{1}})",
"customer123", "ORDER#")
.ToListAsync();
// Get a specific order with all related data
var completeOrder = await ecommerceTable.Query()
.Where<Order>($"{Order.Fields.CustomerId} = {{0}} AND {Order.Fields.OrderId} BETWEEN {{1}} AND {{2}}",
"customer123", "ORDER#order456", "ORDER#order456#~")
.ToCompoundEntityAsync();
Query with Related Entities
// Retrieve order with all line items, payment, and shipment
var order = await ecommerceTable.Query()
.Where<Order>("#pk = :pk AND begins_with(#sk, :skPrefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "customer123")
.WithValue(":skPrefix", "ORDER#order456")
.ToCompoundEntityAsync();
// Access populated related entities
Console.WriteLine($"Order: {order.OrderId}");
Console.WriteLine($"Lines: {order.Lines?.Count ?? 0}");
Console.WriteLine($"Payment: {order.Payment?.Method ?? "None"}");
foreach (var line in order.Lines ?? Enumerable.Empty<OrderLine>())
{
Console.WriteLine($" - {line.ProductName}: {line.Quantity} x ${line.Price}");
}
Query Invoice with Items and Status History
// Retrieve invoice with all items and status history
var invoice = await invoiceTable.Query()
.Where<Invoice>("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "INVOICE#invoice123")
.ToCompoundEntityAsync();
// Access nested collections
foreach (var item in invoice.Items ?? Enumerable.Empty<InvoiceItem>())
{
Console.WriteLine($"Item: {item.Description} - ${item.UnitPrice * item.Quantity}");
}
foreach (var status in invoice.StatusHistory ?? Enumerable.Empty<InvoiceStatus>())
{
Console.WriteLine($"Status: {status.Status} at {status.Timestamp}");
}
Query with Sort Key Range
// Get all orders for a customer with their related entities
var orders = await ecommerceTable.Query()
.Where<Order>("#pk = :pk AND begins_with(#sk, :skPrefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "customer123")
.WithValue(":skPrefix", "ORDER#")
.ToListAsync();
// Get a specific order with all related data
var completeOrder = await ecommerceTable.Query()
.Where<Order>("#pk = :pk AND #sk BETWEEN :skStart AND :skEnd")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "customer123")
.WithValue(":skStart", "ORDER#order456")
.WithValue(":skEnd", "ORDER#order456#~")
.ToCompoundEntityAsync();
Entity Definition
The examples above use these entity definitions with [RelatedEntity] attributes:
// Parent entity with related entity declarations
[DynamoDbTable("ecommerce", IsDefault = true)]
public partial class Order
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
[Computed("ORDER#{OrderId}")]
public string OrderId { get; set; } = string.Empty;
[DynamoDbAttribute("total")]
public decimal Total { get; set; }
[DynamoDbAttribute("status")]
public string Status { get; set; } = "pending";
// Related entities - automatically populated by ToCompoundEntityAsync()
[RelatedEntity("ORDER#{OrderId}#LINE#*")]
public List<OrderLine>? Lines { get; set; }
[RelatedEntity("ORDER#{OrderId}#PAYMENT")]
public Payment? Payment { get; set; }
}
// Child entity - shares same table
[DynamoDbTable("ecommerce")]
public partial class OrderLine
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
[Computed("ORDER#{OrderId}#LINE#{LineNumber:D3}")]
public string OrderId { get; set; } = string.Empty;
public int LineNumber { get; set; }
[DynamoDbAttribute("productName")]
public string ProductName { get; set; } = string.Empty;
[DynamoDbAttribute("quantity")]
public int Quantity { get; set; }
[DynamoDbAttribute("price")]
public decimal Price { get; set; }
}
Key Concepts
| Concept | Description |
|---|---|
[RelatedEntity("PATTERN")] | Declares a relationship based on sort key pattern matching |
ToCompoundEntityAsync() | Retrieves parent entity with all related entities populated |
"PREFIX#*" | Wildcard pattern matches all sort keys starting with prefix |
"EXACT" | Exact pattern matches a single sort key value |
IsDefault = true | Marks the primary entity for table-level operations |
Learn More
- Single-Table Design Guide - Complete guide to multi-entity tables
- Entity Relationships - Deep dive into
[RelatedEntity]patterns - Compound Entities - Advanced compound entity usage
- Transactions - Atomic operations across entity types
- Query & Scan - Basic query patterns