diff --git a/generator/.DevConfigs/ac013f8a-bdb9-4a20-8d70-a9b60fe8c011.json b/generator/.DevConfigs/ac013f8a-bdb9-4a20-8d70-a9b60fe8c011.json new file mode 100644 index 000000000000..909571122170 --- /dev/null +++ b/generator/.DevConfigs/ac013f8a-bdb9-4a20-8d70-a9b60fe8c011.json @@ -0,0 +1,18 @@ +{ + "core": { + "changeLogMessages": [ + "Add support for retrieving DateTime attributes in UTC timezone from a DynamoDB table." + ], + "type": "patch", + "updateMinimum": false + }, + "services": [ + { + "serviceName": "DynamoDBv2", + "type": "patch", + "changeLogMessages": [ + "Add support for retrieving DateTime attributes in UTC timezone from a DynamoDB table." + ] + } + ] +} \ No newline at end of file diff --git a/sdk/src/Core/AWSConfigs.cs b/sdk/src/Core/AWSConfigs.cs index a223bd52a14f..9d3b56533ba0 100644 --- a/sdk/src/Core/AWSConfigs.cs +++ b/sdk/src/Core/AWSConfigs.cs @@ -45,7 +45,7 @@ namespace Amazon /// <proxy host="localhost" port="8888" username="1" password="1" /> /// /// <dynamoDB> - /// <dynamoDBContext tableNamePrefix="Prod-" metadataCachingMode="Default" disableFetchingTableMetadata="false"> + /// <dynamoDBContext tableNamePrefix="Prod-" metadataCachingMode="Default" disableFetchingTableMetadata="false" retrieveDateTimeInUtc="false"> /// /// <tableAliases> /// <alias fromTable="FakeTable" toTable="People" /> diff --git a/sdk/src/Services/DynamoDBv2/Custom/AWSConfigs.DynamoDB.cs b/sdk/src/Services/DynamoDBv2/Custom/AWSConfigs.DynamoDB.cs index 363b0739b9e3..bfc136d806ff 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/AWSConfigs.DynamoDB.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/AWSConfigs.DynamoDB.cs @@ -167,6 +167,12 @@ public partial class DynamoDBContextConfig /// public bool? DisableFetchingTableMetadata { get; set; } + /// + /// If true, all properties are retrieved in UTC timezone while reading data from DynamoDB. Else, the local timezone is used. + /// + /// This setting is only applicable to the high-level library. Service calls made via will always return attributes in UTC. + public bool? RetrieveDateTimeInUtc { get; set; } + /// /// Adds a TableAlias to the TableAliases property. /// An exception is thrown if there is already a TableAlias with the same FromTable configured. @@ -203,6 +209,7 @@ internal void Configure(DynamoDBContextSection section) TableNamePrefix = section.TableNamePrefix; MetadataCachingMode = section.MetadataCachingMode; DisableFetchingTableMetadata = section.DisableFetchingTableMetadata; + RetrieveDateTimeInUtc = section.RetrieveDateTimeInUtc; InternalSDKUtils.FillDictionary(section.TypeMappings.Items, t => t.Type, t => new TypeMapping(t), TypeMappings); InternalSDKUtils.FillDictionary(section.TableAliases.Items, t => t.FromTable, t => t.ToTable, TableAliases); @@ -422,6 +429,7 @@ internal class DynamoDBContextSection : WritableConfigurationElement private const string mappingsKey = "mappings"; private const string metadataCachingModeKey = "metadataCachingMode"; private const string disableFetchingTableMetadataKey = "disableFetchingTableMetadata"; + private const string retrieveDateTimeInUtcKey = "retrieveDateTimeInUtc"; [ConfigurationProperty(tableNamePrefixKey)] public string TableNamePrefix @@ -458,6 +466,12 @@ public bool? DisableFetchingTableMetadata set { this[disableFetchingTableMetadataKey] = value; } } + [ConfigurationProperty(retrieveDateTimeInUtcKey)] + public bool? RetrieveDateTimeInUtc + { + get { return (bool?)this[retrieveDateTimeInUtcKey]; } + set { this[retrieveDateTimeInUtcKey] = value; } + } } /// diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs index 1d4773caeb6d..752009672ccf 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Configs.cs @@ -62,6 +62,7 @@ public DynamoDBContextConfig() Conversion = DynamoDBEntryConversion.CurrentConversion; MetadataCachingMode = AWSConfigsDynamoDB.Context.MetadataCachingMode; DisableFetchingTableMetadata = AWSConfigsDynamoDB.Context.DisableFetchingTableMetadata; + RetrieveDateTimeInUtc = AWSConfigsDynamoDB.Context.RetrieveDateTimeInUtc; } /// @@ -130,6 +131,12 @@ public DynamoDBContextConfig() /// otherwise exceptions may be thrown and/or the results of certain DynamoDB operations may change. /// public bool? DisableFetchingTableMetadata { get; set; } + + /// + /// If true, all properties are retrieved in UTC timezone while reading data from DynamoDB. Else, the local timezone is used. + /// + /// This setting is only applicable to the high-level library. Service calls made via will always return attributes in UTC. + public bool? RetrieveDateTimeInUtc { get; set; } } /// @@ -334,6 +341,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte bool skipVersionCheck = operationConfig.SkipVersionCheck ?? contextConfig.SkipVersionCheck ?? false; bool ignoreNullValues = operationConfig.IgnoreNullValues ?? contextConfig.IgnoreNullValues ?? false; bool disableFetchingTableMetadata = contextConfig.DisableFetchingTableMetadata ?? false; + bool retrieveDateTimeInUtc = operationConfig.RetrieveDateTimeInUtc ?? contextConfig.RetrieveDateTimeInUtc ?? false; bool isEmptyStringValueEnabled = operationConfig.IsEmptyStringValueEnabled ?? contextConfig.IsEmptyStringValueEnabled ?? false; string overrideTableName = @@ -362,6 +370,7 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte Conversion = conversion; MetadataCachingMode = metadataCachingMode; DisableFetchingTableMetadata = disableFetchingTableMetadata; + RetrieveDateTimeInUtc = retrieveDateTimeInUtc; State = new OperationState(); } @@ -454,6 +463,9 @@ public DynamoDBFlatConfig(DynamoDBOperationConfig operationConfig, DynamoDBConte /// public bool DisableFetchingTableMetadata { get; set; } + /// + public bool RetrieveDateTimeInUtc { get; set; } + // Checks if the IndexName is set on the config internal bool IsIndexOperation { get { return !string.IsNullOrEmpty(IndexName); } } diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs index 572e5e82eca3..6d1b01cf2eaf 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs @@ -463,7 +463,14 @@ private object FromDynamoDBEntry(SimplePropertyStorage propertyStorage, DynamoDB var targetType = propertyStorage.MemberType; if (conversion.HasConverter(targetType)) - return conversion.ConvertFromEntry(targetType, entry); + { + var output = conversion.ConvertFromEntry(targetType, entry); + if (targetType == typeof(DateTime) && flatConfig.RetrieveDateTimeInUtc) + { + return ((DateTime)output).ToUniversalTime(); + } + return output; + } else { if (entry is DynamoDBNull) diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DynamoDBEntry.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DynamoDBEntry.cs index c6995923c203..bbc7f2f9a76c 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DynamoDBEntry.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DynamoDBEntry.cs @@ -609,6 +609,16 @@ public virtual DateTime AsDateTime() { throw new InvalidCastException(); } + + /// + /// Explicitly convert DynamoDBEntry to DateTime in UTC + /// + /// DateTime value of this object in UTC + public virtual DateTime AsDateTimeUtc() + { + throw new InvalidCastException(); + } + /// /// Implicitly convert DateTime to DynamoDBEntry /// @@ -1152,6 +1162,15 @@ public override DateTime AsDateTime() return (DateTime)System.Convert.ChangeType(Value, typeof(DateTime), CultureInfo.InvariantCulture); } + /// + /// Return the value as a DateTime in UTC. + /// + /// DateTime value of this object in UTC + public override DateTime AsDateTimeUtc() + { + return AsDateTime().ToUniversalTime(); + } + /// /// Return the value as a Decimal. /// diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Primitive.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Primitive.cs index 2e3190115a41..9f44125b7fdf 100644 --- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Primitive.cs +++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Primitive.cs @@ -605,6 +605,16 @@ public override DateTime AsDateTime() { return V1Conversion.ConvertFromEntry(this); } + + /// + /// Explicitly convert Primitive to DateTime in UTC + /// + /// DateTime value of this object in UTC + public override DateTime AsDateTimeUtc() + { + return AsDateTime().ToUniversalTime(); + } + /// /// Implicitly convert DateTime to Primitive /// diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs index e0a45136f441..8d9b6ae84011 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs @@ -158,6 +158,212 @@ public void TestContext_DisableFetchingTableMetadata_DateTimeAsHashKey() Assert.AreEqual(employee.Age, storedEmployee.Age); } + /// + /// Tests that the DynamoDB operations can retrieve attributes in UTC and local timezone. + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + [DataRow(true)] + [DataRow(false)] + public void TestContext_RetrieveDateTimeInUtc(bool retrieveDateTimeInUtc) + { + TableCache.Clear(); + CleanupTables(); + TableCache.Clear(); + + var config = new DynamoDBContextConfig + { + Conversion = DynamoDBEntryConversion.V2, + RetrieveDateTimeInUtc = retrieveDateTimeInUtc + }; + Context = new DynamoDBContext(Client, config); + + var currTime = DateTime.Now; + + var employee = new AnnotatedNumericEpochEmployee + { + Name = "Bob", + Age = 45, + CreationTime = currTime, + EpochDate2 = currTime, + NonEpochDate1 = currTime, + NonEpochDate2 = currTime + }; + + Context.Save(employee); + var expectedCurrTime = retrieveDateTimeInUtc ? currTime.ToUniversalTime() : currTime.ToLocalTime(); + + // Load + var storedEmployee = Context.Load(employee.CreationTime, employee.Name); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + + // Query + QueryFilter filter = new QueryFilter(); + filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); + storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }).First(); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + + // Scan + storedEmployee = Context.Scan().First(); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + } + + /// + /// Tests that if a custom converter is used, then the is ignored. + /// + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + [DataRow(true)] + [DataRow(false)] + public void TestContext_CustomDateTimeConverter(bool retrieveDateTimeInUtc) + { + TableCache.Clear(); + CleanupTables(); + TableCache.Clear(); + + var config = new DynamoDBContextConfig + { + Conversion = DynamoDBEntryConversion.V2, + RetrieveDateTimeInUtc = retrieveDateTimeInUtc + }; + Context = new DynamoDBContext(Client, config); + + // Add a custom DateTime converter + Context.ConverterCache.Add(typeof(DateTime), new DateTimeUtcConverter()); + + var currTime = DateTime.Now; + + var employee = new AnnotatedNumericEpochEmployee + { + Name = "Bob", + Age = 45, + CreationTime = currTime, + EpochDate2 = currTime, + NonEpochDate1 = currTime, + NonEpochDate2 = currTime + }; + + Context.Save(employee); + + // Since we are adding a custom DateTimeUtcConverter, the expected time will always be in the UTC time zone. + // regardless of RetrieveDateTimeInUtc value. + var expectedCurrTime = currTime.ToUniversalTime(); + + // Load + var storedEmployee = Context.Load(employee.CreationTime, employee.Name); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + + // Query + QueryFilter filter = new QueryFilter(); + filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); + storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }).First(); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + + // Scan + storedEmployee = Context.Scan().First(); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + } + + /// + /// Tests that the DynamoDB operations can retrieve attributes in UTC and local timezone using the + /// + [TestMethod] + [TestCategory("DynamoDBv2")] + [DataRow(true)] + [DataRow(false)] + public void TestContext_RetrieveDateTimeInUtc_OperationConfig(bool retrieveDateTimeInUtc) + { + TableCache.Clear(); + CleanupTables(); + TableCache.Clear(); + + Context = new DynamoDBContext(Client, new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 }); + var operationConfig = new DynamoDBOperationConfig { RetrieveDateTimeInUtc = retrieveDateTimeInUtc }; + + var currTime = DateTime.Now; + + var employee = new AnnotatedNumericEpochEmployee + { + Name = "Bob", + Age = 45, + CreationTime = currTime, + EpochDate2 = currTime, + NonEpochDate1 = currTime, + NonEpochDate2 = currTime + }; + + Context.Save(employee); + var expectedCurrTime = retrieveDateTimeInUtc ? currTime.ToUniversalTime() : currTime.ToLocalTime(); + + // Load + var storedEmployee = Context.Load(employee.CreationTime, employee.Name, operationConfig); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + + // Query + QueryFilter filter = new QueryFilter(); + filter.AddCondition("CreationTime", QueryOperator.Equal, currTime); + storedEmployee = Context.FromQuery(new QueryOperationConfig { Filter = filter }, operationConfig).First(); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + + // Scan + storedEmployee = Context.Scan(new List(), operationConfig).First(); + Assert.IsNotNull(storedEmployee); + ApproximatelyEqual(expectedCurrTime, storedEmployee.CreationTime); + ApproximatelyEqual(expectedCurrTime, storedEmployee.EpochDate2); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate1); + ApproximatelyEqual(expectedCurrTime, storedEmployee.NonEpochDate2); + Assert.AreEqual(employee.Name, storedEmployee.Name); + Assert.AreEqual(employee.Age, storedEmployee.Age); + } /// /// Runs the same object-mapper integration tests as , @@ -1699,6 +1905,17 @@ public class AnnotatedNumericEpochEmployee : EpochEmployee public override string Name { get; set; } } + public class DateTimeUtcConverter : IPropertyConverter + { + public DynamoDBEntry ToEntry(object value) => (DateTime)value; + + public object FromEntry(DynamoDBEntry entry) + { + var dateTime = entry.AsDateTime(); + return dateTime.ToUniversalTime(); + } + } + #endregion } } diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs index a7173f61ca13..a55f8309ff6d 100644 --- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs +++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs @@ -1,7 +1,6 @@ using System; using System.Text; using System.Collections.Generic; -using System.Linq; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -10,8 +9,6 @@ using Amazon.DynamoDBv2.Model; using Amazon.DynamoDBv2.DocumentModel; using System.IO; -using ThirdParty.Json.LitJson; -using System.Xml; using ReturnValuesOnConditionCheckFailure = Amazon.DynamoDBv2.DocumentModel.ReturnValuesOnConditionCheckFailure; @@ -78,6 +75,9 @@ public void TestTableOperations() // Test storing some attributes as epoch seconds TestStoreAsEpoch(hashRangeTable, numericHashRangeTable); + + // Test that attributes stored as Datetimes can be retrieved in UTC. + TestAsDateTimeUtc(numericHashRangeTable); } } @@ -154,9 +154,40 @@ public void TestTableOperationsViaBuilder() // Test storing some attributes as epoch seconds TestStoreAsEpoch(hashRangeTable, numericHashRangeTable); + + // Test that attributes stored as Datetimes can be retrieved in UTC. + TestAsDateTimeUtc(numericHashRangeTable); } } + private void TestAsDateTimeUtc(Table numericHashRangeTable) + { + var config = new TableConfig(numericHashRangeTable.TableName) + { + AttributesToStoreAsEpoch = new List { "CreationTime", "EpochDate2" } + }; + var numericEpochTable = Table.LoadTable(Client, config); + + // Capture current time + var currTime = DateTime.Now; + var currTimeUtc = currTime.ToUniversalTime(); + + // Save Item + var doc = new Document(); + doc["Name"] = "Bob"; + doc["Age"] = 42; + doc["CreationTime"] = currTime; + doc["EpochDate2"] = currTime; + doc["NonEpochDate"] = currTime; + numericEpochTable.PutItem(doc); + + // Load Item + var storedDoc = numericEpochTable.GetItem(currTime, "Bob", new GetItemOperationConfig { ConsistentRead = true}); + ApproximatelyEqual(currTimeUtc, storedDoc["CreationTime"].AsDateTimeUtc()); + ApproximatelyEqual(currTimeUtc, storedDoc["EpochDate2"].AsDateTimeUtc()); + ApproximatelyEqual(currTimeUtc, storedDoc["NonEpochDate"].AsDateTimeUtc()); + } + private void TestEmptyString(Table hashTable) { var companyInfo = new DynamoDBList(); diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DynamoDBTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DynamoDBTests.cs index 365d4fddf751..e6bb3158e6a0 100644 --- a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DynamoDBTests.cs +++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DynamoDBTests.cs @@ -383,6 +383,16 @@ public void TestDisableFetchingTableMetadata_UsingGlobalContext() AWSConfigsDynamoDB.Context.DisableFetchingTableMetadata = false; } + [TestMethod] + [TestCategory("DynamoDBv2")] + public void TestRetrieveDateTimeInUtc_UsingGlobalContext() + { + AWSConfigsDynamoDB.Context.RetrieveDateTimeInUtc = true; + var config = new DynamoDBContextConfig(); + Assert.AreEqual(true, config.RetrieveDateTimeInUtc); + AWSConfigsDynamoDB.Context.RetrieveDateTimeInUtc = false; + } + public class Parent { [DynamoDBProperty("actualPropertyName")]