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/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..57a43bdba112 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,22 +372,43 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); + + var counterConditionExpression = BuildCounterConditionExpression(storage); + + Document updateDocument; + Expression versionExpression = null; + + var returnValues=counterConditionExpression == null ? ReturnValues.None : ReturnValues.AllNewAttributes; + if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), null); + updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig() + { + ReturnValues = returnValues + }, counterConditionExpression); } else { - Document expectedDocument = CreateExpectedDocumentForVersion(storage); + var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled); + versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig); SetNewVersion(storage); + var updateItemOperationConfig = new UpdateItemOperationConfig { - Expected = expectedDocument, - ReturnValues = ReturnValues.None, + ReturnValues = returnValues, + ConditionalExpression = versionExpression, }; - table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig); - PopulateInstance(storage, value, flatConfig); + updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression); } + + if (counterConditionExpression == null && versionExpression == null) return; + + if (returnValues == ReturnValues.AllNewAttributes) + { + storage.Document = updateDocument; + } + + PopulateInstance(storage, value, flatConfig); } #if AWS_ASYNC_API @@ -401,23 +425,48 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants if (storage == null) return; Table table = GetTargetTable(storage.Config, flatConfig); + + var counterConditionExpression = BuildCounterConditionExpression(storage); + + Document updateDocument; + Expression versionExpression = null; + + var returnValues = counterConditionExpression == null ? ReturnValues.None : ReturnValues.AllNewAttributes; + if ( (flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion) { - await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), null, cancellationToken).ConfigureAwait(false); + updateDocument = await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig + { + ReturnValues = returnValues + }, counterConditionExpression, cancellationToken).ConfigureAwait(false); } else { - Document expectedDocument = CreateExpectedDocumentForVersion(storage); + 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 { Expected = expectedDocument, ReturnValues = ReturnValues.None }, - cancellationToken).ConfigureAwait(false); - PopulateInstance(storage, value, flatConfig); + new UpdateItemOperationConfig + { + ReturnValues = returnValues, + ConditionalExpression = versionExpression + }, counterConditionExpression, + cancellationToken) + .ConfigureAwait(false); + } + + if (counterConditionExpression == null && versionExpression == null) return; + + if (returnValues == ReturnValues.AllNewAttributes) + { + 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 806354cc9a68..a3ae92804cd8 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,57 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora #endregion + #region Atomic counters + + internal static Expression BuildCounterConditionExpression(ItemStorage storage) + { + var atomicCounters = GetCounterProperties(storage); + Expression counterConditionExpression = null; + + if (atomicCounters.Length != 0) + { + counterConditionExpression = CreateUpdateExpressionForCounterProperties(atomicCounters); + } + + return counterConditionExpression; + } + + private static PropertyStorage[] GetCounterProperties(ItemStorage storage) + { + var counterProperties = storage.Config.BaseTypeStorageConfig.Properties. + Where(propertyStorage => propertyStorage.IsCounter).ToArray(); + + return counterProperties; + } + + 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; + + //CounterDelta is being subtracted from CounterStartValue to compensate it being added back to the starting value + 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 +444,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 +517,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 +533,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..3d925af6bf6a 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)) { diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/TransactWrite.cs index 95a9f39a90d9..eaa5f5ba06a0 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); @@ -432,7 +433,6 @@ private Expression CreateConditionExpressionForVersion(ItemStorage storage) DocumentTransaction.TargetTable.IsEmptyStringValueEnabled); return DynamoDBContext.CreateConditionExpressionForVersion(storage, conversionConfig); } - private void AddDocumentTransaction(ItemStorage storage, Expression conditionExpression) { @@ -463,7 +463,6 @@ private void AddDocumentTransaction(ItemStorage storage, Expression conditionExp } else { - DocumentTransaction.AddDocumentToPut(storage.Document, new TransactWriteItemOperationConfig { ConditionalExpression = conditionExpression, 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/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 f84ab45ad4fa..84034ca03af3 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)}"); } } @@ -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,18 +1359,18 @@ 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 - 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 - 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; @@ -1443,7 +1462,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,7 +1486,7 @@ internal async Task UpdateHelperAsync(Document doc, Key key, UpdateIte this.UpdateRequestUserAgentDetails(req, isAsync: true); - ValidateConditional(currentConfig); + ValidateConditional(currentConfig, updateExpression); if (currentConfig.Expected != null) { @@ -1481,14 +1500,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..457233e96e9f 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; @@ -326,10 +327,10 @@ 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, + + public static void ConvertAttributeUpdatesToUpdateExpression( + Dictionary attributesToUpdates, Expression updateExpression, + Table table, out string statement, out Dictionary expressionAttributeValues, out Dictionary expressionAttributes) @@ -337,6 +338,14 @@ public static void ConvertAttributeUpdatesToUpdateExpression(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; @@ -345,6 +354,8 @@ public static void ConvertAttributeUpdatesToUpdateExpression(Dictionary 0) { - statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "SET {0}", sets.ToString()); + var setStatement= updateExpression!=null ? updateExpression.ExpressionStatement + "," : "SET"; + statementBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0} {1}", setStatement, sets.ToString()); } if (removes.Length > 0) { @@ -568,3 +580,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/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 4d3e85ffa066..8718ebd7631c 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,91 @@ 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(); + + // Initial save + 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.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); + + 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" + }; + + await Context.SaveAsync(versionedAnnotatedEmployee); + var storedVersionEmployee = await Context.LoadAsync(versionedAnnotatedEmployee.Name, 31); + Assert.IsNotNull(storedVersionEmployee); + Assert.AreEqual(0, storedVersionEmployee.Version); + 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(1, storedUpdatedEmployee.CountDefault); + Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic); + + } [TestMethod] [TestCategory("DynamoDBv2")] @@ -663,7 +759,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 +770,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 +851,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 +892,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 +905,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 +924,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 +970,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 +1024,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 +1159,7 @@ private static void TestAnnotatedUnsupportedTypes() } private void TestContextConversions() - { + { var conversionV1 = DynamoDBEntryConversion.V1; var conversionV2 = DynamoDBEntryConversion.V2; @@ -1094,8 +1196,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 +1210,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 +1246,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 +1269,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 +1301,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 +1394,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 +1426,7 @@ private void TestEnumHashKeyObjects() Context.Delete(product1); Context.Delete(product2); } + private void TestHashObjects() { string bucketName = "aws-sdk-net-s3link-" + DateTime.UtcNow.Ticks; @@ -1380,8 +1495,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 +1535,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 +1604,7 @@ private void TestHashObjects() { productIds.Add(p.Id); } + Assert.AreEqual(2, productIds.Count); // Load first product @@ -1493,10 +1615,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 +1626,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 +1860,7 @@ private void TestBatchOperations() Name = productPrefix + i }); } + batchWrite1.AddPutItems(allEmployees); // Write both batches at once @@ -2682,11 +2805,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 +2817,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 +2830,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 +2860,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 +2869,7 @@ public class CompanyInfo /// public class VersionedProduct : Product { - [DynamoDBVersion] - public int? Version { get; set; } + [DynamoDBVersion] public int? Version { get; set; } } @@ -2763,8 +2880,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 +2889,7 @@ public int IdAsInt set { Id = (Status)value; } } - [DynamoDBProperty("Product")] - public string Name { get; set; } + [DynamoDBProperty("Product")] public string Name { get; set; } } /// @@ -2786,8 +2901,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 +2916,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 +2939,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 +2960,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 +3018,8 @@ public class Employee5 : AnnotatedEmployee /// Empty type /// public class EmptyType - { } + { + } /// /// Class representing items in the table [TableNamePrefix]HashTable @@ -2917,14 +3030,23 @@ public class VersionedEmployee : Employee public int? Version { get; set; } } + public class CounterAnnotatedEmployee : AnnotatedEmployee + { + [DynamoDBAtomicCounter] + public int? CountDefault { get; set; } + + [DynamoDBAtomicCounter(delta:2, startValue: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; } } ///