| title | Manual Patterns | ||||||
|---|---|---|---|---|---|---|---|
| category | advanced-topics | ||||||
| order | 5 | ||||||
| keywords |
|
||||||
| related |
|
Documentation > Advanced Topics > Manual Patterns
Previous: Performance Optimization
This guide covers lower-level manual patterns for scenarios where lambda expressions or format strings may not be suitable.
API Style Priority: For most use cases, use lambda expressions (preferred) or format strings (alternative). Manual patterns are the third option, appropriate only for specific scenarios like dynamic queries or legacy code migration.
Manual patterns may be appropriate for:
- Dynamic table names - Table names determined at runtime
- Dynamic schema - Properties not known at compile time
- Legacy code migration - Gradual adoption of source generation
- Complex dynamic queries - Queries built from user input
- Prototyping - Quick experimentation without entity definitions
For production code, lambda expressions are preferred, with format strings as an alternative. Manual patterns are the third option for specific scenarios.
// ✅ PREFERRED: Lambda expressions - type-safe with IntelliSense
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
}
await table.Query
.Where<User>(x => x.UserId == "user123")
.ExecuteAsync();
// ✅ ALTERNATIVE: Format strings - concise with placeholders
await table.Query
.Where($"{UserFields.UserId} = {{0}}", UserKeys.Pk("user123"))
.ExecuteAsync<User>();
// ⚠️ EXPLICIT CONTROL: Manual patterns - for specific scenarios (this guide)
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", UserKeys.Pk("user123"))
.ExecuteAsync();Benefits of Lambda Expressions (Preferred):
- Compile-time type checking
- IntelliSense support
- Refactoring safety
- Automatic parameter generation
Benefits of Format Strings (Alternative):
- Concise syntax
- Automatic parameter generation
- Supports all DynamoDB features
When Manual Patterns Are Appropriate:
- Dynamic table names
- Dynamic schema
- Legacy code migration
- Complex dynamic queries
See Basic Operations and Querying Data for the preferred approaches.
Use DynamoDbTableBase without source generation for dynamic scenarios:
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Oproto.FluentDynamoDb.Storage;
var client = new AmazonDynamoDBClient();
var table = new DynamoDbTableBase(client, "users");
// Manual item construction
var item = new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = "USER#user123" },
["email"] = new AttributeValue { S = "john@example.com" },
["name"] = new AttributeValue { S = "John Doe" },
["age"] = new AttributeValue { N = "30" },
["isActive"] = new AttributeValue { BOOL = true }
};
// Put item
await table.Put
.WithItem(item)
.ExecuteAsync();
// Get item
var response = await table.Get
.WithKey("pk", "USER#user123")
.ExecuteAsync();
// Manual deserialization
if (response.Item != null)
{
var userId = response.Item["pk"].S;
var email = response.Item["email"].S;
var name = response.Item["name"].S;
var age = int.Parse(response.Item["age"].N);
var isActive = response.Item["isActive"].BOOL;
Console.WriteLine($"User: {name}, Email: {email}, Age: {age}");
}public class MultiTenantService
{
private readonly IAmazonDynamoDB _client;
public async Task<Dictionary<string, AttributeValue>?> GetUserAsync(
string tenantId,
string userId)
{
// Table name determined at runtime
var tableName = $"tenant-{tenantId}-users";
var table = new DynamoDbTableBase(_client, tableName);
var response = await table.Get
.WithKey("pk", $"USER#{userId}")
.ExecuteAsync();
return response.Item;
}
}// Define field names as constants
public static class UserFields
{
public const string PartitionKey = "pk";
public const string Email = "email";
public const string Name = "name";
public const string Age = "age";
public const string Status = "status";
public const string CreatedAt = "createdAt";
}
// Use constants for consistency
var item = new Dictionary<string, AttributeValue>
{
[UserFields.PartitionKey] = new AttributeValue { S = "USER#user123" },
[UserFields.Email] = new AttributeValue { S = "john@example.com" },
[UserFields.Name] = new AttributeValue { S = "John Doe" }
};public class User
{
public string UserId { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public bool IsActive { get; set; }
}
public static class UserMapper
{
public static Dictionary<string, AttributeValue> ToAttributeMap(User user)
{
return new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = $"USER#{user.UserId}" },
["email"] = new AttributeValue { S = user.Email },
["name"] = new AttributeValue { S = user.Name },
["age"] = new AttributeValue { N = user.Age.ToString() },
["isActive"] = new AttributeValue { BOOL = user.IsActive }
};
}
public static User FromAttributeMap(Dictionary<string, AttributeValue> item)
{
return new User
{
UserId = item["pk"].S.Replace("USER#", ""),
Email = item["email"].S,
Name = item["name"].S,
Age = int.Parse(item["age"].N),
IsActive = item["isActive"].BOOL
};
}
}
// Usage
var user = new User
{
UserId = "user123",
Email = "john@example.com",
Name = "John Doe",
Age = 30,
IsActive = true
};
await table.Put
.WithItem(UserMapper.ToAttributeMap(user))
.ExecuteAsync();
var response = await table.Get
.WithKey("pk", "USER#user123")
.ExecuteAsync();
var retrievedUser = UserMapper.FromAttributeMap(response.Item);For tables without source generation, you can manually implement Scan() methods:
Manual scan implementation is appropriate when:
- No source generation - Working without the source generator
- Dynamic table scenarios - Table names determined at runtime
- Custom table classes - Extending
DynamoDbTableBasewith custom logic - Legacy code - Maintaining existing code without refactoring
using Oproto.FluentDynamoDb.Storage;
using Oproto.FluentDynamoDb.Requests;
using Oproto.FluentDynamoDb.Requests.Extensions;
public class UsersTable : DynamoDbTableBase
{
public UsersTable(IAmazonDynamoDB client, string tableName)
: base(client, tableName)
{
}
// Parameterless scan method
public ScanRequestBuilder Scan() =>
new ScanRequestBuilder(DynamoDbClient, Logger).ForTable(Name);
// Expression-based scan method with filter
public ScanRequestBuilder Scan(string filterExpression, params object[] values)
{
var builder = Scan();
return WithFilterExpressionExtensions.WithFilter(builder, filterExpression, values);
}
}
// Usage
var table = new UsersTable(client, "users");
// Parameterless scan
var allUsers = await table.Scan()
.ExecuteAsync();
// Scan with filter
var activeUsers = await table.Scan("status = {0}", "active")
.ExecuteAsync();
// Scan with complex filter
var recentUsers = await table.Scan()
.WithFilter("createdAt > {0} AND accountType = {1}",
DateTime.UtcNow.AddDays(-30),
"PREMIUM")
.ExecuteAsync();If you don't want to create a custom table class, use DynamoDbTableBase directly:
var table = new DynamoDbTableBase(client, "users");
// Create scan builder directly
var scanBuilder = new ScanRequestBuilder(client, logger).ForTable("users");
var response = await scanBuilder
.WithFilter("status = {0}", "active")
.ExecuteAsync();Use manual implementation when:
-
Dynamic table names - Table name varies at runtime
public class MultiTenantTable : DynamoDbTableBase { public MultiTenantTable(IAmazonDynamoDB client, string tenantId) : base(client, $"tenant-{tenantId}-data") { } public ScanRequestBuilder Scan() => new ScanRequestBuilder(DynamoDbClient, Logger).ForTable(Name); }
-
Custom table logic - Adding business logic to table operations
public class AuditedUsersTable : DynamoDbTableBase { private readonly IAuditLogger _auditLogger; public ScanRequestBuilder Scan() { _auditLogger.LogScanOperation(Name); return new ScanRequestBuilder(DynamoDbClient, Logger).ForTable(Name); } }
-
Legacy code maintenance - Existing code without source generation
// Existing table class without [Scannable] attribute public class LegacyTable : DynamoDbTableBase { // Add scan support without refactoring to use source generation public ScanRequestBuilder Scan() => new ScanRequestBuilder(DynamoDbClient, Logger).ForTable(Name); }
Manual Implementation:
public class UsersTable : DynamoDbTableBase
{
public ScanRequestBuilder Scan() =>
new ScanRequestBuilder(DynamoDbClient, Logger).ForTable(Name);
public ScanRequestBuilder Scan(string filterExpression, params object[] values)
{
var builder = Scan();
return WithFilterExpressionExtensions.WithFilter(builder, filterExpression, values);
}
}Source Generation (Recommended):
[DynamoDbTable("users")]
[Scannable]
public partial class UsersTable : DynamoDbTableBase
{
// Scan() methods generated automatically
}Benefits of Source Generation:
- No boilerplate code to write
- Consistent implementation across all tables
- Automatic updates when library patterns change
- Compile-time validation
When Manual is Better:
- Dynamic table names
- Custom business logic
- No access to source generator
- Gradual migration scenarios
Use .WithValue() and .WithAttributeName() for manual parameter binding:
// Manual parameter binding with .WithValue()
var response = await table.Query
.Where("pk = :pk AND sk > :minDate")
.WithValue(":pk", "USER#user123")
.WithValue(":minDate", DateTime.UtcNow.AddDays(-7).ToString("o"))
.ExecuteAsync();
// Compare with expression formatting (recommended)
var response = await table.Query
.Where($"{UserFields.PartitionKey} = {{0}} AND {UserFields.SortKey} > {{1:o}}",
UserKeys.Pk("user123"),
DateTime.UtcNow.AddDays(-7))
.ExecuteAsync<User>();// Manual attribute name mapping for reserved words
var response = await table.Query
.Where("#status = :status")
.WithAttributeName("#status", "status") // "status" is a reserved word
.WithValue(":status", "active")
.ExecuteAsync();
// Compare with expression formatting (recommended)
var response = await table.Query
.Where($"{UserFields.Status} = {{0}}", "active")
.ExecuteAsync<User>();
// Expression formatting handles reserved words automatically// Manual: Complex filter with multiple parameters
var response = await table.Query
.Where("pk = :pk")
.WithValue(":pk", "USER#user123")
.WithFilter("#status = :status AND #age > :minAge AND #email CONTAINS :domain")
.WithAttributeName("#status", "status")
.WithAttributeName("#age", "age")
.WithAttributeName("#email", "email")
.WithValue(":status", "active")
.WithValue(":minAge", 18)
.WithValue(":domain", "@example.com")
.ExecuteAsync();
// Compare with expression formatting (recommended)
var response = await table.Query
.Where($"{UserFields.PartitionKey} = {{0}}", UserKeys.Pk("user123"))
.WithFilter($"{UserFields.Status} = {{0}} AND {UserFields.Age} > {{1}} AND contains({UserFields.Email}, {{2}})",
"active", 18, "@example.com")
.ExecuteAsync<User>();// Manual: Update expression
await table.Update
.WithKey("pk", "USER#user123")
.Set("SET #name = :name, #age = :age, #updatedAt = :updatedAt")
.WithAttributeName("#name", "name")
.WithAttributeName("#age", "age")
.WithAttributeName("#updatedAt", "updatedAt")
.WithValue(":name", "Jane Doe")
.WithValue(":age", 31)
.WithValue(":updatedAt", DateTime.UtcNow.ToString("o"))
.ExecuteAsync();
// Compare with expression formatting (recommended)
await table.Update
.WithKey(UserFields.PartitionKey, UserKeys.Pk("user123"))
.Set($"SET {UserFields.Name} = {{0}}, {UserFields.Age} = {{1}}, {UserFields.UpdatedAt} = {{2:o}}",
"Jane Doe", 31, DateTime.UtcNow)
.ExecuteAsync();// Manual: Conditional put
await table.Put
.WithItem(item)
.Where("attribute_not_exists(pk) OR #version < :newVersion")
.WithAttributeName("#version", "version")
.WithValue(":newVersion", 2)
.ExecuteAsync();
// Compare with expression formatting (recommended)
await table.Put
.WithItem(user)
.Where($"attribute_not_exists({UserFields.PartitionKey}) OR {UserFields.Version} < {{0}}", 2)
.ExecuteAsync();public class DynamicQueryService
{
public async Task<List<Dictionary<string, AttributeValue>>> SearchUsersAsync(
Dictionary<string, object> filters)
{
var query = table.Query
.Where("pk = :pk")
.WithValue(":pk", "USER#");
// Build filter expression dynamically
var filterParts = new List<string>();
var paramIndex = 0;
foreach (var (field, value) in filters)
{
var paramName = $":param{paramIndex}";
var attrName = $"#attr{paramIndex}";
filterParts.Add($"{attrName} = {paramName}");
query = query
.WithAttributeName(attrName, field)
.WithValue(paramName, value);
paramIndex++;
}
if (filterParts.Any())
{
query = query.WithFilter(string.Join(" AND ", filterParts));
}
var response = await query.ExecuteAsync();
return response.Items;
}
}
// Usage
var filters = new Dictionary<string, object>
{
["status"] = "active",
["age"] = 25,
["country"] = "US"
};
var results = await service.SearchUsersAsync(filters);public class SchemaDiscoveryService
{
public async Task<Dictionary<string, AttributeValue>> GetItemAsync(
string tableName,
Dictionary<string, string> keys)
{
var table = new DynamoDbTableBase(_client, tableName);
var getBuilder = table.Get;
// Add keys dynamically
foreach (var (keyName, keyValue) in keys)
{
getBuilder = getBuilder.WithKey(keyName, keyValue);
}
var response = await getBuilder.ExecuteAsync();
return response.Item;
}
}
// Usage
var item = await service.GetItemAsync(
"users",
new Dictionary<string, string>
{
["pk"] = "USER#user123",
["sk"] = "PROFILE"
});public class MigrationService
{
// Old approach (manual)
public async Task<Dictionary<string, AttributeValue>> GetUserOldWayAsync(string userId)
{
var response = await table.Get
.WithKey("pk", $"USER#{userId}")
.ExecuteAsync();
return response.Item;
}
// New approach (source generation)
public async Task<User?> GetUserNewWayAsync(string userId)
{
var response = await table.Get
.WithKey(UserFields.UserId, UserKeys.Pk(userId))
.ExecuteAsync<User>();
return response.Item;
}
// Gradual migration: Support both
public async Task<object> GetUserAsync(string userId, bool useNewApproach = true)
{
if (useNewApproach)
{
return await GetUserNewWayAsync(userId);
}
else
{
return await GetUserOldWayAsync(userId);
}
}
}public class MultiTenantRepository
{
private readonly IAmazonDynamoDB _client;
public async Task<Dictionary<string, AttributeValue>?> GetItemAsync(
string tenantId,
string entityType,
string entityId)
{
// Table name varies by tenant
var tableName = $"tenant-{tenantId}-data";
var table = new DynamoDbTableBase(_client, tableName);
// Key format varies by entity type
var pk = $"{entityType.ToUpper()}#{entityId}";
var response = await table.Get
.WithKey("pk", pk)
.ExecuteAsync();
return response.Item;
}
public async Task PutItemAsync(
string tenantId,
string entityType,
Dictionary<string, object> data)
{
var tableName = $"tenant-{tenantId}-data";
var table = new DynamoDbTableBase(_client, tableName);
// Convert data to AttributeValue dictionary
var item = new Dictionary<string, AttributeValue>();
foreach (var (key, value) in data)
{
item[key] = ConvertToAttributeValue(value);
}
await table.Put
.WithItem(item)
.ExecuteAsync();
}
private AttributeValue ConvertToAttributeValue(object value)
{
return value switch
{
string s => new AttributeValue { S = s },
int i => new AttributeValue { N = i.ToString() },
long l => new AttributeValue { N = l.ToString() },
decimal d => new AttributeValue { N = d.ToString() },
bool b => new AttributeValue { BOOL = b },
DateTime dt => new AttributeValue { S = dt.ToString("o") },
List<string> list => new AttributeValue { SS = list },
_ => throw new ArgumentException($"Unsupported type: {value.GetType()}")
};
}
}public class GenericDynamoDbRepository<T> where T : class
{
private readonly DynamoDbTableBase _table;
private readonly Func<T, Dictionary<string, AttributeValue>> _toAttributeMap;
private readonly Func<Dictionary<string, AttributeValue>, T> _fromAttributeMap;
private readonly Func<T, string> _getPartitionKey;
public GenericDynamoDbRepository(
DynamoDbTableBase table,
Func<T, Dictionary<string, AttributeValue>> toAttributeMap,
Func<Dictionary<string, AttributeValue>, T> fromAttributeMap,
Func<T, string> getPartitionKey)
{
_table = table;
_toAttributeMap = toAttributeMap;
_fromAttributeMap = fromAttributeMap;
_getPartitionKey = getPartitionKey;
}
public async Task<T?> GetAsync(string partitionKey)
{
var response = await _table.Get
.WithKey("pk", partitionKey)
.ExecuteAsync();
return response.Item != null ? _fromAttributeMap(response.Item) : null;
}
public async Task PutAsync(T entity)
{
var item = _toAttributeMap(entity);
await _table.Put
.WithItem(item)
.ExecuteAsync();
}
public async Task<List<T>> QueryAsync(string partitionKey)
{
var response = await _table.Query
.Where("pk = :pk")
.WithValue(":pk", partitionKey)
.ExecuteAsync();
return response.Items.Select(_fromAttributeMap).ToList();
}
}
// Usage
var userRepository = new GenericDynamoDbRepository<User>(
table,
UserMapper.ToAttributeMap,
UserMapper.FromAttributeMap,
user => $"USER#{user.UserId}");
var user = await userRepository.GetAsync("USER#user123");public class UserInputQueryBuilder
{
private readonly DynamoDbTableBase _table;
public async Task<List<Dictionary<string, AttributeValue>>> SearchAsync(
string partitionKey,
List<FilterCriteria> filters,
string? sortOrder = null)
{
var query = _table.Query
.Where("pk = :pk")
.WithValue(":pk", partitionKey);
// Build filter expression from user input
if (filters.Any())
{
var filterParts = new List<string>();
for (int i = 0; i < filters.Count; i++)
{
var filter = filters[i];
var attrName = $"#attr{i}";
var paramName = $":val{i}";
var expression = filter.Operator switch
{
"=" => $"{attrName} = {paramName}",
">" => $"{attrName} > {paramName}",
"<" => $"{attrName} < {paramName}",
"contains" => $"contains({attrName}, {paramName})",
"begins_with" => $"begins_with({attrName}, {paramName})",
_ => throw new ArgumentException($"Unsupported operator: {filter.Operator}")
};
filterParts.Add(expression);
query = query
.WithAttributeName(attrName, filter.FieldName)
.WithValue(paramName, filter.Value);
}
query = query.WithFilter(string.Join(" AND ", filterParts));
}
// Apply sort order
if (sortOrder == "desc")
{
query = query.ScanIndexForward(false);
}
var response = await query.ExecuteAsync();
return response.Items;
}
}
public class FilterCriteria
{
public string FieldName { get; set; } = string.Empty;
public string Operator { get; set; } = "=";
public object Value { get; set; } = string.Empty;
}
// Usage
var filters = new List<FilterCriteria>
{
new() { FieldName = "status", Operator = "=", Value = "active" },
new() { FieldName = "age", Operator = ">", Value = 18 },
new() { FieldName = "email", Operator = "contains", Value = "@example.com" }
};
var results = await builder.SearchAsync("USER#", filters, "desc");You can mix all three API styles in the same codebase. Here's how they compare:
// Entity with source generation
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
}
// 1. PREFERRED: Lambda expression - type-safe with IntelliSense
var response = await table.Query
.Where<User>(x => x.UserId == "user123")
.ExecuteAsync();
// 2. ALTERNATIVE: Format string - concise with placeholders
var response = await table.Query
.Where($"{UserFields.UserId} = {{0}}", UserKeys.Pk("user123"))
.ExecuteAsync<User>();
// 3. EXPLICIT CONTROL: Manual - for complex scenarios
var response = await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", UserKeys.Pk("user123"))
.ExecuteAsync<User>();When you don't have source generation, format strings are the preferred alternative:
// Manual table (no entity class)
var table = new DynamoDbTableBase(client, "dynamic-table");
// Use format strings with manual field names (preferred for manual tables)
const string PK = "pk";
const string Status = "status";
const string CreatedAt = "createdAt";
var response = await table.Query
.Where($"{PK} = {{0}} AND {CreatedAt} > {{1:o}}",
"USER#user123",
DateTime.UtcNow.AddDays(-7))
.WithFilter($"{Status} = {{0}}", "active")
.ExecuteAsync();public class HybridUserService
{
// Legacy method (manual)
public async Task<Dictionary<string, AttributeValue>> GetUserLegacyAsync(string userId)
{
var response = await table.Get
.WithKey("pk", $"USER#{userId}")
.ExecuteAsync();
return response.Item;
}
// New method (source generation)
public async Task<User?> GetUserAsync(string userId)
{
var response = await table.Get
.WithKey(UserFields.UserId, UserKeys.Pk(userId))
.ExecuteAsync<User>();
return response.Item;
}
// Wrapper for gradual migration
public async Task<object> GetUserFlexibleAsync(string userId, bool useNewApproach)
{
return useNewApproach
? await GetUserAsync(userId)
: await GetUserLegacyAsync(userId);
}
}Lambda Expressions (Preferred):
- Compile-time type checking
- IntelliSense support
- Refactoring safety
- Automatic parameter generation
- Best for production code
Format Strings (Alternative):
- Concise syntax
- Automatic parameter generation
- Good balance of flexibility and safety
- Suitable when lambda expressions aren't available
Manual Patterns (Explicit Control):
- Runtime overhead for parameter binding
- Manual serialization/deserialization
- More error-prone
- Harder to maintain
- Use only when necessary
Recommendation: Use lambda expressions (preferred) or format strings (alternative) for production code. Reserve manual patterns for dynamic queries, complex scenarios, or legacy code migration.
// ✅ Good: Reuse AttributeValue objects
var activeStatus = new AttributeValue { S = "active" };
for (int i = 0; i < 1000; i++)
{
await table.Put
.WithItem(new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = $"USER#{i}" },
["status"] = activeStatus // Reuse
})
.ExecuteAsync();
}
// ❌ Avoid: Creating new objects repeatedly
for (int i = 0; i < 1000; i++)
{
await table.Put
.WithItem(new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = $"USER#{i}" },
["status"] = new AttributeValue { S = "active" } // New object each time
})
.ExecuteAsync();
}// ✅ PREFERRED: Lambda expressions - type-safe with IntelliSense
await table.Query
.Where<User>(x => x.UserId == "user123")
.ExecuteAsync();
// ✅ ALTERNATIVE: Format strings - concise with placeholders
await table.Query
.Where($"{UserFields.UserId} = {{0}}", UserKeys.Pk("user123"))
.ExecuteAsync<User>();
// ⚠️ EXPLICIT CONTROL: Manual patterns - use only when necessary
var table = new DynamoDbTableBase(client, "dynamic-table");
await table.Query
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", "USER#user123")
.ExecuteAsync();// ✅ Good: Constants prevent typos
public static class Fields
{
public const string PartitionKey = "pk";
public const string Email = "email";
}
var response = await table.Get
.WithKey(Fields.PartitionKey, "USER#user123")
.ExecuteAsync();
// ❌ Avoid: String literals
var response = await table.Get
.WithKey("pk", "USER#user123") // Typo-prone
.ExecuteAsync();// ✅ Good: Validate and sanitize
public async Task<List<Dictionary<string, AttributeValue>>> SearchAsync(
string fieldName,
string value)
{
// Validate field name against whitelist
var allowedFields = new[] { "status", "age", "email" };
if (!allowedFields.Contains(fieldName))
{
throw new ArgumentException($"Invalid field name: {fieldName}");
}
// Sanitize value
value = value.Trim();
var response = await table.Query
.Where("pk = :pk")
.WithValue(":pk", "USER#")
.WithFilter($"#{fieldName} = :value")
.WithAttributeName($"#{fieldName}", fieldName)
.WithValue(":value", value)
.ExecuteAsync();
return response.Items;
}// ✅ Good: Document why manual pattern is used
/// <summary>
/// Uses manual pattern because table name is determined at runtime
/// based on tenant ID. Source generation not applicable here.
/// </summary>
public async Task<Dictionary<string, AttributeValue>?> GetTenantDataAsync(
string tenantId,
string dataId)
{
var tableName = $"tenant-{tenantId}-data";
var table = new DynamoDbTableBase(_client, tableName);
var response = await table.Get
.WithKey("pk", dataId)
.ExecuteAsync();
return response.Item;
}Recommendation: For most use cases, use the preferred approaches documented in these guides:
- Basic Operations - Standard CRUD operations with all three API styles
- Querying Data - Query patterns with lambda expressions (preferred)
- Expression Formatting - Format string approach (alternative)
- Entity Definition - Source generation setup for type-safe operations
Previous: Performance Optimization | Next: Advanced Topics
See Also: