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 } }