Skip to main content

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

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

FeatureLambda ExpressionsFormat StringsManual 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

PropertyTypeDescription
LastEvaluatedKeyDictionary<string, AttributeValue>?The key of the last evaluated item for pagination
ScannedCountintNumber of items scanned before filtering
ResultCountintNumber of items returned after filtering
ConsumedCapacityConsumedCapacity?Read capacity consumed (when requested)
HasMorePagesboolWhether more items are available

ScanOperationResponse Properties

PropertyTypeDescription
LastEvaluatedKeyDictionary<string, AttributeValue>?The key of the last evaluated item for pagination
ScannedCountintNumber of items scanned before filtering
ResultCountintNumber of items returned after filtering
ConsumedCapacityConsumedCapacity?Read capacity consumed (when requested)
HasMorePagesboolWhether 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:

Related topics:

  • Filters - Filter expressions for additional result filtering
  • Updates - Update expressions for modifying items