diff --git a/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs b/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs index f8e4e289c1..c527d3d721 100644 --- a/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs +++ b/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs @@ -328,7 +328,20 @@ public override void WriteValue(decimal value) /// The value to write. public override void WriteValue(DateTime value) { - // We use rount trip format for datetime parsing and trim the additional trailing zeros using a custom "O" format + // We use round trip format for datetime parsing and trim the additional trailing zeros using a custom "O" format + // to maintain milliseconds precision. + this.WriteValue( + value.ToString( + format: CosmosDBToNewtonsoftWriter.RoundTripFormatWithoutTrailingZeros)); + } + + /// + /// Writes a value. + /// + /// The value to write. + public override void WriteValue(DateTimeOffset value) + { + // We use round trip format for datetime parsing and trim the additional trailing zeros using a custom "O" format // to maintain milliseconds precision. this.WriteValue( value.ToString( diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosItemUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosItemUnitTests.cs index 0f5cc0e13e..7c9291f424 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosItemUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosItemUnitTests.cs @@ -253,6 +253,7 @@ public async Task TestGetPartitionKeyValueFromStreamAsync(bool binaryEncodingEna cancellationToken)); DateTime dateTime = new DateTime(2019, 05, 15, 12, 1, 2, 3, DateTimeKind.Utc); + DateTimeOffset dateTimeOffset = new DateTimeOffset(2019, 05, 15, 12, 1, 2, 3, TimeSpan.Zero); Guid guid = Guid.NewGuid(); //Test supported types @@ -273,6 +274,7 @@ public async Task TestGetPartitionKeyValueFromStreamAsync(bool binaryEncodingEna new { pk = char.MaxValue }, new { pk = "test" }, new { pk = dateTime }, + new { pk = dateTimeOffset}, new { pk = guid }, }; @@ -308,6 +310,10 @@ public async Task TestGetPartitionKeyValueFromStreamAsync(bool binaryEncodingEna { Assert.AreEqual(poco.pk.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), stringValue); } + else if (poco.pk is DateTimeOffset) + { + Assert.AreEqual(poco.pk.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), stringValue); + } else { Assert.AreEqual(poco.pk.ToString(), (string)pk); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/NewtonsoftInteropTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/NewtonsoftInteropTests.cs index 42ef816e19..8b4a74a7bb 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/NewtonsoftInteropTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/NewtonsoftInteropTests.cs @@ -203,6 +203,7 @@ public void AllPrimitivesObjectTest() new JProperty("boolean", true), new JProperty("null", null), new JProperty("datetime", DateTime.Parse("2526-07-11T18:18:16.4520716")), + new JProperty("datetimeoffset", DateTimeOffset.Parse("2500-11-12T01:21:00+00:00")), new JProperty("spatialPoint", new JObject( new JProperty("type", "Point"), new JProperty("coordinate", new double[] { 118.9897, -46.6781 }))), @@ -272,6 +273,73 @@ public void AllDatetimeVariationsTest() NewtonsoftInteropTests.VerifyNewtonsoftInterop(jsonObject); } + [TestMethod] + [Owner("mpalaparthi")] + public void AllDateTimeOffsetVariationsTest() + { + // Arrange + JObject jsonObject = new JObject(); + + // 1. Current UTC DateTimeOffset using JProperty + DateTimeOffset utcNow = DateTimeOffset.UtcNow; + JProperty utcNowProperty = new JProperty("currentUtcDateTimeOffset", utcNow); + jsonObject.Add(utcNowProperty); + + // 2. Current Local DateTimeOffset using JProperty + DateTimeOffset localNow = DateTimeOffset.Now; + JProperty localNowProperty = new JProperty("currentLocalDateTimeOffset", localNow); + jsonObject.Add(localNowProperty); + + // 3. Date Only (formatted as string) using JProperty + DateTimeOffset dateOnly = new DateTimeOffset(2023, 10, 26, 0, 0, 0, TimeSpan.Zero); + JProperty dateOnlyProperty = new JProperty("specificDateOnlyOffset", dateOnly.ToString("yyyy-MM-dd")); + jsonObject.Add(dateOnlyProperty); + + // 4. Time Only (formatted as string) using JProperty + DateTimeOffset timeOnly = new DateTimeOffset(1, 1, 1, 14, 30, 0, TimeSpan.Zero); + JProperty timeOnlyProperty = new JProperty("specificTimeOnlyOffset", timeOnly.ToString("HH:mm:ss")); + jsonObject.Add(timeOnlyProperty); + + // 5. DateTimeOffset with milliseconds using JProperty + DateTimeOffset preciseDateTimeOffset = new DateTimeOffset(2024, 5, 29, 10, 15, 30, 123, TimeSpan.FromHours(5.5)); + JProperty preciseDateTimeOffsetProperty = new JProperty("preciseDateTimeOffset", preciseDateTimeOffset); + jsonObject.Add(preciseDateTimeOffsetProperty); + + // 6. DateTimeOffset in ISO 8601 format (UTC) using JProperty + DateTimeOffset isoUtcDateTimeOffset = new DateTimeOffset(2025, 1, 15, 8, 0, 0, TimeSpan.Zero); + JProperty isoUtcDateTimeOffsetProperty = new JProperty("isoUtcDateTimeOffset", isoUtcDateTimeOffset.ToString("o", CultureInfo.InvariantCulture)); + jsonObject.Add(isoUtcDateTimeOffsetProperty); + + // 7. DateTimeOffset in custom format using JProperty + DateTimeOffset customFormattedDateTimeOffset = new DateTimeOffset(2022, 7, 1, 9, 45, 10, TimeSpan.FromHours(-4)); + JProperty customFormattedDateTimeOffsetProperty = new JProperty("customFormattedDateTimeOffset", customFormattedDateTimeOffset.ToString("MM/dd/yyyy HH:mm:ss zzz")); + jsonObject.Add(customFormattedDateTimeOffsetProperty); + + // 8. Nullable DateTimeOffset (representing a missing or optional date) using JProperty + DateTimeOffset? nullableDateTimeOffset = null; + JProperty nullableDateTimeOffsetProperty = new JProperty("nullableDateTimeOffset", nullableDateTimeOffset); + jsonObject.Add(nullableDateTimeOffsetProperty); + + // 9. MinValue DateTimeOffset using JProperty + JProperty minDateTimeOffsetProperty = new JProperty("minDateTimeOffset", DateTimeOffset.MinValue); + jsonObject.Add(minDateTimeOffsetProperty); + + // 10. MaxValue DateTimeOffset using JProperty + JProperty maxDateTimeOffsetProperty = new JProperty("maxDateTimeOffset", DateTimeOffset.MaxValue); + jsonObject.Add(maxDateTimeOffsetProperty); + + // 11. Exact DateTimeOffset using JProperty + JProperty exactDateTimeOffsetProperty = new JProperty("exactDateTimeOffset", DateTimeOffset.Parse("2025-03-26T20:22:20+02:00")); + jsonObject.Add(exactDateTimeOffsetProperty); + + // 12. Various time zone offsets + jsonObject.Add(new JProperty("positiveOffset", new DateTimeOffset(2025, 10, 26, 12, 0, 0, TimeSpan.FromHours(5.5)))); + jsonObject.Add(new JProperty("negativeOffset", new DateTimeOffset(2025, 10, 26, 12, 0, 0, TimeSpan.FromHours(-8)))); + jsonObject.Add(new JProperty("uncommonOffset", new DateTimeOffset(2025, 10, 26, 12, 0, 0, new TimeSpan(12, 45, 0)))); + + NewtonsoftInteropTests.VerifyNewtonsoftInterop(jsonObject); + } + public enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat }; public sealed class ObjectWithAttributes @@ -437,4 +505,4 @@ public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectTy } } } -} \ No newline at end of file +}