Skip to main content

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();
note

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:

MethodDynamoDB ResultUse Case
= nullSET attr = NULLSet attribute to DynamoDB NULL type
.NoUpdate()No operationSkip updating this property conditionally
.Remove()REMOVE attrDelete 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() })
Important

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();
Entity Parameter Restriction

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
DynamoDB Limitation

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​

OperationMethodDynamoDB 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​

OperationMethodDynamoDB 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​