Skip to main content

Access Patterns

Oproto.FluentDynamoDb provides three approaches for writing DynamoDB queries and expressions. Each approach offers different trade-offs between type safety, flexibility, and control.

The Three Approaches​

Overview​

ApproachBest ForType SafetyFlexibility
Lambda ExpressionsNew code, known properties✅ Compile-timeStandard operations
Format StringsBalance of safety and flexibility⚠️ RuntimeAll DynamoDB features
Manual PatternsDynamic queries, complex scenarios⚠️ RuntimeMaximum control

Quick Comparison​

// 1. Lambda Expressions (RECOMMENDED)
// Type-safe with IntelliSense support
await table.Query
.Where<Order>(x => x.CustomerId == customerId && x.OrderDate.StartsWith("2024"))
.WithFilter<Order>(x => x.Status == "ACTIVE" && x.Total >= 100)
.ToListAsync();

// 2. Format Strings (ALTERNATIVE)
// Concise with automatic parameter generation
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND begins_with({Order.Fields.OrderDate}, {{1}})",
Order.Keys.Pk(customerId), "2024")
.WithFilter($"{Order.Fields.Status} = {{0}} AND {Order.Fields.Total} >= {{1}}",
"ACTIVE", 100)
.ToListAsync();

// 3. Manual Patterns (EXPLICIT CONTROL)
// Maximum control with explicit parameter binding
await table.Query
.Where("#pk = :pk AND begins_with(#date, :prefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#date", "orderDate")
.WithValue(":pk", Order.Keys.Pk(customerId))
.WithValue(":prefix", "2024")
.WithFilter("#status = :status AND #total >= :total")
.WithAttribute("#status", "status")
.WithAttribute("#total", "total")
.WithValue(":status", "ACTIVE")
.WithValue(":total", 100)
.ToListAsync();

When to Use Each Approach​

Lambda Expressions - Use when:

  • Writing new code
  • Properties are known at compile time
  • You want IntelliSense and refactoring support
  • Type safety is important

Format Strings - Use when:

  • You need a balance of conciseness and flexibility
  • Working with all DynamoDB features
  • Migrating from string-based code

Manual Patterns - Use when:

  • Building dynamic queries at runtime
  • Working with complex scenarios not covered by other approaches
  • Maintaining existing code that uses this pattern
  • You need explicit control over parameter names

Lambda Expressions​

Lambda expressions provide the most type-safe way to write DynamoDB queries. The compiler validates property names, and you get full IntelliSense support.

Key Benefits​

  • Type Safety: Catch property name typos at compile time
  • IntelliSense Support: Autocomplete for properties and methods
  • Refactoring Safety: Rename properties with confidence
  • Automatic Parameter Generation: No manual parameter naming required
  • AOT Compatible: Works in Native AOT environments

Basic Usage​

// Query with partition key
var orders = await table.Query
.Where<Order>(x => x.CustomerId == customerId)
.ToListAsync();

// Query with partition key and sort key condition
var orders = await table.Query
.Where<Order>(x => x.CustomerId == customerId && x.OrderDate.StartsWith("2024"))
.ToListAsync();

// Query with filter expression
var orders = await table.Query
.Where<Order>(x => x.CustomerId == customerId)
.WithFilter<Order>(x => x.Status == "ACTIVE")
.ToListAsync();

Comparison Operators​

All standard comparison operators are supported:

// Equality
table.Query.Where<User>(x => x.UserId == userId);

// Inequality
table.Query.WithFilter<User>(x => x.Status != "DELETED");

// Less than / Greater than
table.Query.WithFilter<User>(x => x.Age < 65);
table.Query.WithFilter<User>(x => x.Score > 100);

// Less than or equal / Greater than or equal
table.Query.WithFilter<User>(x => x.Age <= 18);
table.Query.WithFilter<User>(x => x.Score >= 50);

Logical Operators​

Combine conditions with AND, OR, and NOT:

// AND - both conditions must be true
table.Query.Where<User>(x => x.PartitionKey == userId && x.SortKey == sortKey);

// OR - either condition can be true
table.Query.WithFilter<User>(x => x.Type == "A" || x.Type == "B");

// NOT - negate a condition
table.Query.WithFilter<User>(x => !x.Deleted);

// Complex combinations with parentheses
table.Query.WithFilter<User>(x =>
(x.Active && x.Score > 50) || x.Premium);

DynamoDB Functions​

Lambda expressions support common DynamoDB functions:

// StartsWith (begins_with) - prefix matching
table.Query
.Where<Order>(x => x.CustomerId == customerId && x.OrderId.StartsWith("ORDER#"))
.ToListAsync();

// Contains - substring matching
table.Query
.WithFilter<User>(x => x.Email.Contains("@example.com"))
.ToListAsync();

// Between - range queries
table.Query
.Where<User>(x => x.PartitionKey == userId && x.SortKey.Between("2024-01", "2024-12"))
.ToListAsync();

// AttributeExists - check if attribute exists
table.Query
.WithFilter<User>(x => x.PhoneNumber.AttributeExists())
.ToListAsync();

// AttributeNotExists - check if attribute does not exist
// Note: Scan requires [Scannable] attribute on the entity
table.Users.Scan()
.WithFilter(x => x.DeletedAt.AttributeNotExists())
.ToListAsync();

// Size - get collection size
table.Query
.WithFilter<User>(x => x.Items.Size() > 5)
.ToListAsync();

Value Capture​

Lambda expressions automatically capture values from your code:

// Constants
table.Query.Where<User>(x => x.Id == "USER#123");

// Local variables
var userId = "USER#123";
var minAge = 18;
table.Query
.Where<User>(x => x.PartitionKey == userId)
.WithFilter<User>(x => x.Age >= minAge)
.ToListAsync();

// Object properties
var user = GetCurrentUser();
table.Query
.Where<Order>(x => x.CustomerId == user.Id)
.ToListAsync();

// Method calls on captured values (not entity properties)
var date = DateTime.Now;
table.Query
.WithFilter<Order>(x => x.CreatedDate > date.AddDays(-30))
.ToListAsync();

Where vs WithFilter​

Understanding the difference is crucial for efficient queries:

// Where() - Key Condition Expression
// Applied BEFORE reading items - reduces consumed capacity
table.Query
.Where<User>(x => x.PartitionKey == userId && x.SortKey.StartsWith("ORDER#"))
.ToListAsync();

// WithFilter() - Filter Expression
// Applied AFTER reading items - reduces data transfer only
table.Query
.Where<User>(x => x.PartitionKey == userId)
.WithFilter<User>(x => x.Status == "ACTIVE")
.ToListAsync();

Key Condition (Where): Only partition key and sort key properties allowed. Most efficient.

Filter Expression (WithFilter): Any property allowed. Applied after items are read.

Common Mistakes​

// ❌ Error: Non-key property in Where()
table.Query
.Where<User>(x => x.PartitionKey == userId && x.Status == "ACTIVE")
.ToListAsync();
// Throws: InvalidKeyExpressionException

// ✅ Correct: Move non-key condition to WithFilter()
table.Query
.Where<User>(x => x.PartitionKey == userId)
.WithFilter<User>(x => x.Status == "ACTIVE")
.ToListAsync();

// ❌ Error: Method call on entity property
table.Query
.WithFilter<User>(x => x.Name.ToUpper() == "JOHN")
.ToListAsync();

// ✅ Correct: Transform value before query
var upperName = "JOHN";
table.Query
.WithFilter<User>(x => x.Name == upperName)
.ToListAsync();

Format Strings​

Format strings use String.Format-style syntax with placeholders. They offer a balance between conciseness and flexibility, with automatic parameter generation.

Basic Syntax​

Use {0}, {1}, etc. as placeholders for values:

// Basic query with placeholders
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk("customer123"))
.ToListAsync();

// Multiple placeholders
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND {Order.Fields.OrderDate} > {{1}}",
Order.Keys.Pk("customer123"),
"2024-01-01")
.ToListAsync();

// Filter expression
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk("customer123"))
.WithFilter($"{Order.Fields.Status} = {{0}} AND {Order.Fields.Total} > {{1}}",
"pending",
100.00m)
.ToListAsync();

Using Generated Field Constants​

Combine format strings with generated Fields constants for compile-time validation of attribute names:

// Using generated field constants
await table.Query
.Where($"{User.Fields.UserId} = {{0}}", User.Keys.Pk("user123"))
.WithFilter($"{User.Fields.Status} = {{0}}", "active")
.ToListAsync();

// Benefits:
// - Attribute names validated at compile time
// - Refactoring safe
// - IntelliSense support for field names

Format Specifiers​

Format specifiers control how values are serialized:

DateTime Formatting​

// ISO 8601 format (recommended for timestamps)
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND {Order.Fields.CreatedAt} > {{1:o}}",
Order.Keys.Pk("customer123"),
DateTime.UtcNow.AddDays(-7))
.ToListAsync();

// Date only format
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND {Order.Fields.OrderDate} = {{1:yyyy-MM-dd}}",
Order.Keys.Pk("customer123"),
DateTime.Today)
.ToListAsync();

// Year-month format
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND begins_with({Order.Fields.OrderDate}, {{1:yyyy-MM}})",
Order.Keys.Pk("customer123"),
DateTime.Today)
.ToListAsync();

Common DateTime Format Strings:

  • {0:o} - ISO 8601 round-trip format (e.g., "2024-03-15T14:30:00.0000000Z")
  • {0:yyyy-MM-dd} - Date only (e.g., "2024-03-15")
  • {0:yyyy-MM-ddTHH:mm:ss} - ISO 8601 without milliseconds
  • {0:yyyy-MM} - Year and month (e.g., "2024-03")

Numeric Formatting​

// Fixed decimal places
await table.Query
.WithFilter($"{Product.Fields.Price} >= {{0:F2}}", 19.99m)
.ToListAsync();

// Zero-padded integers (useful for sortable keys)
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND {Order.Fields.SequenceNumber} > {{1:D8}}",
Order.Keys.Pk("customer123"),
1000)
.ToListAsync();

Common Numeric Format Strings:

  • {0:F2} - Fixed-point with 2 decimal places
  • {0:D8} - Zero-padded to 8 digits
  • {0:N2} - Number format with thousand separators

DynamoDB Functions​

Format strings support all DynamoDB functions:

// begins_with
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND begins_with({Order.Fields.OrderId}, {{1}})",
Order.Keys.Pk("customer123"),
"ORDER#2024")
.ToListAsync();

// contains
await table.Query
.WithFilter($"contains({User.Fields.Email}, {{0}})", "@example.com")
.ToListAsync();

// between
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}} AND {Order.Fields.OrderDate} BETWEEN {{1}} AND {{2}}",
Order.Keys.Pk("customer123"),
"2024-01-01",
"2024-12-31")
.ToListAsync();

// attribute_exists / attribute_not_exists
await table.Query
.WithFilter($"attribute_exists({Order.Fields.Discount})")
.ToListAsync();

await table.Query
.WithFilter($"attribute_not_exists({Order.Fields.CancellationReason})")
.ToListAsync();

// size
await table.Query
.WithFilter($"size({Order.Fields.Items}) > {{0}}", 5)
.ToListAsync();

Attribute Name Mapping​

For reserved words or special characters, use WithAttribute():

// When attribute name is a reserved word
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk("customer123"))
.WithFilter($"#status = {{0}}", "active")
.WithAttribute("#status", "status")
.ToListAsync();

Combining Conditions​

// AND conditions
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk("customer123"))
.WithFilter($"{Order.Fields.Status} = {{0}} AND {Order.Fields.Total} > {{1}}",
"pending",
100.00m)
.ToListAsync();

// OR conditions
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk("customer123"))
.WithFilter($"{Order.Fields.Status} = {{0}} OR {Order.Fields.Status} = {{1}}",
"pending",
"processing")
.ToListAsync();

// Complex conditions with parentheses
await table.Query
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk("customer123"))
.WithFilter($"({Order.Fields.Status} = {{0}} AND {Order.Fields.Total} > {{1}}) OR {Order.Fields.Priority} = {{2}}",
"pending",
100.00m,
"HIGH")
.ToListAsync();

Manual Patterns​

Manual patterns provide explicit control over parameter binding using WithValue() and WithAttribute() methods. Use this approach for dynamic queries or when you need maximum control.

Basic Syntax​

await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", Order.Keys.Pk("customer123"))
.ToListAsync();

Naming Conventions:

  • #name - Expression attribute names (map to actual attribute names)
  • :value - Expression attribute values (the actual values to compare)

WithValue() Method​

Use WithValue() to bind values to placeholders:

// Single value
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "CUSTOMER#123")
.ToListAsync();

// Multiple values
await table.Query
.Where("#pk = :pk AND #sk > :sk")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "CUSTOMER#123")
.WithValue(":sk", "ORDER#2024-01-01")
.ToListAsync();

// Different value types
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "CUSTOMER#123")
.WithFilter("#status = :status AND #total >= :total AND #active = :active")
.WithAttribute("#status", "status")
.WithAttribute("#total", "total")
.WithAttribute("#active", "active")
.WithValue(":status", "pending")
.WithValue(":total", 100.00m)
.WithValue(":active", true)
.ToListAsync();

WithAttribute() Method​

Use WithAttribute() to map placeholder names to actual DynamoDB attribute names:

// Map expression attribute names
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk") // #pk maps to "pk" attribute
.WithValue(":pk", "CUSTOMER#123")
.WithFilter("#status = :status")
.WithAttribute("#status", "status") // #status maps to "status" attribute
.WithValue(":status", "active")
.ToListAsync();

Why Use Attribute Names?

  • Handle reserved words (e.g., status, name, type)
  • Handle attribute names with special characters
  • Provide explicit mapping for clarity

Dynamic Query Building​

Manual patterns excel at building queries dynamically:

public async Task<List<Order>> SearchOrders(
string customerId,
string? status = null,
decimal? minTotal = null,
DateTime? startDate = null)
{
var query = table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", Order.Keys.Pk(customerId));

var filterConditions = new List<string>();

// Add optional status filter
if (!string.IsNullOrEmpty(status))
{
filterConditions.Add("#status = :status");
query = query
.WithAttribute("#status", "status")
.WithValue(":status", status);
}

// Add optional minimum total filter
if (minTotal.HasValue)
{
filterConditions.Add("#total >= :minTotal");
query = query
.WithAttribute("#total", "total")
.WithValue(":minTotal", minTotal.Value);
}

// Add optional date filter
if (startDate.HasValue)
{
filterConditions.Add("#orderDate >= :startDate");
query = query
.WithAttribute("#orderDate", "orderDate")
.WithValue(":startDate", startDate.Value.ToString("o"));
}

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

var response = await query.ToListAsync();
return response.Items.Select(OrderMapper.FromAttributeMap).ToList();
}

DynamoDB Functions​

// begins_with
await table.Query
.Where("#pk = :pk AND begins_with(#sk, :prefix)")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "CUSTOMER#123")
.WithValue(":prefix", "ORDER#2024")
.ToListAsync();

// contains
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "CUSTOMER#123")
.WithFilter("contains(#email, :domain)")
.WithAttribute("#email", "email")
.WithValue(":domain", "@example.com")
.ToListAsync();

// between
await table.Query
.Where("#pk = :pk AND #sk BETWEEN :start AND :end")
.WithAttribute("#pk", "pk")
.WithAttribute("#sk", "sk")
.WithValue(":pk", "CUSTOMER#123")
.WithValue(":start", "ORDER#2024-01-01")
.WithValue(":end", "ORDER#2024-12-31")
.ToListAsync();

// attribute_exists / attribute_not_exists
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "CUSTOMER#123")
.WithFilter("attribute_exists(#discount)")
.WithAttribute("#discount", "discount")
.ToListAsync();

// size
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "CUSTOMER#123")
.WithFilter("size(#items) > :minSize")
.WithAttribute("#items", "items")
.WithValue(":minSize", 5)
.ToListAsync();

Use Cases for Manual Patterns​

1. Dynamic Queries Built at Runtime

var conditions = new List<string>();
var paramIndex = 0;

if (includeActive)
{
conditions.Add($"#status = :status{paramIndex}");
query = query
.WithAttribute("#status", "status")
.WithValue($":status{paramIndex}", "ACTIVE");
paramIndex++;
}

2. Complex Expressions Not Supported by Lambda

// attribute_type function
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "CUSTOMER#123")
.WithFilter("attribute_type(#data, :type)")
.WithAttribute("#data", "data")
.WithValue(":type", "M") // Map type
.ToListAsync();

3. Migrating Existing Code

// Existing code using manual patterns can continue to work
// while new code uses lambda expressions

Entity Accessors for Queries​

Entity accessors provide a cleaner way to write queries without generic type parameters. Instead of table.Query<Order>(), you can use table.Orders.Query().

Generic-Free Query Patterns​

Entity accessors eliminate the need for generic type parameters:

// Entity accessor approach (recommended)
var orders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.ToListAsync();

// Generic method approach (also available)
var orders = await table.Query<Order>()
.Where(x => x.CustomerId == customerId)
.ToListAsync();

Benefits of Entity Accessors​

BenefitDescription
Cleaner syntaxtable.Orders.Query() vs table.Query<Order>()
Better IntelliSenseIDE shows only relevant methods
Type inferenceNo need to specify type parameters
Consistent patternSame accessor for all operations

Query Examples with Entity Accessors​

// Basic query
var orders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.ToListAsync();

// Query with filter
var activeOrders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.WithFilter(x => x.Status == "ACTIVE")
.ToListAsync();

// Query with sort key condition
var recentOrders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId && x.OrderDate.StartsWith("2024"))
.ToListAsync();

// Query on GSI
var pendingOrders = await table.Orders.Query()
.UsingIndex(Order.Indexes.StatusIndex)
.Where(x => x.Status == "pending")
.ToListAsync();

Combining with Format Strings​

Entity accessors work with all three query approaches:

// Lambda expressions
var orders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.WithFilter(x => x.Total > 100)
.ToListAsync();

// Format strings
var orders = await table.Orders.Query()
.Where($"{Order.Fields.CustomerId} = {{0}}", Order.Keys.Pk(customerId))
.WithFilter($"{Order.Fields.Total} > {{0}}", 100)
.ToListAsync();

// Manual patterns
var orders = await table.Orders.Query()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", Order.Keys.Pk(customerId))
.ToListAsync();

Multi-Entity Table Queries​

For tables with multiple entity types, each entity has its own accessor:

// Query orders
var orders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.ToListAsync();

// Query order lines for the same customer
var orderLines = await table.OrderLines.Query()
.Where(x => x.OrderId == orderId)
.ToListAsync();

// Query order statuses
var statuses = await table.OrderStatuses.Query()
.Where(x => x.OrderId == orderId)
.ToListAsync();

Available Query Methods​

Each entity accessor provides these query-related methods:

MethodDescription
table.Entity.Query()Start a query builder
table.Entity.Get(key)Start a get builder
table.Entity.GetAsync(key)Express-route get

Pattern Comparison​

// Entity accessor (cleaner)
var order = await table.Orders.GetAsync("order123");
var orders = await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.ToListAsync();

// Generic method (more verbose)
var order = await table.Get<Order>()
.WithKey("pk", "order123")
.GetItemAsync();
var orders = await table.Query<Order>()
.Where(x => x.CustomerId == customerId)
.ToListAsync();

Comparison and Best Practices​

Detailed Comparison​

FeatureLambda ExpressionsFormat StringsManual Patterns
Type Safety✅ Compile-time⚠️ Runtime⚠️ Runtime
IntelliSense✅ Full support⚠️ Field constants only❌ None
Refactoring✅ Automatic⚠️ Field constants only❌ Manual
VerbosityLowMediumHigh
Dynamic Queries❌ Limited⚠️ Possible✅ Excellent
All DynamoDB Features⚠️ Most common✅ All✅ All
Learning CurveLowLowMedium
AOT Compatible✅ Yes✅ Yes✅ Yes

Choosing the Right Approach​

┌─────────────────────────────────────────────────────────────┐
│ Decision Guide │
├─────────────────────────────────────────────────────────────┤
│ │
│ Is the query built dynamically at runtime? │
│ YES → Use Manual Patterns │
│ NO ↓ │
│ │
│ Do you need a DynamoDB feature not supported by lambdas? │
│ YES → Use Format Strings │
│ NO ↓ │
│ │
│ Use Lambda Expressions (recommended) │
│ │
└─────────────────────────────────────────────────────────────┘

Best Practices​

1. Prefer Lambda Expressions for New Code​

// ✅ Recommended - type-safe, refactoring-friendly
await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.WithFilter(x => x.Status == "ACTIVE")
.ToListAsync();

// ⚠️ Avoid for new code unless needed
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", customerId)
.ToListAsync();

2. Use Generated Field Constants​

// ✅ Good - compile-time validation
.Where($"{Order.Fields.CustomerId} = {{0}}", customerId)

// ❌ Avoid - typos not caught until runtime
.Where("customerId = {0}", customerId)

3. Use Key Conditions Over Filters​

// ✅ Efficient - uses sort key condition
await table.Orders.Query()
.Where(x => x.CustomerId == customerId && x.CreatedAt > startDate)
.ToListAsync();

// ⚠️ Less efficient - filter applied after read
await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.WithFilter(x => x.CreatedAt > startDate)
.ToListAsync();

4. Use Entity Accessors for Cleaner Code​

// ✅ Clean - no generic parameters
var order = await table.Orders.GetAsync("order123");

// ⚠️ More verbose
var order = await table.Get<Order>()
.WithKey("pk", "order123")
.GetItemAsync();

5. Mix Approaches When Needed​

You can combine approaches in the same query:

// Lambda for key condition, format string for complex filter
await table.Orders.Query()
.Where(x => x.CustomerId == customerId)
.WithFilter($"attribute_type({Order.Fields.Metadata}, {{0}})", "M")
.ToListAsync();

Migration Strategy​

When migrating from manual patterns to lambda expressions:

// Step 1: Start with manual patterns (existing code)
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", customerId)
.WithFilter("#status = :status")
.WithAttribute("#status", "status")
.WithValue(":status", "ACTIVE")
.ToListAsync();

// Step 2: Migrate key condition to lambda
await table.Query
.Where<Order>(x => x.CustomerId == customerId)
.WithFilter("#status = :status")
.WithAttribute("#status", "status")
.WithValue(":status", "ACTIVE")
.ToListAsync();

// Step 3: Migrate filter to lambda
await table.Query
.Where<Order>(x => x.CustomerId == customerId)
.WithFilter<Order>(x => x.Status == "ACTIVE")
.ToListAsync();

Performance Considerations​

All three approaches have similar runtime performance:

  • Lambda expressions: Expression tree built at compile time, translation cached
  • Format strings: Minimal parsing overhead, parameters auto-generated
  • Manual patterns: Direct pass-through to DynamoDB SDK

The performance difference is negligible (< 5% overhead). Choose based on code maintainability, not performance.

Next Steps​

  • Repositories - Extend generated tables with custom methods
  • Single-Table Design - Multi-entity patterns and compound entities
  • Tables - Learn about generated table classes and operations