diff --git a/generator/.DevConfigs/e5fd3631-e192-4dcb-931b-69d45722ae02.json b/generator/.DevConfigs/e5fd3631-e192-4dcb-931b-69d45722ae02.json new file mode 100644 index 000000000000..f2707bb13f51 --- /dev/null +++ b/generator/.DevConfigs/e5fd3631-e192-4dcb-931b-69d45722ae02.json @@ -0,0 +1,11 @@ +{ + "services": [ + { + "serviceName": "DynamoDBv2", + "type": "patch", + "changeLogMessages": [ + "Allow to set DynamoDBEntryConversion per table." + ] + } + ] +} \ No newline at end of file diff --git a/sdk/src/Services/DynamoDBv2/Custom/Conversion/DynamoDBEntryConversion.cs b/sdk/src/Services/DynamoDBv2/Custom/Conversion/DynamoDBEntryConversion.cs index 43236d04dbc7..5a121a3324b8 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/Conversion/DynamoDBEntryConversion.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/Conversion/DynamoDBEntryConversion.cs @@ -30,42 +30,54 @@ namespace Amazon.DynamoDBv2 { + /// - /// Available conversion schemas. + /// Specifies the conversion schema used to map the types to DynamoDB types. + /// This schema influences how items are serialized and deserialized when interacting with DynamoDB. /// - internal enum ConversionSchema + public enum ConversionSchema { /// - /// Default schema before 2014 L, M, BOOL, NULL support - /// - /// The following .NET types are converted into the following DynamoDB types: - /// Number types (byte, int, float, decimal, etc.) are converted to N - /// String and char are converted to S - /// Bool is converted to N (0=false, 1=true) - /// DateTime and Guid are converto to S - /// MemoryStream and byte[] are converted to B - /// List, HashSet, and array of numerics types are converted to NS - /// List, HashSet, and array of string-based types are converted to SS - /// List, HashSet, and array of binary-based types are converted to BS - /// Dictionary{string,object} are converted to M + /// Indicates that no schema has been explicitly set. + /// + Unset = -1, + + /// + /// Legacy conversion schema (and current default for context-level configurations). + /// + /// This schema pre-dates support for native DynamoDB types such as L (list), M (map), BOOL, and NULL. + /// Common .NET type mappings: + /// /// V1 = 0, /// - /// Schema fully supporting 2014 L, M, BOOL, NULL additions + /// Enhanced conversion schema that supports native DynamoDB types including L (list), M (map), BOOL, and NULL. /// - /// The following .NET types are converted into the following DynamoDB types: - /// Number types (byte, int, float, decimal, etc.) are converted to N - /// String and char are converted to S - /// Bool is converted to BOOL - /// DateTime and Guid are converto to S - /// MemoryStream and byte[] are converted to B - /// HashSet of numerics types are converted to NS - /// HashSet of string-based types are converted to SS - /// HashSet of binary-based types are converted to BS - /// List and array of numerics, string-based types, and binary-based types - /// are converted to L type. - /// Dictionary{string,object} are converted to M + /// Common .NET type mappings: + /// + /// Recommended for applications that need full fidelity with native DynamoDB types. /// V2 = 1, } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs index 525ffa78ba2c..5d3affa53f91 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs @@ -52,12 +52,25 @@ public sealed class DynamoDBTableAttribute : DynamoDBAttribute /// public bool LowerCamelCaseProperties { get; set; } + /// + /// Gets and sets the used for mapping between .NET and DynamoDB types. + /// + /// The conversion schema determines how types are serialized and deserialized during data persistence. + /// When resolving the effective schema, the following precedence is applied: + /// 1. If set on the operation configuration, it takes the highest precedence. + /// 2. If not set on the operation, but specified at the table level, the table configuration is used. + /// 3. If neither is set, the context-level configuration is used as the default fallback. + /// + public ConversionSchema Conversion { get; set; } + /// /// Construct an instance of DynamoDBTableAttribute /// /// public DynamoDBTableAttribute(string tableName) - : this(tableName, false) { } + : this(tableName, false, ConversionSchema.Unset) + { + } /// /// Construct an instance of DynamoDBTableAttribute @@ -65,9 +78,21 @@ public DynamoDBTableAttribute(string tableName) /// /// public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties) + : this(tableName, lowerCamelCaseProperties, ConversionSchema.Unset) + { + } + + /// + /// Construct an instance of DynamoDBTableAttribute + /// + /// + /// + /// + public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties, ConversionSchema conversion) { TableName = tableName; LowerCamelCaseProperties = lowerCamelCaseProperties; + Conversion = conversion; } } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs index 4e7686c9069d..0f6bc30780d5 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs @@ -432,7 +432,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte bool ignoreNullValues = operationConfig.IgnoreNullValues ?? contextConfig.IgnoreNullValues ?? false; bool retrieveDateTimeInUtc = operationConfig.RetrieveDateTimeInUtc ?? contextConfig.RetrieveDateTimeInUtc ?? true; bool isEmptyStringValueEnabled = operationConfig.IsEmptyStringValueEnabled ?? contextConfig.IsEmptyStringValueEnabled ?? false; - DynamoDBEntryConversion conversion = operationConfig.Conversion ?? contextConfig.Conversion ?? DynamoDBEntryConversion.CurrentConversion; + DynamoDBEntryConversion conversion = contextConfig.Conversion ?? DynamoDBEntryConversion.CurrentConversion; string tableNamePrefix = operationConfig.TableNamePrefix ?? contextConfig.TableNamePrefix ?? string.Empty; // These properties can only be set at the operation level @@ -463,7 +463,8 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte IndexName = indexName; QueryFilter = queryFilter; ConditionalOperator = conditionalOperator; - Conversion = conversion; + ContextConversion = conversion; + OperationConversion = operationConfig.Conversion; MetadataCachingMode = metadataCachingMode; DisableFetchingTableMetadata = disableFetchingTableMetadata; RetrieveDateTimeInUtc = retrieveDateTimeInUtc; @@ -471,7 +472,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte State = new OperationState(); } - + /// /// Property that directs DynamoDBContext to use consistent reads. /// If property is not set, behavior defaults to non-consistent reads. @@ -552,10 +553,31 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte public List QueryFilter { get; set; } /// - /// Conversion specification which controls how conversion between + /// Specifies the conversion behavior for .NET objects (entities) mapped to DynamoDB items. + /// + /// This setting controls how conversion between .NET and DynamoDB types happens + /// on classes annotated with + /// + public DynamoDBEntryConversion ItemConversion { get; set; } + + + /// + /// Operation Conversion specification which controls how conversion between /// .NET and DynamoDB types happens. /// - public DynamoDBEntryConversion Conversion { get; set; } + private DynamoDBEntryConversion OperationConversion { get; } + + /// + /// Context Conversion specification which controls how conversion between + /// .NET and DynamoDB types happens. + /// + public DynamoDBEntryConversion Conversion => OperationConversion ?? ItemConversion ?? ContextConversion; + + /// + /// Context Conversion specification which controls how conversion between + /// .NET and DynamoDB types happens. + /// + private DynamoDBEntryConversion ContextConversion { get; } /// public bool DisableFetchingTableMetadata { get; set; } @@ -564,7 +586,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte public bool RetrieveDateTimeInUtc { get; set; } // Checks if the IndexName is set on the config - internal bool IsIndexOperation { get { return !string.IsNullOrEmpty(IndexName); } } + internal bool IsIndexOperation => !string.IsNullOrEmpty(IndexName); // State of the operation using this config internal OperationState State { get; private set; } @@ -584,6 +606,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte /// public string DerivedTypeAttributeName { get; set; } + public class OperationState { private CircularReferenceTracking referenceTracking; diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs index 0b7d47943567..0cfc7965f16c 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs @@ -428,6 +428,9 @@ internal class ItemStorageConfig // indexName to GSIConfig mapping public Dictionary IndexNameToGSIMapping { get; set; } + // entity conversion + public DynamoDBEntryConversion Conversion { get; set; } + public bool StorePolymorphicTypes => this.PolymorphicTypesStorageConfig.Any(); @@ -735,6 +738,10 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants if (tableCache.Cache.TryGetValue(actualTableName, out config)) { + if (flatConfig == null) + throw new ArgumentNullException("flatConfig"); + + flatConfig.ItemConversion = config.Conversion; return config; } } @@ -759,6 +766,8 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants if (tableCache == null) { var baseStorageConfig = CreateStorageConfig(type, actualTableName: null, flatConfig); + flatConfig.ItemConversion = baseStorageConfig.Conversion; + tableCache = new ConfigTableCache(baseStorageConfig); Cache[type] = tableCache; } @@ -780,6 +789,7 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants } config = CreateStorageConfig(type, actualTableName, flatConfig); + flatConfig.ItemConversion = config.Conversion; tableCache.Cache[actualTableName] = config; return config; @@ -858,6 +868,13 @@ private static void PopulateConfigFromType(ItemStorageConfig config, [Dynamicall if (string.IsNullOrEmpty(tableAttribute.TableName)) throw new InvalidOperationException("DynamoDBTableAttribute.Table is empty or null"); config.TableName = tableAttribute.TableName; config.LowerCamelCaseProperties = tableAttribute.LowerCamelCaseProperties; + + config.Conversion = tableAttribute.Conversion switch + { + ConversionSchema.V1 => DynamoDBEntryConversion.V1, + ConversionSchema.V2 => DynamoDBEntryConversion.V2, + _ => config.Conversion + }; } string tableAlias; diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index 9677280def08..4d3e85ffa066 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -1114,6 +1114,45 @@ private void TestContextConversions() #pragma warning restore CS0618 // Re-enable the warning } + { + +#pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors + ProductV2 productV2 = new ProductV2 + { + Id = 1, + Name = "CloudSpotter", + CompanyName = "CloudsAreGrate", + Price = 1200, + TagSet = new HashSet { "Prod", "1.0" }, + CurrentStatus = Status.Active, + FormerStatus = Status.Upcoming, + Supports = Support.Unix | Support.Windows, + PreviousSupport = null, + InternalId = "T1000", + IsPublic = true, + AlwaysN = true, + Rating = 4, + Components = new List { "Code", "Coffee" }, + KeySizes = new List { 16, 64, 128 }, + CompanyInfo = new CompanyInfo + { + Name = "MyCloud", + Founded = new DateTime(1994, 7, 6), + Revenue = 9001 + } + }; + + using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 })) + { + var docV1 = contextV1.ToDocument(productV2, new ToDocumentConfig { Conversion = conversionV1 }); + var docV2 = contextV1.ToDocument(productV2, new ToDocumentConfig { }); + VerifyConversions(docV1, docV2); + } + +#pragma warning restore CS0618 // Re-enable the warning + + } + // Introduce a circular reference and try to serialize { product.CompanyInfo = new CompanyInfo @@ -2632,6 +2671,11 @@ public object FromEntry(DynamoDBEntry entry) } } + [DynamoDBTable("HashTable", false, ConversionSchema.V2)] + public class ProductV2 : Product + { + } + /// /// Class representing items in the table [TableNamePrefix]HashTable ///