diff --git a/generator/.DevConfigs/89c824fe-c511-4f08-b98f-95619c4d19a9.json b/generator/.DevConfigs/89c824fe-c511-4f08-b98f-95619c4d19a9.json
new file mode 100644
index 000000000000..b86093ec298b
--- /dev/null
+++ b/generator/.DevConfigs/89c824fe-c511-4f08-b98f-95619c4d19a9.json
@@ -0,0 +1,11 @@
+{
+ "services": [
+ {
+ "serviceName": "DynamoDBv2",
+ "type": "patch",
+ "changeLogMessages": [
+ "Introduce support for the [DynamoDbFlatten] attribute in the DynamoDB Object Persistence Model`"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
index 030a1e7c0a06..e3b879e5b9e2 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
@@ -181,6 +181,35 @@ public DynamoDBPolymorphicTypeAttribute(string typeDiscriminator,
}
}
+ ///
+ /// Indicates that the properties of the decorated field or property type should be "flattened"
+ /// into the parent object's attribute structure in DynamoDB. When applied, all public properties
+ /// of the referenced type are serialized as individual top-level attributes of the parent item,
+ /// rather than as a nested object or map.
+ ///
+ /// Example:
+ ///
+ /// public class Address
+ /// {
+ /// public string Street { get; set; }
+ /// public string City { get; set; }
+ /// }
+ ///
+ /// public class Person
+ /// {
+ /// public string Name { get; set; }
+ /// [DynamoDBFlatten]
+ /// public Address Address { get; set; }
+ /// }
+ ///
+ /// In this example, the Person table will have top-level attributes for Name, Street, and City.
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
+ public sealed class DynamoDBFlattenAttribute : DynamoDBAttribute
+ {
+ }
+
///
/// 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/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
index a3ae92804cd8..4beb34fd68cb 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
@@ -444,26 +444,52 @@ private void PopulateInstance(ItemStorage storage, object instance, DynamoDBFlat
{
foreach (PropertyStorage propertyStorage in storageConfig.AllPropertyStorage)
{
+ if(propertyStorage.IsFlattened) continue;
string attributeName = propertyStorage.AttributeName;
-
- DynamoDBEntry entry;
- if (document.TryGetValue(attributeName, out entry))
+ if (propertyStorage.ShouldFlattenChildProperties)
{
- if (ShouldSave(entry, true))
+ //create instance of the flatten property
+ var targetType = propertyStorage.MemberType;
+ object flattenedPropertyInstance = Utils.InstantiateConverter(targetType, this);
+
+ //populate the flatten properties
+ foreach (var flattenPropertyStorage in propertyStorage.FlattenProperties)
{
- object value = FromDynamoDBEntry(propertyStorage, entry, flatConfig);
+ string flattenedAttributeName = flattenPropertyStorage.AttributeName;
- if (!TrySetValue(instance, propertyStorage.Member, value))
- {
- throw new InvalidOperationException("Unable to retrieve value from " + attributeName);
- }
+ PopulateProperty(storage, flatConfig, document, flattenedAttributeName, flattenPropertyStorage, flattenedPropertyInstance);
+ }
+ if (!TrySetValue(instance, propertyStorage.Member, flattenedPropertyInstance))
+ {
+ throw new InvalidOperationException("Unable to retrieve value from " + attributeName);
}
-
- if (propertyStorage.IsVersion)
- storage.CurrentVersion = entry as Primitive;
}
+ else
+ {
+ PopulateProperty(storage, flatConfig, document, attributeName, propertyStorage, instance);
+ }
+ }
+ }
+ }
+
+ private void PopulateProperty(ItemStorage storage, DynamoDBFlatConfig flatConfig, Document document,
+ string attributeName, PropertyStorage propertyStorage, object instance)
+ {
+ DynamoDBEntry entry;
+ if (!document.TryGetValue(attributeName, out entry)) return;
+
+ if (ShouldSave(entry, true))
+ {
+ object value = FromDynamoDBEntry(propertyStorage, entry, flatConfig);
+
+ if (!TrySetValue(instance, propertyStorage.Member, value))
+ {
+ throw new InvalidOperationException("Unable to retrieve value from " + attributeName);
}
}
+
+ if (propertyStorage.IsVersion)
+ storage.CurrentVersion = entry as Primitive;
}
///
@@ -521,30 +547,46 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
if (keysOnly && !propertyStorage.IsHashKey && !propertyStorage.IsRangeKey &&
!propertyStorage.IsVersion && !propertyStorage.IsCounter) continue;
+ if (propertyStorage.IsFlattened) continue;
+
string propertyName = propertyStorage.PropertyName;
string attributeName = propertyStorage.AttributeName;
object value;
if (TryGetValue(toStore, propertyStorage.Member, out value))
{
- DynamoDBEntry dbe = ToDynamoDBEntry(propertyStorage, value, flatConfig);
+ DynamoDBEntry dbe = ToDynamoDBEntry(propertyStorage, value, flatConfig, propertyStorage.ShouldFlattenChildProperties);
if (ShouldSave(dbe, ignoreNullValues))
{
- Primitive dbePrimitive = dbe as Primitive;
- if (propertyStorage.IsHashKey || propertyStorage.IsRangeKey ||
- propertyStorage.IsVersion || propertyStorage.IsLSIRangeKey ||
- propertyStorage.IsCounter)
+
+ if (propertyStorage.ShouldFlattenChildProperties)
{
- if (dbe != null && dbePrimitive == null)
- throw new InvalidOperationException("Property " + propertyName +
- " is a hash key, range key, atomic counter or version property and must be Primitive");
- }
+ if (dbe == null) continue;
- document[attributeName] = dbe;
+ if (dbe is not Document innerDocument) continue;
- if (propertyStorage.IsVersion)
- storage.CurrentVersion = dbePrimitive;
+ foreach (var pair in innerDocument)
+ {
+ document[pair.Key] = pair.Value;
+ }
+ }
+ else
+ {
+ Primitive dbePrimitive = dbe as Primitive;
+ if (propertyStorage.IsHashKey || propertyStorage.IsRangeKey ||
+ propertyStorage.IsVersion || propertyStorage.IsLSIRangeKey)
+ {
+ if (dbe != null && dbePrimitive == null)
+ throw new InvalidOperationException("Property " + propertyName +
+ " is a hash key, range key or version property and must be Primitive");
+ }
+
+ document[attributeName] = dbe;
+
+ if (propertyStorage.IsVersion)
+ storage.CurrentVersion = dbePrimitive;
+ }
}
}
else
@@ -724,7 +766,8 @@ internal DynamoDBEntry ToDynamoDBEntry(SimplePropertyStorage propertyStorage, ob
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072",
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 DynamoDBEntry ToDynamoDBEntry(SimplePropertyStorage propertyStorage, object value, DynamoDBFlatConfig flatConfig, bool canReturnScalarInsteadOfList)
+ private DynamoDBEntry ToDynamoDBEntry(SimplePropertyStorage propertyStorage, object value,
+ DynamoDBFlatConfig flatConfig, bool canReturnScalarInsteadOfList)
{
if (value == null)
return null;
@@ -803,7 +846,7 @@ private bool TryToMap(object value, [DynamicallyAccessedMembers(InternalConstant
if (item == null)
entry = DynamoDBNull.Null;
else
- entry = ToDynamoDBEntry(propertyStorage, item, flatConfig);
+ entry = ToDynamoDBEntry(propertyStorage, item, flatConfig, false);
output[key] = entry;
}
@@ -839,7 +882,7 @@ private bool TryToList(object value, [DynamicallyAccessedMembers(DynamicallyAcce
entry = DynamoDBNull.Null;
else
{
- entry = ToDynamoDBEntry(propertyStorage, item, flatConfig);
+ entry = ToDynamoDBEntry(propertyStorage, item, flatConfig, false);
}
output.Add(entry);
@@ -1187,7 +1230,7 @@ private static List CreateQueryConditions(DynamoDBFlatConfig fla
// Key creation
private DynamoDBEntry ValueToDynamoDBEntry(PropertyStorage propertyStorage, object value, DynamoDBFlatConfig flatConfig)
{
- var entry = ToDynamoDBEntry(propertyStorage, value, flatConfig);
+ var entry = ToDynamoDBEntry(propertyStorage, value, flatConfig, false);
return entry;
}
private static void ValidateKey(Key key, ItemStorageConfig storageConfig)
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
index 3d925af6bf6a..65eefb12aebb 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
@@ -146,9 +146,17 @@ internal class PropertyStorage : SimplePropertyStorage
// whether to store Type Discriminator for polymorphic serialization
public bool PolymorphicProperty { get; set; }
+ // whether to store child properties at the same level as the parent property
+ public bool ShouldFlattenChildProperties { get; set; }
+
+ // whether to store property at parent level
+ public bool IsFlattened { get; set; }
+
// corresponding IndexNames, if applicable
public List IndexNames { get; set; }
+ public List FlattenProperties { get; set; }
+
public bool IsCounter { get; set; }
public long CounterDelta { get; set; }
@@ -228,6 +236,9 @@ public void Validate(DynamoDBContext context)
if (PolymorphicProperty)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as derived types.");
+ if (ShouldFlattenChildProperties)
+ throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as flatten types.");
+
if (StoreAsEpoch || StoreAsEpochLong)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as StoreAsEpoch or StoreAsEpochLong is set to true");
@@ -257,6 +268,7 @@ internal PropertyStorage(MemberInfo member)
{
IndexNames = new List();
Indexes = new List();
+ FlattenProperties = new List();
}
}
@@ -317,8 +329,19 @@ internal class StorageConfig
internal void AddPropertyStorage(string propertyName, PropertyStorage propertyStorage)
{
+ // Check for existing property with the same attribute name
+ foreach (var existing in PropertyToPropertyStorageMapping.Values)
+ {
+ if (string.Equals(existing.AttributeName, propertyStorage.AttributeName))
+ {
+ throw new InvalidOperationException(
+ $"A property with attribute name '{propertyStorage.AttributeName}' already exists (property: '{existing.PropertyName}'). " +
+ $"Cannot add property '{propertyName}' with the same attribute name.");
+ }
+ }
PropertyToPropertyStorageMapping[propertyName] = propertyStorage;
}
+
public PropertyStorage GetPropertyStorage(string propertyName)
{
PropertyStorage storage;
@@ -544,16 +567,57 @@ public void Denormalize(DynamoDBContext context, string derivedTypeAttributeName
// all data must exist in PropertyStorage objects prior to denormalization
foreach (var property in this.BaseTypeStorageConfig.Properties)
+ {
+ ProcessProperty(property, this.BaseTypeStorageConfig, true);
+ }
+
+ foreach (var polymorphicTypesProperty in this.PolymorphicTypesStorageConfig)
+ {
+ foreach (var polymorphicProperty in polymorphicTypesProperty.Value.Properties)
+ {
+ ProcessProperty(polymorphicProperty, polymorphicTypesProperty.Value, false);
+ }
+ }
+
+ if (StorePolymorphicTypes)
+ {
+ AttributesToGet.Add(derivedTypeAttributeName);
+ }
+
+ if (this.BaseTypeStorageConfig.Properties.Count == 0)
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
+ "Type {0} is unsupported, it has no supported members", this.BaseTypeStorageConfig.TargetType.FullName));
+ return;
+
+ void ProcessProperty(PropertyStorage property, StorageConfig storageConfig, bool setKeyProperties)
{
// only add non-ignored properties
- if (property.IsIgnored) continue;
+ if (property.IsIgnored)
+ return;
property.Validate(context);
- AddPropertyStorage(property, this.BaseTypeStorageConfig);
+
+ SetPropertyConfig(property, storageConfig, setKeyProperties);
+
+ if (!property.ShouldFlattenChildProperties) return;
+
+ // flatten properties
+ foreach (var flattenProperty in property.FlattenProperties)
+ {
+ ProcessProperty(flattenProperty, storageConfig, setKeyProperties);
+ }
+ }
+
+ void SetPropertyConfig(PropertyStorage property, StorageConfig storageConfig, bool setKeyProperties)
+ {
+ AddPropertyStorage(property, storageConfig);
string propertyName = property.PropertyName;
- AddKeyPropertyNames(property, propertyName);
+ if (setKeyProperties)
+ {
+ AddKeyPropertyNames(property, propertyName);
+ }
foreach (var index in property.Indexes)
{
@@ -566,47 +630,8 @@ public void Denormalize(DynamoDBContext context, string derivedTypeAttributeName
AddLSIConfigs(lsi.IndexNames, propertyName);
}
}
-
- 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.BaseTypeStorageConfig.Properties.Count == 0)
- throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
- "Type {0} is unsupported, it has no supported members", this.BaseTypeStorageConfig.TargetType.FullName));
}
-
public void AddPolymorphicPropertyStorageConfiguration(string typeDiscriminator, Type derivedType, StorageConfig polymorphicStorageConfig)
{
this.PolymorphicTypesStorageConfig.Add(typeDiscriminator, polymorphicStorageConfig);
@@ -635,6 +660,7 @@ private void AddPropertyStorage(PropertyStorage value, StorageConfig config)
indexes = new List();
AttributeToIndexesNameMapping[attributeName] = indexes;
}
+
foreach (var index in value.IndexNames)
{
if (!indexes.Contains(index))
@@ -915,6 +941,11 @@ private static void PopulateConfigFromType(ItemStorageConfig config, [Dynamicall
{
var propertyStorage = MemberInfoToPropertyStorage(config, member);
+ if (!propertyStorage.IsIgnored)
+ {
+ ValidateAttributeName(config, propertyStorage.AttributeName);
+ }
+
config.BaseTypeStorageConfig.Properties.Add(propertyStorage);
}
@@ -950,6 +981,8 @@ private static void PopulateConfigFromType(ItemStorageConfig config, [Dynamicall
}
}
+ [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.")]
private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig config, MemberInfo member)
{
// prepare basic info
@@ -958,11 +991,45 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con
// run through all DDB attributes
List allAttributes = Utils.GetAttributes(member);
+
+ if(allAttributes.Count>1 &&
+ allAttributes.Any(a => a is DynamoDBFlattenAttribute))
+ {
+ throw new InvalidOperationException("DynamoDBFlatten cannot be combined with other annotations.");
+ }
+
foreach (var attribute in allAttributes)
{
// filter out ignored properties
if (attribute is DynamoDBIgnoreAttribute)
+ {
propertyStorage.IsIgnored = true;
+ continue;
+ }
+
+ // flatten properties
+ if (attribute is DynamoDBFlattenAttribute)
+ {
+ propertyStorage.ShouldFlattenChildProperties = true;
+
+ var type = Utils.GetType(member);
+
+ if (Utils.IsCollectionType(type) || Utils.IsPrimitive(type))
+ {
+ throw new InvalidOperationException("Cannot flatten primitive types or collections. Only complex objects are supported.");
+ }
+
+ var members = Utils.GetMembersFromType(type);
+
+ foreach (var memberInfo in members)
+ {
+ var flattenPropertyStorage = MemberInfoToPropertyStorage(config, memberInfo);
+
+ flattenPropertyStorage.IsFlattened = true;
+
+ propertyStorage.FlattenProperties.Add(flattenPropertyStorage);
+ }
+ }
if (attribute is DynamoDBVersionAttribute)
propertyStorage.IsVersion = true;
@@ -1031,7 +1098,7 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con
}
}
- return propertyStorage;
+ return propertyStorage;
}
private static void PopulateConfigFromTable(ItemStorageConfig config, Table table)
@@ -1181,6 +1248,18 @@ private static void ValidateProperty(bool value, string propertyName, string mes
}
}
+ private static void ValidateAttributeName(ItemStorageConfig config, string attributeName)
+ {
+ foreach (var property in config.BaseTypeStorageConfig.Properties)
+ {
+ if (string.Equals(property.AttributeName, attributeName) && !property.IsIgnored)
+ {
+ throw new InvalidOperationException(
+ $"Attempt to add an attribute that is already defined.[Attribute name: {attributeName}]");
+ }
+ }
+ }
+
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
index 29643d9d31f8..5d72b6dd6d3e 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
@@ -334,6 +334,17 @@ internal static string ToLowerCamelCase(string value)
new Type[] { typeof(DynamoDBContext) }
};
+ internal static bool IsCollectionType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
+ {
+ if (type == typeof(string))
+ return false;
+
+ if (type.IsGenericType && typeof(IEnumerable).IsAssignableFrom(type))
+ return true;
+
+ return typeof(IEnumerable).IsAssignableFrom(type);
+ }
+
internal static object InstantiateConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type objectType, IDynamoDBContext context)
{
return InstantiateHelper(objectType, validConverterConstructorInputs, new object[] { context });
@@ -421,12 +432,13 @@ private static bool CanInstantiateHelper([DynamicallyAccessedMembers(Dynamically
return true;
}
+
internal static Type GetType(MemberInfo member)
{
var pi = member as PropertyInfo;
var fi = member as FieldInfo;
if (pi == null && fi == null)
- throw new ArgumentOutOfRangeException("member", "member must be of type PropertyInfo or FieldInfo");
+ throw new ArgumentOutOfRangeException(nameof(member), "member must be of type PropertyInfo or FieldInfo");
return (pi != null ? pi.PropertyType : fi.FieldType);
}
@@ -528,7 +540,6 @@ internal static List GetMembersFromType([DynamicallyAccessedMembers(
return members.Values.ToList();
}
-#endregion
-
+ #endregion
}
}
diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
index 8718ebd7631c..b5389a4dae68 100644
--- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
+++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
@@ -11,6 +11,7 @@
using Amazon.DynamoDBv2.DocumentModel;
using Amazon.DynamoDBv2.DataModel;
using System.Threading.Tasks;
+using static AWSSDK_DotNet.IntegrationTests.Tests.DynamoDB.DynamoDBTests;
namespace AWSSDK_DotNet.IntegrationTests.Tests.DynamoDB
@@ -585,6 +586,7 @@ public async Task TestContext_SaveAndLoad_WithDerivedTypeItems()
var storedModel = await Context.LoadAsync(id);
Assert.AreEqual(model.Id, storedModel.Id);
Assert.AreEqual(model.GetType(), storedModel.GetType());
+ Assert.IsNotNull(storedModel.FlatAddress);
var myType = model as ModelA1;
var myStoredModel = storedModel as ModelA1;
@@ -1007,6 +1009,105 @@ public void TestWithBuilderContext()
}
}
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public async Task Test_FlattenAttribute_With_Annotations()
+ {
+ CleanupTables();
+ TableCache.Clear();
+
+ // flatten with version
+ var product = new ProductFlat
+ {
+ Id = 1,
+ Name = "TestProduct",
+ Details = new ProductDetails()
+ {
+ Description = "Test",
+ Name = "TestProductDetails",
+ }
+ };
+
+ await Context.SaveAsync(product);
+ var savedProductFlat = await Context.LoadAsync(product.Id);
+ Assert.IsNotNull(savedProductFlat);
+ Assert.AreEqual(product.Id, savedProductFlat.Id);
+ Assert.IsNotNull(savedProductFlat.Details);
+ Assert.AreEqual(product.Details.Description, savedProductFlat.Details.Description);
+ Assert.AreEqual(0, savedProductFlat.Details.Version);
+ Assert.AreEqual("TestProduct",savedProductFlat.Name);
+ Assert.AreEqual("TestProductDetails", savedProductFlat.Details.Name);
+
+ // flattened property, which itself contains another flattened property.
+ var flatEmployee = new EmployeeNonFlat()
+ {
+ EmployeeId = 2,
+ Contact = new ContactInfo()
+ {
+ Email = "test@email.com",
+ Address = new Address()
+ {
+ City = "Seattle",
+ Street = "N/A",
+ }
+ }
+ };
+ await Context.SaveAsync(flatEmployee);
+ var savedFlatEmployee = await Context.LoadAsync(flatEmployee.EmployeeId);
+ Assert.IsNotNull(savedFlatEmployee);
+ Assert.AreEqual(flatEmployee.EmployeeId, savedFlatEmployee.EmployeeId);
+ Assert.AreEqual(flatEmployee.Contact.Address.City, savedFlatEmployee.City);
+ Assert.AreEqual(flatEmployee.Contact.Address.Street, savedFlatEmployee.Street);
+ Assert.AreEqual(flatEmployee.Contact.Email, savedFlatEmployee.Email);
+
+ //flattened property contains a property with a custom converter.
+ var eventToSave = new Event()
+ {
+ Id = 5,
+ Details = new EventDetails()
+ {
+ EventDate = DateTime.Today
+ }
+ };
+ await Context.SaveAsync(eventToSave);
+ var savedEvent = await Context.LoadAsync(eventToSave.Id);
+ Assert.IsNotNull(savedEvent);
+ Assert.AreEqual(eventToSave.Id, savedEvent.Id);
+ Assert.IsNotNull(savedEvent.Details);
+ Assert.AreEqual(eventToSave.Details.EventDate.ToUniversalTime(), savedEvent.Details.EventDate);
+
+ // Flattened Property with Global Secondary Index
+ var order = new Order()
+ {
+ Id = 6,
+ Payment = new PaymentInfo()
+ {
+ CompanyName = "TestCompany",
+ Price = 1000
+
+ }
+ };
+
+ await Context.SaveAsync(order);
+ var savedOrders = Context.Query(
+ order.Payment.CompanyName, // Hash-key for the index is Company
+ QueryOperator.Equal, // Range-key for the index is Price, so the
+ new object[] { 1000 }, // condition is against a numerical value
+ new QueryConfig // Configure the index to use
+ {
+ IndexName = "GlobalIndex",
+ });
+ Assert.IsNotNull(savedOrders);
+ var savedOrder = savedOrders.FirstOrDefault();
+ Assert.IsNotNull(savedOrder);
+ Assert.AreEqual(order.Id, savedOrder.Id);
+ Assert.IsNotNull(savedOrder.Payment);
+ Assert.AreEqual(order.Payment.Price, savedOrder.Payment.Price);
+ Assert.AreEqual(order.Payment.CompanyName, savedOrder.Payment.CompanyName);
+
+ }
+
+
private static void TestEmptyStringsWithFeatureEnabled()
{
var product = new Product
@@ -2752,7 +2853,12 @@ private ModelA CreateNestedTypeItem(out Guid id)
S2 = 2,
S3 = 3
},
- MyClasses = new List { a1, b1 }
+ MyClasses = new List { a1, b1 },
+ FlatAddress = new Address()
+ {
+ City = "Seattle",
+ Street = "Street"
+ }
};
return model;
}
@@ -2904,7 +3010,6 @@ public class EnumProduct2
[DynamoDBProperty("Product")] public string Name { get; set; }
}
-
///
/// Class representing items in the table [TableNamePrefix]HashRangeTable
///
@@ -3092,14 +3197,12 @@ public class EpochEmployee : Employee
[DynamoDBTable("NumericHashRangeTable")]
public class AnnotatedEpochEmployee
{
- [DynamoDBRangeKey]
- public string Name { get; set; }
+ [DynamoDBRangeKey] public string Name { get; set; }
public int Age { get; set; }
// Hash key
- [DynamoDBHashKey(StoreAsEpoch = true)]
- public virtual DateTime CreationTime { get; set; }
+ [DynamoDBHashKey(StoreAsEpoch = true)] public virtual DateTime CreationTime { get; set; }
[DynamoDBProperty(StoreAsEpoch = true)]
public DateTime EpochDate2 { get; set; }
@@ -3144,11 +3247,9 @@ public class BadNumericEpochEmployee : NumericEpochEmployee
[DynamoDBTable("NumericHashRangeTable")]
public class AnnotatedNumericEpochEmployee : EpochEmployee
{
- [DynamoDBHashKey(StoreAsEpoch = true)]
- public override DateTime CreationTime { get; set; }
+ [DynamoDBHashKey(StoreAsEpoch = true)] public override DateTime CreationTime { get; set; }
- [DynamoDBRangeKey]
- public override string Name { get; set; }
+ [DynamoDBRangeKey] public override string Name { get; set; }
}
///
@@ -3157,8 +3258,7 @@ public class AnnotatedNumericEpochEmployee : EpochEmployee
[DynamoDBTable("NumericHashRangeTable")]
public class PropertyConverterEmployee
{
- [DynamoDBHashKey(StoreAsEpoch = true)]
- public DateTime CreationTime { get; set; }
+ [DynamoDBHashKey(StoreAsEpoch = true)] public DateTime CreationTime { get; set; }
[DynamoDBRangeKey]
[DynamoDBProperty(Converter = typeof(EnumAsStringConverter))]
@@ -3169,19 +3269,16 @@ public class PropertyConverterEmployee
public class AnnotatedRangeTable
{
// Hash key
- [DynamoDBHashKey]
- public string Name { get; set; }
+ [DynamoDBHashKey] public string Name { get; set; }
// Range key
- [DynamoDBRangeKey]
- internal int Age { get; set; }
+ [DynamoDBRangeKey] internal int Age { get; set; }
}
[DynamoDBTable("HashRangeTable")]
public class IgnoreAnnotatedRangeTable : AnnotatedRangeTable
{
- [DynamoDBIgnore]
- internal int IgnoreAttribute { get; set; }
+ [DynamoDBIgnore] internal int IgnoreAttribute { get; set; }
}
@@ -3191,8 +3288,6 @@ public class AnnotatedRangeTable2 : AnnotatedRangeTable
internal int NotAnnotatedAttribute { get; set; }
}
-
-
public class DateTimeUtcConverter : IPropertyConverter
{
public DynamoDBEntry ToEntry(object value) => (DateTime)value;
@@ -3217,6 +3312,112 @@ public object FromEntry(DynamoDBEntry entry)
}
}
+ #region Flatten
+
+ ///
+ /// A class has a flattened property, and the version attribute is on the flattened child.
+ ///
+ [DynamoDBTable("HashTable")]
+ public class ProductFlat
+ {
+ [DynamoDBHashKey] public int Id { get; set; }
+
+ [DynamoDBFlatten]
+ public ProductDetails Details { get; set; }
+
+ public string Name { get; set; }
+ }
+
+ public class ProductDetails
+ {
+ [DynamoDBVersion]
+ public int? Version { get; set; }
+
+ public string Description { get; set; }
+
+ [DynamoDBProperty("DetailsName")]
+ public string Name { get; set; }
+ }
+
+ ///
+ /// A class has a flattened property, which itself contains another flattened property.
+ ///
+ [DynamoDBTable("HashTable")]
+ public class EmployeeNonFlat
+ {
+ [DynamoDBHashKey("Id")]
+ public int EmployeeId { get; set; }
+
+ [DynamoDBFlatten]
+ public ContactInfo Contact { get; set; }
+ }
+
+ public class ContactInfo
+ {
+ public string Email { get; set; }
+
+ [DynamoDBFlatten]
+ public Address Address { get; set; }
+ }
+
+ ///
+ /// A class has a flattened structure
+ ///
+ [DynamoDBTable("HashTable")]
+ public class EmployeeFlatten
+ {
+ [DynamoDBHashKey("Id")]
+ public int EmployeeId { get; set; }
+
+ public string Email { get; set; }
+
+ public string Street { get; set; }
+
+ public string City { get; set; }
+ }
+
+ [DynamoDBTable("HashTable")]
+ public class Order
+ {
+ [DynamoDBHashKey]
+ public int Id { get; set; }
+
+ [DynamoDBFlatten]
+ public PaymentInfo Payment { get; set; }
+ }
+
+ public class PaymentInfo
+ {
+ [DynamoDBGlobalSecondaryIndexHashKey("GlobalIndex", AttributeName = "Company")]
+ public string CompanyName { get; set; }
+
+ [DynamoDBGlobalSecondaryIndexRangeKey("GlobalIndex")]
+ public int Price { get; set; }
+ }
+
+ ///
+ /// A flattened property contains a property with a custom converter.
+ ///
+ [DynamoDBTable("HashTable")]
+ public class Event
+ {
+ [DynamoDBHashKey]
+ public int Id { get; set; }
+
+ [DynamoDBFlatten]
+ public EventDetails Details { get; set; }
+ }
+
+ public class EventDetails
+ {
+ [DynamoDBProperty(typeof(DateTimeUtcConverter))]
+ public DateTime EventDate { get; set; }
+ }
+
+
+ #endregion
+
+ #region PolymorphicType
[DynamoDBPolymorphicType("B1", typeof(B))]
[DynamoDBPolymorphicType("C", typeof(C))]
@@ -3229,8 +3430,8 @@ public class A
public interface IInterface
{
- string S1 { get; set; }
- int S2 { get; set; }
+ string S1 { get; set; }
+ int S2 { get; set; }
}
public class InterfaceA : IInterface
@@ -3266,7 +3467,7 @@ 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; }
@@ -3279,6 +3480,14 @@ public class ModelA
[DynamoDBLocalSecondaryIndexRangeKey("LocalIndex", AttributeName = "Manager")]
public string ManagerName { get; set; }
+
+ [DynamoDBFlatten] public Address FlatAddress { get; set; }
+ }
+
+ public class Address
+ {
+ public string Street { get; set; }
+ public string City { get; set; }
}
public class ModelA1 : ModelA
@@ -3298,5 +3507,7 @@ public class ModelA2 : ModelA
}
#endregion
+
+ #endregion
}
}