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