diff --git a/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs b/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs
index e9fb71156f..f8e4e289c1 100644
--- a/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs
+++ b/Microsoft.Azure.Cosmos/src/Json/Interop/CosmosDBToNewtonsoftWriter.cs
@@ -3,7 +3,7 @@
//------------------------------------------------------------
namespace Microsoft.Azure.Cosmos.Json.Interop
{
- using System;
+ using System;
using System.IO;
using System.Text;
@@ -17,7 +17,13 @@ namespace Microsoft.Azure.Cosmos.Json.Interop
internal
#endif
sealed class CosmosDBToNewtonsoftWriter : Newtonsoft.Json.JsonWriter
- {
+ {
+ ///
+ /// The built-in DateTime format "o"/ "O" is comparable to the custom format of: "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK".
+ /// In order to remove the trailing zeros from the milli-second precision, we replace the lower-case f's with upper case ones.
+ ///
+ private const string RoundTripFormatWithoutTrailingZeros = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFFK";
+
///
/// A CosmosDB JSON writer used for the actual implementation.
///
@@ -314,17 +320,21 @@ public override void WriteValue(sbyte value)
public override void WriteValue(decimal value)
{
this.WriteValue((double)value);
- }
-
+ }
+
///
/// Writes a value.
///
- /// The value to write.
- public override void WriteValue(DateTime value)
- {
- this.WriteValue(value.ToString("O"));
- }
-
+ /// 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
+ // to maintain milliseconds precision.
+ this.WriteValue(
+ value.ToString(
+ format: CosmosDBToNewtonsoftWriter.RoundTripFormatWithoutTrailingZeros));
+ }
+
///
/// Writes a [] value.
///
diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemIntegrationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemIntegrationTests.cs
index 5ee7266408..b0efe4208a 100644
--- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemIntegrationTests.cs
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemIntegrationTests.cs
@@ -1,9 +1,11 @@
namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests
{
using System;
- using System.Collections.Generic;
+ using System.Collections.Generic;
+ using System.IO;
using System.Linq;
- using System.Net;
+ using System.Net;
+ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
@@ -31,8 +33,8 @@ public class CosmosItemIntegrationTests
[TestInitialize]
public async Task TestInitAsync()
- {
- this.connectionString = ConfigurationManager.GetEnvironmentVariable("COSMOSDB_MULTI_REGION", null);
+ {
+ this.connectionString = ConfigurationManager.GetEnvironmentVariable("COSMOSDB_MULTI_REGION", null);
JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
{
@@ -148,7 +150,93 @@ public async Task ReadMany2UnreachablePartitionsTest()
rule.Disable();
fiClient.Dispose();
}
- }
+ }
+
+ [TestMethod]
+ [Timeout(70000)]
+ [TestCategory("MultiRegion")]
+ public async Task DateTimeArrayRoundtrip_BinaryEncoding_CompareExtraDates_IntegrationTest()
+ {
+ string binaryEncodingEnabled = "binaryEncodingEnabled" + Guid.NewGuid().ToString("N");
+ string binaryEncodingDisabled = "binaryEncodingDisabled" + Guid.NewGuid().ToString("N");
+ string pk = "pk";
+ string testId = Guid.NewGuid().ToString();
+
+ string[] dateStrings =
+ {
+ "12/25/2023","2023-12-25","12-25-2023","25.12.2023","25/12/2023",
+ "Dec 25, 2023","Dec 25 2023","2023-12-25T10:00:00","2023-12-25T10:00:00.123",
+ "12/25/2023 10:00 AM","12/25/2023 10:00:00 AM","12/25/2023 10:00:00.123 AM","9999-12-31T23:59:59",
+ "2023-12-25T10:00:00.1","2023-12-25T10:00:00.12",
+ "2023-12-25T10:00:00.1234","2023-12-25T10:00:00.1234567"
+ };
+ string[] formats =
+ {
+ "MM/dd/yyyy","yyyy-MM-dd","MM-dd-yyyy","dd.MM.yyyy","dd/MM/yyyy",
+ "MMM dd, yyyy","MMM dd yyyy","yyyy-MM-ddTHH:mm:ss","yyyy-MM-ddTHH:mm:ss.fff",
+ "yyyy-MM-ddTHH:mm:ss.f","yyyy-MM-ddTHH:mm:ss.ff","yyyy-MM-ddTHH:mm:ss.ffff",
+ "yyyy-MM-ddTHH:mm:ss.fffffff","MM/dd/yyyy hh:mm tt","MM/dd/yyyy hh:mm:ss tt",
+ "MM/dd/yyyy hh:mm:ss.fff tt"
+ };
+ DateTime[] parsedDates = dateStrings
+ .Select(s => DateTime.ParseExact(s, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None))
+ .ToArray();
+
+ TestCosmosItem testItem = new TestCosmosItem(
+ id: testId,
+ pk: pk,
+ title: "title",
+ email: "test@example.com",
+ body: "Binary encoding test document.",
+ createdUtc: DateTime.UtcNow,
+ modifiedUtc: DateTime.Parse("2025-03-26T20:22:20Z", null, System.Globalization.DateTimeStyles.AdjustToUniversal),
+ extraDates: parsedDates);
+
+ Database db = this.database;
+ ContainerResponse containerBEEnabledResponse = await db.CreateContainerAsync(binaryEncodingEnabled, "/pk");
+ ContainerResponse containerBEDisabledResponse = await db.CreateContainerAsync(binaryEncodingDisabled, "/pk");
+
+ try
+ {
+ // BinaryEncodingEnabled = True
+ Environment.SetEnvironmentVariable(ConfigurationManager.BinaryEncodingEnabled, "True");
+ string rawJsonBEEnabled;
+ string rawJsonBEDisabled;
+ using (CosmosClient clientBinaryEncodingEnabled = new CosmosClient(this.connectionString))
+ {
+ Container containerBinaryEncodingEnabled = clientBinaryEncodingEnabled.GetDatabase(db.Id).GetContainer(binaryEncodingEnabled);
+ await containerBinaryEncodingEnabled.CreateItemAsync(testItem, new Microsoft.Azure.Cosmos.PartitionKey(pk));
+ using ResponseMessage response = await containerBinaryEncodingEnabled.ReadItemStreamAsync(testId, new Microsoft.Azure.Cosmos.PartitionKey(pk));
+ using StreamReader reader = new StreamReader(response.Content, Encoding.UTF8);
+ rawJsonBEEnabled = await reader.ReadToEndAsync();
+
+ }
+
+ // BinaryEncodingEnabled = False
+ Environment.SetEnvironmentVariable(ConfigurationManager.BinaryEncodingEnabled, "False");
+ using (CosmosClient clientBinaryEncodingDisabled = new CosmosClient(this.connectionString))
+ {
+ Container containerBinaryEncodingDisabled = clientBinaryEncodingDisabled.GetDatabase(db.Id).GetContainer(binaryEncodingDisabled);
+ await containerBinaryEncodingDisabled.CreateItemAsync(testItem, new Microsoft.Azure.Cosmos.PartitionKey(pk));
+ using ResponseMessage response = await containerBinaryEncodingDisabled.ReadItemStreamAsync(testId, new Microsoft.Azure.Cosmos.PartitionKey(pk));
+ using StreamReader reader = new StreamReader(response.Content, Encoding.UTF8);
+ rawJsonBEDisabled = await reader.ReadToEndAsync();
+ }
+
+ using JsonDocument docTrue = JsonDocument.Parse(rawJsonBEEnabled);
+ using JsonDocument docFalse = JsonDocument.Parse(rawJsonBEDisabled);
+
+ string extraDatesTrue = docTrue.RootElement.GetProperty("ExtraDates").GetRawText();
+ string extraDatesFalse = docFalse.RootElement.GetProperty("ExtraDates").GetRawText();
+
+ Assert.AreEqual(extraDatesTrue, extraDatesFalse, $"ExtraDates JSON mismatch:\nTrue: {extraDatesTrue}\nFalse: {extraDatesFalse}");
+ }
+ finally
+ {
+ await containerBEEnabledResponse.Container.DeleteContainerAsync();
+ await containerBEDisabledResponse.Container.DeleteContainerAsync();
+ }
+ }
[TestMethod]
[TestCategory("MultiRegion")]
@@ -1376,6 +1464,41 @@ await this.container.DeleteItemAsync(
{
// Ignore
}
+ }
+
+ public sealed class TestCosmosItem
+ {
+ [JsonConstructor]
+ public TestCosmosItem(
+ string id,
+ string pk,
+ string title,
+ string email,
+ string body,
+ DateTime createdUtc,
+ DateTime modifiedUtc,
+ DateTime[] extraDates)
+ {
+ this.id = id;
+ this.pk = pk;
+ this.title = title;
+ this.email = email;
+ this.body = body;
+ this.CreatedUtc = createdUtc;
+ this.ModifiedUtc = modifiedUtc;
+ this.ExtraDates = extraDates;
+ }
+
+#pragma warning disable IDE1006
+ public string id { get; }
+ public string pk { get; }
+ public string title { get; }
+ public string email { get; }
+ public string body { get; }
+#pragma warning restore IDE1006 // Naming Styles
+ public DateTime CreatedUtc { get; }
+ public DateTime ModifiedUtc { get; }
+ public DateTime[] ExtraDates { get; }
}
}
}
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 b0e2b85958..42ef816e19 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
@@ -210,6 +210,68 @@ public void AllPrimitivesObjectTest()
NewtonsoftInteropTests.VerifyNewtonsoftInterop(value);
}
+ [TestMethod]
+ [Owner("dkunda")]
+ public void AllDatetimeVariationsTest()
+ {
+ // Arrange
+ JObject jsonObject = new JObject();
+
+ // 1. Current UTC DateTime using JProperty
+ DateTime utcNow = DateTime.UtcNow;
+ JProperty utcNowProperty = new JProperty("currentUtcDateTime", utcNow);
+ jsonObject.Add(utcNowProperty);
+
+ // 2. Current Local DateTime using JProperty
+ DateTime localNow = DateTime.Now;
+ JProperty localNowProperty = new JProperty("currentLocalDateTime", localNow);
+ jsonObject.Add(localNowProperty);
+
+ // 3. Date Only (formatted as string) using JProperty
+ DateTime dateOnly = new DateTime(2023, 10, 26, 0, 0, 0, DateTimeKind.Unspecified);
+ JProperty dateOnlyProperty = new JProperty("specificDateOnly", dateOnly.ToString("yyyy-MM-dd"));
+ jsonObject.Add(dateOnlyProperty);
+
+ // 4. Time Only (formatted as string) using JProperty
+ DateTime timeOnly = new DateTime(1, 1, 1, 14, 30, 0, DateTimeKind.Unspecified);
+ JProperty timeOnlyProperty = new JProperty("specificTimeOnly", timeOnly.ToString("HH:mm:ss"));
+ jsonObject.Add(timeOnlyProperty);
+
+ // 5. DateTime with milliseconds using JProperty
+ DateTime preciseDateTime = new DateTime(2024, 5, 29, 10, 15, 30, 123, DateTimeKind.Local);
+ JProperty preciseDateTimeProperty = new JProperty("preciseDateTime", preciseDateTime);
+ jsonObject.Add(preciseDateTimeProperty);
+
+ // 6. DateTime in ISO 8601 format (UTC) using JProperty
+ DateTime isoUtcDateTime = new DateTime(2025, 1, 15, 8, 0, 0, DateTimeKind.Utc);
+ JProperty isoUtcDateTimeProperty = new JProperty("isoUtcDateTime", isoUtcDateTime.ToString("o", CultureInfo.InvariantCulture));
+ jsonObject.Add(isoUtcDateTimeProperty);
+
+ // 7. DateTime in custom format using JProperty
+ DateTime customFormattedDateTime = new DateTime(2022, 7, 1, 9, 45, 10, DateTimeKind.Local);
+ JProperty customFormattedDateTimeProperty = new JProperty("customFormattedDateTime", customFormattedDateTime.ToString("MM/dd/yyyy HH:mm:ss"));
+ jsonObject.Add(customFormattedDateTimeProperty);
+
+ // 8. Nullable DateTime (representing a missing or optional date) using JProperty
+ DateTime? nullableDateTime = null;
+ JProperty nullableDateTimeProperty = new JProperty("nullableDateTime", nullableDateTime);
+ jsonObject.Add(nullableDateTimeProperty);
+
+ // 9. MinValue DateTime using JProperty
+ JProperty minDateTimeProperty = new JProperty("minDateTime", DateTime.MinValue);
+ jsonObject.Add(minDateTimeProperty);
+
+ // 10. MaxValue DateTime using JProperty
+ JProperty maxDateTimeProperty = new JProperty("maxDateTime", DateTime.MaxValue);
+ jsonObject.Add(maxDateTimeProperty);
+
+ // 11. MaxValue DateTime using JProperty
+ JProperty exactDateTimeProperty = new JProperty("exactDateTime", DateTime.Parse("2025-03-26T20:22:20Z"));
+ jsonObject.Add(exactDateTimeProperty);
+
+ NewtonsoftInteropTests.VerifyNewtonsoftInterop(jsonObject);
+ }
+
public enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
public sealed class ObjectWithAttributes