From 6ab2dec151493612118b3390a1d903af997b8ed0 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Tue, 6 May 2025 17:25:57 +0300 Subject: [PATCH 01/11] add new atribute and update internal config --- .../DynamoDBv2/Custom/DataModel/Attributes.cs | 67 +++++++++++++++++++ .../Custom/DataModel/ContextInternal.cs | 15 +++-- .../Custom/DataModel/InternalModel.cs | 21 +++++- .../DynamoDBv2/Custom/DataModel/Utils.cs | 2 +- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs index 5d3affa53f91..dbfb01e8f311 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs @@ -254,6 +254,73 @@ public DynamoDBVersionAttribute(string attributeName) } } + /// + /// Marks a property or field as an atomic counter in DynamoDB. + /// + /// This attribute indicates that the associated property or field should be treated as an atomic counter, + /// which can be incremented or decremented directly in DynamoDB during update operations. + /// It is useful for scenarios where you need to maintain a counter that is updated concurrently by multiple clients + /// without conflicts. + /// + /// The attribute also allows specifying an alternate attribute name in DynamoDB using the `AttributeName` property, + /// as well as configuring the increment or decrement value (`Delta`) and the starting value (`StartValue`). + /// + /// + /// Example usage: + /// + /// public class Example + /// { + /// [DynamoDBAtomicCounter] + /// public long Counter { get; set; } + /// + /// [DynamoDBAtomicCounter("CustomCounterName", delta: 5, startValue: 100)] + /// public long CustomCounter { get; set; } + /// } + /// + /// In this example: + /// - `Counter` will be treated as an atomic counter with the same name in DynamoDB. + /// - `CustomCounter` will be treated as an atomic counter with the attribute name "CustomCounterName" in DynamoDB, + /// incremented by 5 for each update, and starting with an initial value of 100. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class DynamoDBAtomicCounterAttribute : DynamoDBRenamableAttribute + { + /// + /// The value to increment (positive) or decrement (negative) the counter with for each update. + /// + public long Delta { get; } + + /// + /// The starting value of the counter. + /// + public long StartValue { get; } + + /// + /// Default constructor + /// + public DynamoDBAtomicCounterAttribute() + : base() + { + Delta = 1; + StartValue = 0; + } + + /// + /// Constructor that specifies an alternate attribute name + /// + /// + /// Name of attribute to be associated with property or field. + /// + /// The value to increment (positive) or decrement (negative) the counter with for each update. + /// The starting value of the counter. + public DynamoDBAtomicCounterAttribute(string attributeName, long delta, long startValue) + : base(attributeName) + { + Delta = delta; + StartValue = startValue; + } + } + /// /// DynamoDB property attribute. diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 806354cc9a68..c83ab2d88308 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -392,7 +392,6 @@ private void PopulateInstance(ItemStorage storage, object instance, DynamoDBFlat { foreach (PropertyStorage propertyStorage in storageConfig.AllPropertyStorage) { - string propertyName = propertyStorage.PropertyName; string attributeName = propertyStorage.AttributeName; DynamoDBEntry entry; @@ -410,6 +409,9 @@ private void PopulateInstance(ItemStorage storage, object instance, DynamoDBFlat if (propertyStorage.IsVersion) storage.CurrentVersion = entry as Primitive; + + if (propertyStorage.IsCounter) + storage.CurrentCount = entry as Primitive; } } } @@ -466,8 +468,9 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl { // if only keys are being serialized, skip non-key properties // still include version, however, to populate the storage.CurrentVersion field + // and include counter, to populate the storage.CurrentCount field if (keysOnly && !propertyStorage.IsHashKey && !propertyStorage.IsRangeKey && - !propertyStorage.IsVersion) continue; + !propertyStorage.IsVersion && !propertyStorage.IsCounter) continue; string propertyName = propertyStorage.PropertyName; string attributeName = propertyStorage.AttributeName; @@ -481,17 +484,21 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl { Primitive dbePrimitive = dbe as Primitive; if (propertyStorage.IsHashKey || propertyStorage.IsRangeKey || - propertyStorage.IsVersion || propertyStorage.IsLSIRangeKey) + propertyStorage.IsVersion || propertyStorage.IsLSIRangeKey || + propertyStorage.IsCounter) { if (dbe != null && dbePrimitive == null) throw new InvalidOperationException("Property " + propertyName + - " is a hash key, range key or version property and must be Primitive"); + " is a hash key, range key, atomic counter or version property and must be Primitive"); } document[attributeName] = dbe; if (propertyStorage.IsVersion) storage.CurrentVersion = dbePrimitive; + + if (propertyStorage.IsCounter) + storage.CurrentCount = dbePrimitive; } } else diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index eb1085ff9570..21a573016c9d 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -149,6 +149,12 @@ internal class PropertyStorage : SimplePropertyStorage // corresponding IndexNames, if applicable public List IndexNames { get; set; } + public bool IsCounter { get; set; } + + public long CounterDelta { get; set; } + + public long CounterStartValue { get; set; } + public void AddIndex(DynamoDBGlobalSecondaryIndexHashKeyAttribute gsiHashKey) { AddIndex(new GSI(true, gsiHashKey.AttributeName, gsiHashKey.IndexNames)); @@ -209,7 +215,10 @@ public GSI(bool isHashKey, string attributeName, params string[] indexNames) public void Validate(DynamoDBContext context) { if (IsVersion) - Utils.ValidateVersionType(MemberType); // no conversion is possible, so type must be a nullable primitive + Utils.ValidateNumericType(MemberType); // no conversion is possible, so type must be a nullable primitive + + if (IsCounter) + Utils.ValidateNumericType(MemberType); // no conversion is possible, so type must be a nullable primitive if (IsHashKey && IsRangeKey) throw new InvalidOperationException("Property " + PropertyName + " cannot be both hash and range key"); @@ -260,6 +269,7 @@ internal class ItemStorage public Document Document { get; set; } public ItemStorageConfig Config { get; set; } public Primitive CurrentVersion { get; set; } + public Primitive CurrentCount { get; set; } public HashSet ConvertedObjects { get; private set; } public ItemStorage(ItemStorageConfig storageConfig) @@ -958,6 +968,14 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con if (attribute is DynamoDBVersionAttribute) propertyStorage.IsVersion = true; + DynamoDBAtomicCounterAttribute counterAttribute = attribute as DynamoDBAtomicCounterAttribute; + if (counterAttribute != null) + { + propertyStorage.IsCounter = true; + propertyStorage.CounterDelta = counterAttribute.Delta; + propertyStorage.CounterStartValue = counterAttribute.StartValue; + } + DynamoDBRenamableAttribute renamableAttribute = attribute as DynamoDBRenamableAttribute; if (renamableAttribute != null && !string.IsNullOrEmpty(renamableAttribute.AttributeName)) { @@ -1109,6 +1127,7 @@ private static void PopulateConfigFromMappings(ItemStorageConfig config, Diction propertyStorage.ConverterType = propertyConfig.Converter; propertyStorage.IsIgnored = propertyConfig.Ignore; propertyStorage.IsVersion = propertyConfig.Version; + //propertyStorage.IsCounter = propertyConfig.Counter; propertyStorage.StoreAsEpoch = propertyConfig.StoreAsEpoch; propertyStorage.StoreAsEpochLong = propertyConfig.StoreAsEpochLong; } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs index 95b50af1a440..29643d9d31f8 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs @@ -141,7 +141,7 @@ internal static void ValidatePrimitiveType() ValidatePrimitiveType(typeof(T)); } - internal static void ValidateVersionType(Type memberType) + internal static void ValidateNumericType(Type memberType) { if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Nullable<>) && (memberType.IsAssignableFrom(typeof(Byte)) || From a76578835d9d496b9424cd53084123f95f814ef3 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Sat, 10 May 2025 12:11:44 +0300 Subject: [PATCH 02/11] wip --- .../DynamoDBv2/Custom/DataModel/Attributes.cs | 12 + .../DynamoDBv2/Custom/DataModel/Context.cs | 55 +++- .../Custom/DataModel/ContextInternal.cs | 85 +++++- .../Custom/DataModel/InternalModel.cs | 1 - .../Custom/DataModel/TransactWrite.cs | 35 +++ .../Custom/DocumentModel/Expression.cs | 47 +++ .../DynamoDBv2/Custom/DocumentModel/Table.cs | 6 + .../IntegrationTests/DataModelTests.cs | 284 ++++++++++++------ 8 files changed, 423 insertions(+), 102 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs index dbfb01e8f311..030a1e7c0a06 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs @@ -319,6 +319,18 @@ public DynamoDBAtomicCounterAttribute(string attributeName, long delta, long sta Delta = delta; StartValue = startValue; } + + /// + /// Constructor that specifies an alternate attribute name + /// + /// The value to increment (positive) or decrement (negative) the counter with for each update. + /// The starting value of the counter. + public DynamoDBAtomicCounterAttribute(long delta, long startValue) + : base() + { + Delta = delta; + StartValue = startValue; + } } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 37e886672e3a..a5fcce5c4fc6 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -16,6 +16,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; using System.Threading; #if AWS_ASYNC_API using System.Threading.Tasks; @@ -23,6 +25,7 @@ #endif using Amazon.DynamoDBv2.DocumentModel; using ThirdParty.RuntimeBackports; +using Expression = Amazon.DynamoDBv2.DocumentModel.Expression; namespace Amazon.DynamoDBv2.DataModel { @@ -369,18 +372,26 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); + var updateExpression = CreateUpdateExpressionForCounterProperties(storage); + SetAtomicCounters(storage); if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), null); + table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig + { + ReturnValues = ReturnValues.None, + UpdateExpression = updateExpression + }); } else { - Document expectedDocument = CreateExpectedDocumentForVersion(storage); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); var updateItemOperationConfig = new UpdateItemOperationConfig { - Expected = expectedDocument, ReturnValues = ReturnValues.None, + ConditionalExpression = versionExpression, + UpdateExpression = updateExpression }; table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig); PopulateInstance(storage, value, flatConfig); @@ -401,21 +412,51 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); + + var counterConditionExpression = CreateUpdateExpressionForCounterProperties(storage); + SetAtomicCounters(storage); if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), null, cancellationToken).ConfigureAwait(false); + await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig + { + ReturnValues = ReturnValues.None, + UpdateExpression = counterConditionExpression + }, cancellationToken).ConfigureAwait(false); } else { - Document expectedDocument = CreateExpectedDocumentForVersion(storage); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); + + //if (counterConditionExpression != null) + //{ + // versionExpression.ExpressionStatement += " \n" + counterConditionExpression.ExpressionStatement; + // versionExpression.ExpressionAttributeNames = + // versionExpression.ExpressionAttributeNames.Union(counterConditionExpression.ExpressionAttributeNames). + // ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); + + // if (versionExpression.ExpressionAttributeValues != null) + // { + // versionExpression.ExpressionAttributeValues = + // versionExpression.ExpressionAttributeValues.Union(counterConditionExpression.ExpressionAttributeValues). + // ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); + // } + //} + await table.UpdateHelperAsync( storage.Document, table.MakeKey(storage.Document), - new UpdateItemOperationConfig { Expected = expectedDocument, ReturnValues = ReturnValues.None }, - cancellationToken).ConfigureAwait(false); + new UpdateItemOperationConfig + { + ReturnValues = ReturnValues.None, + ConditionalExpression = versionExpression, + UpdateExpression = counterConditionExpression + }, + cancellationToken) + .ConfigureAwait(false); PopulateInstance(storage, value, flatConfig); } } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index c83ab2d88308..8dbf073d8a49 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -70,6 +70,7 @@ private static void IncrementVersion(Type memberType, ref Primitive version) else if (memberType.IsAssignableFrom(typeof(short))) version = version.AsShort() + 1; else if (memberType.IsAssignableFrom(typeof(ushort))) version = version.AsUShort() + 1; } + private static Document CreateExpectedDocumentForVersion(ItemStorage storage) { Document document = new Document(); @@ -117,6 +118,84 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora #endregion + #region Atomic counters + + + internal static void SetAtomicCounters(ItemStorage storage) + { + + var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. + Where(propertyStorage => propertyStorage.IsCounter).ToList(); + + if (counterProperties.Count==0) return; + // Set the initial value of the counter properties + foreach (var propertyStorage in counterProperties) + { + Primitive counter; + string versionAttributeName = propertyStorage.AttributeName; + + if (storage.Document.TryGetValue(versionAttributeName, out var counterEntry)) + counter = counterEntry as Primitive; + else + counter = null; + + if (counter != null && counter.Value != null) + { + if (counter.Type != DynamoDBEntryType.Numeric) throw new InvalidOperationException("Atomic Counter property must be numeric"); + IncrementCounter(propertyStorage.MemberType, ref counter, propertyStorage.CounterDelta); + } + else + { + counter = new Primitive(propertyStorage.CounterStartValue.ToString(), true); + } + storage.Document[versionAttributeName] = counter; + } + + } + + + private static void IncrementCounter(Type memberType, ref Primitive counter,long delta) + { + if (memberType.IsAssignableFrom(typeof(Byte))) counter = counter.AsByte() + delta; + else if (memberType.IsAssignableFrom(typeof(SByte))) counter = counter.AsSByte() + delta; + else if (memberType.IsAssignableFrom(typeof(int))) counter = counter.AsInt() + delta; + else if (memberType.IsAssignableFrom(typeof(uint))) counter = counter.AsUInt() + delta; + else if (memberType.IsAssignableFrom(typeof(long))) counter = counter.AsLong() + delta; + //else if (memberType.IsAssignableFrom(typeof(ulong))) counter = counter.AsULong() + delta; + else if (memberType.IsAssignableFrom(typeof(short))) counter = counter.AsShort() + delta; + else if (memberType.IsAssignableFrom(typeof(ushort))) counter = counter.AsUShort() + delta; + } + + internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorage storage) + { + Expression updateExpression = null; + var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. + Where(propertyStorage => propertyStorage.IsCounter).ToList(); + + if (counterProperties.Count != 0) + { + updateExpression = new Expression(); + var asserts = string.Empty; + foreach (var propertyStorage in counterProperties) + { + string startValueName = $":s_{propertyStorage.AttributeName}"; + string deltaValueName = $":d_{propertyStorage.AttributeName}"; + string counterAttributeName = Common.GetAttributeReference(propertyStorage.AttributeName); + asserts += $"{counterAttributeName} = " + + $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; + updateExpression.ExpressionAttributeNames[counterAttributeName] = propertyStorage.AttributeName; + updateExpression.ExpressionAttributeValues[deltaValueName] = propertyStorage.CounterDelta; + updateExpression.ExpressionAttributeValues[startValueName] = + propertyStorage.CounterStartValue - propertyStorage.CounterDelta; + } + updateExpression.ExpressionStatement = $" {asserts.Substring(0, asserts.Length - 2)}"; + } + + return updateExpression; + } + + #endregion + #region Table methods // Retrieves the target table for the specified type @@ -409,9 +488,6 @@ private void PopulateInstance(ItemStorage storage, object instance, DynamoDBFlat if (propertyStorage.IsVersion) storage.CurrentVersion = entry as Primitive; - - if (propertyStorage.IsCounter) - storage.CurrentCount = entry as Primitive; } } } @@ -496,9 +572,6 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl if (propertyStorage.IsVersion) storage.CurrentVersion = dbePrimitive; - - if (propertyStorage.IsCounter) - storage.CurrentCount = dbePrimitive; } } else diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index 21a573016c9d..eb153b3abb02 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -269,7 +269,6 @@ internal class ItemStorage public Document Document { get; set; } public ItemStorageConfig Config { get; set; } public Primitive CurrentVersion { get; set; } - public Primitive CurrentCount { get; set; } public HashSet ConvertedObjects { get; private set; } public ItemStorage(ItemStorageConfig storageConfig) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 95a9f39a90d9..0441b4c8dcd6 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs @@ -225,8 +225,28 @@ public void AddSaveItem(T item) ItemStorage storage = _context.ObjectToItemStorageHelper(item, _storageConfig, _config, keysOnly: false, _config.IgnoreNullValues ?? false); if (storage == null) return; + Expression conditionExpression = CreateConditionExpressionForVersion(storage); SetNewVersion(storage); + + Expression counterConditionExpression = CreateConditionExpressionForCounter(storage); + + if (counterConditionExpression != null) + { + conditionExpression.ExpressionStatement += " \n" + counterConditionExpression.ExpressionStatement; + conditionExpression.ExpressionAttributeNames = + conditionExpression.ExpressionAttributeNames.Union(counterConditionExpression.ExpressionAttributeNames). + ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); + + if (conditionExpression.ExpressionAttributeValues != null) + { + conditionExpression.ExpressionAttributeValues = + conditionExpression.ExpressionAttributeValues.Union(counterConditionExpression.ExpressionAttributeValues). + ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); + } + } + + SetAtomicCounters(storage); AddDocumentTransaction(storage, conditionExpression); @@ -408,6 +428,11 @@ private bool ShouldUseVersioning() return !skipVersionCheck && _storageConfig.HasVersion; } + private bool ShouldUseCounter() + { + return false; //_storageConfig.; + } + private void CheckUseVersioning() { if (_config.SkipVersionCheck == true) @@ -432,6 +457,11 @@ private Expression CreateConditionExpressionForVersion(ItemStorage storage) DocumentTransaction.TargetTable.IsEmptyStringValueEnabled); return DynamoDBContext.CreateConditionExpressionForVersion(storage, conversionConfig); } + + private Expression CreateConditionExpressionForCounter(ItemStorage storage) + { + return DynamoDBContext.CreateUpdateExpressionForCounterProperties(storage); + } private void AddDocumentTransaction(ItemStorage storage, Expression conditionExpression) @@ -477,6 +507,11 @@ private void SetNewVersion(ItemStorage storage) if (!ShouldUseVersioning()) return; DynamoDBContext.SetNewVersion(storage); } + + private void SetAtomicCounters(ItemStorage storage) + { + DynamoDBContext.SetAtomicCounters(storage); + } } /// diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs index 9e144c6cf7da..66ccc845ae4b 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs @@ -127,6 +127,53 @@ internal void ApplyExpression(UpdateItemRequest request, Table table) } } + internal void ApplyUpdateExpression(UpdateItemRequest request, Table table) + { + request.UpdateExpression += $" {this.ExpressionStatement}"; + //todo make this work properly + //if (request.UpdateExpression!=null) + //{ + // int removeIndex = request.UpdateExpression.IndexOf(" REMOVE", StringComparison.OrdinalIgnoreCase); + // int setIndex = request.UpdateExpression.IndexOf("SET", StringComparison.OrdinalIgnoreCase); + // if (removeIndex >= 0) + // { + // string setPart = request.UpdateExpression.Substring(setIndex, removeIndex); + // setPart += $" ,{this.ExpressionStatement}"; + // string removePart = request.UpdateExpression.Substring(removeIndex); + // request.UpdateExpression = $"{setPart}{removePart}"; + // } + //} + + if (request.ExpressionAttributeNames == null) + { + if (this.ExpressionAttributeNames?.Count > 0) + { + request.ExpressionAttributeNames = new Dictionary(this.ExpressionAttributeNames); + } + } + else + { + foreach (var kvp in this.ExpressionAttributeNames) + request.ExpressionAttributeNames.Add(kvp.Key, kvp.Value); + } + + var attributeValues = ConvertToAttributeValues(this.ExpressionAttributeValues, table); + if (!(attributeValues?.Count > 0)) return; + + if (request.ExpressionAttributeValues == null) + { + if (this.ExpressionAttributeValues?.Count > 0) + { + request.ExpressionAttributeValues = attributeValues; + } + } + else + { + foreach (var kvp in attributeValues) + request.ExpressionAttributeValues.Add(kvp.Key, kvp.Value); + } + } + internal void ApplyExpression(Get request, Table table) { request.ProjectionExpression = ExpressionStatement; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs index f84ab45ad4fa..c8afb9b0cfdc 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs @@ -1491,6 +1491,7 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, out statement, out expressionAttributeValues, out expressionAttributeNames); req.AttributeUpdates = null; + // req.ConditionExpression = statement req.UpdateExpression = statement; if (req.ExpressionAttributeValues == null) @@ -1510,6 +1511,11 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte } } + if (currentConfig.UpdateExpression is { IsSet: true }) + { + currentConfig.UpdateExpression.ApplyUpdateExpression(req, this); + } + var resp = await DDBClient.UpdateItemAsync(req, cancellationToken).ConfigureAwait(false); var returnedAttributes = resp.Attributes; doc.CommitChanges(); diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index 4d3e85ffa066..e7da5f057c2c 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -23,7 +23,8 @@ public void TestContextWithEmptyStringEnabled() { // It is a known bug that this test currently fails due to an AOT-compilation // issue, on iOS using mono2x. - foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) + foreach (var conversion in new DynamoDBEntryConversion[] + { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) { TableCache.Clear(); @@ -210,7 +211,7 @@ public void TestTransactWrite_AddSaveItem_DocumentTransaction() TableCache.Clear(); CreateContext(DynamoDBEntryConversion.V2, true, true); - + { var hashRangeOnly = new AnnotatedRangeTable @@ -313,8 +314,11 @@ public void TestContext_RetrieveDateTimeInUtc(bool retrieveDateTimeInUtc) //This is a valid use of .ToLocalTime var expectedCurrTime = retrieveDateTimeInUtc ? currTime.ToUniversalTime() : currTime.ToLocalTime(); - var expectedLongEpochTime = retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); - var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc ? longEpochTimeBefore1970.ToUniversalTime() : longEpochTimeBefore1970.ToLocalTime(); + var expectedLongEpochTime = + retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); + var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc + ? longEpochTimeBefore1970.ToUniversalTime() + : longEpochTimeBefore1970.ToLocalTime(); // Load var storedEmployee = Context.Load(employee.CreationTime, employee.Name); @@ -335,7 +339,8 @@ public void TestContext_RetrieveDateTimeInUtc(bool retrieveDateTimeInUtc) // Query QueryFilter filter = new QueryFilter(); filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); - storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }).First(); + storedEmployee = Context + .FromQuery(new QueryOperationConfig { Filter = filter }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -440,7 +445,8 @@ public void TestContext_CustomDateTimeConverter(bool retrieveDateTimeInUtc) // Query QueryFilter filter = new QueryFilter(); filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); - storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }).First(); + storedEmployee = Context + .FromQuery(new QueryOperationConfig { Filter = filter }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -486,7 +492,8 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT TableCache.Clear(); #pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors - Context = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 }); + Context = new DynamoDBContext(Client, + new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 }); #pragma warning restore CS0618 // Re-enable the warning var operationConfig = new DynamoDBOperationConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }; @@ -510,11 +517,15 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT //This is a valid use of .ToLocalTime var expectedCurrTime = retrieveDateTimeInUtc ? currTime.ToUniversalTime() : currTime.ToLocalTime(); - var expectedLongEpochTime = retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); - var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc ? longEpochTimeBefore1970.ToUniversalTime() : longEpochTimeBefore1970.ToLocalTime(); + var expectedLongEpochTime = + retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); + var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc + ? longEpochTimeBefore1970.ToUniversalTime() + : longEpochTimeBefore1970.ToLocalTime(); // Load - var storedEmployee = Context.Load(employee.CreationTime, employee.Name, new LoadConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc}); + var storedEmployee = Context.Load(employee.CreationTime, employee.Name, + new LoadConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -529,8 +540,8 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT QueryFilter filter = new QueryFilter(); filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); storedEmployee = Context.FromQuery( - new QueryOperationConfig { Filter = filter }, - new FromQueryConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc}).First(); + new QueryOperationConfig { Filter = filter }, + new FromQueryConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -543,7 +554,7 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT // Scan storedEmployee = Context.Scan( - new List(), + new List(), new ScanConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); @@ -619,13 +630,13 @@ public async Task TestContext_TransactWriteAndLoad_WithDerivedTypeItems() }, DictionaryClasses = new Dictionary() { - {"A", new A{ Name = "A1", MyPropA = 1 }}, - {"B", new B{ Name = "A1", MyPropA = 1, MyPropB = 2}} + { "A", new A { Name = "A1", MyPropA = 1 } }, + { "B", new B { Name = "A1", MyPropA = 1, MyPropB = 2 } } } }; var transactWrite = Context.CreateTransactWrite(); - transactWrite.AddSaveItems(new []{ model1 , model2}); + transactWrite.AddSaveItems(new[] { model1, model2 }); await transactWrite.ExecuteAsync(); var storedModel1 = await Context.LoadAsync(id); @@ -642,6 +653,77 @@ public async Task TestContext_TransactWriteAndLoad_WithDerivedTypeItems() } + /// + /// Tests that the DynamoDB operations can read and write items. + /// + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_AtomicCounterAnnotation() + { + TableCache.Clear(); + CleanupTables(); + TableCache.Clear(); + + CounterAnnotatedEmployee employee = new CounterAnnotatedEmployee + { + Name = "Mark", + Age = 31, + Score = 120, + ManagerName = "Harmony" + }; + + await Context.SaveAsync(employee); + var storedEmployee = await Context.LoadAsync(employee.Name, 31); + Assert.IsNotNull(storedEmployee); + Assert.AreEqual(employee.Name, storedEmployee.Name); + // Assert.AreEqual(0, storedEmployee.Version); + Assert.AreEqual(0, storedEmployee.CountDefault); + Assert.AreEqual(10, storedEmployee.CountAtomic); + + //// Update the employee + //storedEmployee.ManagerName = "Helena"; + + //await Context.SaveAsync(storedEmployee); + //var storedUpdatedEmployee = await Context.LoadAsync(storedEmployee.Name, 31); + //Assert.IsNotNull(storedUpdatedEmployee); + //Assert.AreEqual(employee.Name, storedUpdatedEmployee.Name); + //Assert.AreEqual(1, storedUpdatedEmployee.Version); + //Assert.AreEqual(1, storedUpdatedEmployee.CountDefault); + //Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); + + + //var batchWrite = Context.CreateBatchWrite(); + //batchWrite.AddPutItem(new CounterAnnotatedEmployee + //{ + // Name = "Helena", + // Age = 25, + // Score = 140 + //}); + + //await batchWrite.ExecuteAsync(); + //var storedEmployee2 = await Context.LoadAsync("Helena"); + //Assert.IsNotNull(storedEmployee2); + //Assert.AreEqual("Helena", storedEmployee2.Name); + //Assert.IsNull(storedEmployee2.CountDefault); + //Assert.IsNull(storedEmployee2.CountAtomic); + + + + VersionedAnnotatedEmployee model = new VersionedAnnotatedEmployee + { + Name = "Mark", + Age = 31, + Score = 120, + ManagerName = "Harmony" + }; + + var transactWrite = Context.CreateTransactWrite(); + transactWrite.AddSaveItems(new[] { model }); + await transactWrite.ExecuteAsync(); + var storedEmployee2 = await Context.LoadAsync("Mark",31); + + } [TestMethod] [TestCategory("DynamoDBv2")] @@ -663,7 +745,7 @@ public async Task TestContext_TransactWriteAndLoad_WithLocalSecondaryIndexRangeK }; var transactWrite = Context.CreateTransactWrite(); - transactWrite.AddSaveItems(new[] { model}); + transactWrite.AddSaveItems(new[] { model }); await transactWrite.ExecuteAsync(); var storedModel = await Context.LoadAsync(model.Id); @@ -674,7 +756,8 @@ public async Task TestContext_TransactWriteAndLoad_WithLocalSecondaryIndexRangeK Assert.AreEqual(model.DictionaryClasses.Count, myStoredModel.DictionaryClasses.Count); Assert.AreEqual(model.DictionaryClasses["A"].GetType(), myStoredModel.DictionaryClasses["A"].GetType()); Assert.AreEqual(model.DictionaryClasses["B"].GetType(), myStoredModel.DictionaryClasses["B"].GetType()); - Assert.AreEqual(((B)model.DictionaryClasses["B"]).MyPropB, ((B)myStoredModel.DictionaryClasses["B"]).MyPropB); + Assert.AreEqual(((B)model.DictionaryClasses["B"]).MyPropB, + ((B)myStoredModel.DictionaryClasses["B"]).MyPropB); Assert.AreEqual(model.ManagerName, myStoredModel.ManagerName); } @@ -754,7 +837,7 @@ public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() var model1 = new ModelA2 { Id = Guid.NewGuid(), - MyType = new C { Name = "AType1", MyPropA = 5, MyPropC = "test"}, + MyType = new C { Name = "AType1", MyPropA = 5, MyPropC = "test" }, DictionaryClasses = new Dictionary { { "A", new A { Name = "A1", MyPropA = 1 } }, @@ -795,7 +878,8 @@ public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() Assert.AreEqual(model1.DictionaryClasses.Count, storedModel.DictionaryClasses.Count); Assert.AreEqual(model1.DictionaryClasses["A"].GetType(), storedModel.DictionaryClasses["A"].GetType()); Assert.AreEqual(model1.DictionaryClasses["B"].GetType(), storedModel.DictionaryClasses["B"].GetType()); - Assert.AreEqual(((B)model1.DictionaryClasses["B"]).MyPropB, ((B)storedModel.DictionaryClasses["B"]).MyPropB); + Assert.AreEqual(((B)model1.DictionaryClasses["B"]).MyPropB, + ((B)storedModel.DictionaryClasses["B"]).MyPropB); Assert.AreEqual(model1.ManagerName, storedModel.ManagerName); } @@ -807,7 +891,8 @@ public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() [TestCategory("DynamoDBv2")] public void TestWithBuilderTables() { - foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) + foreach (var conversion in new DynamoDBEntryConversion[] + { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) { // Cleanup existing data in the tables CleanupTables(); @@ -825,21 +910,23 @@ public void TestWithBuilderTables() #pragma warning restore CS0618 // Re-enable the warning Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-HashRangeTable") - .AddHashKey("Name", DynamoDBEntryType.String) - .AddRangeKey("Age", DynamoDBEntryType.Numeric) - .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Score", DynamoDBEntryType.Numeric) - .AddLocalSecondaryIndex("LocalIndex", "Manager", DynamoDBEntryType.String) - .Build()); + .AddHashKey("Name", DynamoDBEntryType.String) + .AddRangeKey("Age", DynamoDBEntryType.Numeric) + .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Score", + DynamoDBEntryType.Numeric) + .AddLocalSecondaryIndex("LocalIndex", "Manager", DynamoDBEntryType.String) + .Build()); Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-HashTable") - .AddHashKey("Id", DynamoDBEntryType.Numeric) - .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Price", DynamoDBEntryType.Numeric) - .Build()); + .AddHashKey("Id", DynamoDBEntryType.Numeric) + .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Price", + DynamoDBEntryType.Numeric) + .Build()); Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-NumericHashRangeTable") - .AddHashKey("CreationTime", DynamoDBEntryType.Numeric) - .AddRangeKey("Name", DynamoDBEntryType.String) - .Build()); + .AddHashKey("CreationTime", DynamoDBEntryType.Numeric) + .AddRangeKey("Name", DynamoDBEntryType.String) + .Build()); TestEmptyStringsWithFeatureEnabled(); @@ -869,7 +956,8 @@ public void TestWithBuilderTables() [TestCategory("DynamoDBv2")] public void TestWithBuilderContext() { - foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) + foreach (var conversion in new DynamoDBEntryConversion[] + { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) { // Cleanup existing data in the tables CleanupTables(); @@ -922,7 +1010,7 @@ private static void TestEmptyStringsWithFeatureEnabled() Name = string.Empty, AllProducts = new List { - new Product {Id = 12, Name = string.Empty} + new Product { Id = 12, Name = string.Empty } }, }, Components = new List // SS @@ -1057,7 +1145,7 @@ private static void TestAnnotatedUnsupportedTypes() } private void TestContextConversions() - { + { var conversionV1 = DynamoDBEntryConversion.V1; var conversionV2 = DynamoDBEntryConversion.V2; @@ -1094,8 +1182,10 @@ private void TestContextConversions() { #pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors - using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) - using (var contextV2 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV2 })) + using (var contextV1 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + using (var contextV2 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV2 })) { var docV1 = contextV1.ToDocument(product); var docV2 = contextV2.ToDocument(product); @@ -1106,7 +1196,8 @@ private void TestContextConversions() { #pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors - using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + using (var contextV1 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) { contextV1.Save(product); contextV1.Save(product, new SaveConfig { Conversion = conversionV2 }); @@ -1141,8 +1232,9 @@ private void TestContextConversions() Revenue = 9001 } }; - - using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + + using (var contextV1 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) { var docV1 = contextV1.ToDocument(productV2, new ToDocumentConfig { Conversion = conversionV1 }); var docV2 = contextV1.ToDocument(productV2, new ToDocumentConfig { }); @@ -1163,8 +1255,12 @@ private void TestContextConversions() MostPopularProduct = product }; AssertExtensions.ExpectException(() => Context.ToDocument(product), typeof(InvalidOperationException)); - AssertExtensions.ExpectException(() => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV1 }), typeof(InvalidOperationException)); - AssertExtensions.ExpectException(() => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV2 }), typeof(InvalidOperationException)); + AssertExtensions.ExpectException( + () => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV1 }), + typeof(InvalidOperationException)); + AssertExtensions.ExpectException( + () => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV2 }), + typeof(InvalidOperationException)); // Remove circular dependence product.CompanyInfo.MostPopularProduct = new Product @@ -1191,8 +1287,10 @@ private void TestContextConversions() // Add circular references docV1["CompanyInfo"].AsDocument()["MostPopularProduct"] = docV1; docV2["CompanyInfo"].AsDocument()["MostPopularProduct"] = docV2; - AssertExtensions.ExpectException(() => Context.FromDocument(docV1, new FromDocumentConfig { Conversion = conversionV1 })); - AssertExtensions.ExpectException(() => Context.FromDocument(docV2, new FromDocumentConfig { Conversion = conversionV2 })); + AssertExtensions.ExpectException(() => + Context.FromDocument(docV1, new FromDocumentConfig { Conversion = conversionV1 })); + AssertExtensions.ExpectException(() => + Context.FromDocument(docV2, new FromDocumentConfig { Conversion = conversionV2 })); // Remove circular references docV1["CompanyInfo"].AsDocument()["MostPopularProduct"] = null; @@ -1282,9 +1380,11 @@ private void TestEmptyCollections(DynamoDBEntryConversion conversion) Assert.IsNotNull(retrieved.Components); Assert.AreEqual(0, retrieved.Components.Count); } + Assert.IsNotNull(retrieved.Map); Assert.AreEqual(0, retrieved.Map.Count); } + private void TestEnumHashKeyObjects() { // Create and save item @@ -1312,6 +1412,7 @@ private void TestEnumHashKeyObjects() Context.Delete(product1); Context.Delete(product2); } + private void TestHashObjects() { string bucketName = "aws-sdk-net-s3link-" + DateTime.UtcNow.Ticks; @@ -1380,8 +1481,10 @@ private void TestHashObjects() } }; - product.FullProductDescription = S3Link.Create(Context, bucketName, "my-product", Amazon.RegionEndpoint.USEast1); - product.FullProductDescription.UploadStream(new MemoryStream(UTF8Encoding.UTF8.GetBytes("Lots of data"))); + product.FullProductDescription = + S3Link.Create(Context, bucketName, "my-product", Amazon.RegionEndpoint.USEast1); + product.FullProductDescription.UploadStream( + new MemoryStream(UTF8Encoding.UTF8.GetBytes("Lots of data"))); Context.Save(product); @@ -1418,14 +1521,18 @@ private void TestHashObjects() Assert.AreEqual(product.CompanyInfo.AllProducts.Count, retrieved.CompanyInfo.AllProducts.Count); Assert.AreEqual(product.CompanyInfo.AllProducts[0].Id, retrieved.CompanyInfo.AllProducts[0].Id); Assert.AreEqual(product.CompanyInfo.AllProducts[1].Id, retrieved.CompanyInfo.AllProducts[1].Id); - Assert.AreEqual(product.CompanyInfo.FeaturedProducts.Length, retrieved.CompanyInfo.FeaturedProducts.Length); - Assert.AreEqual(product.CompanyInfo.FeaturedProducts[0].Id, retrieved.CompanyInfo.FeaturedProducts[0].Id); - Assert.AreEqual(product.CompanyInfo.FeaturedProducts[1].Id, retrieved.CompanyInfo.FeaturedProducts[1].Id); + Assert.AreEqual(product.CompanyInfo.FeaturedProducts.Length, + retrieved.CompanyInfo.FeaturedProducts.Length); + Assert.AreEqual(product.CompanyInfo.FeaturedProducts[0].Id, + retrieved.CompanyInfo.FeaturedProducts[0].Id); + Assert.AreEqual(product.CompanyInfo.FeaturedProducts[1].Id, + retrieved.CompanyInfo.FeaturedProducts[1].Id); Assert.AreEqual(product.CompanyInfo.FeaturedBrands.Length, retrieved.CompanyInfo.FeaturedBrands.Length); Assert.AreEqual(product.CompanyInfo.FeaturedBrands[0], retrieved.CompanyInfo.FeaturedBrands[0]); Assert.AreEqual(product.CompanyInfo.FeaturedBrands[1], retrieved.CompanyInfo.FeaturedBrands[1]); Assert.AreEqual(product.Map.Count, retrieved.Map.Count); - Assert.AreEqual(product.CompanyInfo.CompetitorProducts.Count, retrieved.CompanyInfo.CompetitorProducts.Count); + Assert.AreEqual(product.CompanyInfo.CompetitorProducts.Count, + retrieved.CompanyInfo.CompetitorProducts.Count); var productCloudsAreOkay = product.CompanyInfo.CompetitorProducts["CloudsAreOK"]; var retrievedCloudsAreOkay = retrieved.CompanyInfo.CompetitorProducts["CloudsAreOK"]; @@ -1483,6 +1590,7 @@ private void TestHashObjects() { productIds.Add(p.Id); } + Assert.AreEqual(2, productIds.Count); // Load first product @@ -1493,10 +1601,10 @@ private void TestHashObjects() // Query GlobalIndex products = Context.Query( - product.CompanyName, // Hash-key for the index is Company - QueryOperator.GreaterThan, // Range-key for the index is Price, so the - new object[] { 90 }, // condition is against a numerical value - new QueryConfig // Configure the index to use + product.CompanyName, // Hash-key for the index is Company + QueryOperator.GreaterThan, // Range-key for the index is Price, so the + new object[] { 90 }, // condition is against a numerical value + new QueryConfig // Configure the index to use { IndexName = "GlobalIndex", }); @@ -1504,10 +1612,10 @@ private void TestHashObjects() // Query GlobalIndex with an additional non-key condition products = Context.Query( - product.CompanyName, // Hash-key for the index is Company - QueryOperator.GreaterThan, // Range-key for the index is Price, so the - new object[] { 90 }, // condition is against a numerical value - new QueryConfig // Configure the index to use + product.CompanyName, // Hash-key for the index is Company + QueryOperator.GreaterThan, // Range-key for the index is Price, so the + new object[] { 90 }, // condition is against a numerical value + new QueryConfig // Configure the index to use { IndexName = "GlobalIndex", QueryFilter = new List @@ -1738,6 +1846,7 @@ private void TestBatchOperations() Name = productPrefix + i }); } + batchWrite1.AddPutItems(allEmployees); // Write both batches at once @@ -2682,11 +2791,9 @@ public class ProductV2 : Product [DynamoDBTable("HashTable")] public class Product { - [DynamoDBHashKey] - public int Id { get; set; } + [DynamoDBHashKey] public int Id { get; set; } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex", AttributeName = "Company")] public string CompanyName { get; set; } @@ -2696,8 +2803,7 @@ public class Product [DynamoDBGlobalSecondaryIndexRangeKey("GlobalIndex")] public int Price { get; set; } - [DynamoDBProperty("Tags")] - public HashSet TagSet { get; set; } + [DynamoDBProperty("Tags")] public HashSet TagSet { get; set; } public MemoryStream Data { get; set; } @@ -2710,8 +2816,7 @@ public class Product public Support? PreviousSupport { get; set; } - [DynamoDBIgnore] - public string InternalId { get; set; } + [DynamoDBIgnore] public string InternalId { get; set; } public bool IsPublic { get; set; } @@ -2741,8 +2846,7 @@ public class CompanyInfo public string[] FeaturedBrands { get; set; } public Dictionary> CompetitorProducts { get; set; } - [DynamoDBIgnore] - public decimal Revenue { get; set; } + [DynamoDBIgnore] public decimal Revenue { get; set; } } /// @@ -2751,8 +2855,7 @@ public class CompanyInfo /// public class VersionedProduct : Product { - [DynamoDBVersion] - public int? Version { get; set; } + [DynamoDBVersion] public int? Version { get; set; } } @@ -2763,8 +2866,7 @@ public class VersionedProduct : Product [DynamoDBTable("HashTable")] public class EnumProduct1 { - [DynamoDBIgnore] - public Status Id { get; set; } + [DynamoDBIgnore] public Status Id { get; set; } [DynamoDBHashKey("Id")] public int IdAsInt @@ -2773,8 +2875,7 @@ public int IdAsInt set { Id = (Status)value; } } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } } /// @@ -2786,8 +2887,7 @@ public class EnumProduct2 { public Status Id { get; set; } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } } @@ -2802,7 +2902,9 @@ public class Employee { // Hash key public virtual string Name { get; set; } + public string MiddleName { get; set; } + // Range key internal virtual int Age { get; set; } @@ -2823,12 +2925,10 @@ public class Employee public class AnnotatedEmployee : Employee { // Hash key - [DynamoDBHashKey] - public override string Name { get; set; } + [DynamoDBHashKey] public override string Name { get; set; } // Range key - [DynamoDBRangeKey] - internal override int Age { get; set; } + [DynamoDBRangeKey] internal override int Age { get; set; } [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex", AttributeName = "Company")] public override string CompanyName { get; set; } @@ -2846,12 +2946,10 @@ public class AnnotatedEmployee : Employee public class PartiallyAnnotatedEmployee : Employee { // Hash key - [DynamoDBHashKey] - public override string Name { get; set; } + [DynamoDBHashKey] public override string Name { get; set; } // Range key - [DynamoDBRangeKey] - internal override int Age { get; set; } + [DynamoDBRangeKey] internal override int Age { get; set; } [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex")] public override string CompanyName { get; set; } @@ -2906,7 +3004,8 @@ public class Employee5 : AnnotatedEmployee /// Empty type /// public class EmptyType - { } + { + } /// /// Class representing items in the table [TableNamePrefix]HashTable @@ -2917,14 +3016,23 @@ public class VersionedEmployee : Employee public int? Version { get; set; } } + public class CounterAnnotatedEmployee : AnnotatedEmployee + { + [DynamoDBAtomicCounter] + public int? CountDefault { get; set; } + + [DynamoDBAtomicCounter(2, 10)] + public int? CountAtomic { get; set; } + } + + /// /// Class representing items in the table [TableNamePrefix]HashTable /// This class uses optimistic locking via the Version field /// - public class VersionedAnnotatedEmployee : AnnotatedEmployee + public class VersionedAnnotatedEmployee : CounterAnnotatedEmployee { - [DynamoDBVersion] - public int? Version { get; set; } + [DynamoDBVersion] public int? Version { get; set; } } /// From 3a8374f3bf50951a665e000c87181f059542dc46 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Mon, 12 May 2025 16:21:30 +0300 Subject: [PATCH 03/11] wip --- .../DynamoDBv2/Custom/DataModel/Context.cs | 32 ++------ .../Custom/DataModel/ContextInternal.cs | 2 +- .../Custom/DataModel/TransactWrite.cs | 25 ------ .../DynamoDBv2/Custom/DocumentModel/Table.cs | 22 +++--- .../DynamoDBv2/Custom/DocumentModel/Util.cs | 76 +++++++++++++++++++ .../DocumentModel/_async/Table.Async.cs | 16 ++-- .../IntegrationTests/DataModelTests.cs | 56 +++----------- 7 files changed, 111 insertions(+), 118 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index a5fcce5c4fc6..655df3ddc3c1 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -378,8 +378,7 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr { table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig { - ReturnValues = ReturnValues.None, - UpdateExpression = updateExpression + ReturnValues = ReturnValues.None }); } else @@ -390,8 +389,7 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr var updateItemOperationConfig = new UpdateItemOperationConfig { ReturnValues = ReturnValues.None, - ConditionalExpression = versionExpression, - UpdateExpression = updateExpression + ConditionalExpression = versionExpression }; table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig); PopulateInstance(storage, value, flatConfig); @@ -419,11 +417,7 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig - { - ReturnValues = ReturnValues.None, - UpdateExpression = counterConditionExpression - }, cancellationToken).ConfigureAwait(false); + await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), null, counterConditionExpression, cancellationToken).ConfigureAwait(false); } else { @@ -431,30 +425,14 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); - //if (counterConditionExpression != null) - //{ - // versionExpression.ExpressionStatement += " \n" + counterConditionExpression.ExpressionStatement; - // versionExpression.ExpressionAttributeNames = - // versionExpression.ExpressionAttributeNames.Union(counterConditionExpression.ExpressionAttributeNames). - // ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); - - // if (versionExpression.ExpressionAttributeValues != null) - // { - // versionExpression.ExpressionAttributeValues = - // versionExpression.ExpressionAttributeValues.Union(counterConditionExpression.ExpressionAttributeValues). - // ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); - // } - //} - await table.UpdateHelperAsync( storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig { ReturnValues = ReturnValues.None, - ConditionalExpression = versionExpression, - UpdateExpression = counterConditionExpression - }, + ConditionalExpression = versionExpression + }, counterConditionExpression, cancellationToken) .ConfigureAwait(false); PopulateInstance(storage, value, flatConfig); diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 8dbf073d8a49..011406cde8f0 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -188,7 +188,7 @@ internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorag updateExpression.ExpressionAttributeValues[startValueName] = propertyStorage.CounterStartValue - propertyStorage.CounterDelta; } - updateExpression.ExpressionStatement = $" {asserts.Substring(0, asserts.Length - 2)}"; + updateExpression.ExpressionStatement = $"SET {asserts.Substring(0, asserts.Length - 2)}"; } return updateExpression; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 0441b4c8dcd6..09ef17840063 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs @@ -228,25 +228,6 @@ public void AddSaveItem(T item) Expression conditionExpression = CreateConditionExpressionForVersion(storage); SetNewVersion(storage); - - Expression counterConditionExpression = CreateConditionExpressionForCounter(storage); - - if (counterConditionExpression != null) - { - conditionExpression.ExpressionStatement += " \n" + counterConditionExpression.ExpressionStatement; - conditionExpression.ExpressionAttributeNames = - conditionExpression.ExpressionAttributeNames.Union(counterConditionExpression.ExpressionAttributeNames). - ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); - - if (conditionExpression.ExpressionAttributeValues != null) - { - conditionExpression.ExpressionAttributeValues = - conditionExpression.ExpressionAttributeValues.Union(counterConditionExpression.ExpressionAttributeValues). - ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value); - } - } - - SetAtomicCounters(storage); AddDocumentTransaction(storage, conditionExpression); @@ -458,12 +439,6 @@ private Expression CreateConditionExpressionForVersion(ItemStorage storage) return DynamoDBContext.CreateConditionExpressionForVersion(storage, conversionConfig); } - private Expression CreateConditionExpressionForCounter(ItemStorage storage) - { - return DynamoDBContext.CreateUpdateExpressionForCounterProperties(storage); - } - - private void AddDocumentTransaction(ItemStorage storage, Expression conditionExpression) { var hashKeyPropertyNames = storage.Config.HashKeyPropertyNames; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs index c8afb9b0cfdc..11b090a927a4 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs @@ -344,7 +344,7 @@ private static ScalarAttributeType PrimitiveToScalar(DynamoDBEntryType primitive case DynamoDBEntryType.Binary: return ScalarAttributeType.B; default: - throw new ArgumentOutOfRangeException(nameof(primitiveEntryType), $"{primitiveEntryType} is not a known DynamoDB {nameof(ScalarAttributeType)}"); ; + throw new ArgumentOutOfRangeException(nameof(primitiveEntryType), $"{primitiveEntryType} is not a known DynamoDB {nameof(ScalarAttributeType)}"); } } @@ -1345,10 +1345,10 @@ internal Document UpdateHelper(Document doc, Primitive hashKey, Primitive rangeK } #if AWS_ASYNC_API - internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config, CancellationToken cancellationToken) + internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config, Expression expression, CancellationToken cancellationToken) { Key key = (hashKey != null || rangeKey != null) ? MakeKey(hashKey, rangeKey) : MakeKey(doc); - return UpdateHelperAsync(doc, key, config, cancellationToken); + return UpdateHelperAsync(doc, key, config, expression, cancellationToken); } #endif @@ -1443,7 +1443,7 @@ internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig } #if AWS_ASYNC_API - internal async Task UpdateHelperAsync(Document doc, Key key, UpdateItemOperationConfig config, CancellationToken cancellationToken) + internal async Task UpdateHelperAsync(Document doc, Key key, UpdateItemOperationConfig config, Expression updateExpression, CancellationToken cancellationToken) { var currentConfig = config ?? new UpdateItemOperationConfig(); @@ -1467,6 +1467,7 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte this.UpdateRequestUserAgentDetails(req, isAsync: true); + //todo: add support for updateExpression ValidateConditional(currentConfig); if (currentConfig.Expected != null) @@ -1481,17 +1482,17 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } - else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) + else if (currentConfig.ConditionalExpression is { IsSet: true } || updateExpression is { IsSet: true }) { - currentConfig.ConditionalExpression.ApplyExpression(req, this); + currentConfig.ConditionalExpression?.ApplyExpression(req, this); string statement; Dictionary expressionAttributeValues; Dictionary expressionAttributeNames; - Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, out statement, out expressionAttributeValues, out expressionAttributeNames); + + Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, updateExpression,this, out statement, out expressionAttributeValues, out expressionAttributeNames); req.AttributeUpdates = null; - // req.ConditionExpression = statement req.UpdateExpression = statement; if (req.ExpressionAttributeValues == null) @@ -1511,11 +1512,6 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte } } - if (currentConfig.UpdateExpression is { IsSet: true }) - { - currentConfig.UpdateExpression.ApplyUpdateExpression(req, this); - } - var resp = await DDBClient.UpdateItemAsync(req, cancellationToken).ConfigureAwait(false); var returnedAttributes = resp.Attributes; doc.CommitChanges(); diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs index 4c3468cf45b7..5cd2f321df9f 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using Amazon.DynamoDBv2.Model; @@ -388,6 +389,80 @@ public static void ConvertAttributeUpdatesToUpdateExpression(Dictionary attributesToUpdates, Expression updateExpression, + Table table, + out string statement, + out Dictionary expressionAttributeValues, + out Dictionary expressionAttributes) + { + expressionAttributeValues = new Dictionary(StringComparer.Ordinal); + expressionAttributes = new Dictionary(StringComparer.Ordinal); + + if (updateExpression != null) + { + expressionAttributeValues = Expression.ConvertToAttributeValues(updateExpression.ExpressionAttributeValues,table); + expressionAttributes=updateExpression.ExpressionAttributeNames; + } + + var attributeNames = expressionAttributes.Select(pair => pair.Value).ToList(); + + // Build an expression string with a SET clause for the added/modified attributes and + // REMOVE clause for the attributes set to null. + int attributeCount = 0; + StringBuilder sets = new StringBuilder(); + StringBuilder removes = new StringBuilder(); + foreach (var kvp in attributesToUpdates) + { + var attribute = kvp.Key; + if (!attributeNames.Contains(attribute)) + { + var update = kvp.Value; + + string variableName = GetVariableName(ref attributeCount); + var attributeReference = GetAttributeReference(variableName); + var attributeValueReference = GetAttributeValueReference(variableName); + + if (update.Action == AttributeAction.DELETE) + { + if (removes.Length > 0) + removes.Append(", "); + removes.Append(attributeReference); + } + else + { + if (sets.Length > 0) + sets.Append(", "); + sets.AppendFormat("{0} = {1}", attributeReference, attributeValueReference); + + // Add the attribute value for the variable in the added in the expression + expressionAttributeValues.Add(attributeValueReference, update.Value); + } + + // Add the attribute name for the variable in the added in the expression + expressionAttributes.Add(attributeReference, attribute); + } + } + + // Combine the SET and REMOVE clause + StringBuilder statementBuilder = new StringBuilder(); + if (sets.Length > 0) + { + var setStatement= updateExpression!=null ? updateExpression.ExpressionStatement + "," : "SET"; + statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0} {1}", setStatement, sets.ToString()); + } + if (removes.Length > 0) + { + if (sets.Length > 0) + statementBuilder.Append(" "); + + statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "REMOVE {0}", removes.ToString()); + } + + statement = statementBuilder.ToString(); + } + public static void ConvertAttributesToGetToProjectionExpression(QueryRequest request) { if (request.IsSetAttributesToGet() && @@ -568,3 +643,4 @@ private static void WriteNextKey(Dictionary nextKey, Utf } } } + \ No newline at end of file diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs index 63b37ad4165b..8b8abf7110ad 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs @@ -367,7 +367,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, null, null, null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, null, null, null, null, cancellationToken).ConfigureAwait(false); } } @@ -377,7 +377,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, null, null, config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, null, null, config, null,cancellationToken).ConfigureAwait(false); } } @@ -387,7 +387,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, MakeKey(key), null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, MakeKey(key), null, null, cancellationToken).ConfigureAwait(false); } } @@ -397,7 +397,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, MakeKey(key), config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, MakeKey(key), config, null, cancellationToken).ConfigureAwait(false); } } @@ -407,7 +407,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, null, null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, null, null, null, cancellationToken).ConfigureAwait(false); } } @@ -417,7 +417,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, null, config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, null, config, null, cancellationToken).ConfigureAwait(false); } } @@ -427,7 +427,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, rangeKey, null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, rangeKey, null, null, cancellationToken).ConfigureAwait(false); } } @@ -437,7 +437,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, rangeKey, config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, rangeKey, config, null, cancellationToken).ConfigureAwait(false); } } diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index e7da5f057c2c..e6cc597fe632 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -665,7 +665,7 @@ public async Task TestContext_AtomicCounterAnnotation() CleanupTables(); TableCache.Clear(); - CounterAnnotatedEmployee employee = new CounterAnnotatedEmployee + VersionedAnnotatedEmployee employee = new VersionedAnnotatedEmployee { Name = "Mark", Age = 31, @@ -674,55 +674,23 @@ public async Task TestContext_AtomicCounterAnnotation() }; await Context.SaveAsync(employee); - var storedEmployee = await Context.LoadAsync(employee.Name, 31); + var storedEmployee = await Context.LoadAsync(employee.Name, 31); Assert.IsNotNull(storedEmployee); Assert.AreEqual(employee.Name, storedEmployee.Name); // Assert.AreEqual(0, storedEmployee.Version); Assert.AreEqual(0, storedEmployee.CountDefault); Assert.AreEqual(10, storedEmployee.CountAtomic); - //// Update the employee - //storedEmployee.ManagerName = "Helena"; - - //await Context.SaveAsync(storedEmployee); - //var storedUpdatedEmployee = await Context.LoadAsync(storedEmployee.Name, 31); - //Assert.IsNotNull(storedUpdatedEmployee); - //Assert.AreEqual(employee.Name, storedUpdatedEmployee.Name); - //Assert.AreEqual(1, storedUpdatedEmployee.Version); - //Assert.AreEqual(1, storedUpdatedEmployee.CountDefault); - //Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); - - - //var batchWrite = Context.CreateBatchWrite(); - //batchWrite.AddPutItem(new CounterAnnotatedEmployee - //{ - // Name = "Helena", - // Age = 25, - // Score = 140 - //}); - - //await batchWrite.ExecuteAsync(); - //var storedEmployee2 = await Context.LoadAsync("Helena"); - //Assert.IsNotNull(storedEmployee2); - //Assert.AreEqual("Helena", storedEmployee2.Name); - //Assert.IsNull(storedEmployee2.CountDefault); - //Assert.IsNull(storedEmployee2.CountAtomic); - - - - VersionedAnnotatedEmployee model = new VersionedAnnotatedEmployee - { - Name = "Mark", - Age = 31, - Score = 120, - ManagerName = "Harmony" - }; - - var transactWrite = Context.CreateTransactWrite(); - transactWrite.AddSaveItems(new[] { model }); - await transactWrite.ExecuteAsync(); - var storedEmployee2 = await Context.LoadAsync("Mark",31); + // Update the employee + storedEmployee.ManagerName = "Helena"; + await Context.SaveAsync(storedEmployee); + var storedUpdatedEmployee = await Context.LoadAsync(storedEmployee.Name, 31); + Assert.IsNotNull(storedUpdatedEmployee); + Assert.AreEqual(employee.Name, storedUpdatedEmployee.Name); + Assert.AreEqual(1, storedUpdatedEmployee.Version); + Assert.AreEqual(1, storedUpdatedEmployee.CountDefault); + Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); } [TestMethod] @@ -964,7 +932,7 @@ public void TestWithBuilderContext() // Clear existing SDK-wide cache TableCache.Clear(); - + Context = new DynamoDBContextBuilder() .ConfigureContext(x => { From 16cbd539fef1b99257725fe6a4a2309c0a9a9572 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Tue, 6 May 2025 17:25:57 +0300 Subject: [PATCH 04/11] add new atribute and update internal config add new atribute and update internal config --- .../DynamoDBv2/Custom/DataModel/Attributes.cs | 79 ++++++ .../DynamoDBv2/Custom/DataModel/Context.cs | 33 ++- .../Custom/DataModel/ContextInternal.cs | 88 +++++- .../Custom/DataModel/InternalModel.cs | 20 +- .../Custom/DataModel/TransactWrite.cs | 12 +- .../DynamoDBv2/Custom/DataModel/Utils.cs | 2 +- .../Custom/DocumentModel/Expression.cs | 47 ++++ .../DynamoDBv2/Custom/DocumentModel/Table.cs | 16 +- .../DynamoDBv2/Custom/DocumentModel/Util.cs | 76 ++++++ .../DocumentModel/_async/Table.Async.cs | 16 +- .../IntegrationTests/DataModelTests.cs | 254 ++++++++++++------ 11 files changed, 525 insertions(+), 118 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs index 5d3affa53f91..030a1e7c0a06 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs @@ -254,6 +254,85 @@ public DynamoDBVersionAttribute(string attributeName) } } + /// + /// Marks a property or field as an atomic counter in DynamoDB. + /// + /// This attribute indicates that the associated property or field should be treated as an atomic counter, + /// which can be incremented or decremented directly in DynamoDB during update operations. + /// It is useful for scenarios where you need to maintain a counter that is updated concurrently by multiple clients + /// without conflicts. + /// + /// The attribute also allows specifying an alternate attribute name in DynamoDB using the `AttributeName` property, + /// as well as configuring the increment or decrement value (`Delta`) and the starting value (`StartValue`). + /// + /// + /// Example usage: + /// + /// public class Example + /// { + /// [DynamoDBAtomicCounter] + /// public long Counter { get; set; } + /// + /// [DynamoDBAtomicCounter("CustomCounterName", delta: 5, startValue: 100)] + /// public long CustomCounter { get; set; } + /// } + /// + /// In this example: + /// - `Counter` will be treated as an atomic counter with the same name in DynamoDB. + /// - `CustomCounter` will be treated as an atomic counter with the attribute name "CustomCounterName" in DynamoDB, + /// incremented by 5 for each update, and starting with an initial value of 100. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class DynamoDBAtomicCounterAttribute : DynamoDBRenamableAttribute + { + /// + /// The value to increment (positive) or decrement (negative) the counter with for each update. + /// + public long Delta { get; } + + /// + /// The starting value of the counter. + /// + public long StartValue { get; } + + /// + /// Default constructor + /// + public DynamoDBAtomicCounterAttribute() + : base() + { + Delta = 1; + StartValue = 0; + } + + /// + /// Constructor that specifies an alternate attribute name + /// + /// + /// Name of attribute to be associated with property or field. + /// + /// The value to increment (positive) or decrement (negative) the counter with for each update. + /// The starting value of the counter. + public DynamoDBAtomicCounterAttribute(string attributeName, long delta, long startValue) + : base(attributeName) + { + Delta = delta; + StartValue = startValue; + } + + /// + /// Constructor that specifies an alternate attribute name + /// + /// The value to increment (positive) or decrement (negative) the counter with for each update. + /// The starting value of the counter. + public DynamoDBAtomicCounterAttribute(long delta, long startValue) + : base() + { + Delta = delta; + StartValue = startValue; + } + } + /// /// DynamoDB property attribute. diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 37e886672e3a..655df3ddc3c1 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -16,6 +16,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; using System.Threading; #if AWS_ASYNC_API using System.Threading.Tasks; @@ -23,6 +25,7 @@ #endif using Amazon.DynamoDBv2.DocumentModel; using ThirdParty.RuntimeBackports; +using Expression = Amazon.DynamoDBv2.DocumentModel.Expression; namespace Amazon.DynamoDBv2.DataModel { @@ -369,18 +372,24 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); + var updateExpression = CreateUpdateExpressionForCounterProperties(storage); + SetAtomicCounters(storage); if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), null); + table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig + { + ReturnValues = ReturnValues.None + }); } else { - Document expectedDocument = CreateExpectedDocumentForVersion(storage); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); var updateItemOperationConfig = new UpdateItemOperationConfig { - Expected = expectedDocument, ReturnValues = ReturnValues.None, + ConditionalExpression = versionExpression }; table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig); PopulateInstance(storage, value, flatConfig); @@ -401,21 +410,31 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); + + var counterConditionExpression = CreateUpdateExpressionForCounterProperties(storage); + SetAtomicCounters(storage); if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), null, cancellationToken).ConfigureAwait(false); + await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), null, counterConditionExpression, cancellationToken).ConfigureAwait(false); } else { - Document expectedDocument = CreateExpectedDocumentForVersion(storage); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); + await table.UpdateHelperAsync( storage.Document, table.MakeKey(storage.Document), - new UpdateItemOperationConfig { Expected = expectedDocument, ReturnValues = ReturnValues.None }, - cancellationToken).ConfigureAwait(false); + new UpdateItemOperationConfig + { + ReturnValues = ReturnValues.None, + ConditionalExpression = versionExpression + }, counterConditionExpression, + cancellationToken) + .ConfigureAwait(false); PopulateInstance(storage, value, flatConfig); } } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 806354cc9a68..011406cde8f0 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -70,6 +70,7 @@ private static void IncrementVersion(Type memberType, ref Primitive version) else if (memberType.IsAssignableFrom(typeof(short))) version = version.AsShort() + 1; else if (memberType.IsAssignableFrom(typeof(ushort))) version = version.AsUShort() + 1; } + private static Document CreateExpectedDocumentForVersion(ItemStorage storage) { Document document = new Document(); @@ -117,6 +118,84 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora #endregion + #region Atomic counters + + + internal static void SetAtomicCounters(ItemStorage storage) + { + + var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. + Where(propertyStorage => propertyStorage.IsCounter).ToList(); + + if (counterProperties.Count==0) return; + // Set the initial value of the counter properties + foreach (var propertyStorage in counterProperties) + { + Primitive counter; + string versionAttributeName = propertyStorage.AttributeName; + + if (storage.Document.TryGetValue(versionAttributeName, out var counterEntry)) + counter = counterEntry as Primitive; + else + counter = null; + + if (counter != null && counter.Value != null) + { + if (counter.Type != DynamoDBEntryType.Numeric) throw new InvalidOperationException("Atomic Counter property must be numeric"); + IncrementCounter(propertyStorage.MemberType, ref counter, propertyStorage.CounterDelta); + } + else + { + counter = new Primitive(propertyStorage.CounterStartValue.ToString(), true); + } + storage.Document[versionAttributeName] = counter; + } + + } + + + private static void IncrementCounter(Type memberType, ref Primitive counter,long delta) + { + if (memberType.IsAssignableFrom(typeof(Byte))) counter = counter.AsByte() + delta; + else if (memberType.IsAssignableFrom(typeof(SByte))) counter = counter.AsSByte() + delta; + else if (memberType.IsAssignableFrom(typeof(int))) counter = counter.AsInt() + delta; + else if (memberType.IsAssignableFrom(typeof(uint))) counter = counter.AsUInt() + delta; + else if (memberType.IsAssignableFrom(typeof(long))) counter = counter.AsLong() + delta; + //else if (memberType.IsAssignableFrom(typeof(ulong))) counter = counter.AsULong() + delta; + else if (memberType.IsAssignableFrom(typeof(short))) counter = counter.AsShort() + delta; + else if (memberType.IsAssignableFrom(typeof(ushort))) counter = counter.AsUShort() + delta; + } + + internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorage storage) + { + Expression updateExpression = null; + var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. + Where(propertyStorage => propertyStorage.IsCounter).ToList(); + + if (counterProperties.Count != 0) + { + updateExpression = new Expression(); + var asserts = string.Empty; + foreach (var propertyStorage in counterProperties) + { + string startValueName = $":s_{propertyStorage.AttributeName}"; + string deltaValueName = $":d_{propertyStorage.AttributeName}"; + string counterAttributeName = Common.GetAttributeReference(propertyStorage.AttributeName); + asserts += $"{counterAttributeName} = " + + $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; + updateExpression.ExpressionAttributeNames[counterAttributeName] = propertyStorage.AttributeName; + updateExpression.ExpressionAttributeValues[deltaValueName] = propertyStorage.CounterDelta; + updateExpression.ExpressionAttributeValues[startValueName] = + propertyStorage.CounterStartValue - propertyStorage.CounterDelta; + } + updateExpression.ExpressionStatement = $"SET {asserts.Substring(0, asserts.Length - 2)}"; + } + + return updateExpression; + } + + #endregion + #region Table methods // Retrieves the target table for the specified type @@ -392,7 +471,6 @@ private void PopulateInstance(ItemStorage storage, object instance, DynamoDBFlat { foreach (PropertyStorage propertyStorage in storageConfig.AllPropertyStorage) { - string propertyName = propertyStorage.PropertyName; string attributeName = propertyStorage.AttributeName; DynamoDBEntry entry; @@ -466,8 +544,9 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl { // if only keys are being serialized, skip non-key properties // still include version, however, to populate the storage.CurrentVersion field + // and include counter, to populate the storage.CurrentCount field if (keysOnly && !propertyStorage.IsHashKey && !propertyStorage.IsRangeKey && - !propertyStorage.IsVersion) continue; + !propertyStorage.IsVersion && !propertyStorage.IsCounter) continue; string propertyName = propertyStorage.PropertyName; string attributeName = propertyStorage.AttributeName; @@ -481,11 +560,12 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl { Primitive dbePrimitive = dbe as Primitive; if (propertyStorage.IsHashKey || propertyStorage.IsRangeKey || - propertyStorage.IsVersion || propertyStorage.IsLSIRangeKey) + propertyStorage.IsVersion || propertyStorage.IsLSIRangeKey || + propertyStorage.IsCounter) { if (dbe != null && dbePrimitive == null) throw new InvalidOperationException("Property " + propertyName + - " is a hash key, range key or version property and must be Primitive"); + " is a hash key, range key, atomic counter or version property and must be Primitive"); } document[attributeName] = dbe; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index eb1085ff9570..eb153b3abb02 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -149,6 +149,12 @@ internal class PropertyStorage : SimplePropertyStorage // corresponding IndexNames, if applicable public List IndexNames { get; set; } + public bool IsCounter { get; set; } + + public long CounterDelta { get; set; } + + public long CounterStartValue { get; set; } + public void AddIndex(DynamoDBGlobalSecondaryIndexHashKeyAttribute gsiHashKey) { AddIndex(new GSI(true, gsiHashKey.AttributeName, gsiHashKey.IndexNames)); @@ -209,7 +215,10 @@ public GSI(bool isHashKey, string attributeName, params string[] indexNames) public void Validate(DynamoDBContext context) { if (IsVersion) - Utils.ValidateVersionType(MemberType); // no conversion is possible, so type must be a nullable primitive + Utils.ValidateNumericType(MemberType); // no conversion is possible, so type must be a nullable primitive + + if (IsCounter) + Utils.ValidateNumericType(MemberType); // no conversion is possible, so type must be a nullable primitive if (IsHashKey && IsRangeKey) throw new InvalidOperationException("Property " + PropertyName + " cannot be both hash and range key"); @@ -958,6 +967,14 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con if (attribute is DynamoDBVersionAttribute) propertyStorage.IsVersion = true; + DynamoDBAtomicCounterAttribute counterAttribute = attribute as DynamoDBAtomicCounterAttribute; + if (counterAttribute != null) + { + propertyStorage.IsCounter = true; + propertyStorage.CounterDelta = counterAttribute.Delta; + propertyStorage.CounterStartValue = counterAttribute.StartValue; + } + DynamoDBRenamableAttribute renamableAttribute = attribute as DynamoDBRenamableAttribute; if (renamableAttribute != null && !string.IsNullOrEmpty(renamableAttribute.AttributeName)) { @@ -1109,6 +1126,7 @@ private static void PopulateConfigFromMappings(ItemStorageConfig config, Diction propertyStorage.ConverterType = propertyConfig.Converter; propertyStorage.IsIgnored = propertyConfig.Ignore; propertyStorage.IsVersion = propertyConfig.Version; + //propertyStorage.IsCounter = propertyConfig.Counter; propertyStorage.StoreAsEpoch = propertyConfig.StoreAsEpoch; propertyStorage.StoreAsEpochLong = propertyConfig.StoreAsEpochLong; } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 95a9f39a90d9..09ef17840063 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs @@ -225,6 +225,7 @@ public void AddSaveItem(T item) ItemStorage storage = _context.ObjectToItemStorageHelper(item, _storageConfig, _config, keysOnly: false, _config.IgnoreNullValues ?? false); if (storage == null) return; + Expression conditionExpression = CreateConditionExpressionForVersion(storage); SetNewVersion(storage); @@ -408,6 +409,11 @@ private bool ShouldUseVersioning() return !skipVersionCheck && _storageConfig.HasVersion; } + private bool ShouldUseCounter() + { + return false; //_storageConfig.; + } + private void CheckUseVersioning() { if (_config.SkipVersionCheck == true) @@ -432,7 +438,6 @@ private Expression CreateConditionExpressionForVersion(ItemStorage storage) DocumentTransaction.TargetTable.IsEmptyStringValueEnabled); return DynamoDBContext.CreateConditionExpressionForVersion(storage, conversionConfig); } - private void AddDocumentTransaction(ItemStorage storage, Expression conditionExpression) { @@ -477,6 +482,11 @@ private void SetNewVersion(ItemStorage storage) if (!ShouldUseVersioning()) return; DynamoDBContext.SetNewVersion(storage); } + + private void SetAtomicCounters(ItemStorage storage) + { + DynamoDBContext.SetAtomicCounters(storage); + } } /// diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs index 95b50af1a440..29643d9d31f8 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs @@ -141,7 +141,7 @@ internal static void ValidatePrimitiveType() ValidatePrimitiveType(typeof(T)); } - internal static void ValidateVersionType(Type memberType) + internal static void ValidateNumericType(Type memberType) { if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Nullable<>) && (memberType.IsAssignableFrom(typeof(Byte)) || diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs index 9e144c6cf7da..66ccc845ae4b 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs @@ -127,6 +127,53 @@ internal void ApplyExpression(UpdateItemRequest request, Table table) } } + internal void ApplyUpdateExpression(UpdateItemRequest request, Table table) + { + request.UpdateExpression += $" {this.ExpressionStatement}"; + //todo make this work properly + //if (request.UpdateExpression!=null) + //{ + // int removeIndex = request.UpdateExpression.IndexOf(" REMOVE", StringComparison.OrdinalIgnoreCase); + // int setIndex = request.UpdateExpression.IndexOf("SET", StringComparison.OrdinalIgnoreCase); + // if (removeIndex >= 0) + // { + // string setPart = request.UpdateExpression.Substring(setIndex, removeIndex); + // setPart += $" ,{this.ExpressionStatement}"; + // string removePart = request.UpdateExpression.Substring(removeIndex); + // request.UpdateExpression = $"{setPart}{removePart}"; + // } + //} + + if (request.ExpressionAttributeNames == null) + { + if (this.ExpressionAttributeNames?.Count > 0) + { + request.ExpressionAttributeNames = new Dictionary(this.ExpressionAttributeNames); + } + } + else + { + foreach (var kvp in this.ExpressionAttributeNames) + request.ExpressionAttributeNames.Add(kvp.Key, kvp.Value); + } + + var attributeValues = ConvertToAttributeValues(this.ExpressionAttributeValues, table); + if (!(attributeValues?.Count > 0)) return; + + if (request.ExpressionAttributeValues == null) + { + if (this.ExpressionAttributeValues?.Count > 0) + { + request.ExpressionAttributeValues = attributeValues; + } + } + else + { + foreach (var kvp in attributeValues) + request.ExpressionAttributeValues.Add(kvp.Key, kvp.Value); + } + } + internal void ApplyExpression(Get request, Table table) { request.ProjectionExpression = ExpressionStatement; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs index f84ab45ad4fa..11b090a927a4 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs @@ -344,7 +344,7 @@ private static ScalarAttributeType PrimitiveToScalar(DynamoDBEntryType primitive case DynamoDBEntryType.Binary: return ScalarAttributeType.B; default: - throw new ArgumentOutOfRangeException(nameof(primitiveEntryType), $"{primitiveEntryType} is not a known DynamoDB {nameof(ScalarAttributeType)}"); ; + throw new ArgumentOutOfRangeException(nameof(primitiveEntryType), $"{primitiveEntryType} is not a known DynamoDB {nameof(ScalarAttributeType)}"); } } @@ -1345,10 +1345,10 @@ internal Document UpdateHelper(Document doc, Primitive hashKey, Primitive rangeK } #if AWS_ASYNC_API - internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config, CancellationToken cancellationToken) + internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config, Expression expression, CancellationToken cancellationToken) { Key key = (hashKey != null || rangeKey != null) ? MakeKey(hashKey, rangeKey) : MakeKey(doc); - return UpdateHelperAsync(doc, key, config, cancellationToken); + return UpdateHelperAsync(doc, key, config, expression, cancellationToken); } #endif @@ -1443,7 +1443,7 @@ internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig } #if AWS_ASYNC_API - internal async Task UpdateHelperAsync(Document doc, Key key, UpdateItemOperationConfig config, CancellationToken cancellationToken) + internal async Task UpdateHelperAsync(Document doc, Key key, UpdateItemOperationConfig config, Expression updateExpression, CancellationToken cancellationToken) { var currentConfig = config ?? new UpdateItemOperationConfig(); @@ -1467,6 +1467,7 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte this.UpdateRequestUserAgentDetails(req, isAsync: true); + //todo: add support for updateExpression ValidateConditional(currentConfig); if (currentConfig.Expected != null) @@ -1481,14 +1482,15 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } - else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) + else if (currentConfig.ConditionalExpression is { IsSet: true } || updateExpression is { IsSet: true }) { - currentConfig.ConditionalExpression.ApplyExpression(req, this); + currentConfig.ConditionalExpression?.ApplyExpression(req, this); string statement; Dictionary expressionAttributeValues; Dictionary expressionAttributeNames; - Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, out statement, out expressionAttributeValues, out expressionAttributeNames); + + Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, updateExpression,this, out statement, out expressionAttributeValues, out expressionAttributeNames); req.AttributeUpdates = null; req.UpdateExpression = statement; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs index 4c3468cf45b7..5cd2f321df9f 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using Amazon.DynamoDBv2.Model; @@ -388,6 +389,80 @@ public static void ConvertAttributeUpdatesToUpdateExpression(Dictionary attributesToUpdates, Expression updateExpression, + Table table, + out string statement, + out Dictionary expressionAttributeValues, + out Dictionary expressionAttributes) + { + expressionAttributeValues = new Dictionary(StringComparer.Ordinal); + expressionAttributes = new Dictionary(StringComparer.Ordinal); + + if (updateExpression != null) + { + expressionAttributeValues = Expression.ConvertToAttributeValues(updateExpression.ExpressionAttributeValues,table); + expressionAttributes=updateExpression.ExpressionAttributeNames; + } + + var attributeNames = expressionAttributes.Select(pair => pair.Value).ToList(); + + // Build an expression string with a SET clause for the added/modified attributes and + // REMOVE clause for the attributes set to null. + int attributeCount = 0; + StringBuilder sets = new StringBuilder(); + StringBuilder removes = new StringBuilder(); + foreach (var kvp in attributesToUpdates) + { + var attribute = kvp.Key; + if (!attributeNames.Contains(attribute)) + { + var update = kvp.Value; + + string variableName = GetVariableName(ref attributeCount); + var attributeReference = GetAttributeReference(variableName); + var attributeValueReference = GetAttributeValueReference(variableName); + + if (update.Action == AttributeAction.DELETE) + { + if (removes.Length > 0) + removes.Append(", "); + removes.Append(attributeReference); + } + else + { + if (sets.Length > 0) + sets.Append(", "); + sets.AppendFormat("{0} = {1}", attributeReference, attributeValueReference); + + // Add the attribute value for the variable in the added in the expression + expressionAttributeValues.Add(attributeValueReference, update.Value); + } + + // Add the attribute name for the variable in the added in the expression + expressionAttributes.Add(attributeReference, attribute); + } + } + + // Combine the SET and REMOVE clause + StringBuilder statementBuilder = new StringBuilder(); + if (sets.Length > 0) + { + var setStatement= updateExpression!=null ? updateExpression.ExpressionStatement + "," : "SET"; + statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0} {1}", setStatement, sets.ToString()); + } + if (removes.Length > 0) + { + if (sets.Length > 0) + statementBuilder.Append(" "); + + statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "REMOVE {0}", removes.ToString()); + } + + statement = statementBuilder.ToString(); + } + public static void ConvertAttributesToGetToProjectionExpression(QueryRequest request) { if (request.IsSetAttributesToGet() && @@ -568,3 +643,4 @@ private static void WriteNextKey(Dictionary nextKey, Utf } } } + \ No newline at end of file diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs index 63b37ad4165b..8b8abf7110ad 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs @@ -367,7 +367,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, null, null, null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, null, null, null, null, cancellationToken).ConfigureAwait(false); } } @@ -377,7 +377,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, null, null, config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, null, null, config, null,cancellationToken).ConfigureAwait(false); } } @@ -387,7 +387,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, MakeKey(key), null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, MakeKey(key), null, null, cancellationToken).ConfigureAwait(false); } } @@ -397,7 +397,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, MakeKey(key), config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, MakeKey(key), config, null, cancellationToken).ConfigureAwait(false); } } @@ -407,7 +407,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, null, null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, null, null, null, cancellationToken).ConfigureAwait(false); } } @@ -417,7 +417,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, null, config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, null, config, null, cancellationToken).ConfigureAwait(false); } } @@ -427,7 +427,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, rangeKey, null, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, rangeKey, null, null, cancellationToken).ConfigureAwait(false); } } @@ -437,7 +437,7 @@ public partial class Table : ITable var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return await UpdateHelperAsync(doc, hashKey, rangeKey, config, cancellationToken).ConfigureAwait(false); + return await UpdateHelperAsync(doc, hashKey, rangeKey, config, null, cancellationToken).ConfigureAwait(false); } } diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index 4d3e85ffa066..e6cc597fe632 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -23,7 +23,8 @@ public void TestContextWithEmptyStringEnabled() { // It is a known bug that this test currently fails due to an AOT-compilation // issue, on iOS using mono2x. - foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) + foreach (var conversion in new DynamoDBEntryConversion[] + { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) { TableCache.Clear(); @@ -210,7 +211,7 @@ public void TestTransactWrite_AddSaveItem_DocumentTransaction() TableCache.Clear(); CreateContext(DynamoDBEntryConversion.V2, true, true); - + { var hashRangeOnly = new AnnotatedRangeTable @@ -313,8 +314,11 @@ public void TestContext_RetrieveDateTimeInUtc(bool retrieveDateTimeInUtc) //This is a valid use of .ToLocalTime var expectedCurrTime = retrieveDateTimeInUtc ? currTime.ToUniversalTime() : currTime.ToLocalTime(); - var expectedLongEpochTime = retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); - var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc ? longEpochTimeBefore1970.ToUniversalTime() : longEpochTimeBefore1970.ToLocalTime(); + var expectedLongEpochTime = + retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); + var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc + ? longEpochTimeBefore1970.ToUniversalTime() + : longEpochTimeBefore1970.ToLocalTime(); // Load var storedEmployee = Context.Load(employee.CreationTime, employee.Name); @@ -335,7 +339,8 @@ public void TestContext_RetrieveDateTimeInUtc(bool retrieveDateTimeInUtc) // Query QueryFilter filter = new QueryFilter(); filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); - storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }).First(); + storedEmployee = Context + .FromQuery(new QueryOperationConfig { Filter = filter }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -440,7 +445,8 @@ public void TestContext_CustomDateTimeConverter(bool retrieveDateTimeInUtc) // Query QueryFilter filter = new QueryFilter(); filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); - storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }).First(); + storedEmployee = Context + .FromQuery(new QueryOperationConfig { Filter = filter }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -486,7 +492,8 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT TableCache.Clear(); #pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors - Context = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 }); + Context = new DynamoDBContext(Client, + new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 }); #pragma warning restore CS0618 // Re-enable the warning var operationConfig = new DynamoDBOperationConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }; @@ -510,11 +517,15 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT //This is a valid use of .ToLocalTime var expectedCurrTime = retrieveDateTimeInUtc ? currTime.ToUniversalTime() : currTime.ToLocalTime(); - var expectedLongEpochTime = retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); - var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc ? longEpochTimeBefore1970.ToUniversalTime() : longEpochTimeBefore1970.ToLocalTime(); + var expectedLongEpochTime = + retrieveDateTimeInUtc ? longEpochTime.ToUniversalTime() : longEpochTime.ToLocalTime(); + var expectedLongEpochTimeBefore1970 = retrieveDateTimeInUtc + ? longEpochTimeBefore1970.ToUniversalTime() + : longEpochTimeBefore1970.ToLocalTime(); // Load - var storedEmployee = Context.Load(employee.CreationTime, employee.Name, new LoadConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc}); + var storedEmployee = Context.Load(employee.CreationTime, employee.Name, + new LoadConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -529,8 +540,8 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT QueryFilter filter = new QueryFilter(); filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); storedEmployee = Context.FromQuery( - new QueryOperationConfig { Filter = filter }, - new FromQueryConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc}).First(); + new QueryOperationConfig { Filter = filter }, + new FromQueryConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); @@ -543,7 +554,7 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT // Scan storedEmployee = Context.Scan( - new List(), + new List(), new ScanConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }).First(); Assert.IsNotNull(storedEmployee); ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); @@ -619,13 +630,13 @@ public async Task TestContext_TransactWriteAndLoad_WithDerivedTypeItems() }, DictionaryClasses = new Dictionary() { - {"A", new A{ Name = "A1", MyPropA = 1 }}, - {"B", new B{ Name = "A1", MyPropA = 1, MyPropB = 2}} + { "A", new A { Name = "A1", MyPropA = 1 } }, + { "B", new B { Name = "A1", MyPropA = 1, MyPropB = 2 } } } }; var transactWrite = Context.CreateTransactWrite(); - transactWrite.AddSaveItems(new []{ model1 , model2}); + transactWrite.AddSaveItems(new[] { model1, model2 }); await transactWrite.ExecuteAsync(); var storedModel1 = await Context.LoadAsync(id); @@ -642,6 +653,45 @@ public async Task TestContext_TransactWriteAndLoad_WithDerivedTypeItems() } + /// + /// Tests that the DynamoDB operations can read and write items. + /// + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_AtomicCounterAnnotation() + { + TableCache.Clear(); + CleanupTables(); + TableCache.Clear(); + + VersionedAnnotatedEmployee employee = new VersionedAnnotatedEmployee + { + Name = "Mark", + Age = 31, + Score = 120, + ManagerName = "Harmony" + }; + + await Context.SaveAsync(employee); + var storedEmployee = await Context.LoadAsync(employee.Name, 31); + Assert.IsNotNull(storedEmployee); + Assert.AreEqual(employee.Name, storedEmployee.Name); + // Assert.AreEqual(0, storedEmployee.Version); + Assert.AreEqual(0, storedEmployee.CountDefault); + Assert.AreEqual(10, storedEmployee.CountAtomic); + + // Update the employee + storedEmployee.ManagerName = "Helena"; + + await Context.SaveAsync(storedEmployee); + var storedUpdatedEmployee = await Context.LoadAsync(storedEmployee.Name, 31); + Assert.IsNotNull(storedUpdatedEmployee); + Assert.AreEqual(employee.Name, storedUpdatedEmployee.Name); + Assert.AreEqual(1, storedUpdatedEmployee.Version); + Assert.AreEqual(1, storedUpdatedEmployee.CountDefault); + Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); + } [TestMethod] [TestCategory("DynamoDBv2")] @@ -663,7 +713,7 @@ public async Task TestContext_TransactWriteAndLoad_WithLocalSecondaryIndexRangeK }; var transactWrite = Context.CreateTransactWrite(); - transactWrite.AddSaveItems(new[] { model}); + transactWrite.AddSaveItems(new[] { model }); await transactWrite.ExecuteAsync(); var storedModel = await Context.LoadAsync(model.Id); @@ -674,7 +724,8 @@ public async Task TestContext_TransactWriteAndLoad_WithLocalSecondaryIndexRangeK Assert.AreEqual(model.DictionaryClasses.Count, myStoredModel.DictionaryClasses.Count); Assert.AreEqual(model.DictionaryClasses["A"].GetType(), myStoredModel.DictionaryClasses["A"].GetType()); Assert.AreEqual(model.DictionaryClasses["B"].GetType(), myStoredModel.DictionaryClasses["B"].GetType()); - Assert.AreEqual(((B)model.DictionaryClasses["B"]).MyPropB, ((B)myStoredModel.DictionaryClasses["B"]).MyPropB); + Assert.AreEqual(((B)model.DictionaryClasses["B"]).MyPropB, + ((B)myStoredModel.DictionaryClasses["B"]).MyPropB); Assert.AreEqual(model.ManagerName, myStoredModel.ManagerName); } @@ -754,7 +805,7 @@ public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() var model1 = new ModelA2 { Id = Guid.NewGuid(), - MyType = new C { Name = "AType1", MyPropA = 5, MyPropC = "test"}, + MyType = new C { Name = "AType1", MyPropA = 5, MyPropC = "test" }, DictionaryClasses = new Dictionary { { "A", new A { Name = "A1", MyPropA = 1 } }, @@ -795,7 +846,8 @@ public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() Assert.AreEqual(model1.DictionaryClasses.Count, storedModel.DictionaryClasses.Count); Assert.AreEqual(model1.DictionaryClasses["A"].GetType(), storedModel.DictionaryClasses["A"].GetType()); Assert.AreEqual(model1.DictionaryClasses["B"].GetType(), storedModel.DictionaryClasses["B"].GetType()); - Assert.AreEqual(((B)model1.DictionaryClasses["B"]).MyPropB, ((B)storedModel.DictionaryClasses["B"]).MyPropB); + Assert.AreEqual(((B)model1.DictionaryClasses["B"]).MyPropB, + ((B)storedModel.DictionaryClasses["B"]).MyPropB); Assert.AreEqual(model1.ManagerName, storedModel.ManagerName); } @@ -807,7 +859,8 @@ public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() [TestCategory("DynamoDBv2")] public void TestWithBuilderTables() { - foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) + foreach (var conversion in new DynamoDBEntryConversion[] + { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) { // Cleanup existing data in the tables CleanupTables(); @@ -825,21 +878,23 @@ public void TestWithBuilderTables() #pragma warning restore CS0618 // Re-enable the warning Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-HashRangeTable") - .AddHashKey("Name", DynamoDBEntryType.String) - .AddRangeKey("Age", DynamoDBEntryType.Numeric) - .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Score", DynamoDBEntryType.Numeric) - .AddLocalSecondaryIndex("LocalIndex", "Manager", DynamoDBEntryType.String) - .Build()); + .AddHashKey("Name", DynamoDBEntryType.String) + .AddRangeKey("Age", DynamoDBEntryType.Numeric) + .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Score", + DynamoDBEntryType.Numeric) + .AddLocalSecondaryIndex("LocalIndex", "Manager", DynamoDBEntryType.String) + .Build()); Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-HashTable") - .AddHashKey("Id", DynamoDBEntryType.Numeric) - .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Price", DynamoDBEntryType.Numeric) - .Build()); + .AddHashKey("Id", DynamoDBEntryType.Numeric) + .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Price", + DynamoDBEntryType.Numeric) + .Build()); Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-NumericHashRangeTable") - .AddHashKey("CreationTime", DynamoDBEntryType.Numeric) - .AddRangeKey("Name", DynamoDBEntryType.String) - .Build()); + .AddHashKey("CreationTime", DynamoDBEntryType.Numeric) + .AddRangeKey("Name", DynamoDBEntryType.String) + .Build()); TestEmptyStringsWithFeatureEnabled(); @@ -869,14 +924,15 @@ public void TestWithBuilderTables() [TestCategory("DynamoDBv2")] public void TestWithBuilderContext() { - foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) + foreach (var conversion in new DynamoDBEntryConversion[] + { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 }) { // Cleanup existing data in the tables CleanupTables(); // Clear existing SDK-wide cache TableCache.Clear(); - + Context = new DynamoDBContextBuilder() .ConfigureContext(x => { @@ -922,7 +978,7 @@ private static void TestEmptyStringsWithFeatureEnabled() Name = string.Empty, AllProducts = new List { - new Product {Id = 12, Name = string.Empty} + new Product { Id = 12, Name = string.Empty } }, }, Components = new List // SS @@ -1057,7 +1113,7 @@ private static void TestAnnotatedUnsupportedTypes() } private void TestContextConversions() - { + { var conversionV1 = DynamoDBEntryConversion.V1; var conversionV2 = DynamoDBEntryConversion.V2; @@ -1094,8 +1150,10 @@ private void TestContextConversions() { #pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors - using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) - using (var contextV2 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV2 })) + using (var contextV1 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + using (var contextV2 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV2 })) { var docV1 = contextV1.ToDocument(product); var docV2 = contextV2.ToDocument(product); @@ -1106,7 +1164,8 @@ private void TestContextConversions() { #pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors - using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + using (var contextV1 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) { contextV1.Save(product); contextV1.Save(product, new SaveConfig { Conversion = conversionV2 }); @@ -1141,8 +1200,9 @@ private void TestContextConversions() Revenue = 9001 } }; - - using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + + using (var contextV1 = + new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) { var docV1 = contextV1.ToDocument(productV2, new ToDocumentConfig { Conversion = conversionV1 }); var docV2 = contextV1.ToDocument(productV2, new ToDocumentConfig { }); @@ -1163,8 +1223,12 @@ private void TestContextConversions() MostPopularProduct = product }; AssertExtensions.ExpectException(() => Context.ToDocument(product), typeof(InvalidOperationException)); - AssertExtensions.ExpectException(() => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV1 }), typeof(InvalidOperationException)); - AssertExtensions.ExpectException(() => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV2 }), typeof(InvalidOperationException)); + AssertExtensions.ExpectException( + () => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV1 }), + typeof(InvalidOperationException)); + AssertExtensions.ExpectException( + () => Context.ToDocument(product, new ToDocumentConfig { Conversion = conversionV2 }), + typeof(InvalidOperationException)); // Remove circular dependence product.CompanyInfo.MostPopularProduct = new Product @@ -1191,8 +1255,10 @@ private void TestContextConversions() // Add circular references docV1["CompanyInfo"].AsDocument()["MostPopularProduct"] = docV1; docV2["CompanyInfo"].AsDocument()["MostPopularProduct"] = docV2; - AssertExtensions.ExpectException(() => Context.FromDocument(docV1, new FromDocumentConfig { Conversion = conversionV1 })); - AssertExtensions.ExpectException(() => Context.FromDocument(docV2, new FromDocumentConfig { Conversion = conversionV2 })); + AssertExtensions.ExpectException(() => + Context.FromDocument(docV1, new FromDocumentConfig { Conversion = conversionV1 })); + AssertExtensions.ExpectException(() => + Context.FromDocument(docV2, new FromDocumentConfig { Conversion = conversionV2 })); // Remove circular references docV1["CompanyInfo"].AsDocument()["MostPopularProduct"] = null; @@ -1282,9 +1348,11 @@ private void TestEmptyCollections(DynamoDBEntryConversion conversion) Assert.IsNotNull(retrieved.Components); Assert.AreEqual(0, retrieved.Components.Count); } + Assert.IsNotNull(retrieved.Map); Assert.AreEqual(0, retrieved.Map.Count); } + private void TestEnumHashKeyObjects() { // Create and save item @@ -1312,6 +1380,7 @@ private void TestEnumHashKeyObjects() Context.Delete(product1); Context.Delete(product2); } + private void TestHashObjects() { string bucketName = "aws-sdk-net-s3link-" + DateTime.UtcNow.Ticks; @@ -1380,8 +1449,10 @@ private void TestHashObjects() } }; - product.FullProductDescription = S3Link.Create(Context, bucketName, "my-product", Amazon.RegionEndpoint.USEast1); - product.FullProductDescription.UploadStream(new MemoryStream(UTF8Encoding.UTF8.GetBytes("Lots of data"))); + product.FullProductDescription = + S3Link.Create(Context, bucketName, "my-product", Amazon.RegionEndpoint.USEast1); + product.FullProductDescription.UploadStream( + new MemoryStream(UTF8Encoding.UTF8.GetBytes("Lots of data"))); Context.Save(product); @@ -1418,14 +1489,18 @@ private void TestHashObjects() Assert.AreEqual(product.CompanyInfo.AllProducts.Count, retrieved.CompanyInfo.AllProducts.Count); Assert.AreEqual(product.CompanyInfo.AllProducts[0].Id, retrieved.CompanyInfo.AllProducts[0].Id); Assert.AreEqual(product.CompanyInfo.AllProducts[1].Id, retrieved.CompanyInfo.AllProducts[1].Id); - Assert.AreEqual(product.CompanyInfo.FeaturedProducts.Length, retrieved.CompanyInfo.FeaturedProducts.Length); - Assert.AreEqual(product.CompanyInfo.FeaturedProducts[0].Id, retrieved.CompanyInfo.FeaturedProducts[0].Id); - Assert.AreEqual(product.CompanyInfo.FeaturedProducts[1].Id, retrieved.CompanyInfo.FeaturedProducts[1].Id); + Assert.AreEqual(product.CompanyInfo.FeaturedProducts.Length, + retrieved.CompanyInfo.FeaturedProducts.Length); + Assert.AreEqual(product.CompanyInfo.FeaturedProducts[0].Id, + retrieved.CompanyInfo.FeaturedProducts[0].Id); + Assert.AreEqual(product.CompanyInfo.FeaturedProducts[1].Id, + retrieved.CompanyInfo.FeaturedProducts[1].Id); Assert.AreEqual(product.CompanyInfo.FeaturedBrands.Length, retrieved.CompanyInfo.FeaturedBrands.Length); Assert.AreEqual(product.CompanyInfo.FeaturedBrands[0], retrieved.CompanyInfo.FeaturedBrands[0]); Assert.AreEqual(product.CompanyInfo.FeaturedBrands[1], retrieved.CompanyInfo.FeaturedBrands[1]); Assert.AreEqual(product.Map.Count, retrieved.Map.Count); - Assert.AreEqual(product.CompanyInfo.CompetitorProducts.Count, retrieved.CompanyInfo.CompetitorProducts.Count); + Assert.AreEqual(product.CompanyInfo.CompetitorProducts.Count, + retrieved.CompanyInfo.CompetitorProducts.Count); var productCloudsAreOkay = product.CompanyInfo.CompetitorProducts["CloudsAreOK"]; var retrievedCloudsAreOkay = retrieved.CompanyInfo.CompetitorProducts["CloudsAreOK"]; @@ -1483,6 +1558,7 @@ private void TestHashObjects() { productIds.Add(p.Id); } + Assert.AreEqual(2, productIds.Count); // Load first product @@ -1493,10 +1569,10 @@ private void TestHashObjects() // Query GlobalIndex products = Context.Query( - product.CompanyName, // Hash-key for the index is Company - QueryOperator.GreaterThan, // Range-key for the index is Price, so the - new object[] { 90 }, // condition is against a numerical value - new QueryConfig // Configure the index to use + product.CompanyName, // Hash-key for the index is Company + QueryOperator.GreaterThan, // Range-key for the index is Price, so the + new object[] { 90 }, // condition is against a numerical value + new QueryConfig // Configure the index to use { IndexName = "GlobalIndex", }); @@ -1504,10 +1580,10 @@ private void TestHashObjects() // Query GlobalIndex with an additional non-key condition products = Context.Query( - product.CompanyName, // Hash-key for the index is Company - QueryOperator.GreaterThan, // Range-key for the index is Price, so the - new object[] { 90 }, // condition is against a numerical value - new QueryConfig // Configure the index to use + product.CompanyName, // Hash-key for the index is Company + QueryOperator.GreaterThan, // Range-key for the index is Price, so the + new object[] { 90 }, // condition is against a numerical value + new QueryConfig // Configure the index to use { IndexName = "GlobalIndex", QueryFilter = new List @@ -1738,6 +1814,7 @@ private void TestBatchOperations() Name = productPrefix + i }); } + batchWrite1.AddPutItems(allEmployees); // Write both batches at once @@ -2682,11 +2759,9 @@ public class ProductV2 : Product [DynamoDBTable("HashTable")] public class Product { - [DynamoDBHashKey] - public int Id { get; set; } + [DynamoDBHashKey] public int Id { get; set; } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex", AttributeName = "Company")] public string CompanyName { get; set; } @@ -2696,8 +2771,7 @@ public class Product [DynamoDBGlobalSecondaryIndexRangeKey("GlobalIndex")] public int Price { get; set; } - [DynamoDBProperty("Tags")] - public HashSet TagSet { get; set; } + [DynamoDBProperty("Tags")] public HashSet TagSet { get; set; } public MemoryStream Data { get; set; } @@ -2710,8 +2784,7 @@ public class Product public Support? PreviousSupport { get; set; } - [DynamoDBIgnore] - public string InternalId { get; set; } + [DynamoDBIgnore] public string InternalId { get; set; } public bool IsPublic { get; set; } @@ -2741,8 +2814,7 @@ public class CompanyInfo public string[] FeaturedBrands { get; set; } public Dictionary> CompetitorProducts { get; set; } - [DynamoDBIgnore] - public decimal Revenue { get; set; } + [DynamoDBIgnore] public decimal Revenue { get; set; } } /// @@ -2751,8 +2823,7 @@ public class CompanyInfo /// public class VersionedProduct : Product { - [DynamoDBVersion] - public int? Version { get; set; } + [DynamoDBVersion] public int? Version { get; set; } } @@ -2763,8 +2834,7 @@ public class VersionedProduct : Product [DynamoDBTable("HashTable")] public class EnumProduct1 { - [DynamoDBIgnore] - public Status Id { get; set; } + [DynamoDBIgnore] public Status Id { get; set; } [DynamoDBHashKey("Id")] public int IdAsInt @@ -2773,8 +2843,7 @@ public int IdAsInt set { Id = (Status)value; } } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } } /// @@ -2786,8 +2855,7 @@ public class EnumProduct2 { public Status Id { get; set; } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } } @@ -2802,7 +2870,9 @@ public class Employee { // Hash key public virtual string Name { get; set; } + public string MiddleName { get; set; } + // Range key internal virtual int Age { get; set; } @@ -2823,12 +2893,10 @@ public class Employee public class AnnotatedEmployee : Employee { // Hash key - [DynamoDBHashKey] - public override string Name { get; set; } + [DynamoDBHashKey] public override string Name { get; set; } // Range key - [DynamoDBRangeKey] - internal override int Age { get; set; } + [DynamoDBRangeKey] internal override int Age { get; set; } [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex", AttributeName = "Company")] public override string CompanyName { get; set; } @@ -2846,12 +2914,10 @@ public class AnnotatedEmployee : Employee public class PartiallyAnnotatedEmployee : Employee { // Hash key - [DynamoDBHashKey] - public override string Name { get; set; } + [DynamoDBHashKey] public override string Name { get; set; } // Range key - [DynamoDBRangeKey] - internal override int Age { get; set; } + [DynamoDBRangeKey] internal override int Age { get; set; } [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex")] public override string CompanyName { get; set; } @@ -2906,7 +2972,8 @@ public class Employee5 : AnnotatedEmployee /// Empty type /// public class EmptyType - { } + { + } /// /// Class representing items in the table [TableNamePrefix]HashTable @@ -2917,14 +2984,23 @@ public class VersionedEmployee : Employee public int? Version { get; set; } } + public class CounterAnnotatedEmployee : AnnotatedEmployee + { + [DynamoDBAtomicCounter] + public int? CountDefault { get; set; } + + [DynamoDBAtomicCounter(2, 10)] + public int? CountAtomic { get; set; } + } + + /// /// Class representing items in the table [TableNamePrefix]HashTable /// This class uses optimistic locking via the Version field /// - public class VersionedAnnotatedEmployee : AnnotatedEmployee + public class VersionedAnnotatedEmployee : CounterAnnotatedEmployee { - [DynamoDBVersion] - public int? Version { get; set; } + [DynamoDBVersion] public int? Version { get; set; } } /// From 575466c45dcbb788f2c498a98c4cc65a969d2aec Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Mon, 12 May 2025 18:01:53 +0300 Subject: [PATCH 05/11] refactoring --- .../DynamoDBv2/Custom/DataModel/Context.cs | 3 +- .../Custom/DataModel/ContextInternal.cs | 50 +------------------ .../Custom/DataModel/TransactWrite.cs | 5 -- 3 files changed, 3 insertions(+), 55 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 655df3ddc3c1..d97c071c5f49 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -373,7 +373,6 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr Table table = GetTargetTable(storage.Config, flatConfig); var updateExpression = CreateUpdateExpressionForCounterProperties(storage); - SetAtomicCounters(storage); if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig @@ -412,7 +411,7 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants Table table = GetTargetTable(storage.Config, flatConfig); var counterConditionExpression = CreateUpdateExpressionForCounterProperties(storage); - SetAtomicCounters(storage); + if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 011406cde8f0..3fc2d9713ef7 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -120,52 +120,6 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora #region Atomic counters - - internal static void SetAtomicCounters(ItemStorage storage) - { - - var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. - Where(propertyStorage => propertyStorage.IsCounter).ToList(); - - if (counterProperties.Count==0) return; - // Set the initial value of the counter properties - foreach (var propertyStorage in counterProperties) - { - Primitive counter; - string versionAttributeName = propertyStorage.AttributeName; - - if (storage.Document.TryGetValue(versionAttributeName, out var counterEntry)) - counter = counterEntry as Primitive; - else - counter = null; - - if (counter != null && counter.Value != null) - { - if (counter.Type != DynamoDBEntryType.Numeric) throw new InvalidOperationException("Atomic Counter property must be numeric"); - IncrementCounter(propertyStorage.MemberType, ref counter, propertyStorage.CounterDelta); - } - else - { - counter = new Primitive(propertyStorage.CounterStartValue.ToString(), true); - } - storage.Document[versionAttributeName] = counter; - } - - } - - - private static void IncrementCounter(Type memberType, ref Primitive counter,long delta) - { - if (memberType.IsAssignableFrom(typeof(Byte))) counter = counter.AsByte() + delta; - else if (memberType.IsAssignableFrom(typeof(SByte))) counter = counter.AsSByte() + delta; - else if (memberType.IsAssignableFrom(typeof(int))) counter = counter.AsInt() + delta; - else if (memberType.IsAssignableFrom(typeof(uint))) counter = counter.AsUInt() + delta; - else if (memberType.IsAssignableFrom(typeof(long))) counter = counter.AsLong() + delta; - //else if (memberType.IsAssignableFrom(typeof(ulong))) counter = counter.AsULong() + delta; - else if (memberType.IsAssignableFrom(typeof(short))) counter = counter.AsShort() + delta; - else if (memberType.IsAssignableFrom(typeof(ushort))) counter = counter.AsUShort() + delta; - } - internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorage storage) { Expression updateExpression = null; @@ -178,8 +132,8 @@ internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorag var asserts = string.Empty; foreach (var propertyStorage in counterProperties) { - string startValueName = $":s_{propertyStorage.AttributeName}"; - string deltaValueName = $":d_{propertyStorage.AttributeName}"; + string startValueName = $":{propertyStorage.AttributeName}StartValue"; + string deltaValueName = $":{propertyStorage.AttributeName}Delta"; string counterAttributeName = Common.GetAttributeReference(propertyStorage.AttributeName); asserts += $"{counterAttributeName} = " + $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 09ef17840063..5ec3d4f7f30b 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs @@ -482,11 +482,6 @@ private void SetNewVersion(ItemStorage storage) if (!ShouldUseVersioning()) return; DynamoDBContext.SetNewVersion(storage); } - - private void SetAtomicCounters(ItemStorage storage) - { - DynamoDBContext.SetAtomicCounters(storage); - } } /// From 820c5c00460fb6bc8dab5fa47de9bb7adb014ece Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Mon, 12 May 2025 18:12:36 +0300 Subject: [PATCH 06/11] refactoring --- .../Custom/DataModel/ContextInternal.cs | 78 ------------------- .../Custom/DataModel/TransactWrite.cs | 6 -- .../Custom/DocumentModel/Expression.cs | 47 ----------- 3 files changed, 131 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 6dda0d73fe02..3fc2d9713ef7 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -150,84 +150,6 @@ internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorag #endregion - #region Atomic counters - - - internal static void SetAtomicCounters(ItemStorage storage) - { - - var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. - Where(propertyStorage => propertyStorage.IsCounter).ToList(); - - if (counterProperties.Count==0) return; - // Set the initial value of the counter properties - foreach (var propertyStorage in counterProperties) - { - Primitive counter; - string versionAttributeName = propertyStorage.AttributeName; - - if (storage.Document.TryGetValue(versionAttributeName, out var counterEntry)) - counter = counterEntry as Primitive; - else - counter = null; - - if (counter != null && counter.Value != null) - { - if (counter.Type != DynamoDBEntryType.Numeric) throw new InvalidOperationException("Atomic Counter property must be numeric"); - IncrementCounter(propertyStorage.MemberType, ref counter, propertyStorage.CounterDelta); - } - else - { - counter = new Primitive(propertyStorage.CounterStartValue.ToString(), true); - } - storage.Document[versionAttributeName] = counter; - } - - } - - - private static void IncrementCounter(Type memberType, ref Primitive counter,long delta) - { - if (memberType.IsAssignableFrom(typeof(Byte))) counter = counter.AsByte() + delta; - else if (memberType.IsAssignableFrom(typeof(SByte))) counter = counter.AsSByte() + delta; - else if (memberType.IsAssignableFrom(typeof(int))) counter = counter.AsInt() + delta; - else if (memberType.IsAssignableFrom(typeof(uint))) counter = counter.AsUInt() + delta; - else if (memberType.IsAssignableFrom(typeof(long))) counter = counter.AsLong() + delta; - //else if (memberType.IsAssignableFrom(typeof(ulong))) counter = counter.AsULong() + delta; - else if (memberType.IsAssignableFrom(typeof(short))) counter = counter.AsShort() + delta; - else if (memberType.IsAssignableFrom(typeof(ushort))) counter = counter.AsUShort() + delta; - } - - internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorage storage) - { - Expression updateExpression = null; - var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. - Where(propertyStorage => propertyStorage.IsCounter).ToList(); - - if (counterProperties.Count != 0) - { - updateExpression = new Expression(); - var asserts = string.Empty; - foreach (var propertyStorage in counterProperties) - { - string startValueName = $":s_{propertyStorage.AttributeName}"; - string deltaValueName = $":d_{propertyStorage.AttributeName}"; - string counterAttributeName = Common.GetAttributeReference(propertyStorage.AttributeName); - asserts += $"{counterAttributeName} = " + - $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; - updateExpression.ExpressionAttributeNames[counterAttributeName] = propertyStorage.AttributeName; - updateExpression.ExpressionAttributeValues[deltaValueName] = propertyStorage.CounterDelta; - updateExpression.ExpressionAttributeValues[startValueName] = - propertyStorage.CounterStartValue - propertyStorage.CounterDelta; - } - updateExpression.ExpressionStatement = $"SET {asserts.Substring(0, asserts.Length - 2)}"; - } - - return updateExpression; - } - - #endregion - #region Table methods // Retrieves the target table for the specified type diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 09ef17840063..03566a67f850 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs @@ -468,7 +468,6 @@ private void AddDocumentTransaction(ItemStorage storage, Expression conditionExp } else { - DocumentTransaction.AddDocumentToPut(storage.Document, new TransactWriteItemOperationConfig { ConditionalExpression = conditionExpression, @@ -482,11 +481,6 @@ private void SetNewVersion(ItemStorage storage) if (!ShouldUseVersioning()) return; DynamoDBContext.SetNewVersion(storage); } - - private void SetAtomicCounters(ItemStorage storage) - { - DynamoDBContext.SetAtomicCounters(storage); - } } /// diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs index 66ccc845ae4b..9e144c6cf7da 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs @@ -127,53 +127,6 @@ internal void ApplyExpression(UpdateItemRequest request, Table table) } } - internal void ApplyUpdateExpression(UpdateItemRequest request, Table table) - { - request.UpdateExpression += $" {this.ExpressionStatement}"; - //todo make this work properly - //if (request.UpdateExpression!=null) - //{ - // int removeIndex = request.UpdateExpression.IndexOf(" REMOVE", StringComparison.OrdinalIgnoreCase); - // int setIndex = request.UpdateExpression.IndexOf("SET", StringComparison.OrdinalIgnoreCase); - // if (removeIndex >= 0) - // { - // string setPart = request.UpdateExpression.Substring(setIndex, removeIndex); - // setPart += $" ,{this.ExpressionStatement}"; - // string removePart = request.UpdateExpression.Substring(removeIndex); - // request.UpdateExpression = $"{setPart}{removePart}"; - // } - //} - - if (request.ExpressionAttributeNames == null) - { - if (this.ExpressionAttributeNames?.Count > 0) - { - request.ExpressionAttributeNames = new Dictionary(this.ExpressionAttributeNames); - } - } - else - { - foreach (var kvp in this.ExpressionAttributeNames) - request.ExpressionAttributeNames.Add(kvp.Key, kvp.Value); - } - - var attributeValues = ConvertToAttributeValues(this.ExpressionAttributeValues, table); - if (!(attributeValues?.Count > 0)) return; - - if (request.ExpressionAttributeValues == null) - { - if (this.ExpressionAttributeValues?.Count > 0) - { - request.ExpressionAttributeValues = attributeValues; - } - } - else - { - foreach (var kvp in attributeValues) - request.ExpressionAttributeValues.Add(kvp.Key, kvp.Value); - } - } - internal void ApplyExpression(Get request, Table table) { request.ProjectionExpression = ExpressionStatement; From 42750b4bc1bb282f21c8c19b9e90e55088290ab0 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Tue, 13 May 2025 10:29:17 +0300 Subject: [PATCH 07/11] unit tests and refactoring --- .../DynamoDBv2/Custom/DataModel/Context.cs | 13 ++- .../Custom/DataModel/ContextInternal.cs | 101 ++++++++++++++---- .../DocumentModel/DocumentTransactWrite.cs | 2 +- .../DynamoDBv2/Custom/DocumentModel/Table.cs | 34 ++++-- .../DynamoDBv2/Custom/DocumentModel/Util.cs | 64 +---------- .../Custom/DocumentModel/_bcl/Table.Sync.cs | 16 +-- .../IntegrationTests/DataModelTests.cs | 33 ++++-- 7 files changed, 148 insertions(+), 115 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index d97c071c5f49..2f1b19f01c37 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -372,13 +372,12 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); - var updateExpression = CreateUpdateExpressionForCounterProperties(storage); + + var counterConditionExpression = BuildCounterConditionExpression(storage); + if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig - { - ReturnValues = ReturnValues.None - }); + table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), null, counterConditionExpression); } else { @@ -390,7 +389,7 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr ReturnValues = ReturnValues.None, ConditionalExpression = versionExpression }; - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig); + table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression); PopulateInstance(storage, value, flatConfig); } } @@ -410,7 +409,7 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants Table table = GetTargetTable(storage.Config, flatConfig); - var counterConditionExpression = CreateUpdateExpressionForCounterProperties(storage); + var counterConditionExpression = BuildCounterConditionExpression(storage); if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 3fc2d9713ef7..c51b5b3c3153 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -119,35 +119,98 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora #endregion #region Atomic counters + - internal static Expression CreateUpdateExpressionForCounterProperties(ItemStorage storage) + internal static Expression BuildCounterConditionExpression(ItemStorage storage) + { + var atomicCounters = GetCounterProperties(storage); + Expression counterConditionExpression = null; + + if (atomicCounters.Length != 0) + { + counterConditionExpression = CreateUpdateExpressionForCounterProperties(atomicCounters); + SetAtomicCounters(storage, atomicCounters); + } + + return counterConditionExpression; + } + + private static PropertyStorage[] GetCounterProperties(ItemStorage storage) { - Expression updateExpression = null; var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. - Where(propertyStorage => propertyStorage.IsCounter).ToList(); + Where(propertyStorage => propertyStorage.IsCounter).ToArray(); + return counterProperties; + } - if (counterProperties.Count != 0) + private static Expression CreateUpdateExpressionForCounterProperties(PropertyStorage[] counterPropertyStorages) + { + if (counterPropertyStorages.Length == 0) return null; + + Expression updateExpression = new Expression(); + var asserts = string.Empty; + + foreach (var propertyStorage in counterPropertyStorages) + { + string startValueName = $":{propertyStorage.AttributeName}Start"; + string deltaValueName = $":{propertyStorage.AttributeName}Delta"; + string counterAttributeName = Common.GetAttributeReference(propertyStorage.AttributeName); + asserts += $"{counterAttributeName} = " + + $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; + updateExpression.ExpressionAttributeNames[counterAttributeName] = propertyStorage.AttributeName; + updateExpression.ExpressionAttributeValues[deltaValueName] = propertyStorage.CounterDelta; + updateExpression.ExpressionAttributeValues[startValueName] = + propertyStorage.CounterStartValue - propertyStorage.CounterDelta; + } + updateExpression.ExpressionStatement = $"SET {asserts.Substring(0, asserts.Length - 2)}"; + + return updateExpression; + } + + private static void SetAtomicCounters(ItemStorage storage, PropertyStorage[] counterPropertyStorages) + { + if (counterPropertyStorages.Length == 0) return; + // Set the initial value of the counter properties + foreach (var propertyStorage in counterPropertyStorages) { - updateExpression = new Expression(); - var asserts = string.Empty; - foreach (var propertyStorage in counterProperties) + Primitive counter; + string versionAttributeName = propertyStorage.AttributeName; + + if (storage.Document.TryGetValue(versionAttributeName, out var counterEntry)) + counter = counterEntry as Primitive; + else + counter = null; + + if (counter != null && counter.Value != null) + { + if (counter.Type != DynamoDBEntryType.Numeric) throw new InvalidOperationException("Atomic Counter property must be numeric."); + IncrementCounter(propertyStorage.MemberType, ref counter, propertyStorage.CounterDelta); + } + else { - string startValueName = $":{propertyStorage.AttributeName}StartValue"; - string deltaValueName = $":{propertyStorage.AttributeName}Delta"; - string counterAttributeName = Common.GetAttributeReference(propertyStorage.AttributeName); - asserts += $"{counterAttributeName} = " + - $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; - updateExpression.ExpressionAttributeNames[counterAttributeName] = propertyStorage.AttributeName; - updateExpression.ExpressionAttributeValues[deltaValueName] = propertyStorage.CounterDelta; - updateExpression.ExpressionAttributeValues[startValueName] = - propertyStorage.CounterStartValue - propertyStorage.CounterDelta; + counter = new Primitive(propertyStorage.CounterStartValue.ToString(), true); } - updateExpression.ExpressionStatement = $"SET {asserts.Substring(0, asserts.Length - 2)}"; + storage.Document[versionAttributeName] = counter; } - return updateExpression; } - + + private static void IncrementCounter(Type memberType, ref Primitive counter, long delta) + { + if (memberType.IsAssignableFrom(typeof(Byte))) counter = counter.AsByte() + delta; + else if (memberType.IsAssignableFrom(typeof(SByte))) counter = counter.AsSByte() + delta; + else if (memberType.IsAssignableFrom(typeof(int))) counter = counter.AsInt() + delta; + else if (memberType.IsAssignableFrom(typeof(long))) counter = counter.AsLong() + delta; + else if (memberType.IsAssignableFrom(typeof(short))) counter = counter.AsShort() + delta; + else + { + if (memberType.IsAssignableFrom(typeof(uint)) || memberType.IsAssignableFrom(typeof(ulong)) || + memberType.IsAssignableFrom(typeof(ushort))) + { + throw new InvalidOperationException("AtomicCounter properties must be signed integral types."); + } + } + } + #endregion #region Table methods diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentTransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentTransactWrite.cs index 6a244ab89aed..cfe4de17166b 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentTransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentTransactWrite.cs @@ -966,7 +966,7 @@ protected override bool TryGetUpdateExpression(out string statement, return false; } - Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, + Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates,null,null, out statement, out expressionAttributeValues, out expressionAttributes); return true; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs index 11b090a927a4..84034ca03af3 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs @@ -535,7 +535,25 @@ private static void ValidateConditional(IConditionalOperationConfig config) conditionsSet += config.ConditionalExpression != null && config.ConditionalExpression.ExpressionStatement != null ? 1 : 0; if (conditionsSet > 1) - throw new InvalidOperationException("Only one of the conditonal properties Expected, ExpectedState and ConditionalExpression can be set."); + throw new InvalidOperationException("Only one of the conditional properties Expected, ExpectedState and ConditionalExpression can be set."); + } + + + private void ValidateConditional(IConditionalOperationConfig config, Expression updateExpression) + { + + if (config == null) + return; + + int conditionsSet = 0; + conditionsSet += config.Expected != null ? 1 : 0; + conditionsSet += config.ExpectedState != null ? 1 : 0; + conditionsSet += + (config.ConditionalExpression is { ExpressionStatement: not null } || updateExpression is { ExpressionStatement: not null }) ? 1 : 0; + + if (conditionsSet > 1) + throw new InvalidOperationException("Only one of the conditional properties Expected, ExpectedState and ConditionalExpression or UpdateExpression can be set."); + } internal void ClearTableData() @@ -1341,7 +1359,7 @@ internal async Task GetItemHelperAsync(Key key, GetItemOperationConfig internal Document UpdateHelper(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config) { Key key = (hashKey != null || rangeKey != null) ? MakeKey(hashKey, rangeKey) : MakeKey(doc); - return UpdateHelper(doc, key, config); + return UpdateHelper(doc, key, config,null); } #if AWS_ASYNC_API @@ -1352,7 +1370,7 @@ internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primi } #endif - internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig config) + internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig config, Expression updateExpression) { var currentConfig = config ?? new UpdateItemOperationConfig(); @@ -1376,7 +1394,7 @@ internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig this.UpdateRequestUserAgentDetails(req, isAsync: false); - ValidateConditional(currentConfig); + ValidateConditional(currentConfig, updateExpression); if (currentConfig.Expected != null) { @@ -1390,14 +1408,15 @@ internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } - else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) + else if (currentConfig.ConditionalExpression is { IsSet: true } || updateExpression is { IsSet: true }) { currentConfig.ConditionalExpression.ApplyExpression(req, this); string statement; Dictionary expressionAttributeValues; Dictionary expressionAttributeNames; - Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, out statement, out expressionAttributeValues, out expressionAttributeNames); + + Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, updateExpression, this, out statement, out expressionAttributeValues, out expressionAttributeNames); req.AttributeUpdates = null; req.UpdateExpression = statement; @@ -1467,8 +1486,7 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte this.UpdateRequestUserAgentDetails(req, isAsync: true); - //todo: add support for updateExpression - ValidateConditional(currentConfig); + ValidateConditional(currentConfig, updateExpression); if (currentConfig.Expected != null) { diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs index 5cd2f321df9f..150790483369 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs @@ -327,69 +327,7 @@ public static string Convert(ConditionalOperatorValues value) internal static class Common { private const string AwsVariablePrefix = "awsavar"; - - // Convert collection of AttributeValueUpdate to an update expression. This is needed when doing an update - // with a conditional expression. - public static void ConvertAttributeUpdatesToUpdateExpression(Dictionary attributesToUpdates, - out string statement, - out Dictionary expressionAttributeValues, - out Dictionary expressionAttributes) - { - expressionAttributeValues = new Dictionary(StringComparer.Ordinal); - expressionAttributes = new Dictionary(StringComparer.Ordinal); - - // Build an expression string with a SET clause for the added/modified attributes and - // REMOVE clause for the attributes set to null. - int attributeCount = 0; - StringBuilder sets = new StringBuilder(); - StringBuilder removes = new StringBuilder(); - foreach (var kvp in attributesToUpdates) - { - var attribute = kvp.Key; - var update = kvp.Value; - - string variableName = GetVariableName(ref attributeCount); - var attributeReference = GetAttributeReference(variableName); - var attributeValueReference = GetAttributeValueReference(variableName); - - if (update.Action == AttributeAction.DELETE) - { - if (removes.Length > 0) - removes.Append(", "); - removes.Append(attributeReference); - } - else - { - if (sets.Length > 0) - sets.Append(", "); - sets.AppendFormat("{0} = {1}", attributeReference, attributeValueReference); - - // Add the attribute value for the variable in the added in the expression - expressionAttributeValues.Add(attributeValueReference, update.Value); - } - - // Add the attribute name for the variable in the added in the expression - expressionAttributes.Add(attributeReference, attribute); - } - - // Combine the SET and REMOVE clause - StringBuilder statementBuilder = new StringBuilder(); - if (sets.Length > 0) - { - statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "SET {0}", sets.ToString()); - } - if (removes.Length > 0) - { - if (sets.Length > 0) - statementBuilder.Append(" "); - - statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "REMOVE {0}", removes.ToString()); - } - - statement = statementBuilder.ToString(); - } - - + public static void ConvertAttributeUpdatesToUpdateExpression( Dictionary attributesToUpdates, Expression updateExpression, Table table, diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs index e5deb5cc0352..2f170790dc5b 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs @@ -319,7 +319,7 @@ public Document UpdateItem(Document doc, UpdateItemOperationConfig config = null var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItem)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return UpdateHelper(doc, MakeKey(doc), config); + return UpdateHelper(doc, MakeKey(doc), config, null); } } @@ -331,7 +331,7 @@ public bool TryUpdateItem(Document doc, UpdateItemOperationConfig config = null) { try { - UpdateHelper(doc, MakeKey(doc), config); + UpdateHelper(doc, MakeKey(doc), config, null); return true; } catch (ConditionalCheckFailedException) @@ -347,7 +347,7 @@ public Document UpdateItem(Document doc, IDictionary key, var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItem)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return UpdateHelper(doc, MakeKey(key), config); + return UpdateHelper(doc, MakeKey(key), config, null); } } @@ -359,7 +359,7 @@ public bool TryUpdateItem(Document doc, IDictionary key, { try { - UpdateHelper(doc, MakeKey(key), config); + UpdateHelper(doc, MakeKey(key), config, null); return true; } catch (ConditionalCheckFailedException) @@ -375,7 +375,7 @@ public Document UpdateItem(Document doc, Primitive hashKey, UpdateItemOperationC var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItem)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return UpdateHelper(doc, MakeKey(hashKey, null), config); + return UpdateHelper(doc, MakeKey(hashKey, null), config, null); } } @@ -387,7 +387,7 @@ public bool TryUpdateItem(Document doc, Primitive hashKey, UpdateItemOperationCo { try { - UpdateHelper(doc, MakeKey(hashKey, null), config); + UpdateHelper(doc, MakeKey(hashKey, null), config, null); return true; } catch (ConditionalCheckFailedException) @@ -403,7 +403,7 @@ public Document UpdateItem(Document doc, Primitive hashKey, Primitive rangeKey, var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItem)); using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT)) { - return UpdateHelper(doc, MakeKey(hashKey, rangeKey), config); + return UpdateHelper(doc, MakeKey(hashKey, rangeKey), config, null); } } @@ -415,7 +415,7 @@ public bool TryUpdateItem(Document doc, Primitive hashKey, Primitive rangeKey, U { try { - UpdateHelper(doc, MakeKey(hashKey, rangeKey), config); + UpdateHelper(doc, MakeKey(hashKey, rangeKey), config, null); return true; } catch (ConditionalCheckFailedException) diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index e6cc597fe632..4c2cd3bd946a 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -665,7 +665,8 @@ public async Task TestContext_AtomicCounterAnnotation() CleanupTables(); TableCache.Clear(); - VersionedAnnotatedEmployee employee = new VersionedAnnotatedEmployee + + CounterAnnotatedEmployee employee = new CounterAnnotatedEmployee { Name = "Mark", Age = 31, @@ -674,23 +675,37 @@ public async Task TestContext_AtomicCounterAnnotation() }; await Context.SaveAsync(employee); - var storedEmployee = await Context.LoadAsync(employee.Name, 31); + var storedEmployee = await Context.LoadAsync(employee.Name, 31); Assert.IsNotNull(storedEmployee); Assert.AreEqual(employee.Name, storedEmployee.Name); - // Assert.AreEqual(0, storedEmployee.Version); Assert.AreEqual(0, storedEmployee.CountDefault); Assert.AreEqual(10, storedEmployee.CountAtomic); + + VersionedAnnotatedEmployee versionedAnnotatedEmployee = new VersionedAnnotatedEmployee + { + Name = "Mark", + Age = 31, + Score = 120, + ManagerName = "Harmony" + }; + + await Context.SaveAsync(versionedAnnotatedEmployee); + var storedVersionEmployee = await Context.LoadAsync(versionedAnnotatedEmployee.Name, 31); + Assert.IsNotNull(storedVersionEmployee); + Assert.AreEqual(0, storedVersionEmployee.Version); + Assert.AreEqual(1, storedVersionEmployee.CountDefault); + Assert.AreEqual(12, storedVersionEmployee.CountAtomic); + // Update the employee - storedEmployee.ManagerName = "Helena"; + versionedAnnotatedEmployee.ManagerName = "Helena"; - await Context.SaveAsync(storedEmployee); - var storedUpdatedEmployee = await Context.LoadAsync(storedEmployee.Name, 31); + await Context.SaveAsync(versionedAnnotatedEmployee); + var storedUpdatedEmployee = await Context.LoadAsync(versionedAnnotatedEmployee.Name, 31); Assert.IsNotNull(storedUpdatedEmployee); - Assert.AreEqual(employee.Name, storedUpdatedEmployee.Name); Assert.AreEqual(1, storedUpdatedEmployee.Version); - Assert.AreEqual(1, storedUpdatedEmployee.CountDefault); - Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); + Assert.AreEqual(2, storedUpdatedEmployee.CountDefault); + Assert.AreEqual(14, storedUpdatedEmployee.CountAtomic); } [TestMethod] From 97ee267733c05795bf610d3687edf71aa602b37c Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Tue, 13 May 2025 11:23:42 +0300 Subject: [PATCH 08/11] refactoring --- .../887577fc-6ac5-40ca-ac67-4b5808a5db14.json | 11 +++++ .../DynamoDBv2/Custom/DataModel/Context.cs | 1 + .../Custom/DataModel/ContextInternal.cs | 3 +- .../Custom/DataModel/TransactWrite.cs | 5 --- .../DynamoDBv2/Custom/DocumentModel/Util.cs | 43 +++++++++---------- .../IntegrationTests/DataModelTests.cs | 2 +- 6 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 generator/.DevConfigs/887577fc-6ac5-40ca-ac67-4b5808a5db14.json diff --git a/generator/.DevConfigs/887577fc-6ac5-40ca-ac67-4b5808a5db14.json b/generator/.DevConfigs/887577fc-6ac5-40ca-ac67-4b5808a5db14.json new file mode 100644 index 000000000000..295e383ef805 --- /dev/null +++ b/generator/.DevConfigs/887577fc-6ac5-40ca-ac67-4b5808a5db14.json @@ -0,0 +1,11 @@ +{ + "services": [ + { + "serviceName": "DynamoDBv2", + "type": "patch", + "changeLogMessages": [ + "Introduce support for the [DynamoDBAtomicCounter] attribute in the DynamoDB Object Persistence Model`" + ] + } + ] +} \ No newline at end of file diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 2f1b19f01c37..73abed88591a 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -384,6 +384,7 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); + var updateItemOperationConfig = new UpdateItemOperationConfig { ReturnValues = ReturnValues.None, diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index c51b5b3c3153..fd3cee7581af 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -119,7 +119,6 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora #endregion #region Atomic counters - internal static Expression BuildCounterConditionExpression(ItemStorage storage) { @@ -139,6 +138,7 @@ private static PropertyStorage[] GetCounterProperties(ItemStorage storage) { var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. Where(propertyStorage => propertyStorage.IsCounter).ToArray(); + return counterProperties; } @@ -169,6 +169,7 @@ private static Expression CreateUpdateExpressionForCounterProperties(PropertySto private static void SetAtomicCounters(ItemStorage storage, PropertyStorage[] counterPropertyStorages) { if (counterPropertyStorages.Length == 0) return; + // Set the initial value of the counter properties foreach (var propertyStorage in counterPropertyStorages) { diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 03566a67f850..eaa5f5ba06a0 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs @@ -409,11 +409,6 @@ private bool ShouldUseVersioning() return !skipVersionCheck && _storageConfig.HasVersion; } - private bool ShouldUseCounter() - { - return false; //_storageConfig.; - } - private void CheckUseVersioning() { if (_config.SkipVersionCheck == true) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs index 150790483369..457233e96e9f 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs @@ -354,33 +354,32 @@ public static void ConvertAttributeUpdatesToUpdateExpression( foreach (var kvp in attributesToUpdates) { var attribute = kvp.Key; - if (!attributeNames.Contains(attribute)) - { - var update = kvp.Value; + if (attributeNames.Contains(attribute)) continue; - string variableName = GetVariableName(ref attributeCount); - var attributeReference = GetAttributeReference(variableName); - var attributeValueReference = GetAttributeValueReference(variableName); + var update = kvp.Value; - if (update.Action == AttributeAction.DELETE) - { - if (removes.Length > 0) - removes.Append(", "); - removes.Append(attributeReference); - } - else - { - if (sets.Length > 0) - sets.Append(", "); - sets.AppendFormat("{0} = {1}", attributeReference, attributeValueReference); + string variableName = GetVariableName(ref attributeCount); + var attributeReference = GetAttributeReference(variableName); + var attributeValueReference = GetAttributeValueReference(variableName); - // Add the attribute value for the variable in the added in the expression - expressionAttributeValues.Add(attributeValueReference, update.Value); - } + if (update.Action == AttributeAction.DELETE) + { + if (removes.Length > 0) + removes.Append(", "); + removes.Append(attributeReference); + } + else + { + if (sets.Length > 0) + sets.Append(", "); + sets.AppendFormat("{0} = {1}", attributeReference, attributeValueReference); - // Add the attribute name for the variable in the added in the expression - expressionAttributes.Add(attributeReference, attribute); + // Add the attribute value for the variable in the added in the expression + expressionAttributeValues.Add(attributeValueReference, update.Value); } + + // Add the attribute name for the variable in the added in the expression + expressionAttributes.Add(attributeReference, attribute); } // Combine the SET and REMOVE clause diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index 4c2cd3bd946a..aeeaa1356350 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -3004,7 +3004,7 @@ public class CounterAnnotatedEmployee : AnnotatedEmployee [DynamoDBAtomicCounter] public int? CountDefault { get; set; } - [DynamoDBAtomicCounter(2, 10)] + [DynamoDBAtomicCounter(delta:2, startValue:10)] public int? CountAtomic { get; set; } } From 097589a3db062eedaf5b3fa3b92a663f8ce88c13 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Tue, 20 May 2025 16:52:12 +0300 Subject: [PATCH 09/11] map in memory value from DB --- .../DynamoDBv2/Custom/DataModel/Context.cs | 46 +++++++++++++----- .../Custom/DataModel/ContextInternal.cs | 47 ------------------- .../Custom/DataModel/InternalModel.cs | 1 - 3 files changed, 33 insertions(+), 61 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 73abed88591a..69487c60c25d 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -375,24 +375,34 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr var counterConditionExpression = BuildCounterConditionExpression(storage); + Document updateDocument; + Expression versionExpression = null; + if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), null, counterConditionExpression); + updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig() + { + ReturnValues = ReturnValues.AllNewAttributes + }, counterConditionExpression); } else { - var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); - var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); var updateItemOperationConfig = new UpdateItemOperationConfig { - ReturnValues = ReturnValues.None, - ConditionalExpression = versionExpression + ReturnValues = ReturnValues.AllNewAttributes, + ConditionalExpression = versionExpression, }; - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression); - PopulateInstance(storage, value, flatConfig); + updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression); } + + if (counterConditionExpression == null && versionExpression == null) return; + + storage.Document = updateDocument; + PopulateInstance(storage, value, flatConfig); } #if AWS_ASYNC_API @@ -412,30 +422,40 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants var counterConditionExpression = BuildCounterConditionExpression(storage); + Document updateDocument; + Expression versionExpression = null; if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), null, counterConditionExpression, cancellationToken).ConfigureAwait(false); + updateDocument = await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig + { + ReturnValues = ReturnValues.AllNewAttributes + }, counterConditionExpression, cancellationToken).ConfigureAwait(false); } else { - var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); - var versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); - await table.UpdateHelperAsync( + updateDocument = await table.UpdateHelperAsync( storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig { - ReturnValues = ReturnValues.None, + ReturnValues = ReturnValues.AllNewAttributes, ConditionalExpression = versionExpression }, counterConditionExpression, cancellationToken) .ConfigureAwait(false); - PopulateInstance(storage, value, flatConfig); } + + + if (counterConditionExpression == null && versionExpression == null) return; + + storage.Document = updateDocument; + PopulateInstance(storage, value, flatConfig); } #endif diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index fd3cee7581af..1fcd7cd8a37e 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -128,7 +128,6 @@ internal static Expression BuildCounterConditionExpression(ItemStorage storage) if (atomicCounters.Length != 0) { counterConditionExpression = CreateUpdateExpressionForCounterProperties(atomicCounters); - SetAtomicCounters(storage, atomicCounters); } return counterConditionExpression; @@ -166,52 +165,6 @@ private static Expression CreateUpdateExpressionForCounterProperties(PropertySto return updateExpression; } - private static void SetAtomicCounters(ItemStorage storage, PropertyStorage[] counterPropertyStorages) - { - if (counterPropertyStorages.Length == 0) return; - - // Set the initial value of the counter properties - foreach (var propertyStorage in counterPropertyStorages) - { - Primitive counter; - string versionAttributeName = propertyStorage.AttributeName; - - if (storage.Document.TryGetValue(versionAttributeName, out var counterEntry)) - counter = counterEntry as Primitive; - else - counter = null; - - if (counter != null && counter.Value != null) - { - if (counter.Type != DynamoDBEntryType.Numeric) throw new InvalidOperationException("Atomic Counter property must be numeric."); - IncrementCounter(propertyStorage.MemberType, ref counter, propertyStorage.CounterDelta); - } - else - { - counter = new Primitive(propertyStorage.CounterStartValue.ToString(), true); - } - storage.Document[versionAttributeName] = counter; - } - - } - - private static void IncrementCounter(Type memberType, ref Primitive counter, long delta) - { - if (memberType.IsAssignableFrom(typeof(Byte))) counter = counter.AsByte() + delta; - else if (memberType.IsAssignableFrom(typeof(SByte))) counter = counter.AsSByte() + delta; - else if (memberType.IsAssignableFrom(typeof(int))) counter = counter.AsInt() + delta; - else if (memberType.IsAssignableFrom(typeof(long))) counter = counter.AsLong() + delta; - else if (memberType.IsAssignableFrom(typeof(short))) counter = counter.AsShort() + delta; - else - { - if (memberType.IsAssignableFrom(typeof(uint)) || memberType.IsAssignableFrom(typeof(ulong)) || - memberType.IsAssignableFrom(typeof(ushort))) - { - throw new InvalidOperationException("AtomicCounter properties must be signed integral types."); - } - } - } - #endregion #region Table methods diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index eb153b3abb02..3d925af6bf6a 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -1126,7 +1126,6 @@ private static void PopulateConfigFromMappings(ItemStorageConfig config, Diction propertyStorage.ConverterType = propertyConfig.Converter; propertyStorage.IsIgnored = propertyConfig.Ignore; propertyStorage.IsVersion = propertyConfig.Version; - //propertyStorage.IsCounter = propertyConfig.Counter; propertyStorage.StoreAsEpoch = propertyConfig.StoreAsEpoch; propertyStorage.StoreAsEpochLong = propertyConfig.StoreAsEpochLong; } From ff5772c99e405d524d7d1588c9f6f1ce05a669ac Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Wed, 21 May 2025 18:14:24 +0300 Subject: [PATCH 10/11] integrarion tests and pr feedback --- .../DynamoDBv2/Custom/DataModel/Context.cs | 19 +++++--- .../Custom/DataModel/ContextInternal.cs | 2 + .../IntegrationTests/DataModelTests.cs | 45 ++++++++++++++++--- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 69487c60c25d..17172e85781e 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -378,11 +378,13 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr Document updateDocument; Expression versionExpression = null; + var returnValues=counterConditionExpression == null ? ReturnValues.None : ReturnValues.AllNewAttributes; + if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig() { - ReturnValues = ReturnValues.AllNewAttributes + ReturnValues = returnValues }, counterConditionExpression); } else @@ -393,15 +395,19 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr var updateItemOperationConfig = new UpdateItemOperationConfig { - ReturnValues = ReturnValues.AllNewAttributes, + ReturnValues = returnValues, ConditionalExpression = versionExpression, }; updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression); } + if (returnValues==ReturnValues.AllNewAttributes) + { + storage.Document = updateDocument; + } + if (counterConditionExpression == null && versionExpression == null) return; - storage.Document = updateDocument; PopulateInstance(storage, value, flatConfig); } @@ -424,13 +430,16 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants Document updateDocument; Expression versionExpression = null; + + var returnValues = counterConditionExpression == null ? ReturnValues.None : ReturnValues.AllNewAttributes; + if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { updateDocument = await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig { - ReturnValues = ReturnValues.AllNewAttributes + ReturnValues = returnValues }, counterConditionExpression, cancellationToken).ConfigureAwait(false); } else @@ -444,7 +453,7 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants table.MakeKey(storage.Document), new UpdateItemOperationConfig { - ReturnValues = ReturnValues.AllNewAttributes, + ReturnValues = returnValues, ConditionalExpression = versionExpression }, counterConditionExpression, cancellationToken) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 1fcd7cd8a37e..a3ae92804cd8 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -157,6 +157,8 @@ private static Expression CreateUpdateExpressionForCounterProperties(PropertySto $"if_not_exists({counterAttributeName},{startValueName}) + {deltaValueName} ,"; updateExpression.ExpressionAttributeNames[counterAttributeName] = propertyStorage.AttributeName; updateExpression.ExpressionAttributeValues[deltaValueName] = propertyStorage.CounterDelta; + + //CounterDelta is being subtracted from CounterStartValue to compensate it being added back to the starting value updateExpression.ExpressionAttributeValues[startValueName] = propertyStorage.CounterStartValue - propertyStorage.CounterDelta; } diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index aeeaa1356350..8718ebd7631c 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -665,7 +665,7 @@ public async Task TestContext_AtomicCounterAnnotation() CleanupTables(); TableCache.Clear(); - + // Initial save CounterAnnotatedEmployee employee = new CounterAnnotatedEmployee { Name = "Mark", @@ -681,12 +681,43 @@ public async Task TestContext_AtomicCounterAnnotation() Assert.AreEqual(0, storedEmployee.CountDefault); Assert.AreEqual(10, storedEmployee.CountAtomic); + // Simulate external update: increment counters by saving again + storedEmployee.CountDefault = null; // Let the context increment + storedEmployee.CountAtomic = null; + await Context.SaveAsync(storedEmployee); - VersionedAnnotatedEmployee versionedAnnotatedEmployee = new VersionedAnnotatedEmployee + var externallyUpdated = await Context.LoadAsync(employee.Name, 31); + Assert.AreEqual(1, externallyUpdated.CountDefault); + Assert.AreEqual(12, externallyUpdated.CountAtomic); + + // Simulate a stale POCO (behind the table value) + var stalePoco = new CounterAnnotatedEmployee { Name = "Mark", Age = 31, Score = 120, + ManagerName = "Harmony", + CountDefault = 0, // behind + CountAtomic = 10 // behind + }; + + // Save the stale POCO, should increment from the current table value + await Context.SaveAsync(stalePoco); + + // After save, the POCO should be updated to the latest value + Assert.AreEqual(2, stalePoco.CountDefault); + Assert.AreEqual(14, stalePoco.CountAtomic); + + // Confirm with a fresh load + var latest = await Context.LoadAsync(employee.Name, 31); + Assert.AreEqual(2, latest.CountDefault); + Assert.AreEqual(14, latest.CountAtomic); + + VersionedAnnotatedEmployee versionedAnnotatedEmployee = new VersionedAnnotatedEmployee + { + Name = "MarkV1", + Age = 31, + Score = 120, ManagerName = "Harmony" }; @@ -694,18 +725,18 @@ public async Task TestContext_AtomicCounterAnnotation() var storedVersionEmployee = await Context.LoadAsync(versionedAnnotatedEmployee.Name, 31); Assert.IsNotNull(storedVersionEmployee); Assert.AreEqual(0, storedVersionEmployee.Version); - Assert.AreEqual(1, storedVersionEmployee.CountDefault); - Assert.AreEqual(12, storedVersionEmployee.CountAtomic); + Assert.AreEqual(0, storedVersionEmployee.CountDefault); + Assert.AreEqual(10, storedVersionEmployee.CountAtomic); // Update the employee versionedAnnotatedEmployee.ManagerName = "Helena"; - await Context.SaveAsync(versionedAnnotatedEmployee); var storedUpdatedEmployee = await Context.LoadAsync(versionedAnnotatedEmployee.Name, 31); Assert.IsNotNull(storedUpdatedEmployee); Assert.AreEqual(1, storedUpdatedEmployee.Version); - Assert.AreEqual(2, storedUpdatedEmployee.CountDefault); - Assert.AreEqual(14, storedUpdatedEmployee.CountAtomic); + Assert.AreEqual(1, storedUpdatedEmployee.CountDefault); + Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); + } [TestMethod] From 3f858d2ae06ffd98d3bc0c4b6cf947a31800ed49 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Fri, 23 May 2025 09:33:13 +0300 Subject: [PATCH 11/11] fix async save issue --- .../Services/DynamoDBv2/Custom/DataModel/Context.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs index 17172e85781e..57a43bdba112 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs @@ -401,13 +401,13 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression); } - if (returnValues==ReturnValues.AllNewAttributes) + if (counterConditionExpression == null && versionExpression == null) return; + + if (returnValues == ReturnValues.AllNewAttributes) { storage.Document = updateDocument; } - if (counterConditionExpression == null && versionExpression == null) return; - PopulateInstance(storage, value, flatConfig); } @@ -460,10 +460,12 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants .ConfigureAwait(false); } - if (counterConditionExpression == null && versionExpression == null) return; - storage.Document = updateDocument; + if (returnValues == ReturnValues.AllNewAttributes) + { + storage.Document = updateDocument; + } PopulateInstance(storage, value, flatConfig); } #endif