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

Prevent DynamoDB SDK converting the datetime in local timezone #1450

Closed
vickyRathee opened this issue Nov 7, 2019 · 18 comments
Closed

Prevent DynamoDB SDK converting the datetime in local timezone #1450

vickyRathee opened this issue Nov 7, 2019 · 18 comments
Labels
bug This issue is a bug. dynamodb p1 This is a high priority issue queued

Comments

@vickyRathee
Copy link

vickyRathee commented Nov 7, 2019

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)

image

While fetching records through SDK

"2019-11-07T12:40:28.445+05:30",
"2019-11-07T12:35:25.071+05:30",

Code to Reproduce :

async Task Main()
{
	var dynamo = new DynamoDbService();
	var result = await dynamo.GetDocumentsAsync<Documents>("abc");
	result.Dump();

}

public class DynamoDbService
{
	private static readonly string awsAccessKey = "access key here";
	private static readonly string awsSecretKey = "secret here";

	private static BasicAWSCredentials awsCredentials = new BasicAWSCredentials(awsAccessKey, awsSecretKey);
	private static AmazonDynamoDBClient client = new AmazonDynamoDBClient(awsCredentials, RegionEndpoint.USEast1);
	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, _operationConfig).GetNextSetAsync();
		return documents;
	}
}

[DynamoDBTable("Table1")]
public class Documents
{
	public string doc_id { get; set; }
	public string name { get; set; }
	public DateTime created { get; set; }
}
@klaytaybai klaytaybai added the duplicate This issue is a duplicate. label Nov 9, 2019
@klaytaybai klaytaybai added the bug This issue is a bug. label Dec 6, 2019
@savagedev
Copy link

I'm also seeing this problem. Is there any workaround available?

@supritaj
Copy link

supritaj commented Apr 9, 2020

Facing same issue, any solution?

@firenero
Copy link

firenero commented Apr 14, 2020

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; }
}

@tenacioustechie
Copy link

Does anyone know how to use the converter when calling DynamoDBContext.LoadAsync()
I have data that I want to roundtrip using datetime as the rangekey, at this point in my code I only have UTC values, and it apears to store correctly, but when trying to use LoadAsync it doens't find the entry correctly.

@i2van
Copy link

i2van commented Sep 21, 2020

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);

DateTimeStyles should be not DateTimeStyles.AssumeUniversal but DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal

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:

UTC:
9/21/2020 8:39:45 PM
[Original] DateTimeStyles.AssumeUniversal:
9/21/2020 11:39:45 PM
[Fixed] DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal:
9/21/2020 8:39:45 PM

Fix:

Use DynamoDBProperty attibute with type converter as @firenero suggested.

@diegosasw
Copy link

diegosasw commented Apr 6, 2021

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: <0001-01-01 00:00:00.000>) but found <01:00:00>, as if the retrieve functionality was applying local time instead of respecting the UTC date.

UPDATE: Actually, I have seen that the document is retrieved with the proper date in UTC, but the problem is in the DynamoDbContext. When it does var document = context.FromDocument<TDocument>(dynamoDbDocument); it's converting the utc date into local time

@diegosasw
Copy link

diegosasw commented May 25, 2021

Is there any workaround that does not involve using [DynamoDBProperty] or any other dependency to the DynamoDB library in the models?

I am using plain DTO without any external dependency, and as per my last comment it seems the context.FromDocument<TDocument>(dynamoDbDocument); converts UTC dates into local time. Does anybody know of any way to overwrite this functionality? Maybe by doing something with DynamoDBOperationConfig or specifying there a custom converter for DateTime would be great.

Thanks

@diegosasw
Copy link

diegosasw commented May 26, 2021

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 DynamoDBContext to convert into my object, when retrieving from database.

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 DeepConvertDateTimesToUtc extension converts any DateTime or DateTime? property to UTC. Here is the extension in case anyone finds it useful

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 ReflectionMagic and TimeZoneConverter

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);
    }
}

@justin-caldicott
Copy link

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 DynamoDBProperty attribute to each DateTime property, add the converter to the ConverterCache after creating the DynamoDBContext.

  dynamoContext.ConverterCache.Add(typeof(DateTime), new DateTimeUtcConverter());

The ConverterCache seems a nice way to keep using POCOs with dynamo in general.

@daniel-botero-cko
Copy link

daniel-botero-cko commented Aug 27, 2021

@justin-caldicott I tried this on my project and the converter isn't triggering. Is there any special configuration to do?

@github-actions
Copy link

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.

@github-actions github-actions bot added closing-soon This issue will automatically close in 4 days unless further comments are made. closed-for-staleness and removed closing-soon This issue will automatically close in 4 days unless further comments are made. labels Aug 28, 2022
@dbmercer
Copy link

This is still an issue. Any chance to get it fixed?

@eddiemcs3 eddiemcs3 reopened this Oct 24, 2023
@eddiemcs3
Copy link

Yes, we have re-opened this issue.

@ashishdhingra ashishdhingra added needs-reproduction This issue needs reproduction. and removed duplicate This issue is a duplicate. closed-for-staleness labels Oct 24, 2023
@ashishdhingra
Copy link
Contributor

ashishdhingra commented Oct 24, 2023

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 DateTime object for created property has Kind property with value Local, value also converted to local time.

@ashishdhingra ashishdhingra added p2 This is a standard priority issue needs-review and removed needs-reproduction This issue needs reproduction. labels Oct 24, 2023
@ashishdhingra ashishdhingra added p1 This is a high priority issue queued and removed p2 This is a standard priority issue needs-review labels Oct 24, 2023
@ashishdhingra
Copy link
Contributor

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 RetrieveDateTimeInUtc in DynamoDBContextConfig to enable this behavior.

Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

@mtschneiders
Copy link

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 RetrieveDateTimeInUtc in DynamoDBContextConfig to enable this behavior.

This only seems to work for DateTime, if you have a DateTime? property, it will still be retrieved in local time if you have the RetrieveDateTimeInUtc set to true.

@dscpinheiro
Copy link
Contributor

@mtschneiders Apologies for the delay, but this has been fixed in version 3.7.304.1 of the AWSSDK.DynamoDBv2 package (#3351).

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue is a bug. dynamodb p1 This is a high priority issue queued
Projects
None yet
Development

No branches or pull requests