diff --git a/generator/.DevConfigs/bc057d5e-a710-458b-ac2e-92e0cf88b741.json b/generator/.DevConfigs/bc057d5e-a710-458b-ac2e-92e0cf88b741.json new file mode 100644 index 000000000000..22c32d2c5a53 --- /dev/null +++ b/generator/.DevConfigs/bc057d5e-a710-458b-ac2e-92e0cf88b741.json @@ -0,0 +1,11 @@ +{ + "services": [ + { + "serviceName": "DynamoDBv2", + "type": "patch", + "changeLogMessages": [ + "Implement DynamoDBDerivedTypeAttribute to enable polymorphism support for nested items on save and load data." + ] + } + ] +} \ 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 587e0edb36a1..875db34dce40 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs @@ -71,6 +71,91 @@ public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties) } } + /// + /// DynamoDB attribute that marks a class or property for polymorphism support. + /// This allows DynamoDB to store and retrieve instances of derived types + /// while preserving their original types during serialization and deserialization. + /// + /// + /// To enable polymorphic serialization, this attribute should be applied multiple times, + /// once for each possible subtype. The serves as a unique + /// identifier used in the database to distinguish between different derived types. + /// + /// The name of the stored discriminator attribute in DynamoDB can be configured via + /// . + /// If not explicitly set, the SDK will use a default attribute name for the discriminator. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = true)] + public sealed class DynamoDBPolymorphicTypeAttribute : DynamoDBAttribute + { + /// + /// Unique name discriminator of the derived type. + /// + /// + /// The is stored in DynamoDB and is used to + /// determine the actual type of the object when deserializing. + /// It should be unique among all declared polymorphic types for a given base type. + /// + /// The attribute name under which this discriminator is stored in DynamoDB + /// is configurable via . + /// + /// Example: + /// + /// [DynamoDBPolymorphicType("dog", typeof(Dog))] + /// [DynamoDBPolymorphicType("cat", typeof(Cat))] + /// public class Animal { } + /// + /// + /// When retrieving an item, DynamoDB will use this discriminator value to + /// deserialize into the appropriate derived type. + /// + public string TypeDiscriminator { get; } + + /// + /// The specific derived type that corresponds to this polymorphic entry. + /// + /// + /// This should be a subclass of the property type where the attribute is applied. + /// When an instance of this type is stored in DynamoDB, the + /// will be saved along with it, allowing correct type resolution during deserialization. + /// + [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] + public Type DerivedType { get; } + + /// + /// Constructs an instance of . + /// + /// + /// A unique string identifier representing this derived type. + /// This value is stored in DynamoDB and used for deserialization. + /// + /// + /// The actual derived type that this attribute represents. + /// Must be a subclass of the property type to which it is applied. + /// + /// + /// Usage for a polymorphic property: + /// + /// public class Zoo + /// { + /// [DynamoDBPolymorphicType("dog", typeof(Dog))] + /// [DynamoDBPolymorphicType("cat", typeof(Cat))] + /// public Animal Resident { get; set; } + /// } + /// + /// + /// In this example, when a `Dog` instance is stored, DynamoDB will save `"dog"` as its discriminator + /// under the attribute name configured in . + /// When retrieved, the stored discriminator ensures that the value is deserialized as a `Dog` instance. + /// + public DynamoDBPolymorphicTypeAttribute(string typeDiscriminator, + [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type derivedType) + { + TypeDiscriminator = typeDiscriminator; + DerivedType = derivedType; + } + } + /// /// DynamoDB attribute that directs the specified attribute not to /// be included when saving or loading objects. diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs index cdc0a536342e..4e7686c9069d 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs @@ -140,6 +140,21 @@ public DynamoDBContextConfig() /// Service calls made via will always return /// attributes in UTC. public bool? RetrieveDateTimeInUtc { get; set; } + + /// + /// Gets or sets the attribute name used to store the type discriminator for polymorphic types in DynamoDB. + /// + /// + /// When working with polymorphic types—where a base class or interface has multiple derived implementations + /// it's essential to preserve the specific type information during serialization and deserialization + /// when using the . + /// + /// By default, the SDK uses a predefined attribute name of "$type" to store this type discriminator in your DynamoDB items. + /// However, you can customize this attribute name to align with your application's naming conventions or to avoid + /// conflicts with existing attributes. + /// + public string DerivedTypeAttributeName { get; set; } + } /// @@ -434,6 +449,9 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte !string.IsNullOrEmpty(operationConfig.IndexName) ? operationConfig.IndexName : DefaultIndexName; List queryFilter = operationConfig.QueryFilter ?? new List(); ConditionalOperatorValues conditionalOperator = operationConfig.ConditionalOperator; + string derivedTypeAttributeName = + //!string.IsNullOrEmpty(operationConfig.DerivedTypeAttributeName) ? operationConfig.DerivedTypeAttributeName : + !string.IsNullOrEmpty(contextConfig.DerivedTypeAttributeName) ? contextConfig.DerivedTypeAttributeName : "$type"; ConsistentRead = consistentRead; SkipVersionCheck = skipVersionCheck; @@ -449,6 +467,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte MetadataCachingMode = metadataCachingMode; DisableFetchingTableMetadata = disableFetchingTableMetadata; RetrieveDateTimeInUtc = retrieveDateTimeInUtc; + DerivedTypeAttributeName = derivedTypeAttributeName; State = new OperationState(); } @@ -550,6 +569,21 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte // State of the operation using this config internal OperationState State { get; private set; } + /// + /// Property indicating the name of the attribute used to store the type discriminator for polymorphic types in DynamoDB. + /// Default value is "$type" if not set in the config. + /// + /// + /// When working with polymorphic types—where a base class or interface has multiple derived implementations + /// it's essential to preserve the specific type information during serialization and deserialization + /// when using the . + /// + /// By default, the SDK uses a predefined attribute name of "$type" to store this type discriminator in your DynamoDB items. + /// However, you can customize this attribute name to align with your application's naming conventions or to avoid + /// conflicts with existing attributes. + /// + public string DerivedTypeAttributeName { get; set; } + public class OperationState { private CircularReferenceTracking referenceTracking; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 781292ac6845..db18fc07fd22 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -281,14 +281,14 @@ private static void CompareKeys(ItemStorageConfig config, Table table, List derivedTypeKeysDictionary, out object output) { return targetType.IsArray ? - TryFromListToArray(targetType, list, flatConfig, out output) : //targetType is Array - TryFromListToIList(targetType, list, flatConfig, out output) ; //targetType is IList or has Add method. + TryFromListToArray(targetType, list, flatConfig, derivedTypeKeysDictionary, out output) : //targetType is Array + TryFromListToIList(targetType, list, flatConfig, derivedTypeKeysDictionary, out output) ; //targetType is IList or has Add method. } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062", Justification = "The user's type has been annotated with InternalConstants.DataModelModeledType with the public API into the library. At this point the type will not be trimmed.")] - private bool TryFromListToIList([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, DynamoDBList list, DynamoDBFlatConfig flatConfig, out object output) + private bool TryFromListToIList([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, DynamoDBList list, DynamoDBFlatConfig flatConfig,Dictionary derivedTypeKeysDictionary, out object output) { if ((!Utils.ImplementsInterface(targetType, typeof(ICollection<>)) && !Utils.ImplementsInterface(targetType, typeof(IList))) || @@ -542,7 +589,7 @@ private bool TryFromListToIList([DynamicallyAccessedMembers(InternalConstants.Da var collection = Utils.Instantiate(targetType); IList ilist = collection as IList; bool useIListInterface = ilist != null; - var propertyStorage = new SimplePropertyStorage(elementType); + var propertyStorage = new SimplePropertyStorage(elementType, derivedTypeKeysDictionary); MethodInfo collectionAdd = null; if (!useIListInterface) @@ -564,7 +611,7 @@ private bool TryFromListToIList([DynamicallyAccessedMembers(InternalConstants.Da return true; } - private bool TryFromListToArray([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, DynamoDBList list, DynamoDBFlatConfig flatConfig, out object output) + private bool TryFromListToArray([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, DynamoDBList list, DynamoDBFlatConfig flatConfig, Dictionary derivedTypeKeysDictionary, out object output) { if (!Utils.CanInstantiateArray(targetType)) { @@ -574,8 +621,7 @@ private bool TryFromListToArray([DynamicallyAccessedMembers(InternalConstants.Da var elementType = Utils.GetElementType(targetType); var array = (Array)Utils.InstantiateArray(targetType,list.Entries.Count); - var propertyStorage = new SimplePropertyStorage(elementType); - + var propertyStorage = new SimplePropertyStorage(elementType, derivedTypeKeysDictionary); for (int i = 0; i < list.Entries.Count; i++) { @@ -590,7 +636,7 @@ private bool TryFromListToArray([DynamicallyAccessedMembers(InternalConstants.Da [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067", Justification = "The user's type has been annotated with InternalConstants.DataModelModeledType with the public API into the library. At this point the type will not be trimmed.")] - private bool TryFromMap([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, Document map, DynamoDBFlatConfig flatConfig, out object output) + private bool TryFromMap([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType, Document map, DynamoDBFlatConfig flatConfig, Dictionary derivedTypeKeysDictionary, out object output) { output = null; @@ -603,7 +649,7 @@ private bool TryFromMap([DynamicallyAccessedMembers(InternalConstants.DataModelM var dictionary = Utils.Instantiate(targetType); var idictionary = dictionary as IDictionary; - var propertyStorage = new SimplePropertyStorage(valueType); + var propertyStorage = new SimplePropertyStorage(valueType, derivedTypeKeysDictionary); foreach (var kvp in map) { @@ -637,7 +683,17 @@ private DynamoDBEntry ToDynamoDBEntry(SimplePropertyStorage propertyStorage, obj return entry; } - var type = propertyStorage.MemberType; + Type type; + string typeDiscriminator = null; + if (propertyStorage.DerivedTypesDictionary.ContainsKey(value.GetType())) + { + typeDiscriminator = propertyStorage.DerivedTypesDictionary[value.GetType()]; + type = value.GetType(); + } + else + { + type = propertyStorage.MemberType; + } if (canReturnScalarInsteadOfList) { @@ -651,20 +707,21 @@ private DynamoDBEntry ToDynamoDBEntry(SimplePropertyStorage propertyStorage, obj else { Document map; - if (TryToMap(value, type, flatConfig, out map)) + if (TryToMap(value, type, flatConfig, propertyStorage.DerivedTypesDictionary, out map)) return map; DynamoDBList list; - if (TryToList(value, type, flatConfig, out list)) + if (TryToList(value, type, flatConfig, propertyStorage.DerivedTypesDictionary, out list)) return list; - return SerializeToDocument(value, type, flatConfig); + return SerializeToDocument(value, type, flatConfig, typeDiscriminator); } } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067", Justification = "The user's type has been annotated with InternalConstants.DataModelModeledType with the public API into the library. At this point the type will not be trimmed.")] - private bool TryToMap(object value, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, DynamoDBFlatConfig flatConfig, out Document output) + private bool TryToMap(object value, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, DynamoDBFlatConfig flatConfig, + Dictionary derivedTypesDictionary, out Document output) { output = null; @@ -677,7 +734,7 @@ private bool TryToMap(object value, [DynamicallyAccessedMembers(InternalConstant return false; output = new Document(); - SimplePropertyStorage propertyStorage = new SimplePropertyStorage(valueType); + SimplePropertyStorage propertyStorage = new SimplePropertyStorage(valueType,derivedTypesDictionary); foreach (object keyValue in idictionary.Keys) { @@ -697,7 +754,8 @@ private bool TryToMap(object value, [DynamicallyAccessedMembers(InternalConstant return true; } - private bool TryToList(object value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, DynamoDBFlatConfig flatConfig, out DynamoDBList output) + private bool TryToList(object value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, DynamoDBFlatConfig flatConfig, + Dictionary derivedTypesDictionary, out DynamoDBList output) { if (!Utils.ImplementsInterface(type, typeof(ICollection<>))) { @@ -715,7 +773,8 @@ private bool TryToList(object value, [DynamicallyAccessedMembers(DynamicallyAcce } Type elementType = Utils.GetElementType(type); - SimplePropertyStorage propertyStorage = new SimplePropertyStorage(elementType); + + SimplePropertyStorage propertyStorage = new SimplePropertyStorage(elementType,derivedTypesDictionary); output = new DynamoDBList(); foreach (var item in enumerable) { @@ -723,7 +782,9 @@ private bool TryToList(object value, [DynamicallyAccessedMembers(DynamicallyAcce if (item == null) entry = DynamoDBNull.Null; else + { entry = ToDynamoDBEntry(propertyStorage, item, flatConfig); + } output.Add(entry); } @@ -752,7 +813,7 @@ private bool TryToScalar(object value, Type type, DynamoDBFlatConfig flatConfig, { try { - entry = SerializeToDocument(value, elementType, flatConfig); + entry = SerializeToDocument(value, elementType, flatConfig, typeDiscriminator: null); return true; } catch { } @@ -807,11 +868,16 @@ private object DeserializeFromDocument(Document document, [DynamicallyAccessedMe /// Serializes a given value to Document /// Use only for property conversions, not for full item conversion /// - private Document SerializeToDocument(object value, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, DynamoDBFlatConfig flatConfig) + private Document SerializeToDocument(object value, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, DynamoDBFlatConfig flatConfig, string typeDiscriminator) { ItemStorageConfig config = StorageConfigCache.GetConfig(type, flatConfig, conversionOnly: true); var itemStorage = ObjectToItemStorageHelper(value, config, flatConfig, keysOnly: false, ignoreNullValues: flatConfig.IgnoreNullValues.Value); var doc = itemStorage.Document; + if (typeDiscriminator != null) + { + var typeAttributeName = flatConfig.DerivedTypeAttributeName; + doc[typeAttributeName] = new Primitive(typeDiscriminator); + } return doc; } @@ -838,6 +904,7 @@ private static bool TrySetValue(object instance, MemberInfo member, object value return false; } } + private static bool TryGetValue(object instance, MemberInfo member, out object value) { FieldInfo fieldInfo = member as FieldInfo; @@ -870,7 +937,7 @@ private ScanFilter ComposeScanFilter(IEnumerable conditions, Item { foreach (var condition in conditions) { - PropertyStorage propertyStorage = storageConfig.GetPropertyStorage(condition.PropertyName); + PropertyStorage propertyStorage = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(condition.PropertyName); List attributeValues = new List(); foreach (var value in condition.Values) { @@ -909,7 +976,7 @@ private QueryFilter ComposeQueryFilter(DynamoDBFlatConfig currentConfig, object string hashKeyProperty = storageConfig.HashKeyPropertyNames[0]; hashKeyProperty = storageConfig.GetCorrectHashKeyProperty(currentConfig, hashKeyProperty); - PropertyStorage propertyStorage = storageConfig.GetPropertyStorage(hashKeyProperty); + PropertyStorage propertyStorage = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(hashKeyProperty); string hashAttributeName = propertyStorage.AttributeName; DynamoDBEntry hashKeyEntry = ValueToDynamoDBEntry(propertyStorage, hashKeyValue, currentConfig); @@ -952,7 +1019,7 @@ private QueryFilter ComposeQueryFilterHelper( // Configure hash-key equality condition string hashKeyProperty = storageConfig.HashKeyPropertyNames[0]; hashKeyProperty = storageConfig.GetCorrectHashKeyProperty(currentConfig, hashKeyProperty); - PropertyStorage propertyStorage = storageConfig.GetPropertyStorage(hashKeyProperty); + PropertyStorage propertyStorage = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(hashKeyProperty); string attributeName = propertyStorage.AttributeName; DynamoDBEntry hashValue = hashKey[attributeName]; filter.AddCondition(attributeName, QueryOperator.Equal, hashValue); @@ -970,7 +1037,7 @@ private QueryFilter ComposeQueryFilterHelper( $"or {nameof(DynamoDBGlobalSecondaryIndexRangeKeyAttribute)}."); } object[] conditionValues = condition.Values; - PropertyStorage conditionProperty = storageConfig.GetPropertyStorage(condition.PropertyName); + PropertyStorage conditionProperty = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(condition.PropertyName); if (conditionProperty.IsLSIRangeKey || conditionProperty.IsGSIKey) indexNames.AddRange(conditionProperty.IndexNames); if (conditionProperty.IsRangeKey) @@ -984,7 +1051,7 @@ private QueryFilter ComposeQueryFilterHelper( foreach (ScanCondition condition in currentConfig.QueryFilter) { object[] conditionValues = condition.Values; - PropertyStorage conditionProperty = storageConfig.GetPropertyStorage(condition.PropertyName); + PropertyStorage conditionProperty = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(condition.PropertyName); List attributeValues = ConvertConditionValues(conditionValues, conditionProperty, currentConfig, canReturnScalarInsteadOfList: true); filter.AddCondition(conditionProperty.AttributeName, condition.Operator, attributeValues); } @@ -1075,13 +1142,13 @@ private static void ValidateKey(Key key, ItemStorageConfig storageConfig) foreach (string hashKey in storageConfig.HashKeyPropertyNames) { - string attributeName = storageConfig.GetPropertyStorage(hashKey).AttributeName; + string attributeName = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(hashKey).AttributeName; if (!key.ContainsKey(attributeName)) throw new InvalidOperationException("Key missing hash key " + hashKey); } foreach (string rangeKey in storageConfig.RangeKeyPropertyNames) { - string attributeName = storageConfig.GetPropertyStorage(rangeKey).AttributeName; + string attributeName = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(rangeKey).AttributeName; if (!key.ContainsKey(attributeName)) throw new InvalidOperationException("Key missing range key " + rangeKey); } @@ -1098,7 +1165,7 @@ internal Key MakeKey(object hashKey, object rangeKey, ItemStorageConfig storageC Key key = new Key(); string hashKeyPropertyName = storageConfig.HashKeyPropertyNames[0]; - PropertyStorage hashKeyProperty = storageConfig.GetPropertyStorage(hashKeyPropertyName); + PropertyStorage hashKeyProperty = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(hashKeyPropertyName); DynamoDBEntry hashKeyEntry = ValueToDynamoDBEntry(hashKeyProperty, hashKey, flatConfig); if (hashKeyEntry == null) throw new InvalidOperationException("Unable to convert hash key value for property " + hashKeyPropertyName); @@ -1116,7 +1183,7 @@ internal Key MakeKey(object hashKey, object rangeKey, ItemStorageConfig storageC } string rangeKeyPropertyName = storageConfig.RangeKeyPropertyNames[0]; - PropertyStorage rangeKeyProperty = storageConfig.GetPropertyStorage(rangeKeyPropertyName); + PropertyStorage rangeKeyProperty = storageConfig.BaseTypeStorageConfig.GetPropertyStorage(rangeKeyPropertyName); DynamoDBEntry rangeKeyEntry = ValueToDynamoDBEntry(rangeKeyProperty, rangeKey, flatConfig); if (rangeKeyEntry == null) throw new InvalidOperationException("Unable to convert range key value for property " + rangeKeyPropertyName); diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index f17d01c5ed2f..f2ba64015f55 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -57,6 +57,18 @@ internal class SimplePropertyStorage // Converter, if one is present public IPropertyConverter Converter { get; protected set; } + public void AddDerivedType(string typeDiscriminator, Type type) + { + DerivedTypesDictionary[type] = typeDiscriminator; + DerivedTypeKeysDictionary[typeDiscriminator] = type; + } + + // derived type information used for polymorphic serialization + public Dictionary DerivedTypesDictionary { get; private set; } + + // derived type information used for polymorphic deserialization + public Dictionary DerivedTypeKeysDictionary { get; private set; } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "The user's type has been annotated with DynamicallyAccessedMemberTypes.All with the public API into the library. At this point the type will not be trimmed.")] internal SimplePropertyStorage(MemberInfo member) @@ -66,9 +78,21 @@ internal SimplePropertyStorage(MemberInfo member) PropertyName = member.Name; } - public SimplePropertyStorage([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)]Type memberType) + internal SimplePropertyStorage([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type memberType) + { + MemberType = memberType; + DerivedTypesDictionary = new Dictionary(); + DerivedTypeKeysDictionary = new Dictionary(); + } + internal SimplePropertyStorage([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type memberType, Dictionary derivedTypesDictionary) + { + MemberType = memberType; + DerivedTypesDictionary = derivedTypesDictionary; + } + internal SimplePropertyStorage([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type memberType, Dictionary derivedTypeKeysDictionary) { MemberType = memberType; + DerivedTypeKeysDictionary = derivedTypeKeysDictionary; } public override string ToString() @@ -97,6 +121,9 @@ internal class PropertyStorage : SimplePropertyStorage // whether to store DateTime as epoch seconds integer public bool StoreAsEpoch { get; set; } + // whether to store Type Discriminator for polymorphic serialization + public bool PolymorphicProperty { get; set; } + // corresponding IndexNames, if applicable public List IndexNames { get; set; } @@ -167,6 +194,9 @@ public void Validate(DynamoDBContext context) if (ConverterType != null) { + if (PolymorphicProperty) + throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as derived types."); + if (StoreAsEpoch) throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as StoreAsEpoch is set to true"); @@ -194,6 +224,7 @@ internal PropertyStorage(MemberInfo member) IndexNames = new List(); Indexes = new List(); } + } /// @@ -241,6 +272,7 @@ internal class StorageConfig public List Properties { get; private set; } // target type + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type TargetType { get; private set; } // target type members @@ -249,7 +281,7 @@ internal class StorageConfig // storage mappings private Dictionary PropertyToPropertyStorageMapping { get; set; } - protected void AddPropertyStorage(string propertyName, PropertyStorage propertyStorage) + internal void AddPropertyStorage(string propertyName, PropertyStorage propertyStorage) { PropertyToPropertyStorageMapping[propertyName] = propertyStorage; } @@ -341,10 +373,17 @@ internal StorageConfig([DynamicallyAccessedMembers(InternalConstants.DataModelMo /// /// Storage information for a specific class that is associated with a table /// - internal class ItemStorageConfig : StorageConfig + internal class ItemStorageConfig { // table public string TableName { get; set; } + + public StorageConfig BaseTypeStorageConfig { get; private set; } + + public Dictionary PolymorphicTypesStorageConfig { get; private set; } + + public Dictionary PolymorphicConfig { get; private set; } + public bool LowerCamelCaseProperties { get; set; } public HashSet AttributesToStoreAsEpoch { get; set; } @@ -368,6 +407,9 @@ internal class ItemStorageConfig : StorageConfig // indexName to GSIConfig mapping public Dictionary IndexNameToGSIMapping { get; set; } + + public bool StorePolymorphicTypes => this.PolymorphicTypesStorageConfig.Any(); + //public void RemovePropertyStorage(string propertyName) //{ // PropertyStorage storage; @@ -398,6 +440,7 @@ public GSIConfig GetGSIConfig(string indexName) gsiConfig = null; return gsiConfig; } + public string GetCorrectHashKeyProperty(DynamoDBFlatConfig currentConfig, string hashKeyProperty) { if (currentConfig.IsIndexOperation) @@ -439,25 +482,27 @@ public PropertyStorage VersionPropertyStorage get { if (!HasVersion) throw new InvalidOperationException("No version field defined for this type"); - return GetPropertyStorage(VersionPropertyName); + return this.BaseTypeStorageConfig.GetPropertyStorage(VersionPropertyName); } } - public void Denormalize(DynamoDBContext context) + public void Denormalize(DynamoDBContext context, string derivedTypeAttributeName) { // analyze all PropertyStorage configs and denormalize data into other properties // all data must exist in PropertyStorage objects prior to denormalization - foreach (var property in Properties) + foreach (var property in this.BaseTypeStorageConfig.Properties) { // only add non-ignored properties if (property.IsIgnored) continue; property.Validate(context); - AddPropertyStorage(property); + AddPropertyStorage(property, this.BaseTypeStorageConfig); string propertyName = property.PropertyName; + AddKeyPropertyNames(property, propertyName); + foreach (var index in property.Indexes) { var gsi = index as PropertyStorage.GSI; @@ -470,21 +515,59 @@ public void Denormalize(DynamoDBContext context) } } + foreach (var polymorphicTypesProperty in this.PolymorphicTypesStorageConfig) + { + foreach (var polymorphicProperty in polymorphicTypesProperty.Value.Properties) + { + // only add non-ignored properties + if (polymorphicProperty.IsIgnored) continue; + + polymorphicProperty.Validate(context); + + string propertyName = polymorphicProperty.PropertyName; + + AddPropertyStorage(polymorphicProperty, polymorphicTypesProperty.Value); + + foreach (var index in polymorphicProperty.Indexes) + { + var gsi = index as PropertyStorage.GSI; + if (gsi != null) + AddGSIConfigs(gsi.IndexNames, propertyName, gsi.IsHashKey); + + var lsi = index as PropertyStorage.LSI; + if (lsi != null) + AddLSIConfigs(lsi.IndexNames, propertyName); + } + } + } + + if (PolymorphicTypesStorageConfig.Any()) + { + AttributesToGet.Add(derivedTypeAttributeName); + } + //if (this.HashKeyPropertyNames.Count == 0) // throw new InvalidOperationException("No hash key configured for type " + TargetTypeInfo.FullName); - if (this.Properties.Count == 0) + if (this.BaseTypeStorageConfig.Properties.Count == 0) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, - "Type {0} is unsupported, it has no supported members", TargetType.FullName)); + "Type {0} is unsupported, it has no supported members", this.BaseTypeStorageConfig.TargetType.FullName)); } - private void AddPropertyStorage(PropertyStorage value) + + public void AddPolymorphicPropertyStorageConfiguration(string typeDiscriminator, Type derivedType, StorageConfig polymorphicStorageConfig) + { + this.PolymorphicTypesStorageConfig.Add(typeDiscriminator, polymorphicStorageConfig); + this.PolymorphicConfig.Add(derivedType, typeDiscriminator); + } + + private void AddPropertyStorage(PropertyStorage value, StorageConfig config) { string propertyName = value.PropertyName; string attributeName = value.AttributeName; - //PropertyToPropertyStorageMapping[propertyName] = value; - AddPropertyStorage(propertyName, value); + config.AddPropertyStorage(propertyName, value); + if (!AttributesToGet.Contains(attributeName)) AttributesToGet.Add(attributeName); if (value.StoreAsEpoch) @@ -504,18 +587,21 @@ private void AddPropertyStorage(PropertyStorage value) indexes.Add(index); } } + } + + private void AddKeyPropertyNames(PropertyStorage value, string propertyName) + { if (value.IsHashKey) HashKeyPropertyNames.Add(propertyName); if (value.IsRangeKey) RangeKeyPropertyNames.Add(propertyName); - if (value.IsVersion) - { - if (!string.IsNullOrEmpty(VersionPropertyName)) - throw new InvalidOperationException("Multiple version properties defined: " + VersionPropertyName + " and " + propertyName); - VersionPropertyName = propertyName; - } + if (!value.IsVersion) return; + + if (!string.IsNullOrEmpty(VersionPropertyName)) + throw new InvalidOperationException("Multiple version properties defined: " + VersionPropertyName + " and " + propertyName); + VersionPropertyName = propertyName; } - + private void AddLSIConfigs(List lsiIndexNames, string propertyName) { foreach (var index in lsiIndexNames) @@ -548,11 +634,12 @@ private void AddGSIConfigs(List gsiIndexNames, string propertyName, bool } } - // constructor internal ItemStorageConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType) - : base(targetType) { + BaseTypeStorageConfig = new StorageConfig(targetType); + PolymorphicTypesStorageConfig = new Dictionary(); + PolymorphicConfig= new Dictionary(); AttributeToIndexesNameMapping = new Dictionary>(StringComparer.Ordinal); IndexNameToLSIRangePropertiesMapping = new Dictionary>(StringComparer.Ordinal); IndexNameToGSIMapping = new Dictionary(StringComparer.Ordinal); @@ -716,7 +803,7 @@ private ItemStorageConfig CreateStorageConfig([DynamicallyAccessedMembers(Intern } } - config.Denormalize(Context); + config.Denormalize(Context, flatConfig.DerivedTypeAttributeName); if (flatConfig.DisableFetchingTableMetadata) { @@ -754,73 +841,119 @@ private static void PopulateConfigFromType(ItemStorageConfig config, [Dynamicall config.TableName = tableAlias; var members = Utils.GetMembersFromType(type); - + foreach (var member in members) { - // prepare basic info - PropertyStorage propertyStorage = new PropertyStorage(member); - propertyStorage.AttributeName = GetAccurateCase(config, member.Name); + var propertyStorage = MemberInfoToPropertyStorage(config, member); - // run through all DDB attributes - List allAttributes = Utils.GetAttributes(member); - foreach (var attribute in allAttributes) - { - // filter out ignored properties - if (attribute is DynamoDBIgnoreAttribute) - propertyStorage.IsIgnored = true; + config.BaseTypeStorageConfig.Properties.Add(propertyStorage); + } + + DynamoDBPolymorphicTypeAttribute[] polymorphicTypeAttribute = Utils.GetPolymorphicTypesAttribute(type); - if (attribute is DynamoDBVersionAttribute) - propertyStorage.IsVersion = true; + if (polymorphicTypeAttribute is not { Length: > 0 }) return; + { + foreach (var attribute in polymorphicTypeAttribute) + { + if (attribute.DerivedType == null) + { + throw new InvalidOperationException("Invalid polymorphic type: DerivedType is null."); + } - DynamoDBRenamableAttribute renamableAttribute = attribute as DynamoDBRenamableAttribute; - if (renamableAttribute != null && !string.IsNullOrEmpty(renamableAttribute.AttributeName)) + if (!attribute.DerivedType.IsSubclassOf(type)) { - propertyStorage.AttributeName = GetAccurateCase(config, renamableAttribute.AttributeName); + throw new InvalidOperationException($"Invalid polymorphic type: '{attribute.DerivedType.FullName}' must be a subclass of '{type.FullName}'."); } - DynamoDBPropertyAttribute propertyAttribute = attribute as DynamoDBPropertyAttribute; - if (propertyAttribute != null) + var polymorphicStorageConfig = new StorageConfig(attribute.DerivedType); + + var polymorphicTypeMembers = Utils.GetMembersFromType(attribute.DerivedType); + + foreach (var member in polymorphicTypeMembers) { - propertyStorage.StoreAsEpoch = propertyAttribute.StoreAsEpoch; + var propertyStorage = MemberInfoToPropertyStorage(config, member); + polymorphicStorageConfig.Properties.Add(propertyStorage); + } + + config.AddPolymorphicPropertyStorageConfiguration(attribute.TypeDiscriminator, attribute.DerivedType, polymorphicStorageConfig); + + } + } + } + + private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig config, MemberInfo member) + { + // prepare basic info + PropertyStorage propertyStorage = new PropertyStorage(member); + propertyStorage.AttributeName = GetAccurateCase(config, member.Name); + + // run through all DDB attributes + List allAttributes = Utils.GetAttributes(member); + foreach (var attribute in allAttributes) + { + // filter out ignored properties + if (attribute is DynamoDBIgnoreAttribute) + propertyStorage.IsIgnored = true; + + if (attribute is DynamoDBVersionAttribute) + propertyStorage.IsVersion = true; - if (propertyAttribute.Converter != null) - propertyStorage.ConverterType = propertyAttribute.Converter; + DynamoDBRenamableAttribute renamableAttribute = attribute as DynamoDBRenamableAttribute; + if (renamableAttribute != null && !string.IsNullOrEmpty(renamableAttribute.AttributeName)) + { + propertyStorage.AttributeName = GetAccurateCase(config, renamableAttribute.AttributeName); + } + + DynamoDBPropertyAttribute propertyAttribute = attribute as DynamoDBPropertyAttribute; + if (propertyAttribute != null) + { + propertyStorage.StoreAsEpoch = propertyAttribute.StoreAsEpoch; + + if (propertyAttribute.Converter != null) + propertyStorage.ConverterType = propertyAttribute.Converter; - if (propertyAttribute is DynamoDBHashKeyAttribute) + if (propertyAttribute is DynamoDBHashKeyAttribute) + { + var gsiHashAttribute = propertyAttribute as DynamoDBGlobalSecondaryIndexHashKeyAttribute; + if (gsiHashAttribute != null) { - var gsiHashAttribute = propertyAttribute as DynamoDBGlobalSecondaryIndexHashKeyAttribute; - if (gsiHashAttribute != null) - { - propertyStorage.IsGSIHashKey = true; - propertyStorage.AddIndex(gsiHashAttribute); - } - else - propertyStorage.IsHashKey = true; + propertyStorage.IsGSIHashKey = true; + propertyStorage.AddIndex(gsiHashAttribute); } - if (propertyAttribute is DynamoDBRangeKeyAttribute) + else + propertyStorage.IsHashKey = true; + } + if (propertyAttribute is DynamoDBRangeKeyAttribute) + { + var gsiRangeAttribute = propertyAttribute as DynamoDBGlobalSecondaryIndexRangeKeyAttribute; + if (gsiRangeAttribute != null) { - var gsiRangeAttribute = propertyAttribute as DynamoDBGlobalSecondaryIndexRangeKeyAttribute; - if (gsiRangeAttribute != null) - { - propertyStorage.IsGSIRangeKey = true; - propertyStorage.AddIndex(gsiRangeAttribute); - } - else - propertyStorage.IsRangeKey = true; + propertyStorage.IsGSIRangeKey = true; + propertyStorage.AddIndex(gsiRangeAttribute); } + else + propertyStorage.IsRangeKey = true; + } - DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute; - if (lsiRangeKeyAttribute != null) - { - propertyStorage.IsLSIRangeKey = true; - propertyStorage.AddIndex(lsiRangeKeyAttribute); - } + DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute; + if (lsiRangeKeyAttribute != null) + { + propertyStorage.IsLSIRangeKey = true; + propertyStorage.AddIndex(lsiRangeKeyAttribute); } } - - config.Properties.Add(propertyStorage); + + DynamoDBPolymorphicTypeAttribute polymorphicAttribute = attribute as DynamoDBPolymorphicTypeAttribute; + if (polymorphicAttribute != null) + { + propertyStorage.PolymorphicProperty = true; + propertyStorage.AddDerivedType(polymorphicAttribute.TypeDiscriminator, polymorphicAttribute.DerivedType); + } } + + return propertyStorage; } + private static void PopulateConfigFromTable(ItemStorageConfig config, Table table) { PropertyStorage property; @@ -888,7 +1021,7 @@ private static void PopulateConfigFromTable(ItemStorageConfig config, Table tabl } private static void PopulateConfigFromMappings(ItemStorageConfig config, Dictionary typeMappings) { - var baseType = config.TargetType; + var baseType = config.BaseTypeStorageConfig.TargetType; TypeMapping typeMapping; if (typeMappings.TryGetValue(baseType, out typeMapping)) { @@ -903,7 +1036,7 @@ private static void PopulateConfigFromMappings(ItemStorageConfig config, Diction string propertyName = propertyConfig.Name; PropertyStorage propertyStorage; - if (!config.FindPropertyByPropertyName(propertyName, out propertyStorage)) + if (!config.BaseTypeStorageConfig.FindPropertyByPropertyName(propertyName, out propertyStorage)) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "No matching property {0} on type {1}", propertyName, baseType.FullName)); @@ -926,21 +1059,21 @@ private static PropertyStorage GetProperty(ItemStorageConfig config, string attr { PropertyStorage property = null; - bool exists = config.FindSinglePropertyByAttributeName(attributeName, out property); + bool exists = config.BaseTypeStorageConfig.FindSinglePropertyByAttributeName(attributeName, out property); if (!exists) { // property storage doesn't exist yet, create and populate MemberInfo member; // for optional properties/attributes, a null MemberInfo is OK - Validate(config.TargetTypeMembers.TryGetValue(attributeName, out member) || optional, + Validate(config.BaseTypeStorageConfig.TargetTypeMembers.TryGetValue(attributeName, out member) || optional, "Unable to locate property for key attribute {0}", attributeName); if (member != null) { property = new PropertyStorage(member); property.AttributeName = attributeName; - config.Properties.Add(property); + config.BaseTypeStorageConfig.Properties.Add(property); } } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs index 2e60b106e3ca..95b50af1a440 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs @@ -243,12 +243,26 @@ private static bool ItemsToArray([DynamicallyAccessedMembers(DynamicallyAccessed internal static DynamoDBTableAttribute GetTableAttribute(Type targetType) { - DynamoDBTableAttribute tableAttribute = GetAttribute(targetType) as DynamoDBTableAttribute; + if (targetType == null) throw new ArgumentNullException("targetType"); + + object[] attributes = targetType.GetCustomAttributes(typeof(DynamoDBTableAttribute), true); + DynamoDBTableAttribute tableAttribute = GetSingleDDBAttribute(attributes) as DynamoDBTableAttribute; + if (tableAttribute == null) return null; + return tableAttribute; } + internal static DynamoDBPolymorphicTypeAttribute[] GetPolymorphicTypesAttribute(Type targetType) + { + if (targetType == null) throw new ArgumentNullException("targetType"); + + object[] attributes = targetType.GetCustomAttributes(typeof(DynamoDBPolymorphicTypeAttribute), false); + + return attributes as DynamoDBPolymorphicTypeAttribute[]; + } + internal static DynamoDBAttribute GetAttribute(Type targetType) { if (targetType == null) throw new ArgumentNullException("targetType"); diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs index 2edf575fd21e..27b6092264fe 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs @@ -729,7 +729,7 @@ internal static Table CreateTableFromItemStorageConfig(IAmazonDynamoDB client, T // foreach (var hashKeyPropertyName in itemStorageConfig.HashKeyPropertyNames) { - var property = itemStorageConfig.GetPropertyStorage(hashKeyPropertyName); + var property = itemStorageConfig.BaseTypeStorageConfig.GetPropertyStorage(hashKeyPropertyName); var primitiveType = GetPrimitiveEntryTypeForProperty(property, flatConfig); table.HashKeys.Add(property.AttributeName); @@ -748,7 +748,7 @@ internal static Table CreateTableFromItemStorageConfig(IAmazonDynamoDB client, T // foreach (var rangeKeyPropertyName in itemStorageConfig.RangeKeyPropertyNames) { - var property = itemStorageConfig.GetPropertyStorage(rangeKeyPropertyName); + var property = itemStorageConfig.BaseTypeStorageConfig.GetPropertyStorage(rangeKeyPropertyName); var primitiveType = GetPrimitiveEntryTypeForProperty(property, flatConfig); table.RangeKeys.Add(property.AttributeName); @@ -784,7 +784,7 @@ internal static Table CreateTableFromItemStorageConfig(IAmazonDynamoDB client, T foreach (var lsiRangePropertyName in itemStorageConfig.IndexNameToLSIRangePropertiesMapping[lsiIndexName]) { - var lsiRangeProperty = itemStorageConfig.GetPropertyStorage(lsiRangePropertyName); + var lsiRangeProperty = itemStorageConfig.BaseTypeStorageConfig.GetPropertyStorage(lsiRangePropertyName); var primitiveType = GetPrimitiveEntryTypeForProperty(lsiRangeProperty, flatConfig); indexDescription.KeySchema.Add(new KeySchemaElement @@ -818,7 +818,7 @@ internal static Table CreateTableFromItemStorageConfig(IAmazonDynamoDB client, T indexDescription.KeySchema = new List(); } - var hashKeyProperty = itemStorageConfig.GetPropertyStorage(gsi.HashKeyPropertyName); + var hashKeyProperty = itemStorageConfig.BaseTypeStorageConfig.GetPropertyStorage(gsi.HashKeyPropertyName); indexDescription.KeySchema.Add(new KeySchemaElement() { AttributeName = hashKeyProperty.AttributeName, @@ -827,7 +827,7 @@ internal static Table CreateTableFromItemStorageConfig(IAmazonDynamoDB client, T if (!string.IsNullOrEmpty(gsi.RangeKeyPropertyName)) { - var rangeKeyProperty = itemStorageConfig.GetPropertyStorage(gsi.RangeKeyPropertyName); + var rangeKeyProperty = itemStorageConfig.BaseTypeStorageConfig.GetPropertyStorage(gsi.RangeKeyPropertyName); indexDescription.KeySchema.Add(new KeySchemaElement() { AttributeName = rangeKeyProperty.AttributeName, diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index d71902eb30a6..76b862744a22 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -10,6 +10,7 @@ using Amazon.DynamoDBv2.Model; using Amazon.DynamoDBv2.DocumentModel; using Amazon.DynamoDBv2.DataModel; +using System.Threading.Tasks; namespace AWSSDK_DotNet.IntegrationTests.Tests.DynamoDB @@ -475,6 +476,249 @@ public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateT Assert.AreEqual(employee.Age, storedEmployee.Age); } + /// + /// Tests that the DynamoDB operations can read and write polymorphic items. + /// + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_SaveAndLoad_WithDerivedTypeItems() + { + CleanupTables(); + TableCache.Clear(); + + var model = CreateNestedTypeItem(out var id); + + await Context.SaveAsync(model); + + var storedModel = await Context.LoadAsync(id); + Assert.AreEqual(model.Id, storedModel.Id); + Assert.AreEqual(model.GetType(), storedModel.GetType()); + + var myType = model as ModelA1; + var myStoredModel = storedModel as ModelA1; + + Assert.AreEqual(myType.MyType.GetType(), myStoredModel.MyType.GetType()); + Assert.AreEqual(myType.MyType.MyPropA, myStoredModel.MyType.MyPropA); + Assert.AreEqual(myType.MyType.Name, myStoredModel.MyType.Name); + Assert.AreEqual(((B)myType.MyType).MyPropB, ((B)myStoredModel.MyType).MyPropB); + + Assert.AreEqual(myType.MyInterface.GetType(), myStoredModel.MyInterface.GetType()); + + var myInterface = myType.MyInterface as InterfaceA; + var storedInterface = myStoredModel.MyInterface as InterfaceA; + + Assert.AreEqual(myInterface.S3, storedInterface.S3); + + Assert.AreEqual(myType.MyClasses.Count, myStoredModel.MyClasses.Count); + Assert.AreEqual(myType.MyClasses[0].GetType(), myStoredModel.MyClasses[0].GetType()); + Assert.AreEqual(myType.MyClasses[1].GetType(), myStoredModel.MyClasses[1].GetType()); + } + + /// + /// Tests that the DynamoDB operations can read and write polymorphic items. + /// + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_TransactWriteAndLoad_WithDerivedTypeItems() + { + CleanupTables(); + TableCache.Clear(); + + var model1 = CreateNestedTypeItem(out var id); + var model2 = new ModelA2 + { + Id = Guid.NewGuid(), + MyType = new A { Name = "A1", MyPropA = 1 }, + MyInterface = new InterfaceB() + { + S2 = 2, + S1 = "s1", + S4 = "s4" + }, + DictionaryClasses = new Dictionary() + { + {"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}); + await transactWrite.ExecuteAsync(); + + var storedModel1 = await Context.LoadAsync(id); + var storedModel2 = await Context.LoadAsync(model2.Id); + Assert.AreEqual(model1.Id, storedModel1.Id); + Assert.AreEqual(model1.GetType(), storedModel1.GetType()); + Assert.AreEqual(model2.Id, storedModel2.Id); + Assert.AreEqual(model2.GetType(), storedModel2.GetType()); + + var myInterface = model2.MyInterface as InterfaceB; + var storedInterface = model2.MyInterface as InterfaceB; + + Assert.AreEqual(myInterface.S4, storedInterface.S4); + + } + + + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_TransactWriteAndLoad_WithLocalSecondaryIndexRangeKey() + { + CleanupTables(); + TableCache.Clear(); + + var model = new ModelA2 + { + Id = Guid.NewGuid(), + MyType = new A { Name = "AType", MyPropA = 5 }, + DictionaryClasses = new Dictionary + { + { "A", new A { Name = "A1", MyPropA = 1 } }, + { "B", new B { Name = "B1", MyPropA = 2, MyPropB = 3 } } + }, + ManagerName = "TestManager" + }; + + var transactWrite = Context.CreateTransactWrite(); + transactWrite.AddSaveItems(new[] { model}); + await transactWrite.ExecuteAsync(); + + var storedModel = await Context.LoadAsync(model.Id); + Assert.AreEqual(model.Id, storedModel.Id); + Assert.AreEqual(model.GetType(), storedModel.GetType()); + var myStoredModel = storedModel as ModelA2; + Assert.AreEqual(model.MyType.GetType(), myStoredModel.MyType.GetType()); + 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(model.ManagerName, myStoredModel.ManagerName); + } + + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_SaveAndScan_WithGlobalSecondaryIndexRangeKey() + { + CleanupTables(); + TableCache.Clear(); + + var model1 = new ModelA1 + { + Id = Guid.NewGuid(), + MyType = new B { Name = "BType1", MyPropA = 5, MyPropB = 10 }, + MyClasses = new List + { + new A { Name = "A1", MyPropA = 1 }, + new B { Name = "B1", MyPropA = 2, MyPropB = 3 } + }, + CompanyName = "TestCompany", + Price = 100 + }; + + var model2 = new ModelA1 + { + Id = Guid.NewGuid(), + MyType = new B { Name = "BType2", MyPropA = 6, MyPropB = 12 }, + MyClasses = new List + { + new A { Name = "A2", MyPropA = 2 }, + new B { Name = "B2", MyPropA = 3, MyPropB = 4 } + }, + CompanyName = "TestCompany", + Price = 200 + }; + + var transactWrite = Context.CreateTransactWrite(); + transactWrite.AddSaveItems(new[] { model1, model2 }); + await transactWrite.ExecuteAsync(); + + var scanConditions = new[] + { + new ScanCondition("CompanyName", ScanOperator.Equal, "TestCompany") + }; + + var results = Context.Scan(scanConditions).ToList(); + Assert.AreEqual(2, results.Count); + + var storedModel1 = results.FirstOrDefault(m => m.Id == model1.Id) as ModelA1; + var storedModel2 = results.FirstOrDefault(m => m.Id == model2.Id) as ModelA1; + + Assert.IsNotNull(storedModel1); + Assert.IsNotNull(storedModel2); + + Assert.AreEqual(model1.Id, storedModel1.Id); + Assert.AreEqual(model1.MyType.GetType(), storedModel1.MyType.GetType()); + Assert.AreEqual(((B)model1.MyType).MyPropB, ((B)storedModel1.MyType).MyPropB); + Assert.AreEqual(model1.MyClasses.Count, storedModel1.MyClasses.Count); + Assert.AreEqual(model1.CompanyName, storedModel1.CompanyName); + Assert.AreEqual(model1.Price, storedModel1.Price); + + Assert.AreEqual(model2.Id, storedModel2.Id); + Assert.AreEqual(model2.MyType.GetType(), storedModel2.MyType.GetType()); + Assert.AreEqual(((B)model2.MyType).MyPropB, ((B)storedModel2.MyType).MyPropB); + Assert.AreEqual(model2.MyClasses.Count, storedModel2.MyClasses.Count); + Assert.AreEqual(model2.CompanyName, storedModel2.CompanyName); + Assert.AreEqual(model2.Price, storedModel2.Price); + } + + [TestMethod] + [TestCategory("DynamoDBv2")] + public async Task TestContext_SaveAndScan_WithLocalSecondaryIndexRangeKey() + { + CleanupTables(); + TableCache.Clear(); + + var model1 = new ModelA2 + { + Id = Guid.NewGuid(), + MyType = new C { Name = "AType1", MyPropA = 5, MyPropC = "test"}, + DictionaryClasses = new Dictionary + { + { "A", new A { Name = "A1", MyPropA = 1 } }, + { "B", new B { Name = "B1", MyPropA = 2, MyPropB = 3 } } + }, + ManagerName = "Manager1" + }; + + var model2 = new ModelA2 + { + Id = Guid.NewGuid(), + MyType = new A { Name = "AType2", MyPropA = 6 }, + DictionaryClasses = new Dictionary + { + { "A", new A { Name = "A2", MyPropA = 2 } }, + { "B", new B { Name = "B2", MyPropA = 3, MyPropB = 4 } } + }, + ManagerName = "Manager2" + }; + + var transactWrite = Context.CreateTransactWrite(); + transactWrite.AddSaveItems(new[] { model1, model2 }); + await transactWrite.ExecuteAsync(); + + var scanConditions = new[] + { + new ScanCondition("ManagerName", ScanOperator.Equal, "Manager1") + }; + + var results = Context.Scan(scanConditions).ToList(); + Assert.AreEqual(1, results.Count); + + var storedModel = results.FirstOrDefault(m => m.Id == model1.Id) as ModelA2; + + Assert.IsNotNull(storedModel); + Assert.AreEqual(model1.Id, storedModel.Id); + Assert.AreEqual(model1.MyType.GetType(), storedModel.MyType.GetType()); + 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(model1.ManagerName, storedModel.ManagerName); + } + /// /// Runs the same object-mapper integration tests as , /// but using table definitions created by instead of the internal call @@ -2249,25 +2493,46 @@ private void TestOtherContextOperations() Assert.AreEqual(employee1.Data.Length, doc["Data"].AsByteArray().Length); } + private ModelA CreateNestedTypeItem(out Guid id) + { + var a1 = new A { Name = "A1", MyPropA = 1 }; + var b1 = new B { Name = "B1", MyPropA = 2, MyPropB = 3 }; + + id = Guid.NewGuid(); + + var model = new ModelA1 + { + Id = id, + MyType = b1, + MyInterface = new InterfaceA() + { + S1 = "s1", + S2 = 2, + S3 = 3 + }, + MyClasses = new List { a1, b1 } + }; + return model; + } #region OPM definitions public enum Status : long { - Active = 256, - Inactive = 1024, - Upcoming = 9999, - Obsolete = -10, - Removed = 42 + Active = 256, + Inactive = 1024, + Upcoming = 9999, + Obsolete = -10, + Removed = 42 } [Flags] public enum Support { - Windows = 1 << 0, - iOS = 1 << 1, - Unix = 1 << 2, - Abacus = 1 << 3, + Windows = 1 << 0, + iOS = 1 << 1, + Unix = 1 << 2, + Abacus = 1 << 3, } public class StatusConverter : IPropertyConverter @@ -2677,6 +2942,86 @@ public object FromEntry(DynamoDBEntry entry) } } + + [DynamoDBPolymorphicType("B1", typeof(B))] + [DynamoDBPolymorphicType("C", typeof(C))] + public class A + { + public string Name { get; set; } + + public int MyPropA { get; set; } + } + + public interface IInterface + { + string S1 { get; set; } + int S2 { get; set; } + } + + public class InterfaceA : IInterface + { + public string S1 { get; set; } + public int S2 { get; set; } + + public int S3 { get; set; } + } + + public class InterfaceB : IInterface + { + public string S1 { get; set; } + public int S2 { get; set; } + public string S4 { get; set; } + } + + public class B : A + { + public int MyPropB { get; set; } + } + + public class C : A + { + public string MyPropC { get; set; } + } + + [DynamoDBTable("NestedTable")] + [DynamoDBPolymorphicType("A1", typeof(ModelA1))] + [DynamoDBPolymorphicType("A2", typeof(ModelA2))] + public class ModelA + { + [DynamoDBHashKey] public Guid Id { get; set; } + + public A MyType { get; set; } + + [DynamoDBPolymorphicType("I1", typeof(InterfaceA))] + [DynamoDBPolymorphicType("I2", typeof(InterfaceB))] + public IInterface MyInterface { get; set; } + + [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex", AttributeName = "Company")] + public string CompanyName { get; set; } + + [DynamoDBGlobalSecondaryIndexRangeKey("GlobalIndex")] + public int Price { get; set; } + + [DynamoDBLocalSecondaryIndexRangeKey("LocalIndex", AttributeName = "Manager")] + public string ManagerName { get; set; } + } + + public class ModelA1 : ModelA + { + [DynamoDBPolymorphicType("B", typeof(B))] + public new A MyType { get; set; } + + [DynamoDBPolymorphicType("B", typeof(B))] + [DynamoDBProperty("test")] + public List MyClasses { get; set; } + } + + public class ModelA2 : ModelA + { + [DynamoDBPolymorphicType("B", typeof(B))] + public Dictionary DictionaryClasses { get; set; } + } + #endregion } } diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DynamoDBTestsBase.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DynamoDBTestsBase.cs index b04e9828d1d8..708d0a5939a8 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DynamoDBTestsBase.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DynamoDBTestsBase.cs @@ -86,6 +86,7 @@ private static void ClientBeforeRequestEvent(object sender, Amazon.Runtime.Reque public void CleanupTables() { ClearTable(hashTableName); + ClearTable(nestedTableName); ClearTable(hashRangeTableName); ClearTable(numericHashRangeTableName); } @@ -104,6 +105,7 @@ public static void CreateContext(DynamoDBEntryConversion conversion, bool isEmpt .Build(); } + public static string nestedTableName; public static string hashTableName; public static string hashRangeTableName; public static string numericHashRangeTableName; @@ -152,15 +154,22 @@ public static void ClearTable(string tableName) public static void CreateTestTables() { + nestedTableName = TableNamePrefix + "NestedTable"; hashTableName = TableNamePrefix + "HashTable"; hashRangeTableName = TableNamePrefix + "HashRangeTable"; numericHashRangeTableName = TableNamePrefix + "NumericHashRangeTable"; + bool createNestedTable = true; bool createHashTable = true; bool createHashRangeTable = true; bool createNumericHashRangeTable = true; if (ReuseTables) { + if (GetStatus(nestedTableName) != null) + { + WaitForTableStatus(nestedTableName, TableStatus.ACTIVE); + createNestedTable = false; + } if (GetStatus(hashTableName) != null) { WaitForTableStatus(hashTableName, TableStatus.ACTIVE); @@ -178,6 +187,28 @@ public static void CreateTestTables() } } + if (createNestedTable) + { + // Create hash-key table with global index + Client.CreateTable(new CreateTableRequest + { + TableName = nestedTableName, + AttributeDefinitions = new List + { + new AttributeDefinition { AttributeName = "Id", AttributeType = ScalarAttributeType.S }, + }, + KeySchema = new List + { + new KeySchemaElement { KeyType = KeyType.HASH, AttributeName = "Id" } + }, + BillingMode = BillingMode.PAY_PER_REQUEST + }); + CreatedTables.Add(nestedTableName); + + // Wait for table to be ready + WaitForTableStatus(nestedTableName, TableStatus.ACTIVE); + } + if (createHashTable) { // Create hash-key table with global index @@ -298,6 +329,7 @@ public static void CreateTestTables() // Make sure TTL is enabled for the tables and is on the correct attribute + EnsureTTL(nestedTableName); EnsureTTL(hashTableName); EnsureTTL(hashRangeTableName); EnsureTTL(numericHashRangeTableName);