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);