Skip to content

Commit 19697c2

Browse files
authored
Allow to set DynamoDBEntryConversion per table (#3735)
1 parent cabf92d commit 19697c2

File tree

6 files changed

+166
-34
lines changed

6 files changed

+166
-34
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"services": [
3+
{
4+
"serviceName": "DynamoDBv2",
5+
"type": "patch",
6+
"changeLogMessages": [
7+
"Allow to set DynamoDBEntryConversion per table."
8+
]
9+
}
10+
]
11+
}

sdk/src/Services/DynamoDBv2/Custom/Conversion/DynamoDBEntryConversion.cs

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,42 +30,54 @@
3030

3131
namespace Amazon.DynamoDBv2
3232
{
33+
3334
/// <summary>
34-
/// Available conversion schemas.
35+
/// Specifies the conversion schema used to map the types to DynamoDB types.
36+
/// This schema influences how items are serialized and deserialized when interacting with DynamoDB.
3537
/// </summary>
36-
internal enum ConversionSchema
38+
public enum ConversionSchema
3739
{
3840
/// <summary>
39-
/// Default schema before 2014 L, M, BOOL, NULL support
40-
///
41-
/// The following .NET types are converted into the following DynamoDB types:
42-
/// Number types (byte, int, float, decimal, etc.) are converted to N
43-
/// String and char are converted to S
44-
/// Bool is converted to N (0=false, 1=true)
45-
/// DateTime and Guid are converto to S
46-
/// MemoryStream and byte[] are converted to B
47-
/// List, HashSet, and array of numerics types are converted to NS
48-
/// List, HashSet, and array of string-based types are converted to SS
49-
/// List, HashSet, and array of binary-based types are converted to BS
50-
/// Dictionary{string,object} are converted to M
41+
/// Indicates that no schema has been explicitly set.
42+
/// </summary>
43+
Unset = -1,
44+
45+
/// <summary>
46+
/// Legacy conversion schema (and current default for context-level configurations).
47+
///
48+
/// This schema pre-dates support for native DynamoDB types such as L (list), M (map), BOOL, and NULL.
49+
/// Common .NET type mappings:
50+
/// <ul>
51+
/// <li><para>Number types (byte, int, float, decimal, etc.) → DynamoDB N (number)</para></li>
52+
/// <li><para>string, char → S (string)</para></li>
53+
/// <li><para>bool → N ("0" for false, "1" for true)</para></li>
54+
/// <li><para>DateTime, Guid → S (string)</para></li>
55+
/// <li><para>MemoryStream, byte[] → B (binary)</para></li>
56+
/// <li><para>List, HashSet, array of numeric types → NS (number set)</para></li>
57+
/// <li><para>List, HashSet, array of string types → SS (string set)</para></li>
58+
/// <li><para>List, HashSet, array of binary types → BS (binary set)</para></li>
59+
/// <li><para>Dictionary{string, object} → M (map)</para></li>
60+
/// </ul>
5161
/// </summary>
5262
V1 = 0,
5363

5464
/// <summary>
55-
/// Schema fully supporting 2014 L, M, BOOL, NULL additions
65+
/// Enhanced conversion schema that supports native DynamoDB types including L (list), M (map), BOOL, and NULL.
5666
///
57-
/// The following .NET types are converted into the following DynamoDB types:
58-
/// Number types (byte, int, float, decimal, etc.) are converted to N
59-
/// String and char are converted to S
60-
/// Bool is converted to BOOL
61-
/// DateTime and Guid are converto to S
62-
/// MemoryStream and byte[] are converted to B
63-
/// HashSet of numerics types are converted to NS
64-
/// HashSet of string-based types are converted to SS
65-
/// HashSet of binary-based types are converted to BS
66-
/// List and array of numerics, string-based types, and binary-based types
67-
/// are converted to L type.
68-
/// Dictionary{string,object} are converted to M
67+
/// Common .NET type mappings:
68+
/// <ul>
69+
/// <li><para>Number types (byte, int, float, decimal, etc.) → DynamoDB N (number)</para></li>
70+
/// <li><para>string, char → S (string)</para></li>
71+
/// <li><para>bool → BOOL</para></li>
72+
/// <li><para>DateTime, Guid → S (string)</para></li>
73+
/// <li><para>MemoryStream, byte[] → B (binary)</para></li>
74+
/// <li><para>HashSet of numeric types → NS (number set)</para></li>
75+
/// <li><para>HashSet of string types → SS (string set)</para></li>
76+
/// <li><para>HashSet of binary types → BS (binary set)</para></li>
77+
/// <li><para>List, array (numeric, string, binary types) → L (list)</para></li>
78+
/// <li><para>Dictionary{string, object} → M (map)</para></li>
79+
/// </ul>
80+
/// Recommended for applications that need full fidelity with native DynamoDB types.
6981
/// </summary>
7082
V2 = 1,
7183
}

sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,47 @@ public sealed class DynamoDBTableAttribute : DynamoDBAttribute
5252
/// </summary>
5353
public bool LowerCamelCaseProperties { get; set; }
5454

55+
/// <summary>
56+
/// Gets and sets the <see cref="ConversionSchema"/> used for mapping between .NET and DynamoDB types.
57+
///
58+
/// The conversion schema determines how types are serialized and deserialized during data persistence.
59+
/// When resolving the effective schema, the following precedence is applied:
60+
/// 1. If set on the operation configuration, it takes the highest precedence.
61+
/// 2. If not set on the operation, but specified at the table level, the table configuration is used.
62+
/// 3. If neither is set, the context-level configuration is used as the default fallback.
63+
/// </summary>
64+
public ConversionSchema Conversion { get; set; }
65+
5566
/// <summary>
5667
/// Construct an instance of DynamoDBTableAttribute
5768
/// </summary>
5869
/// <param name="tableName"></param>
5970
public DynamoDBTableAttribute(string tableName)
60-
: this(tableName, false) { }
71+
: this(tableName, false, ConversionSchema.Unset)
72+
{
73+
}
6174

6275
/// <summary>
6376
/// Construct an instance of DynamoDBTableAttribute
6477
/// </summary>
6578
/// <param name="tableName"></param>
6679
/// <param name="lowerCamelCaseProperties"></param>
6780
public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties)
81+
: this(tableName, lowerCamelCaseProperties, ConversionSchema.Unset)
82+
{
83+
}
84+
85+
/// <summary>
86+
/// Construct an instance of DynamoDBTableAttribute
87+
/// </summary>
88+
/// <param name="tableName"></param>
89+
/// <param name="lowerCamelCaseProperties"></param>
90+
/// <param name="conversion"></param>
91+
public DynamoDBTableAttribute(string tableName, bool lowerCamelCaseProperties, ConversionSchema conversion)
6892
{
6993
TableName = tableName;
7094
LowerCamelCaseProperties = lowerCamelCaseProperties;
95+
Conversion = conversion;
7196
}
7297
}
7398

sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
432432
bool ignoreNullValues = operationConfig.IgnoreNullValues ?? contextConfig.IgnoreNullValues ?? false;
433433
bool retrieveDateTimeInUtc = operationConfig.RetrieveDateTimeInUtc ?? contextConfig.RetrieveDateTimeInUtc ?? true;
434434
bool isEmptyStringValueEnabled = operationConfig.IsEmptyStringValueEnabled ?? contextConfig.IsEmptyStringValueEnabled ?? false;
435-
DynamoDBEntryConversion conversion = operationConfig.Conversion ?? contextConfig.Conversion ?? DynamoDBEntryConversion.CurrentConversion;
435+
DynamoDBEntryConversion conversion = contextConfig.Conversion ?? DynamoDBEntryConversion.CurrentConversion;
436436
string tableNamePrefix = operationConfig.TableNamePrefix ?? contextConfig.TableNamePrefix ?? string.Empty;
437437

438438
// These properties can only be set at the operation level
@@ -463,15 +463,16 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
463463
IndexName = indexName;
464464
QueryFilter = queryFilter;
465465
ConditionalOperator = conditionalOperator;
466-
Conversion = conversion;
466+
ContextConversion = conversion;
467+
OperationConversion = operationConfig.Conversion;
467468
MetadataCachingMode = metadataCachingMode;
468469
DisableFetchingTableMetadata = disableFetchingTableMetadata;
469470
RetrieveDateTimeInUtc = retrieveDateTimeInUtc;
470471
DerivedTypeAttributeName = derivedTypeAttributeName;
471472

472473
State = new OperationState();
473474
}
474-
475+
475476
/// <summary>
476477
/// Property that directs DynamoDBContext to use consistent reads.
477478
/// If property is not set, behavior defaults to non-consistent reads.
@@ -552,10 +553,31 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
552553
public List<ScanCondition> QueryFilter { get; set; }
553554

554555
/// <summary>
555-
/// Conversion specification which controls how conversion between
556+
/// Specifies the conversion behavior for .NET objects (entities) mapped to DynamoDB items.
557+
///
558+
/// This setting controls how conversion between .NET and DynamoDB types happens
559+
/// on classes annotated with <see cref="DynamoDBTableAttribute"/>
560+
/// </summary>
561+
public DynamoDBEntryConversion ItemConversion { get; set; }
562+
563+
564+
/// <summary>
565+
/// Operation Conversion specification which controls how conversion between
556566
/// .NET and DynamoDB types happens.
557567
/// </summary>
558-
public DynamoDBEntryConversion Conversion { get; set; }
568+
private DynamoDBEntryConversion OperationConversion { get; }
569+
570+
/// <summary>
571+
/// Context Conversion specification which controls how conversion between
572+
/// .NET and DynamoDB types happens.
573+
/// </summary>
574+
public DynamoDBEntryConversion Conversion => OperationConversion ?? ItemConversion ?? ContextConversion;
575+
576+
/// <summary>
577+
/// Context Conversion specification which controls how conversion between
578+
/// .NET and DynamoDB types happens.
579+
/// </summary>
580+
private DynamoDBEntryConversion ContextConversion { get; }
559581

560582
/// <inheritdoc cref="DynamoDBContextConfig.DisableFetchingTableMetadata"/>
561583
public bool DisableFetchingTableMetadata { get; set; }
@@ -564,7 +586,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
564586
public bool RetrieveDateTimeInUtc { get; set; }
565587

566588
// Checks if the IndexName is set on the config
567-
internal bool IsIndexOperation { get { return !string.IsNullOrEmpty(IndexName); } }
589+
internal bool IsIndexOperation => !string.IsNullOrEmpty(IndexName);
568590

569591
// State of the operation using this config
570592
internal OperationState State { get; private set; }
@@ -584,6 +606,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte
584606
/// </remarks>
585607
public string DerivedTypeAttributeName { get; set; }
586608

609+
587610
public class OperationState
588611
{
589612
private CircularReferenceTracking referenceTracking;

sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@ internal class ItemStorageConfig
428428
// indexName to GSIConfig mapping
429429
public Dictionary<string, GSIConfig> IndexNameToGSIMapping { get; set; }
430430

431+
// entity conversion
432+
public DynamoDBEntryConversion Conversion { get; set; }
433+
431434

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

@@ -735,6 +738,10 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants
735738

736739
if (tableCache.Cache.TryGetValue(actualTableName, out config))
737740
{
741+
if (flatConfig == null)
742+
throw new ArgumentNullException("flatConfig");
743+
744+
flatConfig.ItemConversion = config.Conversion;
738745
return config;
739746
}
740747
}
@@ -759,6 +766,8 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants
759766
if (tableCache == null)
760767
{
761768
var baseStorageConfig = CreateStorageConfig(type, actualTableName: null, flatConfig);
769+
flatConfig.ItemConversion = baseStorageConfig.Conversion;
770+
762771
tableCache = new ConfigTableCache(baseStorageConfig);
763772
Cache[type] = tableCache;
764773
}
@@ -780,6 +789,7 @@ public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants
780789
}
781790

782791
config = CreateStorageConfig(type, actualTableName, flatConfig);
792+
flatConfig.ItemConversion = config.Conversion;
783793
tableCache.Cache[actualTableName] = config;
784794

785795
return config;
@@ -858,6 +868,13 @@ private static void PopulateConfigFromType(ItemStorageConfig config, [Dynamicall
858868
if (string.IsNullOrEmpty(tableAttribute.TableName)) throw new InvalidOperationException("DynamoDBTableAttribute.Table is empty or null");
859869
config.TableName = tableAttribute.TableName;
860870
config.LowerCamelCaseProperties = tableAttribute.LowerCamelCaseProperties;
871+
872+
config.Conversion = tableAttribute.Conversion switch
873+
{
874+
ConversionSchema.V1 => DynamoDBEntryConversion.V1,
875+
ConversionSchema.V2 => DynamoDBEntryConversion.V2,
876+
_ => config.Conversion
877+
};
861878
}
862879

863880
string tableAlias;

sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,45 @@ private void TestContextConversions()
11141114
#pragma warning restore CS0618 // Re-enable the warning
11151115
}
11161116

1117+
{
1118+
1119+
#pragma warning disable CS0618 // Disable the warning for the deprecated DynamoDBContext constructors
1120+
ProductV2 productV2 = new ProductV2
1121+
{
1122+
Id = 1,
1123+
Name = "CloudSpotter",
1124+
CompanyName = "CloudsAreGrate",
1125+
Price = 1200,
1126+
TagSet = new HashSet<string> { "Prod", "1.0" },
1127+
CurrentStatus = Status.Active,
1128+
FormerStatus = Status.Upcoming,
1129+
Supports = Support.Unix | Support.Windows,
1130+
PreviousSupport = null,
1131+
InternalId = "T1000",
1132+
IsPublic = true,
1133+
AlwaysN = true,
1134+
Rating = 4,
1135+
Components = new List<string> { "Code", "Coffee" },
1136+
KeySizes = new List<byte> { 16, 64, 128 },
1137+
CompanyInfo = new CompanyInfo
1138+
{
1139+
Name = "MyCloud",
1140+
Founded = new DateTime(1994, 7, 6),
1141+
Revenue = 9001
1142+
}
1143+
};
1144+
1145+
using (var contextV1 = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = conversionV1 }))
1146+
{
1147+
var docV1 = contextV1.ToDocument(productV2, new ToDocumentConfig { Conversion = conversionV1 });
1148+
var docV2 = contextV1.ToDocument(productV2, new ToDocumentConfig { });
1149+
VerifyConversions(docV1, docV2);
1150+
}
1151+
1152+
#pragma warning restore CS0618 // Re-enable the warning
1153+
1154+
}
1155+
11171156
// Introduce a circular reference and try to serialize
11181157
{
11191158
product.CompanyInfo = new CompanyInfo
@@ -2632,6 +2671,11 @@ public object FromEntry(DynamoDBEntry entry)
26322671
}
26332672
}
26342673

2674+
[DynamoDBTable("HashTable", false, ConversionSchema.V2)]
2675+
public class ProductV2 : Product
2676+
{
2677+
}
2678+
26352679
/// <summary>
26362680
/// Class representing items in the table [TableNamePrefix]HashTable
26372681
/// </summary>

0 commit comments

Comments
 (0)