-
Notifications
You must be signed in to change notification settings - Fork 862
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
Prevent DynamoDB SDK converting the datetime in local timezone #1450
Comments
I'm also seeing this problem. Is there any workaround available? |
Facing same issue, any solution? |
We use a custom converter as a workaround for dates stored in UTC public class DateTimeUtcConverter : IPropertyConverter
{
public DynamoDBEntry ToEntry(object value) => (DateTime) value;
public object FromEntry(DynamoDBEntry entry)
{
var dateTime = entry.AsDateTime();
return dateTime.ToUniversalTime();
}
} Applied to properties like that: public class Entity
{
[DynamoDBProperty("ddb_field", typeof(DateTimeUtcConverter))]
public DateTime Timestamp { get; set; }
} |
Does anyone know how to use the converter when calling DynamoDBContext.LoadAsync() |
Looks like there is a bug in SchemaV1.cs at DateTimeConverterV1.TryFrom method: return DateTime.TryParseExact(
p.StringValue, AWSSDKUtils.ISO8601DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result);
Proof: using System;
using System.Globalization;
namespace DateTimeConverterV1Proof
{
class Program
{
static void Main(string[] args)
{
var utcNow = DateTime.UtcNow;
Console.WriteLine("UTC:\n" + utcNow);
var isoString = utcNow.ToString("o");
// Same string as AWSSDKUtils.ISO8601DateFormat except original fractional which is .fff
const string format = "yyyy-MM-dd\\THH:mm:ss.fffffff\\Z";
DateTime.TryParseExact(isoString,
format, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal,
out var dateTime);
Console.WriteLine("[Original] DateTimeStyles.AssumeUniversal:\n" + dateTime);
DateTime.TryParseExact(isoString,
format, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out dateTime);
Console.WriteLine("[Fixed] DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal:\n" + dateTime);
}
}
} Output:
Fix: Use |
Is there any workaround when not using DynamoDb attributes? I'm experiencing the same with this implementation public Task AddOrUpdate<TDocument>(TDocument document, CancellationToken cancellationToken = default)
where TDocument : class, IDocument
{
var tableName = _supportedDocuments.GetTableName(typeof(TDocument));
var table = Table.LoadTable(_amazonDynamoDbClient, tableName);
var context = new DynamoDBContext(_amazonDynamoDbClient);
var dynamoDbDocument = context.ToDocument(document);
return table.UpdateItemAsync(dynamoDbDocument, cancellationToken);
}
public async Task<TDocument> Get<TDocument>(Guid id, CancellationToken cancellationToken = default)
where TDocument : class, IDocument
{
var tableName = _supportedDocuments.GetTableName(typeof(TDocument));
var table = Table.LoadTable(_amazonDynamoDbClient, tableName);
var hashKey = new Primitive(id.ToString());
var dynamoDbDocument = await table.GetItemAsync(hashKey, cancellationToken);
var context = new DynamoDBContext(_amazonDynamoDbClient);
var document = context.FromDocument<TDocument>(dynamoDbDocument);
return document;
} and some unit tests failed due to a strange conversion. // Given
var document =
new Document
{
Id = _id,
CreatedOn = new DateTime(2021, 1, 1, 0,0,0, DateTimeKind.Utc),
Name = "foo"
};
await _sut.AddOrUpdate(document);
var updatedDocument =
new Document
{
Id = _id,
Name = "bar"
};
}
// When
await _sut.AddOrUpdate(_updatedDocument);
// Then
var result = await _sut.Get<Document>(_id);
result.Should().BeEquivalentTo(_updatedDocument); Fails because the expected CreatedOn should be the default DateTime (e.g: UPDATE: Actually, I have seen that the document is retrieved with the proper date in UTC, but the problem is in the |
Is there any workaround that does not involve using I am using plain DTO without any external dependency, and as per my last comment it seems the Thanks |
Since there is no answer on this, I have created a workaround I'm sharing here. I explicitly convert any DateTime within any object, result of retrieving a Document from DynamoDB and using the So for example, I have a wrapper that gets any object from the database. public async Task<TDocument> Get<TDocument>(string id, CancellationToken cancellationToken = default)
where TDocument : class
{
var table = Table.LoadTable(_amazonDynamoDbClient, "my-table-name");
var hashKey = new Primitive(id);
var dynamoDbDocument = await table.GetItemAsync(hashKey, cancellationToken);
var context = new DynamoDBContext(_amazonDynamoDbClient);
var document = context.FromDocument<TDocument>(dynamoDbDocument);
document.DeepConvertDateTimesToUtc(); // https://github.com/aws/aws-sdk-net/issues/1450 issue with UTC dates
return document;
} and the public static class DocumentExtensions
{
public static void DeepConvertDateTimesToUtc(this IDocument document)
{
// ReSharper disable once ConstantConditionalAccessQualifier
document?.DeepConvertToUtc();
}
private static void DeepConvertToUtc(this object obj)
{
if (obj is ICollection items)
{
foreach (var item in items)
{
item.DeepConvertToUtc();
}
return;
}
var properties = obj.GetType().GetProperties();
foreach (var property in properties)
{
if (property.PropertyType == typeof(DateTime)
|| property.PropertyType == typeof(DateTime?))
{
property.ConvertToUtc(obj);
continue;
}
var value = property.GetValue(obj);
if (value is ICollection list)
{
foreach (var item in list)
{
item.DeepConvertToUtc();
}
}
}
}
private static void ConvertToUtc<TInput>(this PropertyInfo prop, TInput obj)
where TInput : class
{
var value = prop.GetValue(obj);
if (value != null)
{
var converted = TimeZoneInfo.ConvertTimeToUtc((DateTime)value);
var setMethod = prop.SetMethod;
if (setMethod != null)
{
setMethod.Invoke(obj, new[] { (object)converted });
}
}
}
} And here there are some tests to validate the extension. Again, not much to do with the AWS library itself, but a way to view its limitations regarding DateTime conversion. public static class DeepConvertDateTimesToUtcTests
{
public class Given_An_Object_With_Local_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Local);
_sut =
new Foo
{
Id = "foo",
DateTime = dateTime
};
var expectedDateTime = new DateTime(2021, 5, 25, 12, 0, 0, DateTimeKind.Utc);
_expectedResult =
new Foo
{
Id = "foo",
DateTime = expectedDateTime
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime DateTime { get; init; }
}
}
public class Given_An_Object_With_Unspecified_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Unspecified);
_sut =
new Foo
{
Id = "foo",
DateTime = dateTime
};
var expectedDateTime = new DateTime(2021, 5, 25, 12, 0, 0, DateTimeKind.Utc);
_expectedResult =
new Foo
{
Id = "foo",
DateTime = expectedDateTime
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime DateTime { get; init; }
}
}
public class Given_An_Object_With_Utc_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Utc);
_sut =
new Foo
{
Id = "foo",
DateTime = dateTime
};
var expectedDateTime = dateTime;
_expectedResult =
new Foo
{
Id = "foo",
DateTime = expectedDateTime
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Same_Utc_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime DateTime { get; init; }
}
}
public class Given_An_Object_With_Nullable_Local_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Local);
_sut =
new Foo
{
Id = "foo",
DateTime = dateTime
};
var expectedDateTime = new DateTime(2021, 5, 25, 12, 0, 0, DateTimeKind.Utc);
_expectedResult =
new Foo
{
Id = "foo",
DateTime = expectedDateTime
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime? DateTime { get; init; }
}
}
public class Given_An_Object_With_Nullable_Unspecified_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Unspecified);
_sut =
new Foo
{
Id = "foo",
DateTime = dateTime
};
var expectedDateTime = new DateTime(2021, 5, 25, 12, 0, 0, DateTimeKind.Utc);
_expectedResult =
new Foo
{
Id = "foo",
DateTime = expectedDateTime
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime? DateTime { get; init; }
}
}
public class Given_An_Object_With_Nullable_Utc_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Utc);
_sut =
new Foo
{
Id = "foo",
DateTime = dateTime
};
var expectedDateTime = dateTime;
_expectedResult =
new Foo
{
Id = "foo",
DateTime = expectedDateTime
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Same_Utc_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime? DateTime { get; init; }
}
}
public class Given_An_Object_With_Null_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
_sut =
new Foo
{
Id = "foo",
DateTime = null
};
_expectedResult =
new Foo
{
Id = "foo",
DateTime = null
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Same_Null_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult);
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public DateTime? DateTime { get; init; }
}
}
public class Given_An_Object_Without_DateTime_When_Converting
: Given_When_Then_Test
{
private Foo _sut;
private Foo _expectedResult;
protected override void Given()
{
_sut =
new Foo
{
Id = "foo",
Text = "bar"
};
_expectedResult =
new Foo
{
Id = "foo",
Text = "bar"
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Same_Document()
{
_sut.Should().BeEquivalentTo(_expectedResult);
}
class Foo
: IDocument
{
public string Id { get; init; } = string.Empty;
public string Text { get; init; } = string.Empty;
}
}
public class Given_An_Object_With_Nested_Local_DateTime_When_Converting
: Given_When_Then_Test
{
private Root _sut;
private Root _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Local);
_sut =
new Root
{
Id = "foo",
Foo =
new Foo
{
Bar =
new Bar
{
DateTime = dateTime
}
}
};
var expectedDateTime = new DateTime(2021, 5, 25, 12, 0, 0, DateTimeKind.Utc);
_expectedResult =
new Root
{
Id = "foo",
Foo =
new Foo
{
Bar =
new Bar
{
DateTime = expectedDateTime
}
}
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Root
: IDocument
{
public string Id { get; init; } = string.Empty;
public Foo Foo { get; set; }
}
class Foo
{
public Bar Bar { get; set; }
}
class Bar
{
public DateTime DateTime { get; init; }
}
}
public class Given_An_Object_With_Nested_Unspecified_DateTime_When_Converting
: Given_When_Then_Test
{
private Root _sut;
private Root _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Unspecified);
_sut =
new Root
{
Id = "foo",
Foo =
new Foo
{
Bar =
new Bar
{
DateTime = dateTime
}
}
};
var expectedDateTime = new DateTime(2021, 5, 25, 12, 0, 0, DateTimeKind.Utc);
_expectedResult =
new Root
{
Id = "foo",
Foo =
new Foo
{
Bar =
new Bar
{
DateTime = expectedDateTime
}
}
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Root
: IDocument
{
public string Id { get; init; } = string.Empty;
public Foo Foo { get; set; }
}
class Foo
{
public Bar Bar { get; set; }
}
class Bar
{
public DateTime DateTime { get; init; }
}
}
public class Given_An_Object_With_Nested_Utc_DateTime_When_Converting
: Given_When_Then_Test
{
private Root _sut;
private Root _expectedResult;
protected override void Given()
{
// Sets fake time zone
_ = new FakeLocalTimeZone(TZConvert.GetTimeZoneInfo("UTC+12"));
var dateTime = new DateTime(2021, 5, 26, 0, 0, 0, DateTimeKind.Utc);
_sut =
new Root
{
Id = "foo",
Foo =
new Foo
{
Bar =
new Bar
{
DateTime = dateTime
}
}
};
var expectedDateTime = dateTime;
_expectedResult =
new Root
{
Id = "foo",
Foo =
new Foo
{
Bar =
new Bar
{
DateTime = expectedDateTime
}
}
};
}
protected override void When()
{
_sut.DeepConvertDateTimesToUtc();
}
[Fact]
public void Then_It_Should_Be_The_Expected_Document_With_Converted_DateTime()
{
_sut.Should().BeEquivalentTo(_expectedResult,options =>
options.Using<DateTime>(ctx => ctx.Expectation.Should().BeIn(DateTimeKind.Utc)).WhenTypeIs<DateTime>());
}
class Root
: IDocument
{
public string Id { get; init; } = string.Empty;
public Foo Foo { get; set; }
}
class Foo
{
public Bar Bar { get; set; }
}
class Bar
{
public DateTime DateTime { get; init; }
}
}
} NOTE: In order to mock the TimeZone I use public class FakeLocalTimeZone
: IDisposable
{
private readonly TimeZoneInfo _actualLocalTimeZoneInfo;
private static void SetLocalTimeZone(TimeZoneInfo timeZoneInfo)
{
typeof(TimeZoneInfo).AsDynamicType().s_cachedData._localTimeZone = timeZoneInfo;
}
public FakeLocalTimeZone(TimeZoneInfo timeZoneInfo)
{
_actualLocalTimeZoneInfo = TimeZoneInfo.Local;
SetLocalTimeZone(timeZoneInfo);
}
public void Dispose()
{
SetLocalTimeZone(_actualLocalTimeZoneInfo);
}
} |
Found a simpler, attribute-free solution. Create the converter as described by @firenero. internal class DateTimeUtcConverter : IPropertyConverter
{
public DynamoDBEntry ToEntry(object value) => (DateTime) value;
public object FromEntry(DynamoDBEntry entry)
{
var dateTime = entry.AsDateTime();
return dateTime.ToUniversalTime();
}
} Then instead of adding a dynamoContext.ConverterCache.Add(typeof(DateTime), new DateTimeUtcConverter()); The ConverterCache seems a nice way to keep using POCOs with dynamo in general. |
@justin-caldicott I tried this on my project and the converter isn't triggering. Is there any special configuration to do? |
We have noticed this issue has not received attention in 1 year. We will close this issue for now. If you think this is in error, please feel free to comment and reopen the issue. |
This is still an issue. Any chance to get it fixed? |
Yes, we have re-opened this issue. |
Reproducible using below code: var dynamo = new DynamoDbService();
await dynamo.InsertDocumentAsync(new Documents() { doc_id = "abc", name = "TestDocument", created = DateTime.UtcNow });
var result = await dynamo.GetDocumentsAsync<Documents>("abc");
await dynamo.InsertDocumentAsync(new Documents() { doc_id = "abc1", name = "TestDocument", created = new DateTime(2023, 6, 25, 5, 20, 45, 456, DateTimeKind.Utc) });
var result1 = await dynamo.GetDocumentsAsync<Documents>("abc1");
public class DynamoDbService
{
private static AmazonDynamoDBClient client = new AmazonDynamoDBClient(RegionEndpoint.USEast2);
private static DynamoDBContext context = new DynamoDBContext(client);
public async Task InsertDocumentAsync<T>(T document)
{
await context.SaveAsync<T>(document);
}
public async Task<List<T>> GetDocumentsAsync<T>(string id, int limit = 10)
{
QueryFilter filter = new QueryFilter();
filter.AddCondition("doc_id", QueryOperator.Equal, id);
QueryOperationConfig queryConfig = new QueryOperationConfig
{
Filter = filter,
Limit = limit,
BackwardSearch = true
};
var documents = await context.FromQueryAsync<T>(queryConfig).GetNextSetAsync();
return documents;
}
}
[DynamoDBTable("testtable")]
public class Documents
{
public string doc_id { get; set; }
public string name { get; set; }
public DateTime created { get; set; }
} In the returned result, the |
AWSSDK.DynamoDBv2 (3.7.300.4) adds support for retrieving DateTime attributes in UTC timezone from a DynamoDB table. You may use newly added boolean flag |
|
This only seems to work for |
@mtschneiders Apologies for the delay, but this has been fixed in version In the future, please don't hesitate to open a new issue; we review those often but it's easy to miss comments on closed issues. |
The DynamoDB SDK is converting the
datetime
property in local timezone while fetching documents from database, which has stored the datetime in UTC correctly.Expected Behavior
DateTime should not be converted to local time zone
Current Behavior
The SDK is converting the date time into local time zone
Correct In Dyanmo DB Table (UTC)
While fetching records through SDK
"2019-11-07T12:40:28.445+05:30",
"2019-11-07T12:35:25.071+05:30",
Code to Reproduce :
The text was updated successfully, but these errors were encountered: