diff --git a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedItem.cs b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedItem.cs index 417cc3b1b6..5e8ac7d438 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedItem.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedItem.cs @@ -57,21 +57,22 @@ namespace Microsoft.Azure.Cosmos class ChangeFeedItem { /// - /// The full fidelity change feed current item. + /// The current version of the item for all versions and deletes change feed mode. + /// It is always null for delete change feed operations. /// [JsonProperty(PropertyName = "current")] [JsonPropertyName("current")] public T Current { get; set; } /// - /// The full fidelity change feed metadata. + /// The item metadata for all versions and deletes change feed mode. /// [JsonProperty(PropertyName = "metadata", NullValueHandling = NullValueHandling.Ignore)] [JsonPropertyName("metadata")] public ChangeFeedMetadata Metadata { get; set; } /// - /// For delete operations, previous image is always going to be provided. The previous image on replace operations is not going to be exposed by default and requires account-level or container-level opt-in. + /// The previous version of the item for all versions and deletes change feed mode. The previous version on delete and replace operations is not exposed by default and requires container-level opt-in. Refer to https://aka.ms/cosmosdb-change-feed-deletes for more information. /// [JsonProperty(PropertyName = "previous", NullValueHandling = NullValueHandling.Ignore)] [JsonPropertyName("previous")] diff --git a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs index 1dae4f1e1b..baae832c46 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs @@ -5,9 +5,8 @@ namespace Microsoft.Azure.Cosmos { using System; - using System.Text.Json; + using System.Collections.Generic; using Microsoft.Azure.Cosmos.Resource.FullFidelity; - using Microsoft.Azure.Cosmos.Resource.FullFidelity.Converters; using Microsoft.Azure.Documents; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -15,44 +14,116 @@ namespace Microsoft.Azure.Cosmos /// /// The metadata of a change feed resource with is initialized to . /// - [System.Text.Json.Serialization.JsonConverter(typeof(ChangeFeedMetadataConverter))] #if PREVIEW public #else internal #endif - class ChangeFeedMetadata + class ChangeFeedMetadata { + private readonly static DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + /// /// The change's conflict resolution timestamp. /// - [JsonProperty(PropertyName = ChangeFeedMetadataFields.ConflictResolutionTimestamp, NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(UnixDateTimeConverter))] - public DateTime ConflictResolutionTimestamp { get; internal set; } + [System.Text.Json.Serialization.JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + public DateTime? ConflictResolutionTimestamp => this.ConflictResolutionTimestampInSeconds.HasValue ? UnixEpoch.AddSeconds(this.ConflictResolutionTimestampInSeconds.Value) : null; + + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.ConflictResolutionTimestamp)] + [JsonProperty(PropertyName = ChangeFeedMetadataFields.ConflictResolutionTimestamp)] + internal double? ConflictResolutionTimestampInSeconds { get; set; } /// /// The current change's logical sequence number. /// + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.Lsn)] [JsonProperty(PropertyName = ChangeFeedMetadataFields.Lsn, NullValueHandling = NullValueHandling.Ignore)] public long Lsn { get; internal set; } /// /// The change's feed operation type . /// + [Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))] + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.OperationType)] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] [JsonProperty(PropertyName = ChangeFeedMetadataFields.OperationType, NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(StringEnumConverter))] public ChangeFeedOperationType OperationType { get; internal set; } /// /// The previous change's logical sequence number. /// + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.PreviousImageLSN)] [JsonProperty(PropertyName = ChangeFeedMetadataFields.PreviousImageLSN, NullValueHandling = NullValueHandling.Ignore)] public long PreviousLsn { get; internal set; } /// - /// Used to distinquish explicit deletes (e.g. via DeleteItem) from deletes caused by TTL expiration (a collection may define time-to-live policy for documents). + /// Used to distinguish explicit deletes (e.g. via DeleteItem) from deletes caused by TTL expiration (a collection may define time-to-live policy for documents). /// + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.TimeToLiveExpired)] [JsonProperty(PropertyName = ChangeFeedMetadataFields.TimeToLiveExpired, NullValueHandling = NullValueHandling.Ignore)] public bool IsTimeToLiveExpired { get; internal set; } + + /// + /// Applicable for delete operations only, otherwise null. + /// The id of the previous item version. + /// + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.Id)] + [JsonProperty(PropertyName = ChangeFeedMetadataFields.Id, NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; internal set; } + + /// + /// Applicable for delete operations only, otherwise null. + /// The partition key of the previous item version represented as a dictionary where the key is the partition key property name + /// and the value is the partition key property value. All levels of hierarchy will be present if a hierarchical partition key (HPK) is used. + /// + /// + /// + /// For single partition key containers, the dictionary will contain one entry with the partition key path name (without the leading '/') + /// as the key and the partition key value as the value. + /// + /// + /// For hierarchical partition key containers, the dictionary will contain multiple entries, one for each level of the hierarchy, + /// as defined in the container's partition key definition. + /// + /// + /// Example for a single partition key container with partition key path "/tenantId": + /// + /// { + /// "tenantId": "tenant123" + /// } + /// + /// + /// + /// Example for a hierarchical partition key container with partition key paths ["/tenantId", "/userId", "/sessionId"]: + /// + /// { + /// "tenantId": "tenant123", + /// "userId": "user456", + /// "sessionId": "session789" + /// } + /// + /// + /// + /// The partition key values can be of different types (string, number, boolean, null) depending on the document's schema. + /// For example, with partition key paths ["/category", "/priority"]: + /// + /// { + /// "category": "electronics", + /// "priority": 1 + /// } + /// + /// + /// + [System.Text.Json.Serialization.JsonInclude] + [System.Text.Json.Serialization.JsonPropertyName(ChangeFeedMetadataFields.PartitionKey)] + [JsonProperty(PropertyName = ChangeFeedMetadataFields.PartitionKey, NullValueHandling = NullValueHandling.Ignore)] + public Dictionary PartitionKey { get; internal set; } } } diff --git a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadataFields.cs b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadataFields.cs index db39a386a9..e0005b8c05 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadataFields.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadataFields.cs @@ -11,5 +11,7 @@ internal class ChangeFeedMetadataFields public const string OperationType = "operationType"; public const string PreviousImageLSN = "previousImageLSN"; public const string TimeToLiveExpired = "timeToLiveExpired"; + public const string Id = "id"; + public const string PartitionKey = "partitionKey"; } } diff --git a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/Converters/ChangeFeedMetadataConverter.cs b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/Converters/ChangeFeedMetadataConverter.cs deleted file mode 100644 index 0b5056051a..0000000000 --- a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/Converters/ChangeFeedMetadataConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -//------------------------------------------------------------ - -namespace Microsoft.Azure.Cosmos.Resource.FullFidelity.Converters -{ - using System; - using System.Globalization; - using System.Text.Json; - using System.Text.Json.Serialization; - using Microsoft.Azure.Cosmos.Resource.FullFidelity; - using Microsoft.Azure.Documents; - - /// - /// Converter used to support System.Text.Json de/serialization of type ChangeFeedMetadata/>. - /// - internal class ChangeFeedMetadataConverter : JsonConverter - { - private readonly static DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - - public override ChangeFeedMetadata Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException(string.Format(CultureInfo.CurrentCulture, RMResources.JsonUnexpectedToken)); - } - - JsonElement element = JsonDocument.ParseValue(ref reader).RootElement; - - ChangeFeedMetadata metadata = new (); - - foreach (JsonProperty property in element.EnumerateObject()) - { - if (property.NameEquals(ChangeFeedMetadataFields.Lsn)) - { - metadata.Lsn = property.Value.GetInt64(); - } - else if (property.NameEquals(ChangeFeedMetadataFields.ConflictResolutionTimestamp)) - { - metadata.ConflictResolutionTimestamp = ChangeFeedMetadataConverter.ToDateTimeFromUnixTimeInSeconds(property.Value.GetInt64()); - } - else if (property.NameEquals(ChangeFeedMetadataFields.OperationType)) - { - metadata.OperationType = (ChangeFeedOperationType)Enum.Parse(enumType: typeof(ChangeFeedOperationType), value: property.Value.GetString(), ignoreCase: true); - } - else if (property.NameEquals(ChangeFeedMetadataFields.TimeToLiveExpired)) - { - metadata.IsTimeToLiveExpired = property.Value.GetBoolean(); - } - else if (property.NameEquals(ChangeFeedMetadataFields.PreviousImageLSN)) - { - metadata.PreviousLsn = property.Value.GetInt64(); - } - } - - return metadata; - } - - public override void Write(Utf8JsonWriter writer, ChangeFeedMetadata value, JsonSerializerOptions options) - { - if (value == null) - { - return; - } - - writer.WriteStartObject(); - - writer.WriteNumber(ChangeFeedMetadataFields.ConflictResolutionTimestamp, ChangeFeedMetadataConverter.ToUnixTimeInSecondsFromDateTime(value.ConflictResolutionTimestamp)); - writer.WriteBoolean(ChangeFeedMetadataFields.TimeToLiveExpired, value.IsTimeToLiveExpired); - writer.WriteNumber(ChangeFeedMetadataFields.Lsn, value.Lsn); - writer.WriteString(ChangeFeedMetadataFields.OperationType, value.OperationType.ToString()); - writer.WriteNumber(ChangeFeedMetadataFields.PreviousImageLSN, value.PreviousLsn); - - writer.WriteEndObject(); - } - - private static long ToUnixTimeInSecondsFromDateTime(DateTime date) - { - return (long)(date - ChangeFeedMetadataConverter.UnixEpoch).TotalSeconds; - } - - private static DateTime ToDateTimeFromUnixTimeInSeconds(long unixTimeInSeconds) - { - return ChangeFeedMetadataConverter.UnixEpoch.AddSeconds(unixTimeInSeconds); - } - } -} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderTests.cs index 669c6bd194..fe549fa145 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderTests.cs @@ -69,7 +69,8 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA Assert.IsTrue(DateTime.TryParse(s: change.Metadata.ConflictResolutionTimestamp.ToString(), out _), message: "Invalid csrt must be a datetime value."); Assert.IsTrue(change.Metadata.Lsn > 0, message: "Invalid lsn must be a long value."); Assert.IsFalse(change.Metadata.IsTimeToLiveExpired); - + Assert.IsNull(change.Metadata.Id); + Assert.IsNull(change.Metadata.PartitionKey); // previous Assert.IsNull(change.Previous); } @@ -84,10 +85,9 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA Assert.IsTrue(change.Metadata.IsTimeToLiveExpired); // previous - Assert.AreEqual(expected: "1", actual: change.Previous.id.ToString()); - Assert.AreEqual(expected: "1", actual: change.Previous.pk.ToString()); - Assert.AreEqual(expected: "Testing TTL on CFP.", actual: change.Previous.description.ToString()); - Assert.AreEqual(expected: ttlInSeconds, actual: change.Previous.ttl); + Assert.AreEqual(expected: "1", actual: change.Metadata.Id.ToString()); + Assert.AreEqual(expected: "1", actual: change.Metadata.PartitionKey.Values.FirstOrDefault()); + Assert.IsNull(change.Previous); // stop after reading delete since it is the last document in feed. stopwatch.Stop(); @@ -145,7 +145,7 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA [TestMethod] [Owner("philipthomas-MSFT")] [Description("Scenario: When a document is created, then updated, and finally deleted, there should be 3 changes that will appear for that " + - "document when using ChangeFeedProcessor with AllVersionsAndDeletes set as the ChangeFeedMode.")] + "document when using ChangeFeedProcessor with AllVersionsAndDeletes set as the ChangeFeedMode and enablePreviousImageForDeleteInFFCF true")] public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync() { ContainerInternal monitoredContainer = await this.CreateMonitoredContainer(ChangeFeedMode.AllVersionsAndDeletes); @@ -155,6 +155,8 @@ public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync() ChangeFeedProcessor processor = monitoredContainer .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes(processorName: "processor", onChangesDelegate: (ChangeFeedProcessorContext context, IReadOnlyCollection> docs, CancellationToken token) => { + string metadataId = default; + string metadataPk = default; string id = default; string pk = default; string description = default; @@ -171,14 +173,13 @@ public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync() } else { - id = change.Previous.id.ToString(); - pk = change.Previous.pk.ToString(); - description = change.Previous.description.ToString(); + metadataId = change.Metadata.Id.ToString(); + metadataPk = change.Metadata.PartitionKey.Values.FirstOrDefault().ToString(); } ChangeFeedOperationType operationType = change.Metadata.OperationType; long previousLsn = change.Metadata.PreviousLsn; - DateTime m = change.Metadata.ConflictResolutionTimestamp; + DateTime? m = change.Metadata.ConflictResolutionTimestamp; long lsn = change.Metadata.Lsn; bool isTimeToLiveExpired = change.Metadata.IsTimeToLiveExpired; } @@ -211,8 +212,9 @@ public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync() ChangeFeedItem deleteChange = docs.ElementAt(2); Assert.IsNull(deleteChange.Current.id); + Assert.AreEqual(expected: "1", actual: deleteChange.Metadata.Id.ToString()); + Assert.AreEqual(expected: "1", actual: deleteChange.Metadata.PartitionKey.Values.FirstOrDefault()); Assert.AreEqual(expected: deleteChange.Metadata.OperationType, actual: ChangeFeedOperationType.Delete); - Assert.AreEqual(expected: replaceChange.Metadata.Lsn, actual: deleteChange.Metadata.PreviousLsn); Assert.IsNotNull(deleteChange.Previous); Assert.AreEqual(expected: "1", actual: deleteChange.Previous.id.ToString()); Assert.AreEqual(expected: "1", actual: deleteChange.Previous.pk.ToString()); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderWithCustomSerializerTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderWithCustomSerializerTests.cs index a6780e4409..242784b4cc 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderWithCustomSerializerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/BuilderWithCustomSerializerTests.cs @@ -39,7 +39,11 @@ public void ValidateNSJAndSTJSerializationOfChangeFeedItemDeleteTimeToLiveExpire ""crts"": 1722511591, ""operationType"": ""delete"", ""timeToLiveExpired"": true, - ""previousImageLSN"": 16 + ""previousImageLSN"": 16, + ""id"": ""1"", + ""partitionKey"": { + ""pk"": ""1"" + } }, ""previous"": { ""id"": ""1"", @@ -92,6 +96,8 @@ static void ValidateDeserialization(List> activitie Assert.IsTrue(deletedChange.Metadata.IsTimeToLiveExpired); Assert.IsNotNull(deletedChange.Previous); Assert.AreEqual(expected: "Testing TTL on CFP.", actual: deletedChange.Previous.description); + Assert.AreEqual(expected: "1", actual: deletedChange.Metadata.Id.ToString()); + Assert.AreEqual(expected: "1", actual: deletedChange.Metadata.PartitionKey.Values.FirstOrDefault().ToString()); Assert.AreEqual(expected: "1", actual: deletedChange.Previous.id); Assert.AreEqual(expected: 5, actual: deletedChange.Previous.ttl); } @@ -216,7 +222,11 @@ public void ValidateNSJAndSTJSerializationOfChangeFeedItemTest(bool propertyName ""lsn"": 376, ""operationType"": ""delete"", ""previousImageLSN"": 375, - ""timeToLiveExpired"": false + ""timeToLiveExpired"": false, + ""id"": ""1"", + ""partitionKey"": { + ""pk"": ""1"" + } }, ""previous"": { ""id"": ""1"", @@ -295,11 +305,173 @@ static void ValidateDeserialization(List> activitie Assert.IsFalse(deletedChange.Metadata.IsTimeToLiveExpired); Assert.IsNotNull(deletedChange.Previous); Assert.AreEqual(expected: "test after replace", actual: deletedChange.Previous.description); + Assert.AreEqual(expected: "1", actual: deletedChange.Metadata.Id.ToString()); + Assert.AreEqual(expected: "1", actual: deletedChange.Metadata.PartitionKey.Values.FirstOrDefault().ToString()); Assert.AreEqual(expected: "1", actual: deletedChange.Previous.id); Assert.AreEqual(expected: 0, actual: deletedChange.Previous.ttl); } } + [TestMethod] + [Owner("trivediyash")] + [Description("Validating to deserization using NSJ and STJ of ChangeFeedItem with HPK (Hierarchical Partition Key) with Create, Replace, and Delete payload.")] + [DataRow(true)] + [DataRow(false)] + public void ValidateNSJAndSTJSerializationOfChangeFeedItemWithHPKTest(bool propertyNameCaseInsensitive) + { + string json = @"[ + { + ""current"": { + ""id"": ""1"", + ""pk1"": ""value1"", + ""pk2"": ""value2"", + ""pk3"": ""value3"", + ""description"": ""original test with HPK"", + ""_rid"": ""HpxDAL+dzLQBAAAAAAAAAA=="", + ""_self"": ""dbs/HpxDAA==/colls/HpxDAL+dzLQ=/docs/HpxDAL+dzLQBAAAAAAAAAA==/"", + ""_etag"": ""\""00000000-0000-0000-e384-28095c1a01da\"""", + ""_attachments"": ""attachments/"", + ""_ts"": 1722455970 + }, + ""metadata"": { + ""crts"": 1722455970, + ""lsn"": 374, + ""operationType"": ""create"", + ""previousImageLSN"": 0, + ""timeToLiveExpired"": false + } + }, + { + ""current"": { + ""id"": ""1"", + ""pk1"": ""value1"", + ""pk2"": ""value2"", + ""pk3"": ""value3"", + ""description"": ""test after replace with HPK"", + ""_rid"": ""HpxDAL+dzLQBAAAAAAAAAA=="", + ""_self"": ""dbs/HpxDAA==/colls/HpxDAL+dzLQ=/docs/HpxDAL+dzLQBAAAAAAAAAA==/"", + ""_etag"": ""\""00000000-0000-0000-e384-28a5abdd01da\"""", + ""_attachments"": ""attachments/"", + ""_ts"": 1722455971 + }, + ""metadata"": { + ""crts"": 1722455971, + ""lsn"": 375, + ""operationType"": ""replace"", + ""previousImageLSN"": 374, + ""timeToLiveExpired"": false + } + }, + { + ""current"": {}, + ""metadata"": { + ""crts"": 1722455972, + ""lsn"": 376, + ""operationType"": ""delete"", + ""previousImageLSN"": 375, + ""timeToLiveExpired"": false, + ""id"": ""1"", + ""partitionKey"": { + ""pk1"": ""value1"", + ""pk2"": ""value2"", + ""pk3"": ""value3"" + } + }, + ""previous"": { + ""id"": ""1"", + ""pk1"": ""value1"", + ""pk2"": ""value2"", + ""pk3"": ""value3"", + ""description"": ""test after replace with HPK"", + ""_rid"": ""HpxDAL+dzLQBAAAAAAAAAA=="", + ""_self"": ""dbs/HpxDAA==/colls/HpxDAL+dzLQ=/docs/HpxDAL+dzLQBAAAAAAAAAA==/"", + ""_etag"": ""\""00000000-0000-0000-e384-28a5abdd01da\"""", + ""_attachments"": ""attachments/"", + ""_ts"": 1722455971 + } + } + ]"; + + ValidateSystemTextJsonDeserialization(json, propertyNameCaseInsensitive); + ValidateNewtonsoftJsonDeserialization(json); + + static void ValidateNewtonsoftJsonDeserialization(string json) + { + ValidateDeserialization(JsonConvert.DeserializeObject>>(json)); + } + + static void ValidateSystemTextJsonDeserialization(string json, bool propertyNameCaseInsensitive) + { + ValidateDeserialization(System.Text.Json.JsonSerializer.Deserialize>>( + json: json, + options: new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = propertyNameCaseInsensitive + })); + } + + static void ValidateDeserialization(List> activities) + { + Assert.IsNotNull(activities); + + ChangeFeedItem createdUpdate = activities.ElementAt(0); + Assert.IsNotNull(createdUpdate); + Assert.IsNotNull(createdUpdate.Current); + Assert.AreEqual(expected: "original test with HPK", actual: createdUpdate.Current.description); + Assert.AreEqual(expected: "1", actual: createdUpdate.Current.id); + Assert.AreEqual(expected: "value1", actual: createdUpdate.Current.pk1); + Assert.AreEqual(expected: "value2", actual: createdUpdate.Current.pk2); + Assert.AreEqual(expected: "value3", actual: createdUpdate.Current.pk3); + Assert.IsNotNull(createdUpdate.Metadata); + Assert.AreEqual(expected: DateTime.Parse("7/31/2024 7:59:30 PM"), actual: createdUpdate.Metadata.ConflictResolutionTimestamp); + Assert.AreEqual(expected: 374, actual: createdUpdate.Metadata.Lsn); + Assert.AreEqual(expected: ChangeFeedOperationType.Create, actual: createdUpdate.Metadata.OperationType); + Assert.AreEqual(expected: 0, actual: createdUpdate.Metadata.PreviousLsn); + Assert.IsFalse(createdUpdate.Metadata.IsTimeToLiveExpired); + Assert.IsNull(createdUpdate.Previous); // No Previous for a Create change. + + ChangeFeedItem replacedChange = activities.ElementAt(1); + Assert.IsNotNull(replacedChange); + Assert.IsNotNull(replacedChange.Current); + Assert.AreEqual(expected: "test after replace with HPK", actual: replacedChange.Current.description); + Assert.AreEqual(expected: "1", actual: replacedChange.Current.id); + Assert.AreEqual(expected: "value1", actual: replacedChange.Current.pk1); + Assert.AreEqual(expected: "value2", actual: replacedChange.Current.pk2); + Assert.AreEqual(expected: "value3", actual: replacedChange.Current.pk3); + Assert.IsNotNull(replacedChange.Metadata); + Assert.AreEqual(expected: DateTime.Parse("7/31/2024 7:59:31 PM"), actual: replacedChange.Metadata.ConflictResolutionTimestamp); + Assert.AreEqual(expected: 375, actual: replacedChange.Metadata.Lsn); + Assert.AreEqual(expected: ChangeFeedOperationType.Replace, actual: replacedChange.Metadata.OperationType); + Assert.AreEqual(expected: 374, actual: replacedChange.Metadata.PreviousLsn); + Assert.IsFalse(replacedChange.Metadata.IsTimeToLiveExpired); + Assert.IsNull(replacedChange.Previous); // No Previous for a Replace change. + + ChangeFeedItem deletedChange = activities.ElementAt(2); + Assert.IsNotNull(deletedChange); + Assert.IsNotNull(deletedChange.Current); // Current is not null, but no data. + Assert.AreEqual(expected: default, actual: deletedChange.Current.description); // No current description for Delete + Assert.AreEqual(expected: default, actual: deletedChange.Current.id); // No current id for Delete + Assert.IsNotNull(deletedChange.Metadata); + Assert.AreEqual(expected: DateTime.Parse("7/31/2024 7:59:32 PM"), actual: deletedChange.Metadata.ConflictResolutionTimestamp); + Assert.AreEqual(expected: 376, actual: deletedChange.Metadata.Lsn); + Assert.AreEqual(expected: ChangeFeedOperationType.Delete, actual: deletedChange.Metadata.OperationType); + Assert.AreEqual(expected: 375, actual: deletedChange.Metadata.PreviousLsn); + Assert.IsFalse(deletedChange.Metadata.IsTimeToLiveExpired); + Assert.IsNotNull(deletedChange.Previous); + Assert.AreEqual(expected: "test after replace with HPK", actual: deletedChange.Previous.description); + Assert.AreEqual(expected: "1", actual: deletedChange.Metadata.Id.ToString()); + Assert.IsNotNull(deletedChange.Metadata.PartitionKey); + Assert.AreEqual(expected: 3, actual: deletedChange.Metadata.PartitionKey.Count); + Assert.AreEqual(expected: "value1", actual: deletedChange.Metadata.PartitionKey["pk1"].ToString()); + Assert.AreEqual(expected: "value2", actual: deletedChange.Metadata.PartitionKey["pk2"].ToString()); + Assert.AreEqual(expected: "value3", actual: deletedChange.Metadata.PartitionKey["pk3"].ToString()); + Assert.AreEqual(expected: "1", actual: deletedChange.Previous.id); + Assert.AreEqual(expected: "value1", actual: deletedChange.Previous.pk1); + Assert.AreEqual(expected: "value2", actual: deletedChange.Previous.pk2); + Assert.AreEqual(expected: "value3", actual: deletedChange.Previous.pk3); + } + } + [TestMethod] [Owner("philipthomas-MSFT")] [Description("Replace and Deletes have full ChangeFeedMetadata.")] @@ -313,18 +485,19 @@ public void ValidateChangeFeedMetadataSerializationReplaceAnDeleteWriteTest(bool Lsn = 374, OperationType = ChangeFeedOperationType.Create, IsTimeToLiveExpired = true, - ConflictResolutionTimestamp = DateTime.Parse("7/31/2024 7:59:30 PM") + ConflictResolutionTimestampInSeconds = 1722455970 }; string json = System.Text.Json.JsonSerializer.Serialize( value: metadata, options: new JsonSerializerOptions { - PropertyNameCaseInsensitive = propertyNameCaseInsensitive + PropertyNameCaseInsensitive = propertyNameCaseInsensitive, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); Assert.AreEqual( - expected: @"{""crts"":1722455970,""timeToLiveExpired"":true,""lsn"":374,""operationType"":""Create"",""previousImageLSN"":15}", + expected: @"{""crts"":1722455970,""lsn"":374,""operationType"":""Create"",""previousImageLSN"":15,""timeToLiveExpired"":true}", actual: json); } @@ -339,18 +512,19 @@ public void ValidateChangeFeedMetadataSerializationCreateWriteTest(bool property { Lsn = 374, OperationType = ChangeFeedOperationType.Create, - ConflictResolutionTimestamp = DateTime.Parse("7/31/2024 7:59:30 PM") + ConflictResolutionTimestampInSeconds = 1722455970 }; string json = System.Text.Json.JsonSerializer.Serialize( value: metadata, options: new JsonSerializerOptions { - PropertyNameCaseInsensitive = propertyNameCaseInsensitive + PropertyNameCaseInsensitive = propertyNameCaseInsensitive, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); Assert.AreEqual( - expected: @"{""crts"":1722455970,""timeToLiveExpired"":false,""lsn"":374,""operationType"":""Create"",""previousImageLSN"":0}", + expected: @"{""crts"":1722455970,""lsn"":374,""operationType"":""Create"",""previousImageLSN"":0,""timeToLiveExpired"":false}", actual: json); } @@ -376,7 +550,7 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA Container leaseContainer = await database.CreateContainerIfNotExistsAsync(containerProperties: new ContainerProperties(id: "leases", partitionKeyPath: "/id")); ContainerInternal monitoredContainer = await this.CreateMonitoredContainer(ChangeFeedMode.AllVersionsAndDeletes, database); Exception exception = default; - int ttlInSeconds = 5; + int ttlInSeconds = 1; Stopwatch stopwatch = new(); ManualResetEvent allDocsProcessed = new ManualResetEvent(false); @@ -401,6 +575,8 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA Assert.IsTrue(DateTime.TryParse(s: change.Metadata.ConflictResolutionTimestamp.ToString(), out _), message: "Invalid csrt must be a datetime value."); Assert.IsTrue(change.Metadata.Lsn > 0, message: "Invalid lsn must be a long value."); Assert.IsFalse(change.Metadata.IsTimeToLiveExpired); + Assert.IsNull(change.Metadata.Id); + Assert.IsNull(change.Metadata.PartitionKey); // previous Assert.IsNull(change.Previous); @@ -414,12 +590,11 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA Assert.IsTrue(DateTime.TryParse(s: change.Metadata.ConflictResolutionTimestamp.ToString(), out _), message: "Invalid csrt must be a datetime value."); Assert.IsTrue(change.Metadata.Lsn > 0, message: "Invalid lsn must be a long value."); Assert.IsTrue(change.Metadata.IsTimeToLiveExpired); + Assert.AreEqual(expected: "1", actual: change.Metadata.Id.ToString()); + Assert.AreEqual(expected: "1", actual: change.Metadata.PartitionKey.Values.FirstOrDefault()); // previous - Assert.AreEqual(expected: "1", actual: change.Previous.id.ToString()); - Assert.AreEqual(expected: "1", actual: change.Previous.pk.ToString()); - Assert.AreEqual(expected: "Testing TTL on CFP.", actual: change.Previous.description.ToString()); - Assert.AreEqual(expected: ttlInSeconds, actual: change.Previous.ttl); + Assert.IsNull(change.Previous); // stop after reading delete since it is the last document in feed. stopwatch.Stop(); @@ -482,21 +657,48 @@ public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsA } [TestMethod] + [TestCategory("ChangeFeed")] [Owner("philipthomas-MSFT")] [Description("Scenario: When a document is created, then updated, and finally deleted, there should be 3 changes that will appear for that " + - "document when using ChangeFeedProcessor with AllVersionsAndDeletes set as the ChangeFeedMode.")] + "document when using ChangeFeedProcessor with AllVersionsAndDeletes set as the ChangeFeedMode. This test runs against the Cosmos DB Emulator" + + " which has enablePreviousImageForDeleteInFFCF set to true.")] [DataRow(true)] [DataRow(false)] - public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(bool propertyNameCaseInsensitive) + public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsyncEmulator(bool propertyNameCaseInsensitive) { - CosmosClient cosmosClient = TestCommon.CreateCosmosClient((cosmosClientBuilder) => - cosmosClientBuilder.WithSystemTextJsonSerializerOptions( - new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = propertyNameCaseInsensitive - }), - useCustomSeralizer: false); + await this.WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(propertyNameCaseInsensitive); + } + + [TestMethod] + [TestCategory("MultiMaster")] + [Owner("philipthomas-MSFT")] + [Description("Scenario: When a document is created, then updated, and finally deleted, there should be 3 changes that will appear for that " + + "document when using ChangeFeedProcessor with AllVersionsAndDeletes set as the ChangeFeedMode. This test runs against a live multi-region" + + " Cosmos DB account which has does not have enablePreviousImageForDeleteInFFCF set.")] + [DataRow(true)] + [DataRow(false)] + public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsyncLiveAccount(bool propertyNameCaseInsensitive) + { + await this.WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(propertyNameCaseInsensitive, true); + } + + private async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(bool propertyNameCaseInsensitive, bool isMultiMaster = false) + { + (string defaultEndpoint, string authKey) = TestCommon.GetAccountInfo(); + string accountEndpoint = TestCommon.GetMultiRegionConnectionString(); + + CosmosClientOptions options = new CosmosClientOptions() + { + UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = propertyNameCaseInsensitive + } + }; + CosmosClient cosmosClient = isMultiMaster + ? new CosmosClient(accountEndpoint, options) + : new CosmosClient(defaultEndpoint, authKey, options); + Database database = await cosmosClient.CreateDatabaseIfNotExistsAsync(id: Guid.NewGuid().ToString()); Container leaseContainer = await database.CreateContainerIfNotExistsAsync(containerProperties: new ContainerProperties(id: "leases", partitionKeyPath: "/id")); ContainerInternal monitoredContainer = await this.CreateMonitoredContainer(ChangeFeedMode.AllVersionsAndDeletes, database); @@ -508,6 +710,8 @@ public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(bool pr { Logger.LogLine($"@ {DateTime.Now}, {nameof(docs)} -> {System.Text.Json.JsonSerializer.Serialize(docs)}"); + string metadataId = default; + string metadataPk = default; string id = default; string pk = default; string description = default; @@ -522,14 +726,13 @@ public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(bool pr } else { - id = change.Previous.id.ToString(); - pk = change.Previous.pk.ToString(); - description = change.Previous.description.ToString(); + metadataId = change.Metadata.Id.ToString(); + metadataPk = change.Metadata.PartitionKey.Values.FirstOrDefault().ToString(); } ChangeFeedOperationType operationType = change.Metadata.OperationType; long previousLsn = change.Metadata.PreviousLsn; - DateTime m = change.Metadata.ConflictResolutionTimestamp; + DateTime? m = change.Metadata.ConflictResolutionTimestamp; long lsn = change.Metadata.Lsn; bool isTimeToLiveExpired = change.Metadata.IsTimeToLiveExpired; } @@ -564,10 +767,18 @@ public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedTestsAsync(bool pr Assert.IsNull(deleteChange.Current.id); Assert.AreEqual(expected: deleteChange.Metadata.OperationType, actual: ChangeFeedOperationType.Delete); Assert.AreEqual(expected: replaceChange.Metadata.Lsn, actual: deleteChange.Metadata.PreviousLsn); - Assert.IsNotNull(deleteChange.Previous); - Assert.AreEqual(expected: "1", actual: deleteChange.Previous.id.ToString()); - Assert.AreEqual(expected: "1", actual: deleteChange.Previous.pk.ToString()); - Assert.AreEqual(expected: "test after replace", actual: deleteChange.Previous.description.ToString()); + + if (isMultiMaster) + { + Assert.IsNull(deleteChange.Previous); + } + else + { + Assert.IsNotNull(deleteChange.Previous); + Assert.AreEqual(expected: "1", actual: deleteChange.Previous.id.ToString()); + } + Assert.AreEqual(expected: "1", actual: deleteChange.Metadata.Id.ToString()); + Assert.AreEqual(expected: "1", actual: deleteChange.Metadata.PartitionKey.Values.FirstOrDefault().ToString()); Assert.IsTrue(condition: createChange.Metadata.ConflictResolutionTimestamp < replaceChange.Metadata.ConflictResolutionTimestamp, message: "The create operation must happen before the replace operation."); Assert.IsTrue(condition: replaceChange.Metadata.ConflictResolutionTimestamp < deleteChange.Metadata.ConflictResolutionTimestamp, message: "The replace operation must happen before the delete operation."); @@ -736,7 +947,7 @@ private async Task CreateMonitoredContainer( if (changeFeedMode == ChangeFeedMode.AllVersionsAndDeletes) { - properties.ChangeFeedPolicy.FullFidelityRetention = TimeSpan.FromMinutes(5); + //properties.ChangeFeedPolicy.FullFidelityRetention = TimeSpan.FromMinutes(5); properties.DefaultTimeToLive = -1; } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/ToDoActivity.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/ToDoActivity.cs index 1058c8f3fe..d8c6df721a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/ToDoActivity.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CFP/AllVersionsAndDeletes/ToDoActivity.cs @@ -15,4 +15,20 @@ public class ToDoActivity public int ttl { get; set; } } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Used for CFP AllVersionsAndDeletes builder tests with HPK without having attribute annotations from STJ or NSJ.")] + public class ToDoActivityWithHPK + { + public string id { get; set; } + + public string pk1 { get; set; } + + public string pk2 { get; set; } + + public string pk3 { get; set; } + + public string description { get; set; } + + public int ttl { get; set; } + } } 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 cea480cb47..4764cecb0f 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemIntegrationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemIntegrationTests.cs @@ -1,21 +1,21 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests { using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; using System.Linq; - using System.Net; - using System.Net.Http; - using System.Text; + using System.Net; + using System.Net.Http; + using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Diagnostics; - using Microsoft.Azure.Cosmos.FaultInjection; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Newtonsoft.Json.Linq; + using Microsoft.Azure.Cosmos.FaultInjection; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json.Linq; using static Microsoft.Azure.Cosmos.Routing.GlobalPartitionEndpointManagerCore; using static Microsoft.Azure.Cosmos.SDK.EmulatorTests.MultiRegionSetupHelpers; @@ -30,15 +30,15 @@ public class CosmosItemIntegrationTests private static string region1; private static string region2; - private static string region3; + private static string region3; private IDictionary readRegionsMapping; private IList thinClientreadRegionalEndpoints; private CosmosSystemTextJsonSerializer cosmosSystemTextJsonSerializer; [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() { @@ -81,7 +81,7 @@ public void TestCleanup() finally { //Do not delete the resources (except MM Write test object), georeplication is slow and we want to reuse the resources - this.client?.Dispose(); + this.client?.Dispose(); Environment.SetEnvironmentVariable(ConfigurationManager.StalePartitionUnavailabilityRefreshIntervalInSeconds, null); } } @@ -155,93 +155,93 @@ public async Task ReadMany2UnreachablePartitionsTest() rule.Disable(); fiClient.Dispose(); } - } - - [TestMethod] + } + + [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 = + [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 = + }; + 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); + }; + 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(); - + + } + + // 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(); - } - } + } + finally + { + await containerBEEnabledResponse.Container.DeleteContainerAsync(); + await containerBEDisabledResponse.Container.DeleteContainerAsync(); + } + } [TestMethod] [TestCategory("MultiRegion")] @@ -469,7 +469,7 @@ await this.container.DeleteItemAsync( } [TestMethod] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [DataRow(ConnectionMode.Direct, "15", "10", DisplayName = "Direct Mode - Scenario when the total iteration count is 15 and circuit breaker consecutive failure threshold is set to 10.")] [DataRow(ConnectionMode.Direct, "25", "20", DisplayName = "Direct Mode - Scenario when the total iteration count is 25 and circuit breaker consecutive failure threshold is set to 20.")] [DataRow(ConnectionMode.Direct, "35", "30", DisplayName = "Direct Mode - Scenario when the total iteration count is 35 and circuit breaker consecutive failure threshold is set to 30.")] @@ -485,8 +485,8 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccountA { // Arrange. Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, circuitBreakerConsecutiveFailureCount); - + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, circuitBreakerConsecutiveFailureCount); + // Enabling fault injection rule to simulate a 503 service unavailable scenario. string serviceUnavailableRuleId = "503-rule-" + Guid.NewGuid().ToString(); FaultInjectionRule serviceUnavailableRule = new FaultInjectionRuleBuilder( @@ -542,7 +542,7 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccountA ItemResponse readResponse = await container.ReadItemAsync( id: itemsList[0].Id, partitionKey: new PartitionKey(itemsList[0].Pk)); - + IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -597,14 +597,14 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccountA finally { Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); await this.TryDeleteItems(itemsList); } - } - + } + [TestMethod] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [DataRow(ConnectionMode.Direct, DisplayName ="Direct Mode")] [DataRow(ConnectionMode.Gateway, DisplayName = "Gateway Mode")] [Owner("nalutripician")] @@ -613,7 +613,7 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndTimeoutCounterOverwr ConnectionMode connectionMode) { // Arrange. - Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); + Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerTimeoutCounterResetWindowInMinutes, "0.0833"); // setting to 5 seconds // Enabling fault injection rule to simulate a 503 service unavailable scenario. @@ -659,9 +659,9 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndTimeoutCounterOverwr await this.TryCreateItems(itemsList); //Must Ensure the data is replicated to all regions - await Task.Delay(3000); - - int readErrorCount = 0; + await Task.Delay(3000); + + int readErrorCount = 0; PartitionKeyRangeFailoverInfo failoverInfo; for (int i = 1; i <= 3; i++) @@ -683,10 +683,10 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndTimeoutCounterOverwr failoverInfo = TestCommon.GetFailoverInfoForFirstPartitionUsingReflection( globalPartitionEndpointManager: cosmosClient.ClientContext.DocumentClient.PartitionKeyRangeLocation, - isReadOnlyOrMultiMaster: true); - + isReadOnlyOrMultiMaster: true); + failoverInfo.SnapshotConsecutiveRequestFailureCount(out readErrorCount, out _); - + Assert.IsTrue(readErrorCount > 0); } catch (CosmosException) @@ -697,40 +697,40 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndTimeoutCounterOverwr { Assert.Fail($"Unhandled Exception was thrown during ReadItemAsync call. Message: {ex.Message}"); } - } - - await Task.Delay(6000); // Wait for the timeout counter to reset - - try - { + } + + await Task.Delay(6000); // Wait for the timeout counter to reset + + try + { ItemResponse readResponse = await container.ReadItemAsync( id: itemsList[0].Id, - partitionKey: new PartitionKey(itemsList[0].Pk)); - } - catch (CosmosException) - { - Assert.Fail("Read Item operation should succeed after the timeout counter is overwritten."); - } - + partitionKey: new PartitionKey(itemsList[0].Pk)); + } + catch (CosmosException) + { + Assert.Fail("Read Item operation should succeed after the timeout counter is overwritten."); + } + failoverInfo = TestCommon.GetFailoverInfoForFirstPartitionUsingReflection( globalPartitionEndpointManager: cosmosClient.ClientContext.DocumentClient.PartitionKeyRangeLocation, - isReadOnlyOrMultiMaster: true); - - failoverInfo.SnapshotConsecutiveRequestFailureCount(out int currentReadErrorCount, out _); - - Assert.AreEqual(1, currentReadErrorCount, "The read error count should be reset after the timeout counter is overwritten. Then after one more failure it should be incremented by 1."); + isReadOnlyOrMultiMaster: true); + + failoverInfo.SnapshotConsecutiveRequestFailureCount(out int currentReadErrorCount, out _); + + Assert.AreEqual(1, currentReadErrorCount, "The read error count should be reset after the timeout counter is overwritten. Then after one more failure it should be incremented by 1."); Assert.IsTrue(readErrorCount > currentReadErrorCount, "The read error count should be greater than the current before the timeout counter is overwritten."); } finally { - Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); + Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerTimeoutCounterResetWindowInMinutes, null); await this.TryDeleteItems(itemsList); } } [TestMethod] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [Owner("dkunda")] [Timeout(70000)] public async Task ReadItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccountAndServiceUnavailableReceivedFromTwoRegions_ShouldApplyPartitionLevelOverrideToThridRegion() @@ -894,7 +894,7 @@ public async Task ReadItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccountA } [TestMethod] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [Owner("dkunda")] [Timeout(70000)] public async Task ReadItemAsync_WithNoPreferredRegionsAndCircuitBreakerEnabledAndSingleMasterAccountAndServiceUnavailableReceived_ShouldApplyPartitionLevelOverride() @@ -1012,7 +1012,7 @@ public async Task ReadItemAsync_WithNoPreferredRegionsAndCircuitBreakerEnabledAn [TestMethod] [Owner("dkunda")] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [Timeout(70000)] public async Task ReadItemAsync_WithCircuitBreakerDisabledAndSingleMasterAccountAndServiceUnavailableReceived_ShouldNotApplyPartitionLevelOverride() { @@ -1101,7 +1101,7 @@ public async Task ReadItemAsync_WithCircuitBreakerDisabledAndSingleMasterAccount } [TestMethod] - [Owner("dkunda")] + [Owner("dkunda")] [TestCategory("MultiRegion")] [Timeout(70000)] public async Task CreateItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccountAndServiceUnavailableReceived_ShouldNotApplyPartitionLevelOverride() @@ -1181,7 +1181,7 @@ public async Task CreateItemAsync_WithCircuitBreakerEnabledAndSingleMasterAccoun [TestMethod] [Owner("dkunda")] - [TestCategory("MultiMaster")] + [TestCategory("MultiMaster")] [DataRow(ConnectionMode.Direct, "15", "10", DisplayName = "Direct Mode - Scenario whtn the total iteration count is 15 and circuit breaker consecutive failure threshold is set to 10.")] [DataRow(ConnectionMode.Direct, "25", "20", DisplayName = "Direct Mode - Scenario whtn the total iteration count is 25 and circuit breaker consecutive failure threshold is set to 20.")] [DataRow(ConnectionMode.Direct, "35", "30", DisplayName = "Direct Mode - Scenario whtn the total iteration count is 35 and circuit breaker consecutive failure threshold is set to 30.")] @@ -1456,7 +1456,7 @@ public async Task CreateAndReadItemAsync_WithCircuitBreakerEnabledAndMultiMaster [TestMethod] [Owner("dkunda")] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [Timeout(70000)] [DataRow(true, DisplayName = "Test scenario when PPAF is enabled at client level.")] [DataRow(false, DisplayName = "Test scenario when PPAF is disabled at client level.")] @@ -1480,35 +1480,35 @@ public async Task ReadItemAsync_WithPPAFEnabledAndSingleMasterAccountWithRespons .Build(); List rules = new List { serviceUnavailableRule }; - FaultInjector faultInjector = new FaultInjector(rules); - - // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that - // the environment variable set above is honored. - HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() - { - ResponseIntercepter = async (response, request) => - { - string json = await response?.Content?.ReadAsStringAsync(); - if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) - { - JObject parsedDatabaseAccountResponse = JObject.Parse(json); - parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = enablePartitionLevelFailover.ToString(); - - HttpResponseMessage interceptedResponse = new() - { - StatusCode = response.StatusCode, - Content = new StringContent(parsedDatabaseAccountResponse.ToString()), - Version = response.Version, - ReasonPhrase = response.ReasonPhrase, - RequestMessage = response.RequestMessage, - }; - - return interceptedResponse; - } - - return response; - }, - }; + FaultInjector faultInjector = new FaultInjector(rules); + + // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that + // the environment variable set above is honored. + HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() + { + ResponseIntercepter = async (response, request) => + { + string json = await response?.Content?.ReadAsStringAsync(); + if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) + { + JObject parsedDatabaseAccountResponse = JObject.Parse(json); + parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = enablePartitionLevelFailover.ToString(); + + HttpResponseMessage interceptedResponse = new() + { + StatusCode = response.StatusCode, + Content = new StringContent(parsedDatabaseAccountResponse.ToString()), + Version = response.Version, + ReasonPhrase = response.ReasonPhrase, + RequestMessage = response.RequestMessage, + }; + + return interceptedResponse; + } + + return response; + }, + }; List preferredRegions = new List { region1, region2, region3 }; CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() @@ -1516,8 +1516,8 @@ public async Task ReadItemAsync_WithPPAFEnabledAndSingleMasterAccountWithRespons ConsistencyLevel = ConsistencyLevel.Session, FaultInjector = faultInjector, RequestTimeout = TimeSpan.FromSeconds(5), - ApplicationPreferredRegions = preferredRegions, - HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), + ApplicationPreferredRegions = preferredRegions, + HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), }; List itemsList = new() @@ -1539,7 +1539,7 @@ public async Task ReadItemAsync_WithPPAFEnabledAndSingleMasterAccountWithRespons ItemResponse readResponse = await container.ReadItemAsync( id: itemsList[0].Id, - partitionKey: new PartitionKey(itemsList[0].Pk)); + partitionKey: new PartitionKey(itemsList[0].Pk)); IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -1573,15 +1573,15 @@ public async Task ReadItemAsync_WithPPAFEnabledAndSingleMasterAccountWithRespons { await this.TryDeleteItems(itemsList); } - } - + } + [TestMethod] [Owner("ntripician")] - [TestCategory("MultiRegion")] - [Timeout(70000 *100)] + [TestCategory("MultiRegion")] + [Timeout(70000 *100)] [DataRow(ConnectionMode.Direct, false, DisplayName = "Test dynamic PPAF enablement with Direct mode.")] - public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPAFInSDK( - ConnectionMode connectionMode, + public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPAFInSDK( + ConnectionMode connectionMode, bool isThinClientEnabled) { // Arrange. @@ -1601,68 +1601,68 @@ public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPA .Build(); List rules = new List { serviceUnavailableRule }; - FaultInjector faultInjector = new FaultInjector(rules); - - bool enablePPAF = false; - - // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that - // the environment variable set above is honored. - HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() - { - ResponseIntercepter = async (response, request) => - { - string json = await response?.Content?.ReadAsStringAsync(); - if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) - { - if (enablePPAF) - { - JObject parsedDatabaseAccountResponse = JObject.Parse(json); - parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = true; - - HttpResponseMessage interceptedResponse = new() - { - StatusCode = response.StatusCode, - Content = new StringContent(parsedDatabaseAccountResponse.ToString()), - Version = response.Version, - ReasonPhrase = response.ReasonPhrase, - RequestMessage = response.RequestMessage, - }; - - return interceptedResponse; - } - else - { - JObject parsedDatabaseAccountResponse = JObject.Parse(json); - parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = false; - - HttpResponseMessage interceptedResponse = new() - { - StatusCode = response.StatusCode, - Content = new StringContent(parsedDatabaseAccountResponse.ToString()), - Version = response.Version, - ReasonPhrase = response.ReasonPhrase, - RequestMessage = response.RequestMessage, - }; - - return interceptedResponse; - } - - } - - return response; - }, - }; - + FaultInjector faultInjector = new FaultInjector(rules); + + bool enablePPAF = false; + + // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that + // the environment variable set above is honored. + HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() + { + ResponseIntercepter = async (response, request) => + { + string json = await response?.Content?.ReadAsStringAsync(); + if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) + { + if (enablePPAF) + { + JObject parsedDatabaseAccountResponse = JObject.Parse(json); + parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = true; + + HttpResponseMessage interceptedResponse = new() + { + StatusCode = response.StatusCode, + Content = new StringContent(parsedDatabaseAccountResponse.ToString()), + Version = response.Version, + ReasonPhrase = response.ReasonPhrase, + RequestMessage = response.RequestMessage, + }; + + return interceptedResponse; + } + else + { + JObject parsedDatabaseAccountResponse = JObject.Parse(json); + parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = false; + + HttpResponseMessage interceptedResponse = new() + { + StatusCode = response.StatusCode, + Content = new StringContent(parsedDatabaseAccountResponse.ToString()), + Version = response.Version, + ReasonPhrase = response.ReasonPhrase, + RequestMessage = response.RequestMessage, + }; + + return interceptedResponse; + } + + } + + return response; + }, + }; + List preferredRegions = new List { region1, region2, region3 }; CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() { ConsistencyLevel = ConsistencyLevel.Session, FaultInjector = faultInjector, RequestTimeout = TimeSpan.FromSeconds(5), - ApplicationPreferredRegions = preferredRegions, - HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), - ConnectionMode = connectionMode, - ApplicationName = "ppafDynamicOverrideTest", + ApplicationPreferredRegions = preferredRegions, + HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), + ConnectionMode = connectionMode, + ApplicationName = "ppafDynamicOverrideTest", }; List itemsList = new() @@ -1680,12 +1680,12 @@ public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPA await this.TryCreateItems(itemsList); //Must Ensure the data is replicated to all regions - await Task.Delay(3000); - + await Task.Delay(3000); + ItemResponse readResponse = await container.ReadItemAsync( id: itemsList[0].Id, - partitionKey: new PartitionKey(itemsList[0].Pk)); - + partitionKey: new PartitionKey(itemsList[0].Pk)); + IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -1696,22 +1696,22 @@ public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPA CosmosTraceDiagnostics traceDiagnostic = readResponse.Diagnostics as CosmosTraceDiagnostics; Assert.IsNotNull(traceDiagnostic); - traceDiagnostic.Value.Data.TryGetValue("Hedge Context", out object hedgeContextNoPPAF); - - Assert.IsNull(hedgeContextNoPPAF); - Assert.IsNull(cosmosClient.DocumentClient.ConnectionPolicy.AvailabilityStrategy); - Assert.IsFalse(cosmosClient.DocumentClient.PartitionKeyRangeLocation.IsPartitionLevelAutomaticFailoverEnabled()); - - // Enable PPAF At the Gateway Layer. - enablePPAF = true; - - //force database account refresh + traceDiagnostic.Value.Data.TryGetValue("Hedge Context", out object hedgeContextNoPPAF); + + Assert.IsNull(hedgeContextNoPPAF); + Assert.IsNull(cosmosClient.DocumentClient.ConnectionPolicy.AvailabilityStrategy); + Assert.IsFalse(cosmosClient.DocumentClient.PartitionKeyRangeLocation.IsPartitionLevelAutomaticFailoverEnabled()); + + // Enable PPAF At the Gateway Layer. + enablePPAF = true; + + //force database account refresh await cosmosClient.DocumentClient.GlobalEndpointManager.RefreshLocationAsync(true); readResponse = await container.ReadItemAsync( id: itemsList[0].Id, - partitionKey: new PartitionKey(itemsList[0].Pk)); - + partitionKey: new PartitionKey(itemsList[0].Pk)); + contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -1728,18 +1728,18 @@ public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPA List hedgedRegions = ((IEnumerable)hedgeContext).ToList(); Assert.IsTrue(hedgedRegions.Count >= 1, "Since the first region is not available, the request should atleast hedge to the next region."); - Assert.IsTrue(cosmosClient.DocumentClient.PartitionKeyRangeLocation.IsPartitionLevelAutomaticFailoverEnabled()); - + Assert.IsTrue(cosmosClient.DocumentClient.PartitionKeyRangeLocation.IsPartitionLevelAutomaticFailoverEnabled()); + // Disable PPAF At the Gateway Layer. - enablePPAF = false; - - //force database account refresh - await cosmosClient.DocumentClient.GlobalEndpointManager.RefreshLocationAsync(true); - + enablePPAF = false; + + //force database account refresh + await cosmosClient.DocumentClient.GlobalEndpointManager.RefreshLocationAsync(true); + readResponse = await container.ReadItemAsync( id: itemsList[0].Id, - partitionKey: new PartitionKey(itemsList[0].Pk)); - + partitionKey: new PartitionKey(itemsList[0].Pk)); + contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -1750,30 +1750,30 @@ public async Task ReadItemAsync_WithPPAFDynamicOverride_ShouldEnableOrDisablePPA traceDiagnostic = readResponse.Diagnostics as CosmosTraceDiagnostics; Assert.IsNotNull(traceDiagnostic); - traceDiagnostic.Value.Data.TryGetValue("Hedge Context", out object hedgeContextNoPPAF2); - - Assert.IsNull(hedgeContextNoPPAF2); - Assert.IsNull(cosmosClient.DocumentClient.ConnectionPolicy.AvailabilityStrategy); + traceDiagnostic.Value.Data.TryGetValue("Hedge Context", out object hedgeContextNoPPAF2); + + Assert.IsNull(hedgeContextNoPPAF2); + Assert.IsNull(cosmosClient.DocumentClient.ConnectionPolicy.AvailabilityStrategy); Assert.IsFalse(cosmosClient.DocumentClient.PartitionKeyRangeLocation.IsPartitionLevelAutomaticFailoverEnabled()); } finally { - await this.TryDeleteItems(itemsList); - - if (isThinClientEnabled) - { - Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, null); + await this.TryDeleteItems(itemsList); + + if (isThinClientEnabled) + { + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, null); } } - } - + } + [TestMethod] [Owner("nalutripician")] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [Timeout(70000)] [DataRow(true, DisplayName = "Test scenario when PPAF is enabled at client level.")] [DataRow(false, DisplayName = "Test scenario when PPAF is disabled at client level.")] - public async Task ReadItemAsync_WithPPAFDiableOverride( + public async Task ReadItemAsync_WithPPAFDiableOverride( bool enablePartitionLevelFailover) { // Arrange. @@ -1793,35 +1793,35 @@ public async Task ReadItemAsync_WithPPAFDiableOverride( .Build(); List rules = new List { serviceUnavailableRule }; - FaultInjector faultInjector = new FaultInjector(rules); - - // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that - // the environment variable set above is honored. - HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() - { - ResponseIntercepter = async (response, request) => - { - string json = await response?.Content?.ReadAsStringAsync(); - if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) - { - JObject parsedDatabaseAccountResponse = JObject.Parse(json); - parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = enablePartitionLevelFailover.ToString(); - - HttpResponseMessage interceptedResponse = new() - { - StatusCode = response.StatusCode, - Content = new StringContent(parsedDatabaseAccountResponse.ToString()), - Version = response.Version, - ReasonPhrase = response.ReasonPhrase, - RequestMessage = response.RequestMessage, - }; - - return interceptedResponse; - } - - return response; - }, - }; + FaultInjector faultInjector = new FaultInjector(rules); + + // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that + // the environment variable set above is honored. + HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() + { + ResponseIntercepter = async (response, request) => + { + string json = await response?.Content?.ReadAsStringAsync(); + if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) + { + JObject parsedDatabaseAccountResponse = JObject.Parse(json); + parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = enablePartitionLevelFailover.ToString(); + + HttpResponseMessage interceptedResponse = new() + { + StatusCode = response.StatusCode, + Content = new StringContent(parsedDatabaseAccountResponse.ToString()), + Version = response.Version, + ReasonPhrase = response.ReasonPhrase, + RequestMessage = response.RequestMessage, + }; + + return interceptedResponse; + } + + return response; + }, + }; List preferredRegions = new List { region1, region2, region3 }; CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() @@ -1829,9 +1829,9 @@ public async Task ReadItemAsync_WithPPAFDiableOverride( ConsistencyLevel = ConsistencyLevel.Session, FaultInjector = faultInjector, RequestTimeout = TimeSpan.FromSeconds(5), - ApplicationPreferredRegions = preferredRegions, - HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), - DisablePartitionLevelFailover = true, // This will disable the PPAF override for this test. + ApplicationPreferredRegions = preferredRegions, + HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), + DisablePartitionLevelFailover = true, // This will disable the PPAF override for this test. }; List itemsList = new() @@ -1853,7 +1853,7 @@ public async Task ReadItemAsync_WithPPAFDiableOverride( ItemResponse readResponse = await container.ReadItemAsync( id: itemsList[0].Id, - partitionKey: new PartitionKey(itemsList[0].Pk)); + partitionKey: new PartitionKey(itemsList[0].Pk)); IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -1876,76 +1876,76 @@ public async Task ReadItemAsync_WithPPAFDiableOverride( { await this.TryDeleteItems(itemsList); } - } - - [TestMethod] - [TestCategory("MultiRegion")] - [Owner("ntripician")] - public async Task AddressRefreshInternalServerErrorTest() - { - FaultInjectionRule internalServerError = new FaultInjectionRuleBuilder( - id: "rule1", - condition: new FaultInjectionConditionBuilder() - .WithOperationType(FaultInjectionOperationType.MetadataRefreshAddresses) - .WithRegion(region1) - .Build(), - result: - FaultInjectionResultBuilder.GetResultBuilder(FaultInjectionServerErrorType.InternalServerError) - .Build()) - .Build(); - - List rules = new List() { internalServerError }; - FaultInjector faultInjector = new FaultInjector(rules); - - internalServerError.Disable(); - - CosmosClientOptions clientOptions = new CosmosClientOptions() - { - ConnectionMode = ConnectionMode.Direct, - Serializer = this.cosmosSystemTextJsonSerializer, - ApplicationRegion = region1, - }; - - using (CosmosClient faultInjectionClient = new CosmosClient( - connectionString: this.connectionString, - clientOptions: faultInjector.GetFaultInjectionClientOptions(clientOptions))) - { - Database database = faultInjectionClient.GetDatabase(MultiRegionSetupHelpers.dbName); - Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); - - internalServerError.Enable(); - - try - { - ItemResponse response = await container.ReadItemAsync("testId", new PartitionKey("pk")); - Assert.IsTrue(internalServerError.GetHitCount() > 0); - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - } - catch (CosmosException ex) - { - Assert.Fail(ex.Message); - } - } } - + + [TestMethod] + [TestCategory("MultiRegion")] + [Owner("ntripician")] + public async Task AddressRefreshInternalServerErrorTest() + { + FaultInjectionRule internalServerError = new FaultInjectionRuleBuilder( + id: "rule1", + condition: new FaultInjectionConditionBuilder() + .WithOperationType(FaultInjectionOperationType.MetadataRefreshAddresses) + .WithRegion(region1) + .Build(), + result: + FaultInjectionResultBuilder.GetResultBuilder(FaultInjectionServerErrorType.InternalServerError) + .Build()) + .Build(); + + List rules = new List() { internalServerError }; + FaultInjector faultInjector = new FaultInjector(rules); + + internalServerError.Disable(); + + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + ConnectionMode = ConnectionMode.Direct, + Serializer = this.cosmosSystemTextJsonSerializer, + ApplicationRegion = region1, + }; + + using (CosmosClient faultInjectionClient = new CosmosClient( + connectionString: this.connectionString, + clientOptions: faultInjector.GetFaultInjectionClientOptions(clientOptions))) + { + Database database = faultInjectionClient.GetDatabase(MultiRegionSetupHelpers.dbName); + Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); + + internalServerError.Enable(); + + try + { + ItemResponse response = await container.ReadItemAsync("testId", new PartitionKey("pk")); + Assert.IsTrue(internalServerError.GetHitCount() > 0); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + catch (CosmosException ex) + { + Assert.Fail(ex.Message); + } + } + } + [TestMethod] [TestCategory("MultiRegion")] [Ignore("We will enable this test once the test staging account used for multi master validation starts supporting thin proxy.")] [DataRow(ConnectionMode.Gateway, "15", "10", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 15 and circuit breaker consecutive failure threshold is set to 10.")] [DataRow(ConnectionMode.Gateway, "25", "20", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 25 and circuit breaker consecutive failure threshold is set to 20.")] - [DataRow(ConnectionMode.Gateway, "35", "30", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 35 and circuit breaker consecutive failure threshold is set to 30.")] + [DataRow(ConnectionMode.Gateway, "35", "30", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 35 and circuit breaker consecutive failure threshold is set to 30.")] [Owner("dkunda")] [Timeout(70000)] public async Task ReadItemAsync_WithThinClientCircuitBreakerEnabledAndSingleMasterAccountAndServiceUnavailableReceived_ShouldApplyPartitionLevelOverride( ConnectionMode connectionMode, string iterationCount, string circuitBreakerConsecutiveFailureCount) - { - // Arrange. - Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "True"); + { + // Arrange. + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "True"); Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, circuitBreakerConsecutiveFailureCount); - + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, circuitBreakerConsecutiveFailureCount); + // Enabling fault injection rule to simulate a 503 service unavailable scenario. string serviceUnavailableRuleId = "503-rule-" + Guid.NewGuid().ToString(); FaultInjectionRule serviceUnavailableRule = new FaultInjectionRuleBuilder( @@ -1981,11 +1981,11 @@ public async Task ReadItemAsync_WithThinClientCircuitBreakerEnabledAndSingleMast try { - using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); - AccountProperties accountInfo = await cosmosClient.ReadAccountAsync(); - - Assert.IsTrue(cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints.Count() >= 2); - this.thinClientreadRegionalEndpoints = cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints; + using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); + AccountProperties accountInfo = await cosmosClient.ReadAccountAsync(); + + Assert.IsTrue(cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints.Count() >= 2); + this.thinClientreadRegionalEndpoints = cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints; Database database = cosmosClient.GetDatabase(MultiRegionSetupHelpers.dbName); Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); @@ -2006,7 +2006,7 @@ public async Task ReadItemAsync_WithThinClientCircuitBreakerEnabledAndSingleMast ItemResponse readResponse = await container.ReadItemAsync( id: itemsList[0].Id, partitionKey: new PartitionKey(itemsList[0].Pk)); - + IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = readResponse.Diagnostics.GetContactedRegions(); HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); @@ -2049,31 +2049,31 @@ public async Task ReadItemAsync_WithThinClientCircuitBreakerEnabledAndSingleMast finally { Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); - Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, null); + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, null); await this.TryDeleteItems(itemsList); } - } - + } + [TestMethod] - [TestCategory("MultiMaster")] + [TestCategory("MultiMaster")] [Ignore ("We will enable this test once the test staging account used for multi master validation starts supporting thin proxy.")] [DataRow(ConnectionMode.Gateway, "15", "10", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 15 and circuit breaker consecutive failure threshold is set to 10.")] [DataRow(ConnectionMode.Gateway, "25", "20", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 25 and circuit breaker consecutive failure threshold is set to 20.")] - [DataRow(ConnectionMode.Gateway, "35", "30", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 35 and circuit breaker consecutive failure threshold is set to 30.")] + [DataRow(ConnectionMode.Gateway, "35", "30", DisplayName = "Thin Client Mode - Scenario when the total iteration count is 35 and circuit breaker consecutive failure threshold is set to 30.")] [Owner("dkunda")] [Timeout(70000)] public async Task CreateItemAsync_WithThinClientEnabledAndCircuitBreakerEnabledAndMultiMasterAccountAndServiceUnavailableReceived_ShouldApplyPartitionLevelOverride( ConnectionMode connectionMode, string iterationCount, string circuitBreakerConsecutiveFailureCount) - { - // Arrange. - Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "True"); + { + // Arrange. + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "True"); Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, circuitBreakerConsecutiveFailureCount); - + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, circuitBreakerConsecutiveFailureCount); + // Enabling fault injection rule to simulate a 503 service unavailable scenario. string serviceUnavailableRuleId = "503-rule-" + Guid.NewGuid().ToString(); FaultInjectionRule serviceUnavailableRule = new FaultInjectionRuleBuilder( @@ -2090,9 +2090,9 @@ public async Task CreateItemAsync_WithThinClientEnabledAndCircuitBreakerEnabledA .Build(); List rules = new List { serviceUnavailableRule }; - FaultInjector faultInjector = new FaultInjector(rules); - - Random random = new(); + FaultInjector faultInjector = new FaultInjector(rules); + + Random random = new(); List itemsCleanupList = new(); List preferredRegions = new List { Regions.WestUS, Regions.EastAsia }; CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() @@ -2111,11 +2111,11 @@ public async Task CreateItemAsync_WithThinClientEnabledAndCircuitBreakerEnabledA try { - using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); - AccountProperties accountInfo = await cosmosClient.ReadAccountAsync(); - - Assert.IsTrue(cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints.Count() >= 2); - this.thinClientreadRegionalEndpoints = cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints; + using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); + AccountProperties accountInfo = await cosmosClient.ReadAccountAsync(); + + Assert.IsTrue(cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints.Count() >= 2); + this.thinClientreadRegionalEndpoints = cosmosClient.DocumentClient.GlobalEndpointManager.ThinClientReadEndpoints; Database database = cosmosClient.GetDatabase(MultiRegionSetupHelpers.dbName); Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); @@ -2188,202 +2188,292 @@ public async Task CreateItemAsync_WithThinClientEnabledAndCircuitBreakerEnabledA finally { Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); - Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, null); + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, null); await this.TryDeleteItems(itemsList); } - } - + } + [TestMethod] [Owner("ntripician")] - [TestCategory("MultiRegion")] + [TestCategory("MultiRegion")] [Timeout(70000)] public async Task ClinetOverrides0msRequestTimeoutValueForPPAF() { // Arrange. - - // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that - // the environment variable set above is honored. - HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() - { - ResponseIntercepter = async (response, request) => - { - string json = await response?.Content?.ReadAsStringAsync(); - if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) - { - JObject parsedDatabaseAccountResponse = JObject.Parse(json); - parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = "true"; - - HttpResponseMessage interceptedResponse = new() - { - StatusCode = response.StatusCode, - Content = new StringContent(parsedDatabaseAccountResponse.ToString()), - Version = response.Version, - ReasonPhrase = response.ReasonPhrase, - RequestMessage = response.RequestMessage, - }; - - return interceptedResponse; - } - - return response; - }, - }; + + // Now that the ppaf enablement flag is returned from gateway, we need to intercept the response and remove the flag from the response, so that + // the environment variable set above is honored. + HttpClientHandlerHelper httpClientHandlerHelper = new HttpClientHandlerHelper() + { + ResponseIntercepter = async (response, request) => + { + string json = await response?.Content?.ReadAsStringAsync(); + if (json.Length > 0 && json.Contains("enablePerPartitionFailoverBehavior")) + { + JObject parsedDatabaseAccountResponse = JObject.Parse(json); + parsedDatabaseAccountResponse.Property("enablePerPartitionFailoverBehavior").Value = "true"; + + HttpResponseMessage interceptedResponse = new() + { + StatusCode = response.StatusCode, + Content = new StringContent(parsedDatabaseAccountResponse.ToString()), + Version = response.Version, + ReasonPhrase = response.ReasonPhrase, + RequestMessage = response.RequestMessage, + }; + + return interceptedResponse; + } + + return response; + }, + }; List preferredRegions = new List { region1, region2, region3 }; CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() { ConsistencyLevel = ConsistencyLevel.Session, RequestTimeout = TimeSpan.FromSeconds(0), - ApplicationPreferredRegions = preferredRegions, - HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), + ApplicationPreferredRegions = preferredRegions, + HttpClientFactory = () => new HttpClient(httpClientHandlerHelper), }; using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); Database database = cosmosClient.GetDatabase(MultiRegionSetupHelpers.dbName); - Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); - - try - { - //request to start document client initiation - _ = await container.ReadItemAsync("id", new PartitionKey("pk1")); - } + Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); + + try + { + //request to start document client initiation + _ = await container.ReadItemAsync("id", new PartitionKey("pk1")); + } catch { } - // Act and Assert. - - CrossRegionHedgingAvailabilityStrategy strat = cosmosClient.DocumentClient.ConnectionPolicy.AvailabilityStrategy as CrossRegionHedgingAvailabilityStrategy; - Assert.IsNotNull(strat); + // Act and Assert. + + CrossRegionHedgingAvailabilityStrategy strat = cosmosClient.DocumentClient.ConnectionPolicy.AvailabilityStrategy as CrossRegionHedgingAvailabilityStrategy; + Assert.IsNotNull(strat); Assert.AreNotEqual(0, strat.Threshold); - } - - [TestMethod] - [TestCategory("MultiRegion")] - [Owner("pkolluri")] - [Timeout(70000)] - public async Task QueryItemAsync_WithCircuitBreakerEnabledMultiRegionAndServiceResponseDelay_ShouldFailOverToNextRegionAsync() - { - // Arrange. - Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, "1"); - - // Enabling fault injection rule to simulate a 503 service unavailable scenario. - string serviceResponseDelayRuleId = "response-delay-rule-" + Guid.NewGuid().ToString(); - FaultInjectionRule serviceResponseDelayRuleFromRegion1 = new FaultInjectionRuleBuilder( - id: serviceResponseDelayRuleId, - condition: - new FaultInjectionConditionBuilder() - .WithOperationType(FaultInjectionOperationType.QueryItem) - .WithConnectionType(FaultInjectionConnectionType.Gateway) - .WithRegion(region1) - .Build(), - result: - FaultInjectionResultBuilder.GetResultBuilder(FaultInjectionServerErrorType.ResponseDelay) - .WithDelay(TimeSpan.FromSeconds(70)) - .Build()) - .Build(); - - serviceResponseDelayRuleFromRegion1.Disable(); - - List rules = new List { serviceResponseDelayRuleFromRegion1}; - FaultInjector faultInjector = new FaultInjector(rules); - - List preferredRegions = new List { region1, region2, region3 }; - CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() - { - ConsistencyLevel = ConsistencyLevel.Session, - FaultInjector = faultInjector, - ApplicationPreferredRegions = preferredRegions, - ConnectionMode = ConnectionMode.Gateway, - }; - - List itemsList = new() - { - new() { Id = "smTestId2", Pk = "smpk1" }, - }; - - try - { - using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); - Database database = cosmosClient.GetDatabase(MultiRegionSetupHelpers.dbName); - Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); - - // Act and Assert. - await this.TryCreateItems(itemsList); - - //Must Ensure the data is replicated to all regions - await Task.Delay(3000); - - bool isRegion1Available = true; - bool isRegion2Available = true; - - int thresholdCounter = 0; - int totalIterations = 7; - int ppcbDefaultThreshold = 1; - int firstRegionServiceUnavailableAttempt = 1; - - for (int attemptCount = 1; attemptCount <= totalIterations; attemptCount++) - { - try - { - string sqlQueryText = $"SELECT * FROM c WHERE c.id = '{itemsList[0].Id}'"; - using FeedIterator feedIterator = container.GetItemQueryIterator(sqlQueryText, requestOptions: new QueryRequestOptions()); - - while (feedIterator.HasMoreResults) - { - FeedResponse response = await feedIterator.ReadNextAsync(); - Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode); - IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = response.Diagnostics.GetContactedRegions(); - HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); - - if (isRegion1Available && isRegion2Available) - { - Assert.IsTrue(contactedRegions.Count == 1, "Assert that, when no failure happened, the query request is being served from region 1."); - Assert.IsTrue(contactedRegions.Contains(region1)); - - // Simulating service unavailable on region 1. - if (attemptCount == firstRegionServiceUnavailableAttempt) - { - isRegion1Available = false; - serviceResponseDelayRuleFromRegion1.Enable(); - } - } - else if (isRegion2Available) - { - if (thresholdCounter <= ppcbDefaultThreshold) - { - Assert.IsTrue(contactedRegions.Count == 2, "Asserting that when the query request succeeds before the consecutive failure count reaches the threshold, the partition didn't fail over to the next region, and the request was retried."); - Assert.IsTrue(contactedRegions.Contains(region1) && contactedRegions.Contains(region2), "Asserting that both region 1 and region 2 were contacted."); - thresholdCounter++; - } - else - { - Assert.IsTrue(contactedRegions.Count == 1, "Asserting that when the consecutive failure count reaches the threshold, the partition was failed over to the next region, and the subsequent query request/s were successful on the next region"); - } - } - } - } - catch (CosmosException ce) - { - Assert.Fail("Query operation should succeed with successful failover to next region." + ce.Diagnostics.ToString()); - } - catch (Exception ex) - { - Assert.Fail($"Unhandled Exception was thrown during Query operation call. Message: {ex.Message}"); - } - } - } - finally - { - Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); - Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); - - await this.TryDeleteItems(itemsList); - } - } - + } + + + [TestMethod] + [TestCategory("MultiRegion")] + [Owner("trivediyash")] + [Description("Scenario: When a document is created, then updated, and finally deleted, the operations must reflect on Change Feed.")] + public async Task WhenADocumentIsCreatedThenUpdatedThenDeletedCFPTests() + { + string testId = "testDoc" + Guid.NewGuid().ToString("N"); + string testPk = "testPk" + Guid.NewGuid().ToString("N"); + + try + { + // Create the document + CosmosIntegrationTestObject createItem = new CosmosIntegrationTestObject + { + Id = testId, + Pk = testPk, + Other = "original test" + }; + + ItemResponse createResponse = await this.container.CreateItemAsync( + createItem, + new PartitionKey(testPk)); + + Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); + Assert.IsNotNull(createResponse.Resource); + Assert.AreEqual(testId, createResponse.Resource.Id); + Assert.AreEqual(testPk, createResponse.Resource.Pk); + Assert.AreEqual("original test", createResponse.Resource.Other); + + // Wait 1 second to ensure different timestamps + await Task.Delay(1000); + + // Update the document + CosmosIntegrationTestObject updateItem = new CosmosIntegrationTestObject + { + Id = testId, + Pk = testPk, + Other = "test after replace" + }; + + ItemResponse updateResponse = await this.container.ReplaceItemAsync( + updateItem, + testId, + new PartitionKey(testPk)); + + Assert.AreEqual(HttpStatusCode.OK, updateResponse.StatusCode); + Assert.IsNotNull(updateResponse.Resource); + Assert.AreEqual(testId, updateResponse.Resource.Id); + Assert.AreEqual(testPk, updateResponse.Resource.Pk); + Assert.AreEqual("test after replace", updateResponse.Resource.Other); + + // Verify the ETag changed + Assert.AreNotEqual(createResponse.ETag, updateResponse.ETag); + + // Wait 1 second to ensure different timestamps + await Task.Delay(1000); + + // Delete the document + ItemResponse deleteResponse = await this.container.DeleteItemAsync( + testId, + new PartitionKey(testPk)); + + Assert.AreEqual(HttpStatusCode.NoContent, deleteResponse.StatusCode); + + // Verify the document no longer exists + try + { + await this.container.ReadItemAsync(testId, new PartitionKey(testPk)); + Assert.Fail("Document should not exist after deletion"); + } + catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + // Expected - document was successfully deleted + } + } + finally + { + // Cleanup in case test failed before deletion + try + { + await this.container.DeleteItemAsync(testId, new PartitionKey(testPk)); + } + catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + // Ignore - document already deleted + } + } + } + + [TestMethod] + [TestCategory("MultiRegion")] + [Owner("pkolluri")] + [Timeout(70000)] + public async Task QueryItemAsync_WithCircuitBreakerEnabledMultiRegionAndServiceResponseDelay_ShouldFailOverToNextRegionAsync() + { + // Arrange. + Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, "True"); + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, "1"); + + // Enabling fault injection rule to simulate a 503 service unavailable scenario. + string serviceResponseDelayRuleId = "response-delay-rule-" + Guid.NewGuid().ToString(); + FaultInjectionRule serviceResponseDelayRuleFromRegion1 = new FaultInjectionRuleBuilder( + id: serviceResponseDelayRuleId, + condition: + new FaultInjectionConditionBuilder() + .WithOperationType(FaultInjectionOperationType.QueryItem) + .WithConnectionType(FaultInjectionConnectionType.Gateway) + .WithRegion(region1) + .Build(), + result: + FaultInjectionResultBuilder.GetResultBuilder(FaultInjectionServerErrorType.ResponseDelay) + .WithDelay(TimeSpan.FromSeconds(70)) + .Build()) + .Build(); + + serviceResponseDelayRuleFromRegion1.Disable(); + + List rules = new List { serviceResponseDelayRuleFromRegion1}; + FaultInjector faultInjector = new FaultInjector(rules); + + List preferredRegions = new List { region1, region2, region3 }; + CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() + { + ConsistencyLevel = ConsistencyLevel.Session, + FaultInjector = faultInjector, + ApplicationPreferredRegions = preferredRegions, + ConnectionMode = ConnectionMode.Gateway, + }; + + List itemsList = new() + { + new() { Id = "smTestId2", Pk = "smpk1" }, + }; + + try + { + using CosmosClient cosmosClient = new(connectionString: this.connectionString, clientOptions: cosmosClientOptions); + Database database = cosmosClient.GetDatabase(MultiRegionSetupHelpers.dbName); + Container container = database.GetContainer(MultiRegionSetupHelpers.containerName); + + // Act and Assert. + await this.TryCreateItems(itemsList); + + //Must Ensure the data is replicated to all regions + await Task.Delay(3000); + + bool isRegion1Available = true; + bool isRegion2Available = true; + + int thresholdCounter = 0; + int totalIterations = 7; + int ppcbDefaultThreshold = 1; + int firstRegionServiceUnavailableAttempt = 1; + + for (int attemptCount = 1; attemptCount <= totalIterations; attemptCount++) + { + try + { + string sqlQueryText = $"SELECT * FROM c WHERE c.id = '{itemsList[0].Id}'"; + using FeedIterator feedIterator = container.GetItemQueryIterator(sqlQueryText, requestOptions: new QueryRequestOptions()); + + while (feedIterator.HasMoreResults) + { + FeedResponse response = await feedIterator.ReadNextAsync(); + Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode); + IReadOnlyList<(string regionName, Uri uri)> contactedRegionMapping = response.Diagnostics.GetContactedRegions(); + HashSet contactedRegions = new(contactedRegionMapping.Select(r => r.regionName)); + + if (isRegion1Available && isRegion2Available) + { + Assert.IsTrue(contactedRegions.Count == 1, "Assert that, when no failure happened, the query request is being served from region 1."); + Assert.IsTrue(contactedRegions.Contains(region1)); + + // Simulating service unavailable on region 1. + if (attemptCount == firstRegionServiceUnavailableAttempt) + { + isRegion1Available = false; + serviceResponseDelayRuleFromRegion1.Enable(); + } + } + else if (isRegion2Available) + { + if (thresholdCounter <= ppcbDefaultThreshold) + { + Assert.IsTrue(contactedRegions.Count == 2, "Asserting that when the query request succeeds before the consecutive failure count reaches the threshold, the partition didn't fail over to the next region, and the request was retried."); + Assert.IsTrue(contactedRegions.Contains(region1) && contactedRegions.Contains(region2), "Asserting that both region 1 and region 2 were contacted."); + thresholdCounter++; + } + else + { + Assert.IsTrue(contactedRegions.Count == 1, "Asserting that when the consecutive failure count reaches the threshold, the partition was failed over to the next region, and the subsequent query request/s were successful on the next region"); + } + } + } + } + catch (CosmosException ce) + { + Assert.Fail("Query operation should succeed with successful failover to next region." + ce.Diagnostics.ToString()); + } + catch (Exception ex) + { + Assert.Fail($"Unhandled Exception was thrown during Query operation call. Message: {ex.Message}"); + } + } + } + finally + { + Environment.SetEnvironmentVariable(ConfigurationManager.PartitionLevelCircuitBreakerEnabled, null); + Environment.SetEnvironmentVariable(ConfigurationManager.CircuitBreakerConsecutiveFailureCountForReads, null); + + await this.TryDeleteItems(itemsList); + } + } + private async Task TryCreateItems(List testItems) { foreach (CosmosIntegrationTestObject item in testItems) @@ -2424,41 +2514,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; } + } + + 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.EmulatorTests/FeedToken/ChangeFeedIteratorCoreTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/FeedToken/ChangeFeedIteratorCoreTests.cs index a43d86faf5..ca679ab6a8 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/FeedToken/ChangeFeedIteratorCoreTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/FeedToken/ChangeFeedIteratorCoreTests.cs @@ -896,6 +896,7 @@ private async Task ValidateChangeFeedIteratorCore_WithQuery( foreach (ChangeFeedItem item in feedResponse) { + Assert.AreEqual(expected: "id3", actual: item.Metadata.Id.ToString()); Assert.AreEqual("id3", item.Previous.Id); Assert.AreEqual(ChangeFeedOperationType.Delete, item.Metadata.OperationType); } @@ -1094,6 +1095,7 @@ public async Task ChangeFeedIteratorCore_FeedRange_VerifyingWireFormatTests() Assert.AreNotEqual(notExpected: default, actual: deleteOperation.Metadata.Lsn); Assert.AreNotEqual(notExpected: default, actual: deleteOperation.Metadata.PreviousLsn); Assert.IsNotNull(deleteOperation.Previous); + Assert.AreEqual(expected: id, actual: deleteOperation.Metadata.Id.ToString()); Assert.AreEqual(expected: id, actual: deleteOperation.Previous.Id); Assert.AreEqual(expected: "205 16th St NW", actual: deleteOperation.Previous.Line1); Assert.AreEqual(expected: "Atlanta", actual: deleteOperation.Previous.City); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json index 8421fc5040..228784345a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json @@ -87,10 +87,12 @@ ], "MethodInfo": "Boolean get_IsTimeToLiveExpired();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Boolean IsTimeToLiveExpired[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"timeToLiveExpired\")]": { + "Boolean IsTimeToLiveExpired[System.Text.Json.Serialization.JsonIncludeAttribute()]-[System.Text.Json.Serialization.JsonPropertyNameAttribute(\"timeToLiveExpired\")]-[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"timeToLiveExpired\")]": { "Type": "Property", "Attributes": [ - "JsonPropertyAttribute" + "JsonIncludeAttribute", + "JsonPropertyAttribute", + "JsonPropertyNameAttribute" ], "MethodInfo": "Boolean IsTimeToLiveExpired;CanRead:True;CanWrite:True;Boolean get_IsTimeToLiveExpired();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, @@ -108,17 +110,21 @@ ], "MethodInfo": "Int64 get_PreviousLsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Int64 Lsn[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"lsn\")]": { + "Int64 Lsn[System.Text.Json.Serialization.JsonIncludeAttribute()]-[System.Text.Json.Serialization.JsonPropertyNameAttribute(\"lsn\")]-[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"lsn\")]": { "Type": "Property", "Attributes": [ - "JsonPropertyAttribute" + "JsonIncludeAttribute", + "JsonPropertyAttribute", + "JsonPropertyNameAttribute" ], "MethodInfo": "Int64 Lsn;CanRead:True;CanWrite:True;Int64 get_Lsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Int64 PreviousLsn[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"previousImageLSN\")]": { + "Int64 PreviousLsn[System.Text.Json.Serialization.JsonIncludeAttribute()]-[System.Text.Json.Serialization.JsonPropertyNameAttribute(\"previousImageLSN\")]-[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"previousImageLSN\")]": { "Type": "Property", "Attributes": [ - "JsonPropertyAttribute" + "JsonIncludeAttribute", + "JsonPropertyAttribute", + "JsonPropertyNameAttribute" ], "MethodInfo": "Int64 PreviousLsn;CanRead:True;CanWrite:True;Int64 get_PreviousLsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, @@ -129,28 +135,61 @@ ], "MethodInfo": "Microsoft.Azure.Cosmos.ChangeFeedOperationType get_OperationType();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Microsoft.Azure.Cosmos.ChangeFeedOperationType OperationType[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"operationType\")]-[Newtonsoft.Json.JsonConverterAttribute(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]": { + "Microsoft.Azure.Cosmos.ChangeFeedOperationType OperationType[Newtonsoft.Json.JsonConverterAttribute(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]-[System.Text.Json.Serialization.JsonIncludeAttribute()]-[System.Text.Json.Serialization.JsonPropertyNameAttribute(\"operationType\")]-[System.Text.Json.Serialization.JsonConverterAttribute(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]-[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"operationType\")]": { "Type": "Property", "Attributes": [ "JsonConverterAttribute", - "JsonPropertyAttribute" + "JsonConverterAttribute", + "JsonIncludeAttribute", + "JsonPropertyAttribute", + "JsonPropertyNameAttribute" ], "MethodInfo": "Microsoft.Azure.Cosmos.ChangeFeedOperationType OperationType;CanRead:True;CanWrite:True;Microsoft.Azure.Cosmos.ChangeFeedOperationType get_OperationType();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "System.DateTime ConflictResolutionTimestamp[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"crts\")]-[Newtonsoft.Json.JsonConverterAttribute(typeof(Microsoft.Azure.Documents.UnixDateTimeConverter))]": { + "System.Collections.Generic.Dictionary`2[System.String,System.Object] get_PartitionKey()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "System.Collections.Generic.Dictionary`2[System.String,System.Object] get_PartitionKey();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Collections.Generic.Dictionary`2[System.String,System.Object] PartitionKey[System.Text.Json.Serialization.JsonIncludeAttribute()]-[System.Text.Json.Serialization.JsonPropertyNameAttribute(\"partitionKey\")]-[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"partitionKey\")]": { "Type": "Property", "Attributes": [ - "JsonConverterAttribute", - "JsonPropertyAttribute" + "JsonIncludeAttribute", + "JsonPropertyAttribute", + "JsonPropertyNameAttribute" + ], + "MethodInfo": "System.Collections.Generic.Dictionary`2[System.String,System.Object] PartitionKey;CanRead:True;CanWrite:True;System.Collections.Generic.Dictionary`2[System.String,System.Object] get_PartitionKey();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Nullable`1[System.DateTime] ConflictResolutionTimestamp[System.Text.Json.Serialization.JsonIgnoreAttribute()]-[Newtonsoft.Json.JsonIgnoreAttribute()]": { + "Type": "Property", + "Attributes": [ + "JsonIgnoreAttribute", + "JsonIgnoreAttribute" ], - "MethodInfo": "System.DateTime ConflictResolutionTimestamp;CanRead:True;CanWrite:True;System.DateTime get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "System.Nullable`1[System.DateTime] ConflictResolutionTimestamp;CanRead:True;CanWrite:False;System.Nullable`1[System.DateTime] get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "System.DateTime get_ConflictResolutionTimestamp()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "System.Nullable`1[System.DateTime] get_ConflictResolutionTimestamp()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.Nullable`1[System.DateTime] get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.String get_Id()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", "Attributes": [ "CompilerGeneratedAttribute" ], - "MethodInfo": "System.DateTime get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "System.String get_Id();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.String Id[System.Text.Json.Serialization.JsonIncludeAttribute()]-[System.Text.Json.Serialization.JsonPropertyNameAttribute(\"id\")]-[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = NullValueHandling.Ignore = 1, PropertyName = \"id\")]": { + "Type": "Property", + "Attributes": [ + "JsonIncludeAttribute", + "JsonPropertyAttribute", + "JsonPropertyNameAttribute" + ], + "MethodInfo": "System.String Id;CanRead:True;CanWrite:True;System.String get_Id();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "Void .ctor()": { "Type": "Constructor",