Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Issues/2576 dynamo db entry conversion #3735

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions generator/.DevConfigs/e5fd3631-e192-4dcb-931b-69d45722ae02.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"services": [
{
"serviceName": "DynamoDBv2",
"type": "patch",
"changeLogMessages": [
"Allow to set DynamoDBEntryConversion per table."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,54 @@

namespace Amazon.DynamoDBv2
{

/// <summary>
/// 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.
/// </summary>
internal enum ConversionSchema
public enum ConversionSchema
{
/// <summary>
/// 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.
/// </summary>
Unset = -1,

/// <summary>
/// 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:
/// <ul>
/// <li><para>Number types (byte, int, float, decimal, etc.) → DynamoDB N (number)</para></li>
/// <li><para>string, char → S (string)</para></li>
/// <li><para>bool → N ("0" for false, "1" for true)</para></li>
/// <li><para>DateTime, Guid → S (string)</para></li>
/// <li><para>MemoryStream, byte[] → B (binary)</para></li>
/// <li><para>List, HashSet, array of numeric types → NS (number set)</para></li>
/// <li><para>List, HashSet, array of string types → SS (string set)</para></li>
/// <li><para>List, HashSet, array of binary types → BS (binary set)</para></li>
/// <li><para>Dictionary{string, object} → M (map)</para></li>
/// </ul>
/// </summary>
V1 = 0,

/// <summary>
/// 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:
/// <ul>
/// <li><para>Number types (byte, int, float, decimal, etc.) → DynamoDB N (number)</para></li>
/// <li><para>string, char → S (string)</para></li>
/// <li><para>bool → BOOL</para></li>
/// <li><para>DateTime, Guid → S (string)</para></li>
/// <li><para>MemoryStream, byte[] → B (binary)</para></li>
/// <li><para>HashSet of numeric types → NS (number set)</para></li>
/// <li><para>HashSet of string types → SS (string set)</para></li>
/// <li><para>HashSet of binary types → BS (binary set)</para></li>
/// <li><para>List, array (numeric, string, binary types) → L (list)</para></li>
/// <li><para>Dictionary{string, object} → M (map)</para></li>
/// </ul>
/// Recommended for applications that need full fidelity with native DynamoDB types.
/// </summary>
V2 = 1,
}
Expand Down
27 changes: 26 additions & 1 deletion sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,47 @@ public sealed class DynamoDBTableAttribute : DynamoDBAttribute
/// </summary>
public bool LowerCamelCaseProperties { get; set; }

/// <summary>
/// Gets and sets the <see cref="ConversionSchema"/> 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.
/// </summary>
public ConversionSchema Conversion { get; set; }

/// <summary>
/// Construct an instance of DynamoDBTableAttribute
/// </summary>
/// <param name="tableName"></param>
public DynamoDBTableAttribute(string tableName)
: this(tableName, false) { }
: this(tableName, false, ConversionSchema.Unset)
{
}

/// <summary>
/// Construct an instance of DynamoDBTableAttribute
/// </summary>
/// <param name="tableName"></param>
/// <param name="lowerCamelCaseProperties"></param>
public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties)
: this(tableName, lowerCamelCaseProperties, ConversionSchema.Unset)
{
}

/// <summary>
/// Construct an instance of DynamoDBTableAttribute
/// </summary>
/// <param name="tableName"></param>
/// <param name="lowerCamelCaseProperties"></param>
/// <param name="conversion"></param>
public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties, ConversionSchema conversion)
{
TableName = tableName;
LowerCamelCaseProperties = lowerCamelCaseProperties;
Conversion = conversion;
}
}

Expand Down
35 changes: 29 additions & 6 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -463,15 +463,16 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
IndexName = indexName;
QueryFilter = queryFilter;
ConditionalOperator = conditionalOperator;
Conversion = conversion;
ContextConversion = conversion;
OperationConversion = operationConfig.Conversion;
MetadataCachingMode = metadataCachingMode;
DisableFetchingTableMetadata = disableFetchingTableMetadata;
RetrieveDateTimeInUtc = retrieveDateTimeInUtc;
DerivedTypeAttributeName = derivedTypeAttributeName;

State = new OperationState();
}

/// <summary>
/// Property that directs DynamoDBContext to use consistent reads.
/// If property is not set, behavior defaults to non-consistent reads.
Expand Down Expand Up @@ -552,10 +553,31 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
public List<ScanCondition> QueryFilter { get; set; }

/// <summary>
/// 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 <see cref="DynamoDBTableAttribute"/>
/// </summary>
public DynamoDBEntryConversion ItemConversion { get; set; }


/// <summary>
/// Operation Conversion specification which controls how conversion between
/// .NET and DynamoDB types happens.
/// </summary>
public DynamoDBEntryConversion Conversion { get; set; }
private DynamoDBEntryConversion OperationConversion { get; }

/// <summary>
/// Context Conversion specification which controls how conversion between
/// .NET and DynamoDB types happens.
/// </summary>
public DynamoDBEntryConversion Conversion => OperationConversion ?? ItemConversion ?? ContextConversion;

/// <summary>
/// Context Conversion specification which controls how conversion between
/// .NET and DynamoDB types happens.
/// </summary>
private DynamoDBEntryConversion ContextConversion { get; }

/// <inheritdoc cref="DynamoDBContextConfig.DisableFetchingTableMetadata"/>
public bool DisableFetchingTableMetadata { get; set; }
Expand All @@ -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; }
Expand All @@ -584,6 +606,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
/// </remarks>
public string DerivedTypeAttributeName { get; set; }


public class OperationState
{
private CircularReferenceTracking referenceTracking;
Expand Down
17 changes: 17 additions & 0 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ internal class ItemStorageConfig
// indexName to GSIConfig mapping
public Dictionary<string, GSIConfig> IndexNameToGSIMapping { get; set; }

// entity conversion
public DynamoDBEntryConversion Conversion { get; set; }


public bool StorePolymorphicTypes => this.PolymorphicTypesStorageConfig.Any();

Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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;
}
Expand All @@ -780,6 +789,7 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants
}

config = CreateStorageConfig(type, actualTableName, flatConfig);
flatConfig.ItemConversion = config.Conversion;
tableCache.Cache[actualTableName] = config;

return config;
Expand Down Expand Up @@ -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;
Expand Down
44 changes: 44 additions & 0 deletions sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> { "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<string> { "Code", "Coffee" },
KeySizes = new List<byte> { 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
Expand Down Expand Up @@ -2632,6 +2671,11 @@ public object FromEntry(DynamoDBEntry entry)
}
}

[DynamoDBTable("HashTable", false, ConversionSchema.V2)]
public class ProductV2 : Product
{
}

/// <summary>
/// Class representing items in the table [TableNamePrefix]HashTable
/// </summary>
Expand Down