diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 6bfc65a005..35dae71952 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -60,7 +60,8 @@ public class CosmosClientOptions /// private int gatewayModeMaxConnectionLimit; private CosmosSerializationOptions serializerOptions; - private CosmosSerializer serializerInternal; + private CosmosSerializer serializerInternal; + private System.Text.Json.JsonSerializerOptions stjSerializerOptions; private ConnectionMode connectionMode; private Protocol connectionProtocol; @@ -392,6 +393,44 @@ public ConnectionMode ConnectionMode /// /// public bool? EnableContentResponseOnWrite { get; set; } + + /// + /// Sets the for the System.Text.Json serializer. + /// Note that if this option is provided, then the SDK will use the System.Text.Json as the default serializer and set + /// the serializer options as the constructor args. + /// + /// + /// An example on how to configure the System.Text.Json serializer options to ignore null values + /// + /// + /// + /// + public System.Text.Json.JsonSerializerOptions UseSystemTextJsonSerializerWithOptions + { + get => this.stjSerializerOptions; + set + { + if (this.Serializer != null || this.SerializerOptions != null) + { + throw new ArgumentException( + $"{nameof(this.UseSystemTextJsonSerializerWithOptions)} is not compatible with {nameof(this.Serializer)} or {nameof(this.SerializerOptions)}. Only one can be set. "); + } + + this.stjSerializerOptions = value; + this.serializerInternal = new CosmosSystemTextJsonSerializer( + this.stjSerializerOptions); + } + } /// /// Gets or sets the advanced replica selection flag. The advanced replica selection logic keeps track of the replica connection @@ -543,10 +582,10 @@ public CosmosSerializationOptions SerializerOptions get => this.serializerOptions; set { - if (this.Serializer != null) + if (this.Serializer != null || this.UseSystemTextJsonSerializerWithOptions != null) { throw new ArgumentException( - $"{nameof(this.SerializerOptions)} is not compatible with {nameof(this.Serializer)}. Only one can be set. "); + $"{nameof(this.SerializerOptions)} is not compatible with {nameof(this.Serializer)} or {nameof(this.UseSystemTextJsonSerializerWithOptions)}. Only one can be set. "); } this.serializerOptions = value; @@ -578,10 +617,10 @@ public CosmosSerializer Serializer get => this.serializerInternal; set { - if (this.SerializerOptions != null) + if (this.SerializerOptions != null || this.UseSystemTextJsonSerializerWithOptions != null) { throw new ArgumentException( - $"{nameof(this.Serializer)} is not compatible with {nameof(this.SerializerOptions)}. Only one can be set. "); + $"{nameof(this.Serializer)} is not compatible with {nameof(this.SerializerOptions)} or {nameof(this.UseSystemTextJsonSerializerWithOptions)}. Only one can be set. "); } this.serializerInternal = value; diff --git a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs index 43232aa422..c614068fae 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs @@ -646,7 +646,22 @@ public CosmosClientBuilder WithContentResponseOnWrite(bool contentResponseOnWrit { this.clientOptions.EnableContentResponseOnWrite = contentResponseOnWrite; return this; - } + } + + /// + /// Configures the to use System.Text.Json for serialization. + /// Use to use System.Text.Json with a default configuration. + /// If no options are specified, Newtonsoft.Json will be used for serialization instead. + /// + /// An instance of + /// containing the system text json serializer options. + /// The object + public CosmosClientBuilder WithSystemTextJsonSerializerOptions( + System.Text.Json.JsonSerializerOptions serializerOptions) + { + this.clientOptions.UseSystemTextJsonSerializerWithOptions = serializerOptions; + return this; + } /// /// The event handler to be invoked before the request is sent. @@ -725,7 +740,7 @@ internal CosmosClientBuilder WithPartitionLevelFailoverEnabled() { this.clientOptions.EnablePartitionLevelFailover = true; return this; - } + } /// /// Enables SDK to inject fault. Used for testing applications. diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAggregateCustomSerializationBaseline.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAggregateCustomSerializationBaseline.cs index ac686b31e1..54af230979 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAggregateCustomSerializationBaseline.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAggregateCustomSerializationBaseline.cs @@ -29,7 +29,6 @@ public class LinqAggregateCustomSerializationBaseline : BaselineTests cosmosClientBuilder.WithCustomSerializer(stjCosmosSerializer)); + => cosmosClientBuilder.WithSystemTextJsonSerializerOptions( + new JsonSerializerOptions()), + useCustomSeralizer: false); // Set a callback to get the handle of the last executed query to do the verification // This is neede because aggregate queries return type is a scalar so it can't be used diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs index 1224f48074..48aed543bf 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs @@ -57,7 +57,10 @@ public async static Task Initialize(TestContext textContext) TestDb = await CosmosClient.CreateDatabaseAsync(dbName); CosmosDefaultSTJClient = TestCommon.CreateCosmosClient((cosmosClientBuilder) - => cosmosClientBuilder.WithCustomSerializer(new CosmosSystemTextJsonSerializer(new JsonSerializerOptions()))); + => cosmosClientBuilder + .WithSystemTextJsonSerializerOptions( + new JsonSerializerOptions()), + useCustomSeralizer: false); string dbNameSTJ = $"{nameof(LinqTranslationBaselineTests)}-{Guid.NewGuid():N}"; TestDbSTJDefault = await CosmosDefaultSTJClient.CreateDatabaseAsync(dbNameSTJ); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json index 89074f0fef..2cc3fd8de5 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json @@ -2988,6 +2988,16 @@ ], "MethodInfo": "System.String get_ApplicationRegion();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "System.Text.Json.JsonSerializerOptions get_UseSystemTextJsonSerializerWithOptions()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.Text.Json.JsonSerializerOptions get_UseSystemTextJsonSerializerWithOptions();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Text.Json.JsonSerializerOptions UseSystemTextJsonSerializerWithOptions": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "System.Text.Json.JsonSerializerOptions UseSystemTextJsonSerializerWithOptions;CanRead:True;CanWrite:True;System.Text.Json.JsonSerializerOptions get_UseSystemTextJsonSerializerWithOptions();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_UseSystemTextJsonSerializerWithOptions(System.Text.Json.JsonSerializerOptions);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.TimeSpan get_RequestTimeout()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", "Attributes": [ @@ -3165,6 +3175,11 @@ ], "MethodInfo": "Void set_TokenCredentialBackgroundRefreshInterval(System.Nullable`1[System.TimeSpan]);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "Void set_UseSystemTextJsonSerializerWithOptions(System.Text.Json.JsonSerializerOptions)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Void set_UseSystemTextJsonSerializerWithOptions(System.Text.Json.JsonSerializerOptions);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Void set_WebProxy(System.Net.IWebProxy)": { "Type": "Method", "Attributes": [], @@ -4737,6 +4752,11 @@ "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithSerializerOptions(Microsoft.Azure.Cosmos.CosmosSerializationOptions);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithSystemTextJsonSerializerOptions(System.Text.Json.JsonSerializerOptions)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithSystemTextJsonSerializerOptions(System.Text.Json.JsonSerializerOptions);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithThrottlingRetryOptions(System.TimeSpan, Int32)": { "Type": "Method", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs index 293c3b14ab..c27ca8abf1 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -13,6 +13,7 @@ namespace Microsoft.Azure.Cosmos.Tests using System.Net.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; + using System.Text; using global::Azure.Core; using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Azure.Documents; @@ -476,6 +477,26 @@ public void UserAgentContainsEnvironmentInformation() Assert.AreEqual(userAgentSuffix, connectionPolicy.UserAgentSuffix); Assert.IsTrue(connectionPolicy.UserAgentContainer.UserAgent.StartsWith(expectedValue)); Assert.IsTrue(connectionPolicy.UserAgentContainer.UserAgent.EndsWith(userAgentSuffix)); + } + + [TestMethod] + public void ValidateThatCustomSerializerGetsOverriddenWhenSTJSerializerEnabled() + { + CosmosClientOptions options = new CosmosClientOptions() + { + UseSystemTextJsonSerializerWithOptions = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, + } + }; + + CosmosClient client = new( + "https://fake-account.documents.azure.com:443/", + Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())), + options + ); + + Assert.AreEqual(typeof(CosmosSystemTextJsonSerializer), client.ClientOptions.Serializer.GetType()); } [TestMethod] @@ -500,8 +521,58 @@ public void ThrowOnCustomSerializerWithSerializerOptions() }; options.Serializer = new CosmosJsonDotNetSerializer(); - } - + } + + [TestMethod] + [DataRow(false, DisplayName = "Test when the client options order is maintained")] + [DataRow(true, DisplayName = "Test when the client options order is reversed")] + [ExpectedException(typeof(ArgumentException))] + public void ThrowOnCustomSerializerWithSTJSerializerEnabled( + bool reverseOrder) + { + if (reverseOrder) + { + CosmosClientOptions options = new CosmosClientOptions() + { + Serializer = new CosmosJsonDotNetSerializer(), + UseSystemTextJsonSerializerWithOptions = new System.Text.Json.JsonSerializerOptions(), + }; + } + else + { + CosmosClientOptions options = new CosmosClientOptions() + { + UseSystemTextJsonSerializerWithOptions = new System.Text.Json.JsonSerializerOptions(), + Serializer = new CosmosJsonDotNetSerializer(), + }; + } + } + + [TestMethod] + [DataRow(false, DisplayName = "Test when the client options order is maintained")] + [DataRow(true, DisplayName = "Test when the client options order is reversed")] + [ExpectedException(typeof(ArgumentException))] + public void ThrowOnSerializerOptionsWithSTJSerializerEnabled( + bool reverseOrder) + { + if (reverseOrder) + { + CosmosClientOptions options = new CosmosClientOptions() + { + SerializerOptions = new CosmosSerializationOptions(), + UseSystemTextJsonSerializerWithOptions = new System.Text.Json.JsonSerializerOptions(), + }; + } + else + { + CosmosClientOptions options = new CosmosClientOptions() + { + UseSystemTextJsonSerializerWithOptions = new System.Text.Json.JsonSerializerOptions(), + SerializerOptions = new CosmosSerializationOptions(), + }; + } + } + [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void ThrowOnNullTokenCredential()