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; }
}
///