diff --git a/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json b/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json
new file mode 100644
index 000000000000..7daf9994c195
--- /dev/null
+++ b/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json
@@ -0,0 +1,11 @@
+{
+ "services": [
+ {
+ "serviceName": "DynamoDBv2",
+ "type": "minor",
+ "changeLogMessages": [
+ "Add support for DynamoDBAutoGeneratedTimestampAttribute that sets current timestamp during persistence operations."
+ ]
+ }
+ ]
+}
\ 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 e3b879e5b9e2..64978ed49e03 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
@@ -688,4 +688,49 @@ public DynamoDBLocalSecondaryIndexRangeKeyAttribute(params string[] indexNames)
IndexNames = indexNames.Distinct(StringComparer.Ordinal).ToArray();
}
}
+
+ ///
+ /// Specifies that the decorated property or field should have its value automatically
+ /// set to the current timestamp during persistence operations.
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
+ public sealed class DynamoDBAutoGeneratedTimestampAttribute : DynamoDBPropertyAttribute
+ {
+
+ ///
+ /// Default constructor. Timestamp is set on both create and update.
+ ///
+ public DynamoDBAutoGeneratedTimestampAttribute()
+ : base()
+ {
+ }
+
+
+ ///
+ /// Constructor that specifies an alternate attribute name.
+ ///
+ /// Name of attribute to be associated with property or field.
+ public DynamoDBAutoGeneratedTimestampAttribute(string attributeName)
+ : base(attributeName)
+ {
+ }
+ ///
+ /// Constructor that specifies a custom converter.
+ ///
+ /// Custom converter type.
+ public DynamoDBAutoGeneratedTimestampAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type converter)
+ : base(converter)
+ {
+ }
+
+ ///
+ /// Constructor that specifies an alternate attribute name and a custom converter.
+ ///
+ /// Name of attribute to be associated with property or field.
+ /// Custom converter type.
+ public DynamoDBAutoGeneratedTimestampAttribute(string attributeName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type converter)
+ : base(attributeName, converter)
+ {
+ }
+ }
}
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
index eb8dd75f3d29..114d24d19dae 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
@@ -457,7 +457,7 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants
.ConfigureAwait(false);
}
- if (counterConditionExpression == null && versionExpression == null) return;
+ if (counterConditionExpression == null && versionExpression == null && !storage.Config.HasAutogeneratedProperties) return;
if (returnValues == ReturnValues.AllNewAttributes)
{
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
index cdb02b131c38..46f3edeab7c6 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
@@ -27,6 +27,7 @@
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
+using Amazon.Util;
using ThirdParty.RuntimeBackports;
using Expression = System.Linq.Expressions.Expression;
@@ -139,7 +140,16 @@ private static PropertyStorage[] GetCounterProperties(ItemStorage storage)
{
var counterProperties = storage.Config.BaseTypeStorageConfig.Properties.
Where(propertyStorage => propertyStorage.IsCounter).ToArray();
-
+ var flatten= storage.Config.BaseTypeStorageConfig.Properties.
+ Where(propertyStorage => propertyStorage.FlattenProperties.Any()).ToArray();
+ while (flatten.Any())
+ {
+ var flattenCounters = flatten.SelectMany(p => p.FlattenProperties.Where(fp => fp.IsCounter)).ToArray();
+ counterProperties = counterProperties.Concat(flattenCounters).ToArray();
+ flatten = flatten.SelectMany(p => p.FlattenProperties.Where(fp => fp.FlattenProperties.Any())).ToArray();
+ }
+
+
return counterProperties;
}
@@ -541,6 +551,8 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
storageConfig = config.BaseTypeStorageConfig;
}
+ var now = AWSSDKUtils.CorrectedUtcNow;
+
foreach (PropertyStorage propertyStorage in storageConfig.AllPropertyStorage)
{
// if only keys are being serialized, skip non-key properties
@@ -557,7 +569,7 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
object value;
if (TryGetValue(toStore, propertyStorage.Member, out value))
{
- DynamoDBEntry dbe = ToDynamoDBEntry(propertyStorage, value, flatConfig, propertyStorage.ShouldFlattenChildProperties);
+ DynamoDBEntry dbe = ToDynamoDBEntry(propertyStorage, value, flatConfig);
if (ShouldSave(dbe, ignoreNullValues))
{
@@ -572,6 +584,14 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
{
document[pair.Key] = pair.Value;
}
+
+ if (propertyStorage.FlattenProperties.Any(p => p.IsVersion))
+ {
+ var innerVersionProperty =
+ propertyStorage.FlattenProperties.First(p => p.IsVersion);
+ storage.CurrentVersion =
+ innerDocument[innerVersionProperty.AttributeName] as Primitive;
+ }
}
else
{
@@ -590,6 +610,11 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
storage.CurrentVersion = dbePrimitive;
}
}
+
+ if (dbe == null && propertyStorage.IsAutoGeneratedTimestamp)
+ {
+ document[attributeName] = new Primitive(now.ToString("o"));
+ }
}
else
throw new InvalidOperationException(
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
index 65eefb12aebb..f26161a1acfd 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
@@ -137,21 +137,36 @@ internal class PropertyStorage : SimplePropertyStorage
public bool IsGSIKey { get { return IsGSIHashKey || IsGSIRangeKey; } }
public bool IsIgnored { get; set; }
- // whether to store DateTime as epoch seconds integer
+ ///
+ /// Whether to store DateTime as epoch seconds integer.
+ ///
public bool StoreAsEpoch { get; set; }
- // whether to store DateTime as epoch seconds integer (with support for dates AFTER 2038)
+ ///
+ /// Whether to store DateTime as epoch seconds integer (with support for dates AFTER 2038).
+ ///
public bool StoreAsEpochLong { get; set; }
- // whether to store Type Discriminator for polymorphic serialization
+ ///
+ /// Whether to store Type Discriminator for polymorphic serialization.
+ ///
public bool PolymorphicProperty { get; set; }
- // whether to store child properties at the same level as the parent property
+ ///
+ /// Whether to store child properties at the same level as the parent property.
+ ///
public bool ShouldFlattenChildProperties { get; set; }
- // whether to store property at parent level
+ ///
+ /// Whether to store property at parent level.
+ ///
public bool IsFlattened { get; set; }
+ ///
+ /// Whether to store the property as a timestamp that is automatically generated.
+ ///
+ public bool IsAutoGeneratedTimestamp { get; set; }
+
// corresponding IndexNames, if applicable
public List IndexNames { get; set; }
@@ -242,6 +257,9 @@ public void Validate(DynamoDBContext context)
if (StoreAsEpoch || StoreAsEpochLong)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as StoreAsEpoch or StoreAsEpochLong is set to true");
+ if (IsAutoGeneratedTimestamp)
+ throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as AutoGeneratedTimestamp is set to true.");
+
if (!Utils.CanInstantiateConverter(ConverterType) || !Utils.ImplementsInterface(ConverterType, typeof(IPropertyConverter)))
throw new InvalidOperationException("Converter for " + PropertyName + " must be instantiable with no parameters and must implement IPropertyConverter");
@@ -250,6 +268,9 @@ public void Validate(DynamoDBContext context)
if (StoreAsEpoch && StoreAsEpochLong)
throw new InvalidOperationException(PropertyName + " must not set both StoreAsEpoch and StoreAsEpochLong as true at the same time.");
+
+ if (IsAutoGeneratedTimestamp)
+ Utils.ValidateTimestampType(MemberType);
IPropertyConverter converter;
if (context.ConverterCache.TryGetValue(MemberType, out converter) && converter != null)
@@ -470,6 +491,8 @@ internal class ItemStorageConfig
public string VersionPropertyName { get; private set; }
public bool HasVersion { get { return !string.IsNullOrEmpty(VersionPropertyName); } }
+ public bool HasAutogeneratedProperties { get; internal set; }
+
// attribute-to-index mapping
public Dictionary> AttributeToIndexesNameMapping { get; set; }
@@ -1082,6 +1105,12 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con
propertyStorage.IsRangeKey = true;
}
+ if (propertyAttribute is DynamoDBAutoGeneratedTimestampAttribute)
+ {
+ propertyStorage.IsAutoGeneratedTimestamp = true;
+ config.HasAutogeneratedProperties = true;
+ }
+
DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute;
if (lsiRangeKeyAttribute != null)
{
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
index 5d72b6dd6d3e..903a4d32092e 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
@@ -158,6 +158,21 @@ internal static void ValidateNumericType(Type memberType)
throw new InvalidOperationException("Version property must be of primitive, numeric, integer, nullable type (e.g. int?, long?, byte?)");
}
+ internal static void ValidateTimestampType(Type memberType)
+ {
+ if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Nullable<>) &&
+ (memberType.IsAssignableFrom(typeof(DateTime)) ||
+ memberType.IsAssignableFrom(typeof(DateTimeOffset))))
+ {
+ return;
+ }
+ throw new InvalidOperationException(
+ $"Timestamp properties must be of type Nullable (DateTime?) or Nullable (DateTimeOffset?). " +
+ $"Invalid type: {memberType.FullName}. " +
+ "Please ensure your property is declared as 'DateTime?' or 'DateTimeOffset?'."
+ );
+ }
+
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)]
internal static Type GetPrimitiveElementType(Type collectionType)
{
diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
index 8d855f4b9e4d..d2bee4a27bc0 100644
--- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
+++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
@@ -1289,6 +1289,60 @@ public async Task TestContext_AtomicCounterAnnotation()
Assert.AreEqual(1, storedUpdatedEmployee.CountDefault);
Assert.AreEqual(12, storedUpdatedEmployee.CountAtomic);
+ // --- Flatten scenario with atomic counter and version ---
+ var product = new ProductFlatWithAtomicCounter
+ {
+ Id = 500,
+ Name = "FlatAtomic",
+ Details = new ProductDetailsWithAtomicCounter
+ {
+ Description = "Flat details",
+ Name = "FlatName"
+ }
+ };
+
+ await Context.SaveAsync(product);
+ var loadedProduct = await Context.LoadAsync(product.Id);
+ Assert.IsNotNull(loadedProduct);
+ Assert.IsNotNull(loadedProduct.Details);
+ Assert.AreEqual(0, loadedProduct.Details.CountDefault);
+ Assert.AreEqual(10, loadedProduct.Details.CountAtomic);
+ Assert.AreEqual(0, loadedProduct.Details.Version);
+
+ // Increment counters via null assignment
+ loadedProduct.Details.CountDefault = null;
+ loadedProduct.Details.CountAtomic = null;
+ await Context.SaveAsync(loadedProduct);
+
+ var loadedProductAfterIncrement = await Context.LoadAsync(product.Id);
+ Assert.AreEqual(1, loadedProductAfterIncrement.Details.CountDefault);
+ Assert.AreEqual(12, loadedProductAfterIncrement.Details.CountAtomic);
+ Assert.AreEqual(1, loadedProductAfterIncrement.Details.Version);
+
+ // Simulate a stale POCO for flattened details
+ var staleFlat = new ProductFlatWithAtomicCounter
+ {
+ Id = 500,
+ Name = "FlatAtomic",
+ Details = new ProductDetailsWithAtomicCounter
+ {
+ Description = "Flat details",
+ Name = "FlatName",
+ CountDefault = 0,
+ CountAtomic = 10,
+ Version = 1
+ }
+ };
+ await Context.SaveAsync(staleFlat);
+
+ Assert.AreEqual(2, staleFlat.Details.CountDefault);
+ Assert.AreEqual(14, staleFlat.Details.CountAtomic);
+ Assert.AreEqual(2, staleFlat.Details.Version);
+
+ var loadedFlatLatest = await Context.LoadAsync(product.Id);
+ Assert.AreEqual(2, loadedFlatLatest.Details.CountDefault);
+ Assert.AreEqual(14, loadedFlatLatest.Details.CountAtomic);
+ Assert.AreEqual(2, loadedFlatLatest.Details.Version);
}
[TestMethod]
@@ -1657,6 +1711,224 @@ public async Task Test_FlattenAttribute_With_Annotations()
}
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public async Task Test_AutoGeneratedTimestampAttribute_CreateMode_Simple()
+ {
+ CleanupTables();
+ TableCache.Clear();
+
+ var product = new ProductWithCreateTimestamp
+ {
+ Id = 999,
+ Name = "SimpleCreate"
+ };
+
+ await Context.SaveAsync(product);
+ var loaded = await Context.LoadAsync(product.Id);
+
+ Assert.IsNotNull(loaded);
+ Assert.AreEqual(product.Id, loaded.Id);
+ Assert.AreEqual("SimpleCreate", loaded.Name);
+ Assert.IsNotNull(loaded.CreatedAt);
+ Assert.IsTrue(loaded.CreatedAt > DateTime.MinValue);
+ Assert.AreEqual(product.CreatedAt, loaded.CreatedAt);
+
+ // Save again and verify CreatedAt does not change
+ var createdAt = loaded.CreatedAt;
+ await Task.Delay(1000);
+ loaded.Name = "UpdatedName";
+ await Context.SaveAsync(loaded);
+ var loadedAfterUpdate = await Context.LoadAsync(product.Id);
+ ApproximatelyEqual(createdAt.Value, loadedAfterUpdate.CreatedAt.Value);
+
+ // Test: StoreAsEpoch with AutoGeneratedTimestamp
+ var now = DateTime.UtcNow;
+ var epochEntity = new AutoGenTimestampEpochEntity
+ {
+ Id = 1,
+ Name = "EpochTest"
+ };
+
+ await Context.SaveAsync(epochEntity);
+ var loadedEpochEntity = await Context.LoadAsync(epochEntity.Id);
+
+ Assert.IsNotNull(loadedEpochEntity);
+ Assert.IsTrue(loadedEpochEntity.CreatedAt > DateTime.MinValue);
+ ApproximatelyEqual(epochEntity.CreatedAt.Value, loadedEpochEntity.CreatedAt.Value);
+
+ // Test: StoreAsEpochLong with AutoGeneratedTimestamp
+ var longEpochEntity = new AutoGenTimestampEpochLongEntity
+ {
+ Id = 2,
+ Name = "LongEpochTest",
+ };
+
+ await Context.SaveAsync(longEpochEntity);
+ var loadedLongEpochEntity = await Context.LoadAsync(longEpochEntity.Id);
+
+ Assert.IsNotNull(loadedLongEpochEntity);
+ Assert.IsTrue(loadedLongEpochEntity.CreatedAt > DateTime.MinValue);
+ ApproximatelyEqual(longEpochEntity.CreatedAt.Value, loadedLongEpochEntity.CreatedAt.Value);
+
+
+ // Test: StoreAsEpoch with AutoGeneratedTimestamp (Create)
+ var epochCreateEntity = new AutoGenTimestampEpochEntity
+ {
+ Id = 3,
+ Name = "EpochCreateTest"
+ };
+
+ await Context.SaveAsync(epochCreateEntity);
+ var loadedEpochCreateEntity = await Context.LoadAsync(epochCreateEntity.Id);
+
+ Assert.IsNotNull(loadedEpochCreateEntity);
+ Assert.IsTrue(loadedEpochCreateEntity.CreatedAt > DateTime.MinValue);
+ ApproximatelyEqual(epochCreateEntity.CreatedAt.Value, loadedEpochCreateEntity.CreatedAt.Value);
+
+ }
+
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public async Task Test_AutoGeneratedTimestampAttribute_TransactWrite_Simple()
+ {
+ CleanupTables();
+ TableCache.Clear();
+
+ var product = new ProductWithCreateTimestamp
+ {
+ Id = 1001,
+ Name = "TransactCreate"
+ };
+
+ // Save using TransactWrite
+ var transactWrite = Context.CreateTransactWrite();
+ transactWrite.AddSaveItem(product);
+ await transactWrite.ExecuteAsync();
+
+ var loaded = await Context.LoadAsync(product.Id);
+
+ Assert.IsNotNull(loaded);
+ Assert.AreEqual(product.Id, loaded.Id);
+ Assert.AreEqual("TransactCreate", loaded.Name);
+ Assert.IsNotNull(loaded.CreatedAt);
+ Assert.IsTrue(loaded.CreatedAt > DateTime.MinValue);
+ Assert.AreEqual(product.CreatedAt, loaded.CreatedAt);
+
+ // Save again using TransactWrite and verify CreatedAt does not change
+ var createdAt = loaded.CreatedAt;
+ await Task.Delay(1000);
+ loaded.Name = "TransactUpdated";
+ var transactWrite2 = Context.CreateTransactWrite();
+ transactWrite2.AddSaveItem(loaded);
+ await transactWrite2.ExecuteAsync();
+ var loadedAfterUpdate = await Context.LoadAsync(product.Id);
+ ApproximatelyEqual(createdAt.Value, loadedAfterUpdate.CreatedAt.Value);
+
+ // Test: StoreAsEpoch with AutoGeneratedTimestamp (Always) using TransactWrite
+ var epochEntity = new AutoGenTimestampEpochEntity
+ {
+ Id = 1002,
+ Name = "TransactEpoch"
+ };
+
+ var transactWrite3 = Context.CreateTransactWrite();
+ transactWrite3.AddSaveItem(epochEntity);
+ await transactWrite3.ExecuteAsync();
+ var loadedEpochEntity = await Context.LoadAsync(epochEntity.Id);
+
+ Assert.IsNotNull(loadedEpochEntity);
+ Assert.IsTrue(loadedEpochEntity.CreatedAt > DateTime.MinValue);
+ ApproximatelyEqual(epochEntity.CreatedAt.Value, loadedEpochEntity.CreatedAt.Value);
+ }
+
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public async Task Test_AutoGeneratedTimestampAttribute_With_Annotations()
+ {
+ CleanupTables();
+ TableCache.Clear();
+
+ // 1. Test: AutoGeneratedTimestamp combined with Version and Flatten
+ var now = DateTime.UtcNow;
+ var product = new ProductFlatWithTimestamp
+ {
+ Id = 100,
+ Name = "TimestampedProduct",
+ Details = new ProductDetailsWithTimestamp
+ {
+ Description = "Timestamped details",
+ Name = "DetailsName",
+ }
+ };
+
+ await Context.SaveAsync(product);
+ var savedProduct = await Context.LoadAsync(product.Id);
+ Assert.IsNotNull(savedProduct);
+ Assert.IsNotNull(savedProduct.Details);
+ Assert.IsTrue(savedProduct.Details.CreatedAt > DateTime.MinValue);
+ Assert.AreEqual(0, savedProduct.Details.Version);
+
+ // 2. Test: AutoGeneratedTimestamp combined with AtomicCounter and GSI
+ var employee = new EmployeeWithTimestampAndCounter
+ {
+ Name = "Alice",
+ Age = 25,
+ CompanyName = "TestCompany",
+ Score = 10,
+ ManagerName = "Bob"
+ };
+ await Context.SaveAsync(employee);
+ var loadedEmployee = await Context.LoadAsync(employee.Name, employee.Age);
+ Assert.IsNotNull(loadedEmployee);
+ Assert.IsTrue(loadedEmployee.LastUpdated > DateTime.MinValue);
+ Assert.AreEqual(0, loadedEmployee.CountDefault);
+
+ // 3. Test: AutoGeneratedTimestamp with TimestampMode.Create
+ var productCreateOnly = new ProductWithCreateTimestamp
+ {
+ Id = 200,
+ Name = "CreateOnly"
+ };
+ await Context.SaveAsync(productCreateOnly);
+ var loadedCreateOnly = await Context.LoadAsync(productCreateOnly.Id);
+ Assert.IsNotNull(loadedCreateOnly);
+ var createdAt = loadedCreateOnly.CreatedAt;
+ Assert.IsTrue(createdAt > DateTime.MinValue);
+
+ // Update and verify CreatedAt does not change
+ await Task.Delay(1000);
+ loadedCreateOnly.Name = "UpdatedName";
+ await Context.SaveAsync(loadedCreateOnly);
+ var loadedAfterUpdate = await Context.LoadAsync(productCreateOnly.Id);
+ ApproximatelyEqual(createdAt.Value, loadedAfterUpdate.CreatedAt.Value);
+ }
+
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public async Task Test_AutoGeneratedTimestampAttribute_With_Annotations_BatchWrite()
+ {
+ CleanupTables();
+ TableCache.Clear();
+
+ var entity = new EmployeeWithTimestampAndCounter
+ {
+ Name = "Alice",
+ Age = 25,
+ CompanyName = "TestCompany",
+ Score = 10,
+ ManagerName = "Bob"
+ };
+
+ var batch = Context.CreateBatchWrite();
+ batch.AddPutItem(entity);
+ await batch.ExecuteAsync();
+
+ var loaded = await Context.LoadAsync(entity.Name, entity.Age);
+
+ Assert.IsNotNull(loaded.LastUpdated, "LastUpdated should be set by AutoGeneratedTimestampAttribute");
+ Assert.IsTrue((DateTime.UtcNow - loaded.LastUpdated.Value).TotalMinutes < 1, "LastUpdated should be recent");
+ }
private static void TestEmptyStringsWithFeatureEnabled()
{
@@ -3415,6 +3687,66 @@ private ModelA CreateNestedTypeItem(out Guid id)
#region OPM definitions
+ // Helper classes for the integration test
+
+ [DynamoDBTable("HashTable")]
+ public class ProductFlatWithTimestamp
+ {
+ [DynamoDBHashKey] public int Id { get; set; }
+ [DynamoDBFlatten] public ProductDetailsWithTimestamp Details { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class ProductDetailsWithTimestamp
+ {
+ [DynamoDBVersion] public int? Version { get; set; }
+ [DynamoDBAutoGeneratedTimestamp] public DateTime? CreatedAt { get; set; }
+ public string Description { get; set; }
+ [DynamoDBProperty("DetailsName")] public string Name { get; set; }
+ }
+
+ [DynamoDBTable("HashRangeTable")]
+ public class EmployeeWithTimestampAndCounter : AnnotatedEmployee
+ {
+ [DynamoDBAutoGeneratedTimestamp] public DateTime? LastUpdated { get; set; }
+ [DynamoDBAtomicCounter] public int? CountDefault { get; set; }
+ }
+
+ [DynamoDBTable("HashTable")]
+ public class ProductWithCreateTimestamp
+ {
+ [DynamoDBHashKey] public int Id { get; set; }
+ public string Name { get; set; }
+ [DynamoDBAutoGeneratedTimestamp]
+ public DateTime? CreatedAt { get; set; }
+ }
+
+ [DynamoDBTable("HashTable")]
+ public class AutoGenTimestampEpochEntity
+ {
+ [DynamoDBHashKey]
+ public int Id { get; set; }
+
+ public string Name { get; set; }
+
+ [DynamoDBAutoGeneratedTimestamp]
+ [DynamoDBProperty(StoreAsEpoch = true)]
+ public DateTime? CreatedAt { get; set; }
+ }
+
+ [DynamoDBTable("HashTable")]
+ public class AutoGenTimestampEpochLongEntity
+ {
+ [DynamoDBHashKey]
+ public int Id { get; set; }
+
+ public string Name { get; set; }
+
+ [DynamoDBAutoGeneratedTimestamp]
+ [DynamoDBProperty(StoreAsEpochLong = true)]
+ public DateTime? CreatedAt { get; set; }
+ }
+
public enum Status : long
{
Active = 256,
@@ -3528,6 +3860,29 @@ public class VersionedProduct : Product
[DynamoDBVersion] public int? Version { get; set; }
}
+ // Flattened scenario classes
+ [DynamoDBTable("HashTable")]
+ public class ProductFlatWithAtomicCounter
+ {
+ [DynamoDBHashKey] public int Id { get; set; }
+ [DynamoDBFlatten] public ProductDetailsWithAtomicCounter Details { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class ProductDetailsWithAtomicCounter
+ {
+ [DynamoDBVersion]
+ public int? Version { get; set; }
+ [DynamoDBAtomicCounter]
+ public int? CountDefault { get; set; }
+ [DynamoDBAtomicCounter(delta: 2, startValue: 10)]
+ public int? CountAtomic { get; set; }
+ public string Description { get; set; }
+ [DynamoDBProperty("DetailsName")]
+ public string Name { get; set; }
+ }
+
+
///
/// Class representing items in the table [TableNamePrefix]HashTable,
diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/AWSSDK.UnitTests.DynamoDBv2.NetFramework.csproj b/sdk/test/Services/DynamoDBv2/UnitTests/AWSSDK.UnitTests.DynamoDBv2.NetFramework.csproj
index 9dc80b7a8ae4..07a50357daa1 100644
--- a/sdk/test/Services/DynamoDBv2/UnitTests/AWSSDK.UnitTests.DynamoDBv2.NetFramework.csproj
+++ b/sdk/test/Services/DynamoDBv2/UnitTests/AWSSDK.UnitTests.DynamoDBv2.NetFramework.csproj
@@ -48,31 +48,31 @@
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/ContextInternalTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs
similarity index 53%
rename from sdk/test/Services/DynamoDBv2/UnitTests/Custom/ContextInternalTests.cs
rename to sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs
index 2d94a202a960..914d72c1b2d2 100644
--- a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/ContextInternalTests.cs
+++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs
@@ -5,6 +5,8 @@
using Moq;
using System;
using System.Linq.Expressions;
+using Amazon.Util;
+using DynamoDBContextConfig = Amazon.DynamoDBv2.DataModel.DynamoDBContextConfig;
namespace AWSSDK_DotNet.UnitTests
{
@@ -17,6 +19,11 @@ public class TestEntity
public int Id { get; set; }
[DynamoDBRangeKey]
public string Name { get; set; }
+ public string Description { get; set; }
+ public int? NullableValue { get; set; }
+
+ [DynamoDBAutoGeneratedTimestamp]
+ public DateTime? UpdatedAt { get; set; }
}
private Mock mockClient;
@@ -169,7 +176,7 @@ public void ConvertQueryByValue_WithHashKeyOnly()
Assert.AreEqual(1,actualResult.Filter.ToConditions().Count);
Assert.IsNull(actualResult.FilterExpression);
Assert.IsNotNull(actualResult.AttributesToGet);
- Assert.AreEqual(2,actualResult.AttributesToGet.Count);
+ Assert.AreEqual(typeof(TestEntity).GetProperties().Length,actualResult.AttributesToGet.Count);
}
[TestMethod]
@@ -209,5 +216,185 @@ public void ConvertQueryByValue_WithHashKeyAndExpressionFilter()
Assert.AreEqual("Name", search.Search.FilterExpression.ExpressionAttributeNames["#C0"]);
Assert.AreEqual("bar", search.Search.FilterExpression.ExpressionAttributeValues[":C0"].ToString());
}
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_SerializesAllProperties()
+ {
+ var entity = new TestEntity
+ {
+ Id = 42,
+ Name = "TestName",
+ Description = "TestDescription",
+ NullableValue = 99
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: false, ignoreNullValues: false);
+
+ Assert.IsNotNull(storage);
+ Assert.AreEqual(42, storage.Document["Id"].AsInt());
+ Assert.AreEqual("TestName", storage.Document["Name"].AsString());
+ Assert.AreEqual("TestDescription", storage.Document["Description"].AsString());
+ Assert.AreEqual(99, storage.Document["NullableValue"].AsInt());
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_KeysOnly_SerializesOnlyKeys()
+ {
+ var entity = new TestEntity
+ {
+ Id = 7,
+ Name = "KeyName",
+ Description = "ShouldNotBeSerialized",
+ NullableValue = 123
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: true, ignoreNullValues: false);
+
+ Assert.IsNotNull(storage);
+ Assert.AreEqual(7, storage.Document["Id"].AsInt());
+ Assert.AreEqual("KeyName", storage.Document["Name"].AsString());
+ Assert.IsFalse(storage.Document.ContainsKey("Description"));
+ Assert.IsFalse(storage.Document.ContainsKey("NullableValue"));
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_IgnoreNullValues_DoesNotSerializeNulls()
+ {
+ var entity = new TestEntity
+ {
+ Id = 1,
+ Name = "NullTest",
+ Description = null,
+ NullableValue = null
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: false, ignoreNullValues: true);
+
+ Assert.IsNotNull(storage);
+ Assert.AreEqual(1, storage.Document["Id"].AsInt());
+ Assert.AreEqual("NullTest", storage.Document["Name"].AsString());
+ Assert.IsFalse(storage.Document.ContainsKey("Description"));
+ Assert.IsFalse(storage.Document.ContainsKey("NullableValue"));
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_NullObject_ThrowsArgumentNullException()
+ {
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+
+ Assert.ThrowsException(() =>
+ context.ObjectToItemStorageHelper(null, config, flatConfig, keysOnly: false, ignoreNullValues: false)
+ );
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_AutoGeneratedTimestamp_NullValue_SetsTimestamp()
+ {
+ var entity = new TestEntity
+ {
+ Id = 100,
+ Name = "TimestampTest",
+ UpdatedAt = null
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: false, ignoreNullValues: false);
+
+ Assert.IsNotNull(storage);
+ Assert.IsTrue(storage.Document.ContainsKey("UpdatedAt"));
+ var timestampValue = storage.Document["UpdatedAt"].AsString();
+ Assert.IsNotNull(timestampValue);
+ Assert.IsTrue(DateTime.TryParse(timestampValue, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed));
+ Assert.IsTrue((DateTime.UtcNow - parsed.ToUniversalTime()).TotalSeconds < 10);
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_AutoGeneratedTimestamp_NonNullValue_DoesNotOverwrite()
+ {
+ var now = DateTime.UtcNow.AddDays(-1);
+ var entity = new TestEntity
+ {
+ Id = 101,
+ Name = "TimestampTest",
+ UpdatedAt = now
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: false, ignoreNullValues: false);
+
+ Assert.IsNotNull(storage);
+ Assert.IsTrue(storage.Document.ContainsKey("UpdatedAt"));
+ var timestampValue = storage.Document["UpdatedAt"].AsDateTime();
+ Assert.AreEqual(now.ToUniversalTime().ToString(AWSSDKUtils.ISO8601DateFormat), timestampValue.ToUniversalTime().ToString(AWSSDKUtils.ISO8601DateFormat));
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_AutoGeneratedTimestamp_IgnoreNullValues_StillSetsTimestamp()
+ {
+ var entity = new TestEntity
+ {
+ Id = 102,
+ Name = "TimestampTest",
+ UpdatedAt = null
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: false, ignoreNullValues: true);
+
+ Assert.IsNotNull(storage);
+ Assert.IsTrue(storage.Document.ContainsKey("UpdatedAt"));
+ var timestampValue = storage.Document["UpdatedAt"].AsString();
+ Assert.IsNotNull(timestampValue);
+ Assert.IsTrue(DateTime.TryParse(timestampValue, null, System.Globalization.DateTimeStyles.RoundtripKind, out _));
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_AutoGeneratedTimestamp_NotMarkedProperty_NullValue_DoesNotSetTimestamp()
+ {
+ var entity = new TestEntity
+ {
+ Id = 103,
+ Name = null, // Not marked as auto-generated timestamp
+ UpdatedAt = null
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: false, ignoreNullValues: true);
+
+ Assert.IsNotNull(storage);
+ Assert.IsFalse(storage.Document.ContainsKey("Name"));
+ }
+
+ [TestMethod]
+ public void ObjectToItemStorageHelper_AutoGeneratedTimestamp_KeysOnly_DoesNotSetTimestamp()
+ {
+ var entity = new TestEntity
+ {
+ Id = 104,
+ Name = "KeysOnlyTest",
+ UpdatedAt = null
+ };
+
+ var flatConfig = new DynamoDBFlatConfig(new DynamoDBOperationConfig(), context.Config);
+ var config = context.StorageConfigCache.GetConfig(flatConfig);
+ var storage = context.ObjectToItemStorageHelper(entity, config, flatConfig, keysOnly: true, ignoreNullValues: false);
+
+ Assert.IsNotNull(storage);
+ // Only keys should be present
+ Assert.IsTrue(storage.Document.ContainsKey("Id"));
+ Assert.IsTrue(storage.Document.ContainsKey("Name"));
+ Assert.IsFalse(storage.Document.ContainsKey("UpdatedAt"));
+ }
}
}
\ No newline at end of file
diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/PropertyStorageTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/PropertyStorageTests.cs
new file mode 100644
index 000000000000..3a19bba97266
--- /dev/null
+++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/PropertyStorageTests.cs
@@ -0,0 +1,278 @@
+using Amazon.DynamoDBv2.DataModel;
+using Amazon.DynamoDBv2.DocumentModel;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using System;
+using System.Collections.Generic;
+using Amazon.DynamoDBv2;
+
+namespace AWSSDK_DotNet.UnitTests
+{
+ [TestClass]
+ public class PropertyStorageTests
+ {
+ private class TestClass
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public int? Counter { get; set; }
+ public int? Version { get; set; }
+ public DateTime? Timestamp { get; set; }
+ }
+
+ private PropertyStorage CreatePropertyStorage(string propertyName = "Id")
+ {
+ var member = typeof(TestClass).GetProperty(propertyName);
+ return new PropertyStorage(member);
+ }
+
+ private class DummyContext : DynamoDBContext
+ {
+
+ public DummyContext(IAmazonDynamoDB client) : base(client, false, null)
+ {
+ }
+
+ }
+
+ private class FakePropertyConverter : IPropertyConverter
+ {
+ public object FromEntry(DynamoDBEntry entry) => null;
+ public DynamoDBEntry ToEntry(object value) => null;
+ }
+
+
+ [TestMethod]
+ public void AddIndex_AddsIndexToIndexesList()
+ {
+ var storage = CreatePropertyStorage();
+ var gsi = new PropertyStorage.GSI(true, "Attr", "Index1");
+ storage.AddIndex(gsi);
+
+ Assert.AreEqual(1, storage.Indexes.Count);
+ Assert.AreSame(gsi, storage.Indexes[0]);
+ }
+
+ [TestMethod]
+ public void AddGsiIndex_AddsGSIIndex()
+ {
+ var storage = CreatePropertyStorage();
+ storage.AddGsiIndex(true, "Attr", "Index1", "Index2");
+
+ Assert.AreEqual(1, storage.Indexes.Count);
+ var gsi = storage.Indexes[0] as PropertyStorage.GSI;
+ Assert.IsNotNull(gsi);
+ Assert.IsTrue(gsi.IsHashKey);
+ CollectionAssert.AreEquivalent(new List { "Index1", "Index2" }, gsi.IndexNames);
+ }
+
+ [TestMethod]
+ public void AddLsiIndex_AddsLSIIndex()
+ {
+ var storage = CreatePropertyStorage();
+ storage.AddLsiIndex("Attr", "Index1");
+
+ Assert.AreEqual(1, storage.Indexes.Count);
+ var lsi = storage.Indexes[0] as PropertyStorage.LSI;
+ Assert.IsNotNull(lsi);
+ Assert.AreEqual("Attr", lsi.AttributeName);
+ CollectionAssert.AreEquivalent(new List { "Index1" }, lsi.IndexNames);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIfBothHashAndRangeKey()
+ {
+ var storage = CreatePropertyStorage("Name");
+ storage.IsHashKey = true;
+ storage.IsRangeKey = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ storage.Validate(null);
+ }
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIfStoreAsEpochAndStoreAsEpochLong()
+ {
+ var storage = CreatePropertyStorage("Timestamp");
+ storage.StoreAsEpoch = true;
+ storage.StoreAsEpochLong = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ storage.Validate(null);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIfConverterTypeAndPolymorphicProperty()
+ {
+ var storage = CreatePropertyStorage("Name");
+ storage.ConverterType = typeof(object); // Not a real converter, but triggers the check
+ storage.PolymorphicProperty = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ storage.Validate(null);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIfConverterTypeAndShouldFlattenChildProperties()
+ {
+ var storage = CreatePropertyStorage("Name");
+ storage.ConverterType = typeof(object);
+ storage.ShouldFlattenChildProperties = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ storage.Validate(null);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIfConverterTypeAndStoreAsEpoch()
+ {
+ var storage = CreatePropertyStorage("Timestamp");
+ storage.ConverterType = typeof(object);
+ storage.StoreAsEpoch = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ storage.Validate(null);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIfConverterTypeAndIsAutoGeneratedTimestamp()
+ {
+ var storage = CreatePropertyStorage("Timestamp");
+ storage.ConverterType = typeof(object);
+ storage.IsAutoGeneratedTimestamp = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ storage.Validate(null);
+ }
+ [TestMethod]
+ public void Validate_AllowsIsVersionOnNumericType()
+ {
+ var mockClient = new Mock();
+ var context = new DummyContext(mockClient.Object);
+
+ var storage = CreatePropertyStorage("Version");
+ storage.IsVersion = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ // Should not throw for int property
+ storage.Validate(context);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIsVersionOnNonNumericType()
+ {
+ var storage = CreatePropertyStorage("Name");
+ storage.IsVersion = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ // Should throw for string property
+ storage.Validate(null);
+ }
+
+ [TestMethod]
+ public void Validate_AllowsIsCounterOnNumericType()
+ {
+ var mockClient = new Mock();
+ var context = new DummyContext(mockClient.Object);
+
+ var storage = CreatePropertyStorage("Counter");
+ storage.IsCounter = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ // Should not throw for int property
+ storage.Validate(context);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIsCounterOnNonNumericType()
+ {
+ var storage = CreatePropertyStorage("Name");
+ storage.IsCounter = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ // Should throw for string property
+ storage.Validate(null);
+ }
+
+
+ [TestMethod]
+ public void Validate_AllowsIsAutoGeneratedTimestampOnDateTime()
+ {
+ var mockClient = new Mock();
+ var context = new DummyContext(mockClient.Object);
+
+ var storage = CreatePropertyStorage("Timestamp");
+ storage.IsAutoGeneratedTimestamp = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ // Should not throw for DateTime property
+ storage.Validate(context);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void Validate_ThrowsIsAutoGeneratedTimestampOnNonDateTime()
+ {
+ var storage = CreatePropertyStorage("Id");
+ storage.IsAutoGeneratedTimestamp = true;
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ // Should throw for int property
+ storage.Validate(null);
+ }
+
+ [TestMethod]
+ public void Validate_UsesConverterFromContextCache()
+ {
+ var mockClient = new Mock();
+ var context = new DummyContext(mockClient.Object);
+
+ var storage = CreatePropertyStorage("Id");
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+
+ var fakeConverter = new FakePropertyConverter();
+ context.ConverterCache[typeof(int)] = fakeConverter;
+
+ storage.Validate(context);
+
+ Assert.AreSame(fakeConverter, storage.Converter);
+ }
+
+ [TestMethod]
+ public void Validate_PopulatesIndexNamesFromIndexes()
+ {
+ var mockClient = new Mock();
+ var context = new DummyContext(mockClient.Object);
+
+ var storage = CreatePropertyStorage("Id");
+ storage.IndexNames = new List();
+ storage.FlattenProperties = new List();
+ storage.AddGsiIndex(true, "Attr", "IndexA", "IndexB");
+
+ storage.Validate(context);
+
+ CollectionAssert.Contains(storage.IndexNames, "IndexA");
+ CollectionAssert.Contains(storage.IndexNames, "IndexB");
+ }
+ }
+}
\ No newline at end of file
diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/UtilsTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/UtilsTests.cs
new file mode 100644
index 000000000000..de5b4e68260d
--- /dev/null
+++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/UtilsTests.cs
@@ -0,0 +1,81 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+
+namespace AWSSDK_DotNet.UnitTests
+{
+ [TestClass]
+ public class UtilsTests
+ {
+ [TestMethod]
+ public void ValidateTimestampType_WithNullableDateTime_DoesNotThrow()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateTimestampType(typeof(DateTime?));
+ }
+
+ [TestMethod]
+ public void ValidateTimestampType_WithNullableDateTimeOffset_DoesNotThrow()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateTimestampType(typeof(DateTimeOffset?));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ValidateTimestampType_WithDateTime_ThrowsException()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateTimestampType(typeof(DateTime));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ValidateTimestampType_WithDateTimeOffset_ThrowsException()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateTimestampType(typeof(DateTimeOffset));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ValidateTimestampType_WithString_ThrowsException()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateTimestampType(typeof(string));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ValidateTimestampType_WithInt_ThrowsException()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateTimestampType(typeof(int));
+ }
+
+ [TestMethod]
+ public void ValidateNumericType_WithNullableInt_DoesNotThrow()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateNumericType(typeof(int?));
+ }
+
+ [TestMethod]
+ public void ValidateNumericType_WithNullableLong_DoesNotThrow()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateNumericType(typeof(long?));
+ }
+
+ [TestMethod]
+ public void ValidateNumericType_WithNullableByte_DoesNotThrow()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateNumericType(typeof(byte?));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ValidateNumericType_WithInt_ThrowsException()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateNumericType(typeof(int));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ValidateNumericType_WithString_ThrowsException()
+ {
+ Amazon.DynamoDBv2.DataModel.Utils.ValidateNumericType(typeof(string));
+ }
+ }
+}