Request Builders
FluentDynamoDB provides three approaches for building queries and operations. Each approach offers different trade-offs between type safety, flexibility, and verbosity.
Three API Styles
1. Lambda Expressions (Recommended)
Use C# lambda expressions that get translated to DynamoDB expressions. This is the preferred approach for most use cases.
await table.Query<User>()
.Where(x => x.PartitionKey == userId && x.SortKey.StartsWith("ORDER#"))
.WithFilter(x => x.Status == "ACTIVE" && x.Age >= 18)
.ToListAsync();
Why Lambda Expressions are Recommended:
- Compile-time type checking catches property name typos before runtime
- IntelliSense support provides autocomplete for properties and methods
- Refactoring safety means property renames update expressions automatically
- Automatic parameter generation eliminates manual parameter naming
- Clear error messages guide you to correct usage
2. Format Strings (Alternative)
Use String.Format-style syntax with placeholders for a concise middle ground:
await table.Query<User>()
.Where($"{UserFields.UserId} = {{0}} AND begins_with({UserFields.SortKey}, {{1}})",
UserKeys.Pk(userId), "ORDER#")
.WithFilter($"{UserFields.Status} = {{0}} AND {UserFields.Age} >= {{1}}",
"ACTIVE", 18)
.ToListAsync();
When to Use Format Strings:
- When you need format specifiers for DateTime or numeric values
- When building dynamic queries at runtime
- When lambda expressions don't support a specific DynamoDB feature
3. Manual Expression Building (Explicit Control)
Use explicit parameter binding with WithValue() and WithAttribute() for maximum control:
await table.Query<User>()
.Where("#pk = :pk AND begins_with(#sk, :prefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", UserKeys.Pk(userId))
.WithValue(":prefix", "ORDER#")
.WithFilter("#status = :status AND #age >= :age")
.WithAttribute("#status", "status")
.WithAttribute("#age", "age")
.WithValue(":status", "ACTIVE")
.WithValue(":age", 18)
.ToListAsync();
When to Use Manual Building:
- Dynamic queries where conditions are built at runtime
- Complex scenarios requiring explicit parameter control
- Migrating existing code that uses raw DynamoDB expressions
- When you need to reuse parameter values multiple times
Comparison
| Feature | Lambda Expressions | Format Strings | Manual Building |
|---|---|---|---|
| Type Safety | ✅ Compile-time checking | ⚠️ Runtime only | ❌ Runtime only |
| IntelliSense | ✅ Full support | ⚠️ Partial | ❌ None |
| Readability | ✅ Clean, concise | ✅ Concise | ⚠️ Verbose |
| Refactoring | ✅ Automatic updates | ⚠️ Manual updates | ❌ Manual updates |
| Flexibility | ⚠️ Supported operators only | ✅ All DynamoDB features | ✅ Full control |
| Dynamic Queries | ❌ Not suitable | ✅ Good support | ✅ Best support |
| Learning Curve | ✅ Familiar C# syntax | ✅ Familiar format syntax | ⚠️ DynamoDB knowledge required |
Side-by-Side Examples
Simple Query
Lambda Expressions (Recommended):
var users = await table.Query<User>()
.Where(x => x.UserId == "user123")
.ToListAsync();
Format Strings:
var users = await table.Query<User>()
.Where($"{UserFields.UserId} = {{0}}", UserKeys.Pk("user123"))
.ToListAsync();
Manual Building:
var users = await table.Query<User>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", UserKeys.Pk("user123"))
.ToListAsync();
Query with Sort Key and Filter
Lambda Expressions (Recommended):
var orders = await table.Query<Order>()
.Where(x => x.CustomerId == customerId && x.OrderId.StartsWith("ORDER#2024"))
.WithFilter(x => x.Status == "pending" && x.Total > 100.00m)
.ToListAsync();
Format Strings:
var orders = await table.Query<Order>()
.Where($"{OrderFields.CustomerId} = {{0}} AND begins_with({OrderFields.OrderId}, {{1}})",
OrderKeys.Pk(customerId), "ORDER#2024")
.WithFilter($"{OrderFields.Status} = {{0}} AND {OrderFields.Total} > {{1:F2}}",
"pending", 100.00m)
.ToListAsync();
Manual Building:
var orders = await table.Query<Order>()
.Where("#pk = :pk AND begins_with(#sk, :prefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", OrderKeys.Pk(customerId))
.WithValue(":prefix", "ORDER#2024")
.WithFilter("#status = :status AND #total > :total")
.WithAttribute("#status", "status")
.WithAttribute("#total", "total")
.WithValue(":status", "pending")
.WithValue(":total", 100.00m)
.ToListAsync();
Update with Condition
Lambda Expressions (Recommended):
await table.Update<User>()
.WithKey(user)
.Set(x => new { Name = "Jane Doe", UpdatedAt = DateTime.UtcNow })
.WithCondition(x => x.Version == currentVersion)
.UpdateAsync();
Format Strings:
await table.Update<User>()
.WithKey(UserFields.UserId, UserKeys.Pk("user123"))
.Set($"SET {UserFields.Name} = {{0}}, {UserFields.UpdatedAt} = {{1:o}}",
"Jane Doe", DateTime.UtcNow)
.Where($"{UserFields.Version} = {{0}}", currentVersion)
.UpdateAsync();
Manual Building:
await table.Update<User>()
.WithKey(UserFields.UserId, UserKeys.Pk("user123"))
.Set("SET #name = :name, #updated = :updated")
.WithAttribute("#name", "name")
.WithAttribute("#updated", "updatedAt")
.WithValue(":name", "Jane Doe")
.WithValue(":updated", DateTime.UtcNow)
.Where("#version = :version")
.WithAttribute("#version", "version")
.WithValue(":version", currentVersion)
.UpdateAsync();
Response Metadata Access
After executing a query or scan operation, you can access response metadata through the builder's .Response property. This provides access to pagination information, consumed capacity, and other operation details.
Accessing Response Metadata
var query = table.Query<Order>()
.Where(x => x.CustomerId == customerId)
.ReturnTotalConsumedCapacity();
var items = await query.ToListAsync();
// Access response metadata via builder.Response
var hasMore = query.Response?.HasMorePages ?? false;
var lastKey = query.Response?.LastEvaluatedKey;
var scannedCount = query.Response?.ScannedCount;
var consumedCapacity = query.Response?.ConsumedCapacity;
QueryOperationResponse Properties
| Property | Type | Description |
|---|---|---|
LastEvaluatedKey | Dictionary<string, AttributeValue>? | The key of the last evaluated item for pagination |
ScannedCount | int | Number of items scanned before filtering |
ResultCount | int | Number of items returned after filtering |
ConsumedCapacity | ConsumedCapacity? | Read capacity consumed (when requested) |
HasMorePages | bool | Whether more items are available |
ScanOperationResponse Properties
| Property | Type | Description |
|---|---|---|
LastEvaluatedKey | Dictionary<string, AttributeValue>? | The key of the last evaluated item for pagination |
ScannedCount | int | Number of items scanned before filtering |
ResultCount | int | Number of items returned after filtering |
ConsumedCapacity | ConsumedCapacity? | Read capacity consumed (when requested) |
HasMorePages | bool | Whether more items are available |
Pagination with GetEncodedPaginationToken
Use the GetEncodedPaginationToken() extension method to get a URL-safe pagination token:
var query = table.Query<Order>()
.Where(x => x.CustomerId == customerId);
var items = await query.ToListAsync();
// Get encoded pagination token for API responses
var paginationToken = query.Response?.GetEncodedPaginationToken() ?? string.Empty;
if (!string.IsNullOrEmpty(paginationToken))
{
// Return token to client for next page request
return new PagedResult<Order>
{
Items = items,
NextPageToken = paginationToken
};
}
Using Pagination Token for Next Page
// Resume from previous pagination token
var query = table.Query<Order>()
.Where(x => x.CustomerId == customerId);
if (!string.IsNullOrEmpty(previousToken))
{
query = query.WithExclusiveStartKey(previousToken);
}
var items = await query.ToListAsync();
var nextToken = query.Response?.GetEncodedPaginationToken() ?? string.Empty;
Monitoring Consumed Capacity
var query = table.Query<Order>()
.Where(x => x.CustomerId == customerId)
.ReturnTotalConsumedCapacity();
var items = await query.ToListAsync();
Console.WriteLine($"Items returned: {items.Count}");
Console.WriteLine($"Items scanned: {query.Response?.ScannedCount}");
Console.WriteLine($"Capacity consumed: {query.Response?.ConsumedCapacity?.CapacityUnits} RCUs");
Next Steps
Learn more about each approach:
- Lambda Expressions - Comprehensive guide to the recommended type-safe approach
- Formatted Expressions - Format string syntax with placeholders and format specifiers
- String Expressions - Manual parameter binding for explicit control
Related topics: