Skip to main content

String Expressions

Build DynamoDB expressions using manual parameter binding with WithValue() and WithAttribute() methods. This approach provides explicit control over parameter naming and is useful for dynamic queries, complex scenarios, and legacy code migration.

Key Benefits

  • Maximum Control: Explicit parameter naming and management
  • Dynamic Queries: Build expressions programmatically at runtime
  • Full DynamoDB Support: Access all DynamoDB expression features
  • Parameter Reuse: Use the same parameter value multiple times in an expression

When to Use Manual Expression Building

Manual expression building is best suited for:

  1. Dynamic Queries: When conditions are built at runtime based on user input or configuration
  2. Complex Scenarios: When you need explicit control over parameter names or need to reuse parameters
  3. Legacy Code Migration: When migrating existing code that uses raw DynamoDB expressions
  4. Unsupported Features: When lambda expressions don't support a specific DynamoDB feature

For most use cases, consider using Lambda Expressions (recommended) or Formatted Expressions instead.

Basic Usage

WithValue() - Binding Expression Values

Use WithValue() to bind values to expression placeholders. Value placeholders start with a colon (:):

// Simple query with value binding
var users = await table.Query<User>()
.Where("pk = :pk")
.WithValue(":pk", "USER#123")
.ToListAsync();

// Multiple value bindings
var orders = await table.Query<Order>()
.Where("pk = :pk AND sk > :sk")
.WithValue(":pk", "CUSTOMER#123")
.WithValue(":sk", "ORDER#2024-01-01")
.ToListAsync();

WithAttribute() - Binding Attribute Names

Use WithAttribute() to bind attribute names to placeholders. Attribute placeholders start with a hash (#):

// Handle reserved words with attribute binding
var users = await table.Query<User>()
.Where("#pk = :pk AND #status = :status")
.WithAttribute("#pk", "pk")
.WithAttribute("#status", "status")
.WithValue(":pk", "USER#123")
.WithValue(":status", "active")
.ToListAsync();

Combined Example

// Full manual expression with both attribute and value bindings
var orders = await table.Query<Order>()
.Where("#pk = :pk AND begins_with(#sk, :prefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "CUSTOMER#123")
.WithValue(":prefix", "ORDER#2024")
.ToListAsync();

Query Operations

Query by Partition Key

var users = await table.Query<User>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", UserKeys.Pk("user123"))
.ToListAsync();

foreach (var user in users)
{
Console.WriteLine($"User: {user.Name}");
}

Query with Sort Key Conditions

// Greater than
var orders = await table.Query<Order>()
.Where("#pk = :pk AND #sk > :sk")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithValue(":sk", "ORDER#2024-01-01")
.ToListAsync();

// Between
var orders = await table.Query<Order>()
.Where("#pk = :pk AND #sk BETWEEN :start AND :end")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithValue(":start", "ORDER#2024-01-01")
.WithValue(":end", "ORDER#2024-12-31")
.ToListAsync();

// Begins with
var orders = await table.Query<Order>()
.Where("#pk = :pk AND begins_with(#sk, :prefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithValue(":prefix", "ORDER#2024-03")
.ToListAsync();

Query with Filter

var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("#status = :status AND #total > :total")
.WithAttribute("#status", "status")
.WithAttribute("#total", "total")
.WithValue(":status", "pending")
.WithValue(":total", 100.00m)
.ToListAsync();

Filter Expressions

Basic Filters

// Single condition
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("#status = :status")
.WithAttribute("#status", "status")
.WithValue(":status", "pending")
.ToListAsync();

Multiple Filter Conditions

// AND conditions
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("#status = :status AND #total > :total")
.WithAttribute("#status", "status")
.WithAttribute("#total", "total")
.WithValue(":status", "pending")
.WithValue(":total", 100.00m)
.ToListAsync();

// OR conditions
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("#status = :status1 OR #status = :status2")
.WithAttribute("#status", "status")
.WithValue(":status1", "pending")
.WithValue(":status2", "processing")
.ToListAsync();

// Complex logical combinations
var users = await table.Query<User>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", UserKeys.Pk("user123"))
.WithFilter("(#active AND #score > :minScore) OR #premium")
.WithAttribute("#active", "active")
.WithAttribute("#score", "score")
.WithAttribute("#premium", "premium")
.WithValue(":minScore", 50)
.ToListAsync();

DynamoDB Functions in Filters

// attribute_exists
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("attribute_exists(#discount)")
.WithAttribute("#discount", "discount")
.ToListAsync();

// attribute_not_exists
var users = await table.Query<User>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", UserKeys.Pk("user123"))
.WithFilter("attribute_not_exists(#deletedAt)")
.WithAttribute("#deletedAt", "deletedAt")
.ToListAsync();

// contains
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("contains(#tags, :tag)")
.WithAttribute("#tags", "tags")
.WithValue(":tag", "priority")
.ToListAsync();

// size
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("size(#items) > :minSize")
.WithAttribute("#items", "items")
.WithValue(":minSize", 5)
.ToListAsync();

Update Expressions

SET Operations

// Simple SET
await table.Update<User>()
.WithKey("pk", "user123")
.Set("SET #name = :name, #updatedAt = :updatedAt")
.WithAttribute("#name", "name")
.WithAttribute("#updatedAt", "updatedAt")
.WithValue(":name", "Jane Doe")
.WithValue(":updatedAt", DateTime.UtcNow)
.UpdateAsync();

// SET with if_not_exists
await table.Update<User>()
.WithKey("pk", "user123")
.Set("SET #name = if_not_exists(#name, :defaultName)")
.WithAttribute("#name", "name")
.WithValue(":defaultName", "Default Name")
.UpdateAsync();

ADD Operations

// Increment counter
await table.Update<User>()
.WithKey("pk", "user123")
.Set("ADD #loginCount :increment")
.WithAttribute("#loginCount", "loginCount")
.WithValue(":increment", 1)
.UpdateAsync();

REMOVE Operations

// Remove attribute
await table.Update<User>()
.WithKey("pk", "user123")
.Set("REMOVE #tempData")
.WithAttribute("#tempData", "tempData")
.UpdateAsync();

Combined Operations

// SET, ADD, and REMOVE in one expression
await table.Update<User>()
.WithKey("pk", "user123")
.Set("SET #name = :name, #updatedAt = :updatedAt " +
"ADD #loginCount :increment " +
"REMOVE #tempData")
.WithAttribute("#name", "name")
.WithAttribute("#updatedAt", "updatedAt")
.WithAttribute("#loginCount", "loginCount")
.WithAttribute("#tempData", "tempData")
.WithValue(":name", "Jane Doe")
.WithValue(":updatedAt", DateTime.UtcNow)
.WithValue(":increment", 1)
.UpdateAsync();

Conditional Updates

// Optimistic locking with version check
await table.Update<User>()
.WithKey("pk", "user123")
.Set("SET #name = :name, #version = #version + :increment")
.WithAttribute("#name", "name")
.WithAttribute("#version", "version")
.WithValue(":name", "Jane Doe")
.WithValue(":increment", 1)
.Where("#version = :currentVersion")
.WithValue(":currentVersion", currentVersion)
.UpdateAsync();

Conditional Writes

Conditional Put

// Prevent overwrites
await table.Put(user)
.Where("attribute_not_exists(#pk)")
.WithAttribute("#pk", "pk")
.PutAsync();

Conditional Delete

// Delete only if status matches
await table.Delete<Order>()
.WithKey("pk", "order123")
.Where("#status = :status")
.WithAttribute("#status", "status")
.WithValue(":status", "cancelled")
.DeleteAsync();

Dynamic Query Building

One of the main advantages of manual expression building is the ability to construct queries dynamically at runtime.

Building Conditions Dynamically

public async Task<List<Order>> SearchOrders(
string customerId,
string? status = null,
decimal? minTotal = null,
DateTime? startDate = null)
{
// Start with required key condition
var keyCondition = "#pk = :pk";
var attributeNames = new Dictionary<string, string> { { "#pk", "pk" } };
var attributeValues = new Dictionary<string, object> { { ":pk", OrderKeys.Pk(customerId) } };

// Build filter conditions dynamically
var filterConditions = new List<string>();

if (!string.IsNullOrEmpty(status))
{
filterConditions.Add("#status = :status");
attributeNames["#status"] = "status";
attributeValues[":status"] = status;
}

if (minTotal.HasValue)
{
filterConditions.Add("#total > :minTotal");
attributeNames["#total"] = "total";
attributeValues[":minTotal"] = minTotal.Value;
}

if (startDate.HasValue)
{
filterConditions.Add("#createdAt > :startDate");
attributeNames["#createdAt"] = "createdAt";
attributeValues[":startDate"] = startDate.Value;
}

// Build the query
var query = table.Query.Where(keyCondition);

// Add attribute names
foreach (var attr in attributeNames)
{
query = query.WithAttribute(attr.Key, attr.Value);
}

// Add attribute values
foreach (var val in attributeValues)
{
query = query.WithValue(val.Key, val.Value);
}

// Add filter if conditions exist
if (filterConditions.Count > 0)
{
var filterExpression = string.Join(" AND ", filterConditions);
query = query.WithFilter(filterExpression);
}

return await query.ToListAsync();
}

Reusing Parameter Values

Manual binding allows you to reference the same parameter multiple times:

// Use the same value in multiple places
var orders = await table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.WithFilter("#createdBy = :userId OR #modifiedBy = :userId")
.WithAttribute("#createdBy", "createdBy")
.WithAttribute("#modifiedBy", "modifiedBy")
.WithValue(":userId", "user123") // Used twice in filter
.ToListAsync();

Comparison with Other Approaches

Manual expression building is the most verbose approach but offers the most control. Here's how the same query looks across all three approaches:

// Most concise, type-safe
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

// Concise with inline formatting
var orders = await table.Query<Order>()
.Where("pk = {0} AND begins_with(sk, {1})",
OrderKeys.Pk(customerId), "ORDER#2024")
.WithFilter("#status = {0} AND #total > {1:F2}", "pending", 100.00m)
.WithAttribute("#status", "status")
.WithAttribute("#total", "total")
.ToListAsync();

Manual Building (This Page)

// Most verbose, maximum control
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();

When to Choose Each Approach

ScenarioRecommended Approach
New code with known propertiesLambda Expressions
Need format specifiers (DateTime, decimals)Format Strings
Dynamic queries built at runtimeManual Building
Complex scenarios with parameter reuseManual Building
Migrating existing DynamoDB codeManual Building
Maximum type safetyLambda Expressions

Best Practices

1. Use Consistent Naming Conventions

// ✅ Good - consistent naming
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithAttribute("#status", "status")
.WithValue(":pk", "USER#123")
.WithValue(":status", "active")

// ❌ Avoid - inconsistent naming
.WithAttribute("#a", "pk")
.WithAttribute("#b", "sk")
.WithValue(":x", "USER#123")

2. Always Use Attribute Placeholders for Reserved Words

// ✅ Good - handles reserved words
.WithFilter("#status = :status AND #name = :name")
.WithAttribute("#status", "status")
.WithAttribute("#name", "name")

// ❌ Avoid - may fail with reserved words
.WithFilter("status = :status AND name = :name")
// ✅ Good - organized bindings
var orders = await table.Query<Order>()
// Key condition
.Where("#pk = :pk AND #sk > :sk")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", customerId)
.WithValue(":sk", startKey)
// Filter condition
.WithFilter("#status = :status")
.WithAttribute("#status", "status")
.WithValue(":status", "active")
.ToListAsync();

4. Use Descriptive Parameter Names

// ✅ Good - descriptive names
.WithValue(":customerId", customerId)
.WithValue(":minTotal", 100.00m)
.WithValue(":startDate", DateTime.UtcNow.AddDays(-30))

// ❌ Avoid - cryptic names
.WithValue(":v1", customerId)
.WithValue(":v2", 100.00m)
.WithValue(":v3", DateTime.UtcNow.AddDays(-30))

Common Patterns

Pagination

var allOrders = new List<Order>();
Dictionary<string, AttributeValue>? lastKey = null;

do
{
var query = table.Query<Order>()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", OrderKeys.Pk("customer123"))
.Take(25);

if (lastKey != null)
{
query = query.StartAt(lastKey);
}

var response = await query.ToDynamoDbResponseAsync();
allOrders.AddRange(response.Items);
lastKey = response.LastEvaluatedKey;

} while (lastKey != null && lastKey.Count > 0);

GSI Queries

var orders = await table.Query<Order>()
.UsingIndex("StatusIndex")
.Where("#status = :status AND #createdAt > :date")
.WithAttribute("#status", "status")
.WithAttribute("#createdAt", "createdAt")
.WithValue(":status", "pending")
.WithValue(":date", DateTime.UtcNow.AddDays(-7))
.ToListAsync();

Next Steps