From 0843cae3c252dd49aa8e392623d7eaaed7eb712b Mon Sep 17 00:00:00 2001 From: j82w Date: Fri, 9 Aug 2019 19:11:56 -0700 Subject: [PATCH] Enabling serialization customization through CosmosSerializerOptions (#650) * Made JsonSerializerSettings internal. Exposed a new constructor with popular flags to make it easier for users that only require basic flags. Refactored all serializer classes into a single folder. * Fixed contract UT and updated changelog * Made CosmosJsonDotNetSerializer internal. Added CosmosSerializerOptions to CosmosClientOptions. Added new UT. * Fixed naming * Updating file name and changelog * Update contract test * Fixed spacing * Adding additional info to ValidateAsync test to understand transient failure * Fixing contract enforcement test * Updated naming * Additional contract checks * Updating contract from VS2017 * Fixed contract test for VS2019 to skip is IsReadOnlyAttribute * Update Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializerOptions.cs Co-Authored-By: Matias Quaranta * Update Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializerOptions.cs Co-Authored-By: Matias Quaranta * Update Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializerOptions.cs Co-Authored-By: Matias Quaranta * Renamed CosmosSerializerOptions to CosmosSerializationOptions * Added more comments * Reverting DocumentClientWithUriParameters changes * Revert "Reverting DocumentClientWithUriParameters changes" This reverts commit febd523286c82eb3fdfcb8b18e8ae234f91239b7. * Reverting accidental changes * Adding comment examples, Converted options to be nullable * Updated contract test * Fixed Unit test * Converted to a class with default values and updated changelog * Updated contract api --- Microsoft.Azure.Cosmos/src/CosmosClient.cs | 13 +- .../src/CosmosClientOptions.cs | 96 +++++++++--- .../CosmosElements/CosmosElementSerializer.cs | 6 +- Microsoft.Azure.Cosmos/src/FeedOptions.cs | Bin 37010 -> 18101 bytes .../src/Fluent/CosmosClientBuilder.cs | 16 +- .../src/RequestOptions/QueryRequestOptions.cs | 2 +- .../Resource/QueryResponses/QueryResponse.cs | 6 +- .../CosmosJsonDotNetSerializer.cs | 51 +++++-- .../CosmosJsonSerializerWrapper.cs | 0 .../Serializer/CosmosPropertyNamingPolicy.cs | 23 +++ .../CosmosSerializationFormatOptions.cs} | 4 +- .../Serializer/CosmosSerializationOptions.cs | 49 ++++++ .../src/{ => Serializer}/CosmosSerializer.cs | 0 .../CosmosItemTests.cs | 2 +- .../ContractEnforcement.cs | 33 ++-- .../CosmosClientOptionsUnitTests.cs | 114 ++++++++++++-- .../DotNetSDKAPI.json | 144 +++++++++++++----- .../LocationCacheTests.cs | 55 +++++-- changelog.md | 5 +- 19 files changed, 482 insertions(+), 137 deletions(-) rename Microsoft.Azure.Cosmos/src/{ => Serializer}/CosmosJsonDotNetSerializer.cs (58%) rename Microsoft.Azure.Cosmos/src/{ => Serializer}/CosmosJsonSerializerWrapper.cs (100%) create mode 100644 Microsoft.Azure.Cosmos/src/Serializer/CosmosPropertyNamingPolicy.cs rename Microsoft.Azure.Cosmos/src/{Resource/CosmosSerializationOptions.cs => Serializer/CosmosSerializationFormatOptions.cs} (94%) create mode 100644 Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationOptions.cs rename Microsoft.Azure.Cosmos/src/{ => Serializer}/CosmosSerializer.cs (100%) diff --git a/Microsoft.Azure.Cosmos/src/CosmosClient.cs b/Microsoft.Azure.Cosmos/src/CosmosClient.cs index 07fb7ea22e..4770d36398 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClient.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClient.cs @@ -150,7 +150,7 @@ protected CosmosClient() /// /// public CosmosClient( - string connectionString, + string connectionString, CosmosClientOptions clientOptions = null) : this( CosmosClientOptions.GetAccountEndpoint(connectionString), @@ -612,18 +612,19 @@ internal void Init( this.RequestHandler = clientPipelineBuilder.Build(); + CosmosSerializer userSerializer = this.ClientOptions.GetCosmosSerializerWithWrapperOrDefault(); this.ResponseFactory = new CosmosResponseFactory( defaultJsonSerializer: this.ClientOptions.PropertiesSerializer, - userJsonSerializer: this.ClientOptions.CosmosSerializerWithWrapperOrDefault); + userJsonSerializer: userSerializer); CosmosSerializer sqlQuerySpecSerializer = CosmosSqlQuerySpecJsonConverter.CreateSqlQuerySpecSerializer( - this.ClientOptions.CosmosSerializerWithWrapperOrDefault, - this.ClientOptions.PropertiesSerializer); + cosmosSerializer: userSerializer, + propertiesSerializer: this.ClientOptions.PropertiesSerializer); this.ClientContext = new ClientContextCore( client: this, clientOptions: this.ClientOptions, - userJsonSerializer: this.ClientOptions.CosmosSerializerWithWrapperOrDefault, + userJsonSerializer: userSerializer, defaultJsonSerializer: this.ClientOptions.PropertiesSerializer, sqlQuerySpecSerializer: sqlQuerySpecSerializer, cosmosResponseFactory: this.ResponseFactory, @@ -632,7 +633,7 @@ internal void Init( documentQueryClient: new DocumentQueryClient(this.DocumentClient)); } - internal async virtual Task GetAccountConsistencyLevelAsync() + internal virtual async Task GetAccountConsistencyLevelAsync() { if (!this.accountConsistencyLevel.HasValue) { diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 5f15d74769..c3a7afd2db 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -8,7 +8,6 @@ namespace Microsoft.Azure.Cosmos using System.Collections.ObjectModel; using System.Data.Common; using System.Linq; - using System.Runtime.ConstrainedExecution; using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; @@ -17,6 +16,18 @@ namespace Microsoft.Azure.Cosmos /// /// Defines all the configurable options that the CosmosClient requires. /// + /// + /// An example on how to configure the serialization option to ignore null values + /// CosmosClientOptions clientOptions = new CosmosClientOptions() + /// { + /// SerializerOptions = new CosmosSerializationOptions(){ + /// IgnoreNullValues = true + /// }, + /// ConnectionMode = ConnectionMode.Gateway, + /// }; + /// + /// CosmosClient client = new CosmosClient("endpoint", "key", clientOptions); + /// public class CosmosClientOptions { /// @@ -39,11 +50,12 @@ public class CosmosClientOptions /// private static readonly CosmosSerializer propertiesSerializer = new CosmosJsonSerializerWrapper(new CosmosJsonDotNetSerializer()); - private readonly Collection customHandlers; private readonly string currentEnvironmentInformation; private int gatewayModeMaxConnectionLimit; private string applicationName; + private CosmosSerializationOptions serializerOptions; + private CosmosSerializer serializer; /// /// Creates a new CosmosClientOptions @@ -59,7 +71,7 @@ public CosmosClientOptions() this.ConnectionMode = CosmosClientOptions.DefaultConnectionMode; this.ConnectionProtocol = CosmosClientOptions.DefaultProtocol; this.ApiType = CosmosClientOptions.DefaultApiType; - this.customHandlers = new Collection(); + this.CustomHandlers = new Collection(); } /// @@ -132,10 +144,7 @@ public int GatewayModeMaxConnectionLimit /// /// [JsonConverter(typeof(ClientOptionJsonConverter))] - public Collection CustomHandlers - { - get => this.customHandlers; - } + public Collection CustomHandlers { get; } /// /// Get or set the connection mode used by the client when connecting to the Azure Cosmos DB service. @@ -157,7 +166,7 @@ public Collection CustomHandlers public ConsistencyLevel? ConsistencyLevel { get; set; } /// - /// Get ot set the number of times client should retry on rate throttled requests. + /// Get or set the number of times client should retry on rate throttled requests. /// /// public int? MaxRetryAttemptsOnRateLimitedRequests { get; set; } @@ -171,6 +180,35 @@ public Collection CustomHandlers /// public TimeSpan? MaxRetryWaitTimeOnRateLimitedRequests { get; set; } + /// + /// Get to set optional serializer options. + /// + /// + /// An example on how to configure the serialization option to ignore null values + /// CosmosClientOptions clientOptions = new CosmosClientOptions() + /// { + /// SerializerOptions = new CosmosSerializationOptions(){ + /// IgnoreNullValues = true + /// } + /// }; + /// + /// CosmosClient client = new CosmosClient("endpoint", "key", clientOptions); + /// + public CosmosSerializationOptions SerializerOptions + { + get => this.serializerOptions; + set + { + if (this.Serializer != null) + { + throw new ArgumentException( + $"{nameof(this.SerializerOptions)} is not compatible with {nameof(this.Serializer)}. Only one can be set. "); + } + + this.serializerOptions = value; + } + } + /// /// Get to set an optional JSON serializer. The client will use it to serialize or de-serialize user's cosmos request/responses. /// SDK owned types such as DatabaseProperties and ContainerProperties will always use the SDK default serializer. @@ -182,10 +220,7 @@ public Collection CustomHandlers /// /// // An example on how to configure the serializer to ignore null values /// CosmosSerializer ignoreNullSerializer = new CosmosJsonDotNetSerializer( - /// new JsonSerializerSettings() - /// { - /// NullValueHandling = NullValueHandling.Ignore - /// }); + /// NullValueHandling = NullValueHandling.Ignore); /// /// CosmosClientOptions clientOptions = new CosmosClientOptions() /// { @@ -195,7 +230,20 @@ public Collection CustomHandlers /// CosmosClient client = new CosmosClient("endpoint", "key", clientOptions); /// [JsonConverter(typeof(ClientOptionJsonConverter))] - public CosmosSerializer Serializer { get; set; } + public CosmosSerializer Serializer + { + get => this.serializer; + set + { + if (this.SerializerOptions != null) + { + throw new ArgumentException( + $"{nameof(this.Serializer)} is not compatible with {nameof(this.SerializerOptions)}. Only one can be set. "); + } + + this.serializer = value; + } + } /// /// A JSON serializer used by the CosmosClient to serialize or de-serialize cosmos request/responses. @@ -205,12 +253,6 @@ public Collection CustomHandlers [JsonConverter(typeof(ClientOptionJsonConverter))] internal CosmosSerializer PropertiesSerializer => CosmosClientOptions.propertiesSerializer; - /// - /// Gets the user json serializer with the CosmosJsonSerializerWrapper or the default - /// - [JsonIgnore] - internal CosmosSerializer CosmosSerializerWithWrapperOrDefault => this.Serializer == null ? this.PropertiesSerializer : new CosmosJsonSerializerWrapper(this.Serializer); - /// /// Gets or sets the connection protocol when connecting to the Azure Cosmos service. /// @@ -309,6 +351,22 @@ public Collection CustomHandlers /// internal bool? EnableCpuMonitor { get; set; } + /// + /// Gets the user json serializer with the CosmosJsonSerializerWrapper or the default + /// + internal CosmosSerializer GetCosmosSerializerWithWrapperOrDefault() + { + if (this.SerializerOptions != null) + { + CosmosJsonDotNetSerializer cosmosJsonDotNetSerializer = new CosmosJsonDotNetSerializer(this.SerializerOptions); + return new CosmosJsonSerializerWrapper(cosmosJsonDotNetSerializer); + } + else + { + return this.Serializer == null ? this.PropertiesSerializer : new CosmosJsonSerializerWrapper(this.Serializer); + } + } + internal CosmosClientOptions Clone() { CosmosClientOptions cloneConfiguration = (CosmosClientOptions)this.MemberwiseClone(); diff --git a/Microsoft.Azure.Cosmos/src/CosmosElements/CosmosElementSerializer.cs b/Microsoft.Azure.Cosmos/src/CosmosElements/CosmosElementSerializer.cs index 72bd457455..ba921dd142 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosElements/CosmosElementSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosElements/CosmosElementSerializer.cs @@ -29,7 +29,7 @@ static class CosmosElementSerializer internal static CosmosArray ToCosmosElements( MemoryStream memoryStream, ResourceType resourceType, - CosmosSerializationOptions cosmosSerializationOptions = null) + CosmosSerializationFormatOptions cosmosSerializationOptions = null) { if (!memoryStream.CanRead) { @@ -103,7 +103,7 @@ internal static Stream ToStream( string containerRid, IEnumerable cosmosElements, ResourceType resourceType, - CosmosSerializationOptions cosmosSerializationOptions = null) + CosmosSerializationFormatOptions cosmosSerializationOptions = null) { IJsonWriter jsonWriter; if (cosmosSerializationOptions != null) @@ -172,7 +172,7 @@ internal static IEnumerable Deserialize( IEnumerable cosmosElements, ResourceType resourceType, CosmosSerializer jsonSerializer, - CosmosSerializationOptions cosmosSerializationOptions = null) + CosmosSerializationFormatOptions cosmosSerializationOptions = null) { if (!cosmosElements.Any()) { diff --git a/Microsoft.Azure.Cosmos/src/FeedOptions.cs b/Microsoft.Azure.Cosmos/src/FeedOptions.cs index fa9f9dbb6276ba61abbd02861c52d7d1d38694ce..6552360e5ffd534a8c6b4bae8a74bb125b328add 100644 GIT binary patch literal 18101 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 diff --git a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs index c5e5b32211..5a0bf04685 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs @@ -255,6 +255,19 @@ public CosmosClientBuilder WithThrottlingRetryOptions(TimeSpan maxRetryWaitTimeO return this; } + /// + /// Set a custom serializer option. + /// + /// The custom class that implements + /// The object + /// + /// + public CosmosClientBuilder WithSerializerOptions(CosmosSerializationOptions cosmosSerializerOptions) + { + this.clientOptions.SerializerOptions = cosmosSerializerOptions; + return this; + } + /// /// Set a custom JSON serializer. /// @@ -262,8 +275,7 @@ public CosmosClientBuilder WithThrottlingRetryOptions(TimeSpan maxRetryWaitTimeO /// The object /// /// - public CosmosClientBuilder WithCustomSerializer( - CosmosSerializer cosmosJsonSerializer) + public CosmosClientBuilder WithCustomSerializer(CosmosSerializer cosmosJsonSerializer) { this.clientOptions.Serializer = cosmosJsonSerializer; return this; diff --git a/Microsoft.Azure.Cosmos/src/RequestOptions/QueryRequestOptions.cs b/Microsoft.Azure.Cosmos/src/RequestOptions/QueryRequestOptions.cs index 43110d3519..ca53dac640 100644 --- a/Microsoft.Azure.Cosmos/src/RequestOptions/QueryRequestOptions.cs +++ b/Microsoft.Azure.Cosmos/src/RequestOptions/QueryRequestOptions.cs @@ -140,7 +140,7 @@ public ConsistencyLevel? ConsistencyLevel /// internal string SessionToken { get; set; } - internal CosmosSerializationOptions CosmosSerializationOptions { get; set; } + internal CosmosSerializationFormatOptions CosmosSerializationOptions { get; set; } /// /// Gets or sets the flag that enables skip take across partitions. diff --git a/Microsoft.Azure.Cosmos/src/Resource/QueryResponses/QueryResponse.cs b/Microsoft.Azure.Cosmos/src/Resource/QueryResponses/QueryResponse.cs index c581a1107e..cd761d3684 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/QueryResponses/QueryResponse.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/QueryResponses/QueryResponse.cs @@ -72,7 +72,7 @@ private QueryResponse( /// internal ClientSideRequestStatistics RequestStatistics { get; } - internal virtual CosmosSerializationOptions CosmosSerializationOptions { get; set; } + internal virtual CosmosSerializationFormatOptions CosmosSerializationOptions { get; set; } internal bool GetHasMoreResults() { @@ -137,7 +137,7 @@ internal class QueryResponse : FeedResponse { private readonly IEnumerable cosmosElements; private readonly CosmosSerializer jsonSerializer; - private readonly CosmosSerializationOptions serializationOptions; + private readonly CosmosSerializationFormatOptions serializationOptions; private IEnumerable resources; private QueryResponse( @@ -145,7 +145,7 @@ private QueryResponse( IEnumerable cosmosElements, CosmosQueryResponseMessageHeaders responseMessageHeaders, CosmosSerializer jsonSerializer, - CosmosSerializationOptions serializationOptions) + CosmosSerializationFormatOptions serializationOptions) { this.cosmosElements = cosmosElements; this.QueryHeaders = responseMessageHeaders; diff --git a/Microsoft.Azure.Cosmos/src/CosmosJsonDotNetSerializer.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonDotNetSerializer.cs similarity index 58% rename from Microsoft.Azure.Cosmos/src/CosmosJsonDotNetSerializer.cs rename to Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonDotNetSerializer.cs index 12aee89848..a0e0187a85 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosJsonDotNetSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonDotNetSerializer.cs @@ -7,11 +7,12 @@ namespace Microsoft.Azure.Cosmos using System.IO; using System.Text; using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; /// - /// The default Cosmos JSON.NET serializer + /// The default Cosmos JSON.NET serializer. /// - public sealed class CosmosJsonDotNetSerializer : CosmosSerializer + internal sealed class CosmosJsonDotNetSerializer : CosmosSerializer { private static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); private readonly JsonSerializer Serializer; @@ -19,20 +20,48 @@ public sealed class CosmosJsonDotNetSerializer : CosmosSerializer /// /// Create a serializer that uses the JSON.net serializer /// - /// Optional serializer settings - public CosmosJsonDotNetSerializer(JsonSerializerSettings jsonSerializerSettings = null) + /// + /// This is internal to reduce exposure of JSON.net types so + /// it is easier to convert to System.Text.Json + /// + internal CosmosJsonDotNetSerializer() { - if (jsonSerializerSettings == null) + this.Serializer = JsonSerializer.Create(); + } + + /// + /// Create a serializer that uses the JSON.net serializer + /// + /// + /// This is internal to reduce exposure of JSON.net types so + /// it is easier to convert to System.Text.Json + /// + internal CosmosJsonDotNetSerializer(CosmosSerializationOptions cosmosSerializerOptions) + { + JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings() { - jsonSerializerSettings = new JsonSerializerSettings() - { - NullValueHandling = NullValueHandling.Include - }; - } + NullValueHandling = cosmosSerializerOptions.IgnoreNullValues ? NullValueHandling.Ignore : NullValueHandling.Include, + Formatting = cosmosSerializerOptions.Indented ? Formatting.Indented : Formatting.None, + ContractResolver = cosmosSerializerOptions.PropertyNamingPolicy == CosmosPropertyNamingPolicy.CamelCase + ? new CamelCasePropertyNamesContractResolver() + : null + }; this.Serializer = JsonSerializer.Create(jsonSerializerSettings); } + /// + /// Create a serializer that uses the JSON.net serializer + /// + /// + /// This is internal to reduce exposure of JSON.net types so + /// it is easier to convert to System.Text.Json + /// + internal CosmosJsonDotNetSerializer(JsonSerializerSettings jsonSerializerSettings) + { + this.Serializer = JsonSerializer.Create(jsonSerializerSettings); + } + /// /// Convert a Stream to the passed in type. /// @@ -45,7 +74,7 @@ public override T FromStream(Stream stream) { if (typeof(Stream).IsAssignableFrom(typeof(T))) { - return (T)(object)(stream); + return (T)(object)stream; } using (StreamReader sr = new StreamReader(stream)) diff --git a/Microsoft.Azure.Cosmos/src/CosmosJsonSerializerWrapper.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonSerializerWrapper.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/CosmosJsonSerializerWrapper.cs rename to Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonSerializerWrapper.cs diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosPropertyNamingPolicy.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosPropertyNamingPolicy.cs new file mode 100644 index 0000000000..ead587fa71 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosPropertyNamingPolicy.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + /// + /// Determines the naming policy used to convert a string-based name to another format, such as a camel-casing where the first letter is lower case. + /// + public enum CosmosPropertyNamingPolicy + { + /// + /// No custom naming policy. + /// The property name will be the same as the source. + /// + Default = 0, + + /// + /// First letter in the property name is lower case. + /// + CamelCase = 1, + } +} diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosSerializationOptions.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationFormatOptions.cs similarity index 94% rename from Microsoft.Azure.Cosmos/src/Resource/CosmosSerializationOptions.cs rename to Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationFormatOptions.cs index 76c4eeb6ca..a9e9ca74a6 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosSerializationOptions.cs +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationFormatOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.Azure.Cosmos using System; using Microsoft.Azure.Cosmos.Json; - internal sealed class CosmosSerializationOptions + internal sealed class CosmosSerializationFormatOptions { public delegate IJsonNavigator CreateCustomNavigator(byte[] content); @@ -27,7 +27,7 @@ internal sealed class CosmosSerializationOptions         /// public CreateCustomWriter CreateCustomWriterCallback { get; } - public CosmosSerializationOptions( + public CosmosSerializationFormatOptions( string contentSerializationFormat, CreateCustomNavigator createCustomNavigator, CreateCustomWriter createCustomWriter) diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationOptions.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationOptions.cs new file mode 100644 index 0000000000..965d824012 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationOptions.cs @@ -0,0 +1,49 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + /// + /// This class provides a way to configure basic + /// serializer settings. + /// + public sealed class CosmosSerializationOptions + { + /// + /// Create an instance of CosmosSerializationOptions + /// with the default values for the Cosmos SDK + /// + public CosmosSerializationOptions() + { + this.IgnoreNullValues = false; + this.Indented = false; + this.PropertyNamingPolicy = CosmosPropertyNamingPolicy.Default; + } + + /// + /// Gets or sets if the serializer should ignore null properties + /// + /// + /// The default value is false + /// + public bool IgnoreNullValues { get; set; } + + /// + /// Gets or sets if the serializer should use indentation + /// + /// + /// The default value is false + /// + public bool Indented { get; set; } + + /// + /// Gets or sets whether the naming policy used to convert a string-based name to another format, + /// such as a camel-casing format. + /// + /// + /// The default value is CosmosPropertyNamingPolicy.Default + /// + public CosmosPropertyNamingPolicy PropertyNamingPolicy { get; set; } + } +} diff --git a/Microsoft.Azure.Cosmos/src/CosmosSerializer.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializer.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/CosmosSerializer.cs rename to Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializer.cs diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs index 89db954e14..c67e3e1048 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -1080,7 +1080,7 @@ public async Task ItemQueryStreamSerializationSetting() IList deleteList = await ToDoActivity.CreateRandomItems(this.Container, 101, randomPartitionKey: true); QueryDefinition sql = new QueryDefinition("SELECT * FROM toDoActivity t ORDER BY t.taskNum"); - CosmosSerializationOptions options = new CosmosSerializationOptions( + CosmosSerializationFormatOptions options = new CosmosSerializationFormatOptions( ContentSerializationFormat.CosmosBinary.ToString(), (content) => JsonNavigator.Create(content), () => JsonWriter.Create(JsonSerializationFormat.Binary)); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ContractEnforcement.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ContractEnforcement.cs index 492a836fec..9bf7b2c51f 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ContractEnforcement.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ContractEnforcement.cs @@ -18,8 +18,8 @@ public class ContractEnforcement [TestMethod] public void ContractChanges() { - Assert.IsTrue( - ContractEnforcement.CheckBreakingChanges("Microsoft.Azure.Cosmos.Client", BaselinePath, BreakingChangesPath), + Assert.IsFalse( + ContractEnforcement.DoesContractContainBreakingChanges("Microsoft.Azure.Cosmos.Client", BaselinePath, BreakingChangesPath), $@"Public API has changed. If this is expected, then refresh {BaselinePath} with {Environment.NewLine} Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/testbaseline.cmd /update after this test is run locally. To see the differences run testbaselines.cmd /diff" ); } @@ -69,7 +69,8 @@ private static IEnumerable RemoveDebugSpecificAttributes(IE !x.AttributeType.Name.Contains("NonVersionableAttribute") && !x.AttributeType.Name.Contains("ReliabilityContractAttribute") && !x.AttributeType.Name.Contains("NonVersionableAttribute") && - !x.AttributeType.Name.Contains("DebuggerStepThroughAttribute") + !x.AttributeType.Name.Contains("DebuggerStepThroughAttribute") && + !x.AttributeType.Name.Contains("IsReadOnlyAttribute") ); } @@ -115,7 +116,7 @@ private static TypeTree BuildTypeTree(TypeTree root, Type[] types) return root; } - private static bool CheckBreakingChanges(string dllName, string baselinePath, string breakingChangesPath) + private static bool DoesContractContainBreakingChanges(string dllName, string baselinePath, string breakingChangesPath) { TypeTree locally = new TypeTree(typeof(object)); ContractEnforcement.BuildTypeTree(locally, ContractEnforcement.GetAssemblyLocally(dllName).GetExportedTypes()); @@ -126,28 +127,16 @@ private static bool CheckBreakingChanges(string dllName, string baselinePath, st File.WriteAllText($"{breakingChangesPath}", localJson); string baselineJson = JsonConvert.SerializeObject(baseline, Formatting.Indented); - for (int i=0; i < baselineJson.Length && i < localJson.Length; i++) + System.Diagnostics.Trace.TraceWarning($"String length Expected: {baselineJson.Length};Actual:{localJson.Length}"); + if (string.Equals(localJson, baselineJson, StringComparison.InvariantCulture)) { - if (baselineJson[i] != localJson[i]) - { - // First byte of diff, trace next 200 bytes if-exists - ContractEnforcement.TraceSubpartIfExists(baselineJson, 0, 200); - ContractEnforcement.TraceSubpartIfExists(localJson, 0, 200); - } - } - - return baselineJson == localJson; - } - - private static void TraceSubpartIfExists(string input, int position, int desiredLength) - { - if (position + desiredLength > input.Length) - { - System.Diagnostics.Trace.TraceWarning($"baseline: {input.Substring(position)}"); + return false; } else { - System.Diagnostics.Trace.TraceWarning($"baseline: {input.Substring(position, desiredLength)}"); + System.Diagnostics.Trace.TraceWarning($"Expected: {baselineJson}"); + System.Diagnostics.Trace.TraceWarning($"Actual: {localJson}"); + return true; } } } 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 e562f6f443..2cf8e442fb 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -5,8 +5,7 @@ namespace Microsoft.Azure.Cosmos.Tests { using System; - using System.Linq; - using System.Reflection; + using System.IO; using Microsoft.Azure.Cosmos.Client.Core.Tests; using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Azure.Documents; @@ -34,6 +33,11 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated() ApiType apiType = ApiType.Sql; int maxRetryAttemptsOnThrottledRequests = 9999; TimeSpan maxRetryWaitTime = TimeSpan.FromHours(6); + CosmosSerializationOptions cosmosSerializerOptions = new CosmosSerializationOptions() + { + IgnoreNullValues = true, + PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase, + }; CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder( accountEndpoint: endpoint, @@ -53,6 +57,8 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated() Assert.AreNotEqual(userAgentSuffix, clientOptions.ApplicationName); Assert.AreNotEqual(apiType, clientOptions.ApiType); Assert.AreEqual(0, clientOptions.CustomHandlers.Count); + Assert.IsNull(clientOptions.SerializerOptions); + Assert.IsNull(clientOptions.Serializer); //Verify GetConnectionPolicy returns the correct values for default ConnectionPolicy policy = clientOptions.GetConnectionPolicy(); @@ -67,7 +73,8 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated() .WithApplicationName(userAgentSuffix) .AddCustomHandlers(preProcessHandler) .WithApiType(apiType) - .WithThrottlingRetryOptions(maxRetryWaitTime, maxRetryAttemptsOnThrottledRequests); + .WithThrottlingRetryOptions(maxRetryWaitTime, maxRetryAttemptsOnThrottledRequests) + .WithSerializerOptions(cosmosSerializerOptions); cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); clientOptions = cosmosClient.ClientOptions; @@ -82,6 +89,9 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated() Assert.AreEqual(apiType, clientOptions.ApiType); Assert.AreEqual(maxRetryAttemptsOnThrottledRequests, clientOptions.MaxRetryAttemptsOnRateLimitedRequests); Assert.AreEqual(maxRetryWaitTime, clientOptions.MaxRetryWaitTimeOnRateLimitedRequests); + Assert.AreEqual(cosmosSerializerOptions.IgnoreNullValues, clientOptions.SerializerOptions.IgnoreNullValues); + Assert.AreEqual(cosmosSerializerOptions.PropertyNamingPolicy, clientOptions.SerializerOptions.PropertyNamingPolicy); + Assert.AreEqual(cosmosSerializerOptions.Indented, clientOptions.SerializerOptions.Indented); //Verify GetConnectionPolicy returns the correct values policy = clientOptions.GetConnectionPolicy(); @@ -119,8 +129,8 @@ public void ThrowOnNullEndpoint() [TestMethod] public void UserAgentContainsEnvironmentInformation() { - var environmentInformation = new EnvironmentInformation(); - var expectedValue = environmentInformation.ToString(); + EnvironmentInformation environmentInformation = new EnvironmentInformation(); + string expectedValue = environmentInformation.ToString(); CosmosClientOptions cosmosClientOptions = new CosmosClientOptions(); string userAgentSuffix = "testSuffix"; cosmosClientOptions.ApplicationName = userAgentSuffix; @@ -133,6 +143,86 @@ public void UserAgentContainsEnvironmentInformation() Assert.IsTrue(connectionPolicy.UserAgentSuffix.Contains(expectedValue)); } + [TestMethod] + public void GetCosmosSerializerWithWrapperOrDefaultTest() + { + CosmosJsonDotNetSerializer serializer = new CosmosJsonDotNetSerializer(); + CosmosClientOptions options = new CosmosClientOptions() + { + Serializer = serializer + }; + + CosmosSerializer cosmosSerializer = options.GetCosmosSerializerWithWrapperOrDefault(); + Assert.AreNotEqual(cosmosSerializer, options.PropertiesSerializer, "Serializer should be custom not the default"); + Assert.AreNotEqual(cosmosSerializer, serializer, "Serializer should be in the CosmosJsonSerializerWrapper"); + + CosmosJsonSerializerWrapper cosmosJsonSerializerWrapper = cosmosSerializer as CosmosJsonSerializerWrapper; + Assert.IsNotNull(cosmosJsonSerializerWrapper); + Assert.AreEqual(cosmosJsonSerializerWrapper.InternalJsonSerializer, serializer); + } + + [TestMethod] + public void GetCosmosSerializerWithWrapperOrDefaultWithOptionsTest() + { + CosmosSerializationOptions serializerOptions = new CosmosSerializationOptions(); + Assert.IsFalse(serializerOptions.IgnoreNullValues); + Assert.IsFalse(serializerOptions.Indented); + Assert.AreEqual(CosmosPropertyNamingPolicy.Default, serializerOptions.PropertyNamingPolicy); + + CosmosClientOptions options = new CosmosClientOptions() + { + SerializerOptions = new CosmosSerializationOptions() + { + IgnoreNullValues = true, + Indented = true, + PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase + } + }; + + CosmosSerializer cosmosSerializer = options.GetCosmosSerializerWithWrapperOrDefault(); + Assert.AreNotEqual(cosmosSerializer, options.PropertiesSerializer, "Serializer should be custom not the default"); + + CosmosJsonSerializerWrapper cosmosJsonSerializerWrapper = cosmosSerializer as CosmosJsonSerializerWrapper; + Assert.IsNotNull(cosmosJsonSerializerWrapper); + + // Verify the custom settings are being honored + dynamic testItem = new { id = "testid", description = (string)null, CamelCaseProperty = "TestCamelCase" }; + using (Stream stream = cosmosSerializer.ToStream(testItem)) + { + using (StreamReader sr = new StreamReader(stream)) + { + string jsonString = sr.ReadToEnd(); + // Notice description is not included, camelCaseProperty starts lower case, the white space shows the indents + string expectedJsonString = "{\r\n \"id\": \"testid\",\r\n \"camelCaseProperty\": \"TestCamelCase\"\r\n}"; + Assert.AreEqual(expectedJsonString, jsonString); + } + } + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ThrowOnSerializerOptionsWithCustomSerializer() + { + CosmosClientOptions options = new CosmosClientOptions() + { + Serializer = new CosmosJsonDotNetSerializer() + }; + + options.SerializerOptions = new CosmosSerializationOptions(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ThrowOnCustomSerializerWithSerializerOptions() + { + CosmosClientOptions options = new CosmosClientOptions() + { + SerializerOptions = new CosmosSerializationOptions() + }; + + options.Serializer = new CosmosJsonDotNetSerializer(); + } + [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void ThrowOnNullConnectionString() @@ -160,19 +250,19 @@ public void ThrowOnMissingAccountEndpointInConnectionString() public void AssertJsonSerializer() { string connectionString = "AccountEndpoint=https://localtestcosmos.documents.azure.com:443/;AccountKey=425Mcv8CXQqzRNCgFNjIhT424GK99CKJvASowTnq15Vt8LeahXTcN5wt3342vQ==;"; - var cosmosClientBuilder = new CosmosClientBuilder(connectionString); - var cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); - Assert.IsInstanceOfType(cosmosClient.ClientOptions.CosmosSerializerWithWrapperOrDefault, typeof(CosmosJsonSerializerWrapper)); - Assert.AreEqual(cosmosClient.ClientOptions.CosmosSerializerWithWrapperOrDefault, cosmosClient.ClientOptions.PropertiesSerializer); + CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(connectionString); + CosmosClient cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); + Assert.IsInstanceOfType(cosmosClient.ClientOptions.GetCosmosSerializerWithWrapperOrDefault(), typeof(CosmosJsonSerializerWrapper)); + Assert.AreEqual(cosmosClient.ClientOptions.GetCosmosSerializerWithWrapperOrDefault(), cosmosClient.ClientOptions.PropertiesSerializer); CosmosSerializer defaultSerializer = cosmosClient.ClientOptions.PropertiesSerializer; CosmosSerializer mockJsonSerializer = new Mock().Object; cosmosClientBuilder.WithCustomSerializer(mockJsonSerializer); - var cosmosClientCustom = cosmosClientBuilder.Build(new MockDocumentClient()); + CosmosClient cosmosClientCustom = cosmosClientBuilder.Build(new MockDocumentClient()); Assert.AreEqual(defaultSerializer, cosmosClientCustom.ClientOptions.PropertiesSerializer); Assert.AreEqual(mockJsonSerializer, cosmosClientCustom.ClientOptions.Serializer); - Assert.IsInstanceOfType(cosmosClientCustom.ClientOptions.CosmosSerializerWithWrapperOrDefault, typeof(CosmosJsonSerializerWrapper)); - Assert.AreEqual(mockJsonSerializer, ((CosmosJsonSerializerWrapper)cosmosClientCustom.ClientOptions.CosmosSerializerWithWrapperOrDefault).InternalJsonSerializer); + Assert.IsInstanceOfType(cosmosClientCustom.ClientOptions.GetCosmosSerializerWithWrapperOrDefault(), typeof(CosmosJsonSerializerWrapper)); + Assert.AreEqual(mockJsonSerializer, ((CosmosJsonSerializerWrapper)cosmosClientCustom.ClientOptions.GetCosmosSerializerWithWrapperOrDefault()).InternalJsonSerializer); } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json index 57b123c6dd..01cbe3d275 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json @@ -1246,11 +1246,19 @@ ], "MethodInfo": "Microsoft.Azure.Cosmos.ConnectionMode get_ConnectionMode()" }, - "Microsoft.Azure.Cosmos.CosmosSerializer get_Serializer()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Microsoft.Azure.Cosmos.CosmosSerializationOptions get_SerializerOptions()": { "Type": "Method", - "Attributes": [ - "CompilerGeneratedAttribute" - ], + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.CosmosSerializationOptions get_SerializerOptions()" + }, + "Microsoft.Azure.Cosmos.CosmosSerializationOptions SerializerOptions": { + "Type": "Property", + "Attributes": [], + "MethodInfo": null + }, + "Microsoft.Azure.Cosmos.CosmosSerializer get_Serializer()": { + "Type": "Method", + "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.CosmosSerializer get_Serializer()" }, "Microsoft.Azure.Cosmos.CosmosSerializer Serializer[Newtonsoft.Json.JsonConverterAttribute(typeof(Microsoft.Azure.Cosmos.CosmosClientOptions+ClientOptionJsonConverter))]": { @@ -1267,9 +1275,11 @@ ], "MethodInfo": null }, - "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.RequestHandler] get_CustomHandlers()": { + "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.RequestHandler] get_CustomHandlers()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", - "Attributes": [], + "Attributes": [ + "CompilerGeneratedAttribute" + ], "MethodInfo": "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.RequestHandler] get_CustomHandlers()" }, "System.Nullable`1[Microsoft.Azure.Cosmos.ConsistencyLevel] ConsistencyLevel": { @@ -1399,12 +1409,15 @@ ], "MethodInfo": "Void set_RequestTimeout(System.TimeSpan)" }, - "Void set_Serializer(Microsoft.Azure.Cosmos.CosmosSerializer)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Void set_Serializer(Microsoft.Azure.Cosmos.CosmosSerializer)": { "Type": "Method", - "Attributes": [ - "CompilerGeneratedAttribute" - ], + "Attributes": [], "MethodInfo": "Void set_Serializer(Microsoft.Azure.Cosmos.CosmosSerializer)" + }, + "Void set_SerializerOptions(Microsoft.Azure.Cosmos.CosmosSerializationOptions)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Void set_SerializerOptions(Microsoft.Azure.Cosmos.CosmosSerializationOptions)" } }, "NestedTypes": {} @@ -1514,51 +1527,97 @@ }, "NestedTypes": {} }, - "CosmosJsonDotNetSerializer": { + "CosmosPropertyNamingPolicy": { "Subclasses": {}, "Members": { - "System.IO.Stream ToStream[T](T)": { + "Int32 value__": { + "Type": "Field", + "Attributes": [], + "MethodInfo": null + }, + "Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy CamelCase": { + "Type": "Field", + "Attributes": [], + "MethodInfo": null + }, + "Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy Default": { + "Type": "Field", + "Attributes": [], + "MethodInfo": null + } + }, + "NestedTypes": {} + }, + "CosmosSerializationOptions": { + "Subclasses": {}, + "Members": { + "Boolean get_IgnoreNullValues()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Boolean get_IgnoreNullValues()" + }, + "Boolean get_Indented()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Boolean get_Indented()" + }, + "Boolean IgnoreNullValues": { + "Type": "Property", "Attributes": [], - "MethodInfo": "System.IO.Stream ToStream[T](T)" + "MethodInfo": null }, - "T FromStream[T](System.IO.Stream)": { + "Boolean Indented": { + "Type": "Property", + "Attributes": [], + "MethodInfo": null + }, + "Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy get_PropertyNamingPolicy()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy get_PropertyNamingPolicy()" + }, + "Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy PropertyNamingPolicy": { + "Type": "Property", "Attributes": [], - "MethodInfo": "T FromStream[T](System.IO.Stream)" + "MethodInfo": null }, - "Void .ctor(Newtonsoft.Json.JsonSerializerSettings)": { + "Void .ctor()": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "Void .ctor(Newtonsoft.Json.JsonSerializerSettings)" + "MethodInfo": "Void .ctor()" + }, + "Void set_IgnoreNullValues(Boolean)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Void set_IgnoreNullValues(Boolean)" + }, + "Void set_Indented(Boolean)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Void set_Indented(Boolean)" + }, + "Void set_PropertyNamingPolicy(Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Void set_PropertyNamingPolicy(Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy)" } }, "NestedTypes": {} }, "CosmosSerializer": { - "Subclasses": { - "CosmosJsonDotNetSerializer": { - "Subclasses": {}, - "Members": { - "System.IO.Stream ToStream[T](T)": { - "Type": "Method", - "Attributes": [], - "MethodInfo": "System.IO.Stream ToStream[T](T)" - }, - "T FromStream[T](System.IO.Stream)": { - "Type": "Method", - "Attributes": [], - "MethodInfo": "T FromStream[T](System.IO.Stream)" - }, - "Void .ctor(Newtonsoft.Json.JsonSerializerSettings)": { - "Type": "Constructor", - "Attributes": [], - "MethodInfo": "Void .ctor(Newtonsoft.Json.JsonSerializerSettings)" - } - }, - "NestedTypes": {} - } - }, + "Subclasses": {}, "Members": { "System.IO.Stream ToStream[T](T)": { "Type": "Method", @@ -2167,6 +2226,11 @@ "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithRequestTimeout(System.TimeSpan)" }, + "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithSerializerOptions(Microsoft.Azure.Cosmos.CosmosSerializationOptions)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithSerializerOptions(Microsoft.Azure.Cosmos.CosmosSerializationOptions)" + }, "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithThrottlingRetryOptions(System.TimeSpan, Int32)": { "Type": "Method", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs index 500a61a076..afda45e375 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/LocationCacheTests.cs @@ -506,12 +506,20 @@ await BackoffRetryUtility.ExecuteAsync( [Owner("atulk")] public async Task ValidateAsync() { - for (int i = 0; i < 8; i++) + bool[] boolValues = new bool[] {true, false}; + + foreach (bool useMultipleWriteEndpoints in boolValues) { - bool useMultipleWriteEndpoints = (i & 1) > 0; - bool endpointDiscoveryEnabled = (i & 2) > 0; - bool isPreferredListEmpty = (i & 4) > 0; - await this.ValidateLocationCacheAsync(useMultipleWriteEndpoints, endpointDiscoveryEnabled, isPreferredListEmpty); + foreach (bool endpointDiscoveryEnabled in boolValues) + { + foreach (bool isPreferredListEmpty in boolValues) + { + await this.ValidateLocationCacheAsync( + useMultipleWriteEndpoints, + endpointDiscoveryEnabled, + isPreferredListEmpty); + } + } } } @@ -642,15 +650,34 @@ private async Task ValidateLocationCacheAsync( preferredAvailableWriteEndpoints, preferredAvailableReadEndpoints); - // wait for TTL on unavailablity info - await Task.Delay( - int.Parse( - System.Configuration.ConfigurationManager.AppSettings["UnavailableLocationsExpirationTimeInSeconds"], - NumberStyles.Integer, - CultureInfo.InvariantCulture) * 1000); - - CollectionAssert.AreEqual(currentWriteEndpoints, this.cache.WriteEndpoints); - CollectionAssert.AreEqual(currentReadEndpoints, this.cache.ReadEndpoints); + // wait for TTL on unavailability info + string expirationTime = System.Configuration.ConfigurationManager.AppSettings["UnavailableLocationsExpirationTimeInSeconds"]; + int delayInMilliSeconds = int.Parse( + expirationTime, + NumberStyles.Integer, + CultureInfo.InvariantCulture) * 1000; + await Task.Delay(delayInMilliSeconds); + + string config = $"Delay{expirationTime};" + + $"useMultipleWriteLocations:{useMultipleWriteLocations};" + + $"endpointDiscoveryEnabled:{endpointDiscoveryEnabled};" + + $"isPreferredListEmpty:{isPreferredListEmpty}"; + + CollectionAssert.AreEqual( + currentWriteEndpoints, + this.cache.WriteEndpoints, + "Write Endpoints failed;" + + $"config:{config};" + + $"Current:{string.Join(",", currentWriteEndpoints)};" + + $"Cache:{string.Join(",", this.cache.WriteEndpoints)};"); + + CollectionAssert.AreEqual( + currentReadEndpoints, + this.cache.ReadEndpoints, + "Read Endpoints failed;" + + $"config:{config};" + + $"Current:{string.Join(",", currentReadEndpoints)};" + + $"Cache:{string.Join(",", this.cache.ReadEndpoints)};"); } } } diff --git a/changelog.md b/changelog.md index b82e187cd1..e602e25936 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- [#650](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/650) CosmosSerializerOptions to customize serialization + ### Fixed - [#612](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/612) Bug fix for ReadFeed with partition-key @@ -20,7 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#541](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/541) Added consistency level to client and query options - [#544](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/544) Added continuation token support for LINQ - [#557](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/557) Added trigger options to item request options -- [#571](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/571) Added a default JSON.net serializer with optional settings - [#572](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/572) Added partition key validation on CreateContainerIfNotExistsAsync - [#581](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/581) Added LINQ to QueryDefinition API - [#592](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/592) Added CreateIfNotExistsAsync to container builder