From fbaf35bdcf4cb12ab11cb811e312e404465cdac1 Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Thu, 4 Sep 2025 18:18:29 +0300 Subject: [PATCH 1/4] implement DynamoDBAutoGeneratedTimestampAttribute --- .../c952ab1e-3056-4598-9d0e-f7f02187e982.json | 11 + .../DynamoDBv2/Custom/DataModel/Attributes.cs | 45 +++ .../DynamoDBv2/Custom/DataModel/Context.cs | 2 +- .../Custom/DataModel/ContextInternal.cs | 29 +- .../Custom/DataModel/InternalModel.cs | 40 +- .../DynamoDBv2/Custom/DataModel/Utils.cs | 15 + .../IntegrationTests/DataModelTests.cs | 356 ++++++++++++++++++ ...K.UnitTests.DynamoDBv2.NetFramework.csproj | 26 +- .../{ => DataModel}/ContextInternalTests.cs | 189 +++++++++- .../DocumentModel/PropertyStorageTests.cs | 278 ++++++++++++++ 10 files changed, 969 insertions(+), 22 deletions(-) create mode 100644 generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json rename sdk/test/Services/DynamoDBv2/UnitTests/Custom/{ => DataModel}/ContextInternalTests.cs (53%) create mode 100644 sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/PropertyStorageTests.cs 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..69a580771fac --- /dev/null +++ b/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json @@ -0,0 +1,11 @@ +{ + "services": [ + { + "serviceName": "DynamoDBv2", + "type": "patch", + "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..c23eb16cdfb2 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,13 @@ 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..34153a067a58 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,225 @@ 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); + // Assert.IsTrue(Math.Abs((createdAt.Value - loadedAfterUpdate.CreatedAt.Value).TotalMilliseconds) < 1); + + // 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 +3688,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 +3861,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..53c53eb8e1bb 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(5,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 From 7e46b36c9b37ef9855be71c8a2fbb1a9642184c7 Mon Sep 17 00:00:00 2001 From: Irina Herciu Date: Mon, 15 Sep 2025 12:41:10 +0300 Subject: [PATCH 2/4] Update sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index 34153a067a58..d2bee4a27bc0 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -1741,7 +1741,6 @@ public async Task Test_AutoGeneratedTimestampAttribute_CreateMode_Simple() await Context.SaveAsync(loaded); var loadedAfterUpdate = await Context.LoadAsync(product.Id); ApproximatelyEqual(createdAt.Value, loadedAfterUpdate.CreatedAt.Value); - // Assert.IsTrue(Math.Abs((createdAt.Value - loadedAfterUpdate.CreatedAt.Value).TotalMilliseconds) < 1); // Test: StoreAsEpoch with AutoGeneratedTimestamp var now = DateTime.UtcNow; From 545727c91b06aae63f1521aba99f655fd502151f Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Mon, 15 Sep 2025 15:02:18 +0300 Subject: [PATCH 3/4] address pr feedback --- .../c952ab1e-3056-4598-9d0e-f7f02187e982.json | 2 +- .../Custom/DataModel/ContextInternalTests.cs | 2 +- .../Custom/DocumentModel/UtilsTests.cs | 81 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/UtilsTests.cs diff --git a/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json b/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json index 69a580771fac..7daf9994c195 100644 --- a/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json +++ b/generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json @@ -2,7 +2,7 @@ "services": [ { "serviceName": "DynamoDBv2", - "type": "patch", + "type": "minor", "changeLogMessages": [ "Add support for DynamoDBAutoGeneratedTimestampAttribute that sets current timestamp during persistence operations." ] diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs index 53c53eb8e1bb..914d72c1b2d2 100644 --- a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs +++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DataModel/ContextInternalTests.cs @@ -176,7 +176,7 @@ public void ConvertQueryByValue_WithHashKeyOnly() Assert.AreEqual(1,actualResult.Filter.ToConditions().Count); Assert.IsNull(actualResult.FilterExpression); Assert.IsNotNull(actualResult.AttributesToGet); - Assert.AreEqual(5,actualResult.AttributesToGet.Count); + Assert.AreEqual(typeof(TestEntity).GetProperties().Length,actualResult.AttributesToGet.Count); } [TestMethod] 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)); + } + } +} From a1dd3ee5733365099ffc0abf775e9b869bfeb10b Mon Sep 17 00:00:00 2001 From: irina-herciu Date: Mon, 13 Oct 2025 09:08:27 +0300 Subject: [PATCH 4/4] remove empty line --- sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index c23eb16cdfb2..f26161a1acfd 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -1109,7 +1109,6 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con { propertyStorage.IsAutoGeneratedTimestamp = true; config.HasAutogeneratedProperties = true; - } DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute;