如何基于ServiceStack.Aws v5.9.0实现DynamoDB乐观锁并解决UpdateItem字段更新异常
Hey there, let's break down what's going on here and fix those issues step by step!
Why [DynamoDBVersion] Didn't Work
First off, your hunch about PutItem overwriting the item is partially right, but the bigger issue is that ServiceStack's PocoDynamo doesn't recognize AWS SDK's [DynamoDBVersion] attribute. That attribute is specific to AWS's native DynamoDBContext—PocoDynamo has its own lightweight approach to DynamoDB interactions and doesn't auto-handle version increments for you using that attribute. So you were right to switch to building custom update expressions!
Why Your UpdateExpression Didn't Save Other Fields
The problem in your second code snippet is that you're completely overwriting the UpdateExpression property. When you call .Set(() => customer), PocoDynamo generates a SET clause that includes all non-key fields of your Customer object. But then you replace that entire expression with just the version increment logic—so only the VersionNumber gets updated, and all other changes are discarded.
The Fix: Append Version Logic to the Auto-Generated Expression
Instead of replacing the UpdateExpression, you need to append your version increment logic to the existing one that PocoDynamo creates. Here's the corrected code:
public Customer Update(Customer customer) { customer.ModifiedDate = DateTime.UtcNow; // Start with the auto-generated SET clause for all non-key fields var expression = db.UpdateExpression<Customer>(customer.CustomerId) .Set(() => customer); // Append the version increment logic (don't overwrite the existing expression!) expression.UpdateExpression += ", #Version = if_not_exists(#Version, :zero) + :incr"; // Add the required expression attributes expression.ExpressionAttributeNames.Add("#Version", "VersionNumber"); expression.ExpressionAttributeValues.Add(":incr", new AttributeValue { N = "1" }); expression.ExpressionAttributeValues.Add(":zero", new AttributeValue { N = "0" }); // Add optimistic lock condition if we have a version to check against if (customer.VersionNumber.HasValue) { expression.Condition(c => c.VersionNumber == customer.VersionNumber); } // Execute the update var success = db.UpdateItem(expression); if (!success) { // Handle concurrent modification or missing item throw new InvalidOperationException("Update failed: concurrent change detected or customer not found"); } // Optional: Fetch the updated item to get the new VersionNumber return db.GetItem<Customer>(customer.CustomerId); }
Key Notes:
- Append, Don't Replace: By using
+=instead of=, we keep all the field updates from.Set(() => customer)while adding the version increment. if_not_existsSafety: This ensures that if the item is new (noVersionNumberyet), it starts at0 + 1 = 1instead of failing.- Optimistic Lock Condition: When a
VersionNumberis provided, the update only succeeds if the stored version matches—preventing concurrent overwrites.
Alternative: Explicitly Update Fields (For Partial Updates)
If you ever want to update only specific fields instead of the entire object, you can use .UpdateOnly() instead of .Set(() => customer), which is useful for reducing payload size:
var expression = db.UpdateExpression<Customer>(customer.CustomerId) .UpdateOnly(c => new { c.ModifiedDate, c.LastName, c.FirstName }) // List fields to update .Set(c => c.VersionNumber, customer.VersionNumber.GetValueOrDefault(0) + 1) .Condition(c => c.VersionNumber == customer.VersionNumber || (c.VersionNumber == null && customer.VersionNumber == null));
This works great for targeted updates, but since your Customer has hundreds of fields, the first approach with .Set(() => customer) is more practical.
内容的提问来源于stack exchange,提问作者Khoa Tong

