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