Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix DynamoDbv2 bug for incorrect DateTime epoch serialization when date falls out of epoch supported range #3672

Open
wants to merge 9 commits into
base: main-staging
Choose a base branch
from
18 changes: 18 additions & 0 deletions generator/.DevConfigs/3436963d-3bfd-4d4b-a929-bc652e5c1b4d.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"core": {
"changeLogMessages": [
"Added method AWSSDKUtils.ConvertFromUnixLongEpochSeconds() for converting Unix epoch seconds to DateTime structure."
],
"type": "patch",
"updateMinimum": false
},
"services": [
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also changed Core (sdk/src/Core/Amazon.Util/AWSSDKUtils.cs) but didn't include it here.

Copy link
Contributor Author

@ashishdhingra ashishdhingra Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching it. Fixed. Used updateMinimum as false since only AWSSDK.DynamoDBv2 uses it.

"serviceName": "DynamoDBv2",
"type": "patch",
"changeLogMessages": [
"Fixed an issue for incorrect DateTime epoch serialization when date falls out of epoch supported range. (Thanks @sander1095 for initial contribution)"
]
}
]
}
10 changes: 10 additions & 0 deletions sdk/src/Core/Amazon.Util/AWSSDKUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,16 @@ public static DateTime ConvertFromUnixEpochSeconds(int seconds)
return new DateTime(seconds * 10000000L + EPOCH_START.Ticks, DateTimeKind.Utc).ToLocalTime();
}

/// <summary>
/// Utility method for converting Unix epoch seconds to DateTime structure.
/// </summary>
/// <param name="seconds">The number of seconds since January 1, 1970.</param>
/// <returns>Converted DateTime structure</returns>
public static DateTime ConvertFromUnixLongEpochSeconds(long seconds)
{
return new DateTime(seconds * 10000000L + EPOCH_START.Ticks, DateTimeKind.Utc).ToLocalTime();
}

/// <summary>
/// Utility method for converting Unix epoch milliseconds to DateTime structure.
/// </summary>
Expand Down
22 changes: 20 additions & 2 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
Expand All @@ -21,7 +21,7 @@ namespace Amazon.DynamoDBv2.DataModel
/// <summary>
/// Base DynamoDB attribute.
/// </summary>
[AttributeUsage(AttributeTargets.All, Inherited=true, AllowMultiple=false)]
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public abstract class DynamoDBAttribute : Attribute
{
}
Expand Down Expand Up @@ -188,6 +188,7 @@ public DynamoDBPropertyAttribute(Type converter)
/// Whether the data should be stored as epoch seconds.
/// If false, data is stored as ISO-8601 string.
/// </param>
[Obsolete("This constructor is obsolete. Set the property " + nameof(StoreAsEpochLong) + " for proper 64-bit support instead.")]
public DynamoDBPropertyAttribute(bool storeAsEpoch)
{
StoreAsEpoch = storeAsEpoch;
Expand Down Expand Up @@ -220,6 +221,7 @@ public DynamoDBPropertyAttribute(string attributeName, Type converter)
/// Whether the data should be stored as epoch seconds.
/// If false, data is stored as ISO-8601 string.
/// </param>
[Obsolete("This constructor is obsolete. Use DynamoDbProperty(string attributeName) and set the property " + nameof(StoreAsEpochLong) + " for proper 64-bit support instead.")]
public DynamoDBPropertyAttribute(string attributeName, bool storeAsEpoch)
: base(attributeName)
{
Expand All @@ -236,8 +238,24 @@ public DynamoDBPropertyAttribute(string attributeName, bool storeAsEpoch)
/// Flag that directs DynamoDBContext to store this data as epoch seconds integer.
/// If false, data is stored as ISO-8601 string.
/// Cannot be set at the same time as Converter.
/// This property does not support dates after 2038-01-19. To store dates after this time, use StoreAsEpochLong instead.
/// </summary>
/// <remarks>
/// For more information on the issues surrounding dates after 2038-01-19, see the following link:
/// https://github.com/aws/aws-sdk-net/issues/3443
/// </remarks>
[Obsolete("This property is obsolete. Dates after 2038-01-19 will NOT be stored in the epoch seconds integer format. To fix this, use " + nameof(StoreAsEpochLong) + " instead.")]
public bool StoreAsEpoch { get; set; }

/// <summary>
/// Flag that directs DynamoDBContext to store this data as epoch seconds integer.
/// If false, data is stored as ISO-8601 string.
/// Cannot be set at the same time as Converter.
/// </summary>
/// <remarks>
/// This property supports dates after 2038-01-19 (known as the Year 2038 problem).
/// </remarks>
public bool StoreAsEpochLong { get; set; }
}

/// <summary>
Expand Down
12 changes: 8 additions & 4 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
Expand Down Expand Up @@ -142,7 +142,7 @@ internal Table GetTargetTable(ItemStorageConfig storageConfig, DynamoDBFlatConfi
ValidateConfigAgainstTable(storageConfig, unconfiguredTable);

var tableConfig = new TableConfig(tableName, flatConfig.Conversion, consumer,
storageConfig.AttributesToStoreAsEpoch, flatConfig.IsEmptyStringValueEnabled,
storageConfig.AttributesToStoreAsEpoch, storageConfig.AttributesToStoreAsEpochLong, flatConfig.IsEmptyStringValueEnabled,
flatConfig.MetadataCachingMode);
var table = unconfiguredTable.Copy(tableConfig);
return table;
Expand Down Expand Up @@ -189,7 +189,7 @@ internal Table GetUnconfiguredTable(string tableName, bool disableFetchingTableM
}

var emptyConfig = new TableConfig(tableName, conversion: null, consumer: Table.DynamoDBConsumer.DataModel,
storeAsEpoch: null, isEmptyStringValueEnabled: false, metadataCachingMode: Config.MetadataCachingMode);
storeAsEpoch: null, storeAsEpochLong: null, isEmptyStringValueEnabled: false, metadataCachingMode: Config.MetadataCachingMode);
table = Table.LoadTable(Client, emptyConfig);
tablesMap[tableName] = table;

Expand Down Expand Up @@ -1074,6 +1074,8 @@ internal Key MakeKey(object hashKey, object rangeKey, ItemStorageConfig storageC
if (hashKeyEntry == null) throw new InvalidOperationException("Unable to convert hash key value for property " + hashKeyPropertyName);
if (storageConfig.AttributesToStoreAsEpoch.Contains(hashKeyProperty.AttributeName))
hashKeyEntry = Document.DateTimeToEpochSeconds(hashKeyEntry, hashKeyProperty.AttributeName);
if (storageConfig.AttributesToStoreAsEpochLong.Contains(hashKeyProperty.AttributeName))
hashKeyEntry = Document.DateTimeToEpochSecondsLong(hashKeyEntry, hashKeyProperty.AttributeName);
var hashKeyEntryAttributeConversionConfig = new DynamoDBEntry.AttributeConversionConfig(flatConfig.Conversion, flatConfig.IsEmptyStringValueEnabled);
key[hashKeyProperty.AttributeName] = hashKeyEntry.ConvertToAttributeValue(hashKeyEntryAttributeConversionConfig);

Expand All @@ -1092,6 +1094,8 @@ internal Key MakeKey(object hashKey, object rangeKey, ItemStorageConfig storageC
if (rangeKeyEntry == null) throw new InvalidOperationException("Unable to convert range key value for property " + rangeKeyPropertyName);
if (storageConfig.AttributesToStoreAsEpoch.Contains(rangeKeyProperty.AttributeName))
rangeKeyEntry = Document.DateTimeToEpochSeconds(rangeKeyEntry, rangeKeyProperty.AttributeName);
if (storageConfig.AttributesToStoreAsEpochLong.Contains(rangeKeyProperty.AttributeName))
rangeKeyEntry = Document.DateTimeToEpochSecondsLong(rangeKeyEntry, rangeKeyProperty.AttributeName);

var rangeKeyEntryAttributeConversionConfig = new DynamoDBEntry.AttributeConversionConfig(flatConfig.Conversion, flatConfig.IsEmptyStringValueEnabled);
key[rangeKeyProperty.AttributeName] = rangeKeyEntry.ConvertToAttributeValue(rangeKeyEntryAttributeConversionConfig);
Expand All @@ -1105,7 +1109,7 @@ internal Key MakeKey<T>(T keyObject, ItemStorageConfig storageConfig, DynamoDBFl
ItemStorage keyAsStorage = ObjectToItemStorageHelper(keyObject, storageConfig, flatConfig, keysOnly: true, ignoreNullValues: true);
if (storageConfig.HasVersion) // if version field is defined, it would have been returned, so remove before making the key
keyAsStorage.Document[storageConfig.VersionPropertyStorage.AttributeName] = null;
Key key = new Key(keyAsStorage.Document.ToAttributeMap(flatConfig.Conversion, storageConfig.AttributesToStoreAsEpoch, flatConfig.IsEmptyStringValueEnabled));
Key key = new Key(keyAsStorage.Document.ToAttributeMap(flatConfig.Conversion, storageConfig.AttributesToStoreAsEpoch, storageConfig.AttributesToStoreAsEpochLong, flatConfig.IsEmptyStringValueEnabled));
ValidateKey(key, storageConfig);
return key;
}
Expand Down
23 changes: 18 additions & 5 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
Expand Down Expand Up @@ -94,6 +94,9 @@ internal class PropertyStorage : SimplePropertyStorage
// whether to store DateTime as epoch seconds integer
public bool StoreAsEpoch { get; set; }

// whether to store DateTime as epoch seconds integer (with support for dates AFTER 2038)
public bool StoreAsEpochLong { get; set; }

// corresponding IndexNames, if applicable
public List<string> IndexNames { get; set; }

Expand Down Expand Up @@ -164,15 +167,18 @@ public void Validate(DynamoDBContext context)

if (ConverterType != null)
{
if (StoreAsEpoch)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as StoreAsEpoch is set to true");
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");

if (!Utils.CanInstantiateConverter(ConverterType) || !Utils.ImplementsInterface(ConverterType, typeof(IPropertyConverter)))
throw new InvalidOperationException("Converter for " + PropertyName + " must be instantiable with no parameters and must implement IPropertyConverter");

this.Converter = Utils.InstantiateConverter(ConverterType, context) as IPropertyConverter;
}

if (StoreAsEpoch && StoreAsEpochLong)
throw new InvalidOperationException(PropertyName + " must not set both StoreAsEpoch and StoreAsEpochLong as true at the same time.");

IPropertyConverter converter;
if (context.ConverterCache.TryGetValue(MemberType, out converter) && converter != null)
{
Expand Down Expand Up @@ -350,6 +356,7 @@ internal class ItemStorageConfig : StorageConfig
public string TableName { get; set; }
public bool LowerCamelCaseProperties { get; set; }
public HashSet<string> AttributesToStoreAsEpoch { get; set; }
public HashSet<string> AttributesToStoreAsEpochLong { get; set; }

// keys
public List<string> HashKeyPropertyNames { get; private set; }
Expand Down Expand Up @@ -492,6 +499,8 @@ private void AddPropertyStorage(PropertyStorage value)
AttributesToGet.Add(attributeName);
if (value.StoreAsEpoch)
AttributesToStoreAsEpoch.Add(attributeName);
if (value.StoreAsEpochLong)
AttributesToStoreAsEpochLong.Add(attributeName);

if (value.IsLSIRangeKey || value.IsGSIKey)
{
Expand Down Expand Up @@ -563,6 +572,7 @@ public ItemStorageConfig(Type targetType)
HashKeyPropertyNames = new List<string>();
RangeKeyPropertyNames = new List<string>();
AttributesToStoreAsEpoch = new HashSet<string>();
AttributesToStoreAsEpochLong = new HashSet<string>();
}
}

Expand Down Expand Up @@ -730,7 +740,7 @@ private ItemStorageConfig CreateStorageConfig(Type baseType, string actualTableN
actualTableName = DynamoDBContext.GetTableName(config.TableName, flatConfig);
}
var emptyConfig = new TableConfig(actualTableName, conversion: null, consumer: Table.DynamoDBConsumer.DataModel,
storeAsEpoch: null, isEmptyStringValueEnabled: false, metadataCachingMode: flatConfig.MetadataCachingMode);
storeAsEpoch: null, storeAsEpochLong: null, isEmptyStringValueEnabled: false, metadataCachingMode: flatConfig.MetadataCachingMode);
var table = Table.CreateTableFromItemStorageConfig(Context.Client, emptyConfig, config, flatConfig);

// The table info must be cached under the actual table name exactly how it exists in the DynamoDB service.
Expand Down Expand Up @@ -786,7 +796,10 @@ private static void PopulateConfigFromType(ItemStorageConfig config, Type type)
DynamoDBPropertyAttribute propertyAttribute = attribute as DynamoDBPropertyAttribute;
if (propertyAttribute != null)
{
#pragma warning disable CS0618 // Type or member is obsolete
propertyStorage.StoreAsEpoch = propertyAttribute.StoreAsEpoch;
#pragma warning restore CS0618 // Type or member is obsolete
propertyStorage.StoreAsEpochLong = propertyAttribute.StoreAsEpochLong;

if (propertyAttribute.Converter != null)
propertyStorage.ConverterType = propertyAttribute.Converter;
Expand Down Expand Up @@ -822,7 +835,7 @@ private static void PopulateConfigFromType(ItemStorageConfig config, Type type)
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: added whitespace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it somehow.

config.Properties.Add(propertyStorage);
}
}
Expand Down
Loading