Update Expressions
Build type-safe update expressions using lambda expressions for DynamoDB UpdateItem operations.
Overview
Update expressions allow you to modify existing items in DynamoDB. FluentDynamoDb provides a type-safe, expression-based approach that offers compile-time validation, IntelliSense support, and automatic parameter generation.
Benefits
- Type Safety - Compile-time checking of property names and types
- IntelliSense - Full IDE support with autocomplete
- Refactoring - Rename properties safely across your codebase
- Automatic Parameters - No manual parameter binding required
SET Operations
SET operations assign values to attributes. This is the most common update operation.
Simple Value Assignment
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Name = "John Doe",
Email = "john@example.com",
Status = "active"
})
.UpdateAsync();
Using Variables
var newName = "John Doe";
var timestamp = DateTime.UtcNow;
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Name = newName,
UpdatedAt = timestamp
})
.UpdateAsync();
Conditional Assignment with if_not_exists
Sets a value only if the attribute doesn't exist:
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
ViewCount = x.ViewCount.IfNotExists(0),
CreatedAt = x.CreatedAt.IfNotExists(DateTime.UtcNow)
})
.UpdateAsync();
ADD Operations
ADD operations atomically increment numbers or add elements to sets.
Atomic Increment
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
LoginCount = x.LoginCount.Add(1),
ViewCount = x.ViewCount.Add(5)
})
.UpdateAsync();
Atomic Decrement
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Credits = x.Credits.Add(-10) // Subtract 10
})
.UpdateAsync();
Add to Set
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Tags = x.Tags.Add("premium", "verified")
})
.UpdateAsync();
REMOVE Operations
REMOVE operations delete entire attributes from an item.
Remove Single Attribute
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
TempData = x.TempData.Remove()
})
.UpdateAsync();
Remove Multiple Attributes
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
TempData = x.TempData.Remove(),
CachedValue = x.CachedValue.Remove()
})
.UpdateAsync();
REMOVE deletes the attribute entirely, which is different from setting it to null. Key attributes (partition and sort keys) cannot be removed.
Conditional Skip with NoUpdate()
Use NoUpdate() to conditionally skip updating a property based on runtime conditions.
Skip Update Conditionally
var shouldUpdateName = false;
var newName = "Jane Doe";
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Name = shouldUpdateName ? newName : x.Name.NoUpdate(), // Skipped
Status = "active" // Always updated
})
.UpdateAsync();
// Only Status is updated; Name is not touched
Null vs NoUpdate() vs Remove()
Understanding the difference between these three approaches is critical:
| Method | DynamoDB Result | Use Case |
|---|---|---|
= null | SET attr = NULL | Set attribute to DynamoDB NULL type |
.NoUpdate() | No operation | Skip updating this property conditionally |
.Remove() | REMOVE attr | Delete the attribute entirely |
// null → SET NULL (attribute exists with NULL value)
.Set(x => new UserUpdateModel { OptionalField = null })
// NoUpdate() → Skip (attribute unchanged)
.Set(x => new UserUpdateModel { Field = condition ? value : x.Field.NoUpdate() })
// Remove() → REMOVE (attribute deleted)
.Set(x => new UserUpdateModel { TempData = x.TempData.Remove() })
NoUpdate() can only be called on the entity parameter (x.Property.NoUpdate()). Calling it directly throws an exception at runtime.
DELETE Operations
DELETE operations remove specific elements from sets (not to be confused with REMOVE which deletes entire attributes).
Delete from String Set
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Tags = x.Tags.Delete("old-tag", "deprecated")
})
.UpdateAsync();
Delete from Number Set
await table.Update<Product>("prod123")
.Set(x => new ProductUpdateModel
{
CategoryIds = x.CategoryIds.Delete(5, 10)
})
.UpdateAsync();
List Operations
Append to List
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
History = x.History.ListAppend("login", "profile-view")
})
.UpdateAsync();
Prepend to List
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
RecentActivity = x.RecentActivity.ListPrepend("new-event")
})
.UpdateAsync();
Arithmetic Operations
Perform arithmetic directly in SET clauses:
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Score = x.Score + 10,
Balance = x.Balance - 5.00m
})
.UpdateAsync();
Property-to-Property Arithmetic
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
TotalScore = x.BaseScore + x.BonusScore
})
.UpdateAsync();
Nested Property Updates
The source generator creates *UpdateModel types for entities with [DynamoDbMap] properties, enabling type-safe nested updates.
Update Single Nested Property
// Update just the city in a nested address
await table.Update<Customer>("customer123")
.Set(x => new CustomerUpdateModel
{
ShippingAddress = new AddressUpdateModel { City = "Portland" }
})
.UpdateAsync();
// Generated: SET #address.#city = :v0
Update Multiple Nested Properties
// Update multiple properties in nested object
await table.Update<Customer>("customer123")
.Set(x => new CustomerUpdateModel
{
ShippingAddress = new AddressUpdateModel
{
City = "Portland",
State = "OR",
ZipCode = "97201"
}
})
.UpdateAsync();
// Generated: SET #address.#city = :v0, #address.#state = :v1, #address.#zipCode = :v2
Multi-Level Nested Updates
// Update deeply nested property
await table.Update<Order>("order123")
.Set(x => new OrderUpdateModel
{
ShippingAddress = new AddressUpdateModel
{
Country = new CountryUpdateModel { Code = "CA" }
}
})
.UpdateAsync();
// Generated: SET #shippingAddress.#country.#code = :v0
Combined Top-Level and Nested Updates
// Update both top-level and nested properties
await table.Update<Customer>("customer123")
.Set(x => new CustomerUpdateModel
{
Name = "John Doe", // Top-level
ShippingAddress = new AddressUpdateModel { City = "Portland" } // Nested
})
.UpdateAsync();
// Generated: SET #name = :v0, #address.#city = :v1
List Operations
FluentDynamoDb provides extension methods for common list operations.
Append to List
Add elements to the end of a list:
// Append single element
await table.Update<Product>("product123")
.Set(x => x.Tags.Append("new-tag"))
.UpdateAsync();
// Generated: SET #tags = list_append(#tags, :v0)
Append Multiple Elements
// Append multiple elements
await table.Update<Product>("product123")
.Set(x => x.Tags.AppendRange(new[] { "tag1", "tag2", "tag3" }))
.UpdateAsync();
// Generated: SET #tags = list_append(#tags, :v0)
Prepend to List
Add elements to the beginning of a list:
// Prepend single element
await table.Update<Product>("product123")
.Set(x => x.Tags.Prepend("priority-tag"))
.UpdateAsync();
// Generated: SET #tags = list_append(:v0, #tags)
Update Element by Index
Use the SetAt extension method to update an element at a specific index:
// Update element at specific index
await table.Update<Product>("product123")
.Set(x => x.Tags.SetAt(0, "updated-first-tag"))
.UpdateAsync();
// Generated: SET #tags[0] = :v0
Remove Element by Index
Use the RemoveAt extension method to remove an element at a specific index:
// Remove element at specific index
await table.Update<Product>("product123")
.Set(x => x.Tags.RemoveAt(2))
.UpdateAsync();
// Generated: REMOVE #tags[2]
Dynamic Index Support
List indices can be variables, method calls, or property accesses:
// Variable index
int index = GetIndexToUpdate();
await table.Update<Product>("product123")
.Set(x => x.Tags.SetAt(index, "updated"))
.UpdateAsync();
// Method call index
await table.Update<Product>("product123")
.Set(x => x.Tags.SetAt(GetTargetIndex(), "updated"))
.UpdateAsync();
// Property access index
var config = GetConfig();
await table.Update<Product>("product123")
.Set(x => x.Tags.SetAt(config.TargetIndex, "updated"))
.UpdateAsync();
The index expression cannot reference the entity parameter:
// ❌ NOT ALLOWED
.Set(x => x.Tags.SetAt(x.LastIndex, "updated"))
// ✅ ALLOWED - capture value first
int index = entity.LastIndex;
.Set(x => x.Tags.SetAt(index, "updated"))
Chaining SetAt Operations
Multiple SetAt calls can be chained:
await table.Update<Product>("product123")
.Set(x => x.Tags.SetAt(0, "first").SetAt(1, "second").SetAt(2, "third"))
.UpdateAsync();
// Generated: SET #tags[0] = :v0, #tags[1] = :v1, #tags[2] = :v2
SetAt cannot be chained with Append, Prepend, or RemoveAt on the same list due to overlapping document paths. Use separate update calls instead.
Nested List Operations
List operations work with nested lists:
// Append to nested list
await table.Update<Product>("product123")
.Set(x => x.Metadata.Keywords.Append("sale"))
.UpdateAsync();
// Generated: SET #metadata.#keywords = list_append(#metadata.#keywords, :v0)
List Operations Reference
| Operation | Method | DynamoDB Expression |
|---|---|---|
| Append single | .Append(item) | SET #attr = list_append(#attr, :val) |
| Append multiple | .AppendRange(items) | SET #attr = list_append(#attr, :val) |
| Prepend single | .Prepend(item) | SET #attr = list_append(:val, #attr) |
| Prepend multiple | .PrependRange(items) | SET #attr = list_append(:val, #attr) |
| Update by index | .SetAt(index, value) | SET #attr[index] = :val |
| Remove by index | .RemoveAt(index) | REMOVE #attr[index] |
| Chained SetAt | .SetAt(0, a).SetAt(1, b) | SET #attr[0] = :v0, #attr[1] = :v1 |
Set Operations
Sets in DynamoDB are unordered collections of unique values. FluentDynamoDb supports string sets (HashSet<string>) and number sets (HashSet<int>, HashSet<decimal>, etc.).
Add to Set
// Add single element
await table.Update<Product>("product123")
.Add(x => x.Categories, "electronics")
.UpdateAsync();
// Generated: ADD #categories :v0
Add Multiple Elements
// Add multiple elements
await table.Update<Product>("product123")
.Add(x => x.Categories, new[] { "electronics", "sale", "featured" })
.UpdateAsync();
// Generated: ADD #categories :v0
Delete from Set
// Delete single element
await table.Update<Product>("product123")
.Delete(x => x.Categories, "clearance")
.UpdateAsync();
// Generated: DELETE #categories :v0
Delete Multiple Elements
// Delete multiple elements
await table.Update<Product>("product123")
.Delete(x => x.Categories, new[] { "clearance", "discontinued" })
.UpdateAsync();
// Generated: DELETE #categories :v0
Numeric Set Operations
// Add to number set
await table.Update<Product>("product123")
.Add(x => x.RelatedProductIds, 42)
.UpdateAsync();
// Generated: ADD #relatedIds :v0
// Add multiple numbers
await table.Update<Product>("product123")
.Add(x => x.RelatedProductIds, new[] { 100, 200, 300 })
.UpdateAsync();
Set Operations Reference
| Operation | Method | DynamoDB Expression |
|---|---|---|
| Add single | .Add(x => x.Set, value) | ADD #attr :val |
| Add multiple | .Add(x => x.Set, values[]) | ADD #attr :val |
| Delete single | .Delete(x => x.Set, value) | DELETE #attr :val |
| Delete multiple | .Delete(x => x.Set, values[]) | DELETE #attr :val |
Important Notes on Sets
- ADD creates if not exists: If the set attribute doesn't exist, ADD creates it
- DELETE requires existing set: DELETE on a non-existent attribute returns an error
- Sets are unordered: Elements have no guaranteed order
- Sets contain unique values: Duplicate values are automatically deduplicated
Anonymous Types for Multiple Properties
Use anonymous types to update multiple properties in a single expression. This is the recommended pattern for updating several fields at once.
Basic Pattern
await table.Update<User>("user123")
.Set(x => new { Name = "John Doe", Age = 30 })
.UpdateAsync();
With Mixed Operations
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
// SET operations
Name = "John Doe",
Status = "active",
UpdatedAt = DateTime.UtcNow,
// ADD operations
LoginCount = x.LoginCount.Add(1),
Tags = x.Tags.Add("premium"),
// List operations
History = x.History.ListAppend("profile-update"),
// REMOVE operations
TempData = x.TempData.Remove()
})
.UpdateAsync();
Using Variables in Anonymous Types
var newName = "Jane Doe";
var newStatus = "active";
var timestamp = DateTime.UtcNow;
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Name = newName,
Status = newStatus,
UpdatedAt = timestamp,
LoginCount = x.LoginCount.Add(1)
})
.UpdateAsync();
Conditional Updates
Combine Set() with Where() to perform conditional updates that only succeed when certain conditions are met.
Basic Conditional Update
await table.Update<User>("user123")
.Where(x => x.Status == "active")
.Set(x => new UserUpdateModel { Name = "Jane Doe" })
.UpdateAsync();
Optimistic Locking with Version
Use version checking to prevent concurrent update conflicts:
var currentVersion = 5;
await table.Update<User>("user123")
.Where(x => x.Version == currentVersion)
.Set(x => new UserUpdateModel
{
Name = "Jane Doe",
Version = currentVersion + 1
})
.UpdateAsync();
Only Update If Exists
await table.Update<User>("user123")
.Where(x => x.UserId.AttributeExists())
.Set(x => new UserUpdateModel { Name = "Jane Doe" })
.UpdateAsync();
Complex Conditions
await table.Update<User>("user123")
.Where(x => x.Status == "active" && x.Age >= 18)
.Set(x => new UserUpdateModel
{
Verified = true,
VerifiedAt = DateTime.UtcNow
})
.UpdateAsync();
Transactions with Updates
Updates can be included in transactions for atomic multi-item operations:
await DynamoDbTransactions.Write
.Add(table.Update<Order>("customer123", "ORDER#order456")
.Set(x => new { Status = "shipped" }))
.Add(table.Update<Inventory>("product789")
.Where(x => x.Quantity >= 1)
.Set(x => new { Quantity = x.Quantity.Add(-1) }))
.CommitAsync();
Format String Alternative
For scenarios requiring more control, you can use format strings:
// Format string approach
await table.Update<User>("user123")
.Set($"SET {User.Fields.Name} = {{0}}, {User.Fields.Status} = {{1}}",
"Jane Doe", "active")
.UpdateAsync();
// ADD with format string
await table.Update<User>("user123")
.Set($"ADD {User.Fields.LoginCount} {{0}}", 1)
.UpdateAsync();
// REMOVE with format string
await table.Update<User>("user123")
.Set($"REMOVE {User.Fields.TempField}")
.UpdateAsync();
Best Practices
Use ADD for Counters
// ✅ Recommended: ADD creates attribute if it doesn't exist
LoginCount = x.LoginCount.Add(1)
// ⚠️ Arithmetic requires attribute to exist
LoginCount = x.LoginCount + 1
Combine Operations Efficiently
// ✅ Good: Single update with multiple operations
await table.Update<User>("user123")
.Set(x => new UserUpdateModel
{
Name = "John",
LoginCount = x.LoginCount.Add(1),
TempData = x.TempData.Remove()
})
.UpdateAsync();
// ❌ Avoid: Multiple separate updates
await table.Update<User>("user123")
.Set(x => new UserUpdateModel { Name = "John" })
.UpdateAsync();
await table.Update<User>("user123")
.Set(x => new UserUpdateModel { LoginCount = x.LoginCount.Add(1) })
.UpdateAsync();
Use Optimistic Locking for Concurrent Updates
// ✅ Prevents lost updates in concurrent scenarios
await table.Update<User>("user123")
.Where(x => x.Version == currentVersion)
.Set(x => new UserUpdateModel
{
Data = newData,
Version = currentVersion + 1
})
.UpdateAsync();
Next Steps
- Lambda Expressions - Type-safe query building (recommended)
- Formatted Expressions - Format string syntax
- String Expressions - Manual expression building for explicit control
- Filters - Filter expressions for queries