diff --git a/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs b/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs index 15e2189629..b27d5a8f7d 100644 --- a/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs +++ b/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs @@ -142,13 +142,56 @@ public override async Task> ExecuteItemQueryAsync( ITrace trace, CancellationToken cancellationToken) { - requestOptions.MaxItemCount = pageSize; + // Create a copy of the requestOptions to avoid modifying the original object + // that users might be caching. This fixes issue #5225. + QueryRequestOptions requestOptionsCopy = new QueryRequestOptions + { + ResponseContinuationTokenLimitInKb = requestOptions.ResponseContinuationTokenLimitInKb, + EnableScanInQuery = requestOptions.EnableScanInQuery, + EnableLowPrecisionOrderBy = requestOptions.EnableLowPrecisionOrderBy, + EnableOptimisticDirectExecution = requestOptions.EnableOptimisticDirectExecution, + MaxBufferedItemCount = requestOptions.MaxBufferedItemCount, + MaxItemCount = requestOptions.MaxItemCount, + MaxConcurrency = requestOptions.MaxConcurrency, + PartitionKey = requestOptions.PartitionKey, + PopulateIndexMetrics = requestOptions.PopulateIndexMetrics, + PopulateQueryAdvice = requestOptions.PopulateQueryAdvice, + ConsistencyLevel = requestOptions.ConsistencyLevel, + SessionToken = requestOptions.SessionToken, + DedicatedGatewayRequestOptions = requestOptions.DedicatedGatewayRequestOptions, + QueryTextMode = requestOptions.QueryTextMode, + CosmosElementContinuationToken = requestOptions.CosmosElementContinuationToken, + StartId = requestOptions.StartId, + EndId = requestOptions.EndId, + EnumerationDirection = requestOptions.EnumerationDirection, + CosmosSerializationFormatOptions = requestOptions.CosmosSerializationFormatOptions, + SupportedSerializationFormats = requestOptions.SupportedSerializationFormats, + ReturnResultsInDeterministicOrder = requestOptions.ReturnResultsInDeterministicOrder, + TestSettings = requestOptions.TestSettings, + FeedRange = requestOptions.FeedRange, + IsHybridSearchQueryPlanOptimizationDisabled = requestOptions.IsHybridSearchQueryPlanOptimizationDisabled, + EnableDistributedQueryGatewayMode = requestOptions.EnableDistributedQueryGatewayMode, + // Base RequestOptions properties + IfMatchEtag = requestOptions.IfMatchEtag, + IfNoneMatchEtag = requestOptions.IfNoneMatchEtag, + Properties = requestOptions.Properties, + AddRequestHeaders = requestOptions.AddRequestHeaders, + PriorityLevel = requestOptions.PriorityLevel, + CosmosThresholdOptions = requestOptions.CosmosThresholdOptions, + ExcludeRegions = requestOptions.ExcludeRegions, + AvailabilityStrategy = requestOptions.AvailabilityStrategy, + IsEffectivePartitionKeyRouting = requestOptions.IsEffectivePartitionKeyRouting, + BaseConsistencyLevel = requestOptions.BaseConsistencyLevel + }; + + // Now modify the copy instead of the original + requestOptionsCopy.MaxItemCount = pageSize; ResponseMessage message = await this.clientContext.ProcessResourceOperationStreamAsync( resourceUri: resourceUri, resourceType: resourceType, operationType: operationType, - requestOptions: requestOptions, + requestOptions: requestOptionsCopy, feedRange: feedRange, cosmosContainerCore: this.cosmosContainerCore, streamPayload: this.clientContext.SerializerCore.ToStreamSqlQuerySpec(sqlQuerySpec, resourceType), diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryRequestOptionsUniTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryRequestOptionsUniTests.cs index 0aa7e1e268..6a84702653 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryRequestOptionsUniTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryRequestOptionsUniTests.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos { using System; using System.Collections.Generic; + using System.IO; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,5 +23,78 @@ public void StatelessTest() Assert.IsNull(testMessage.Headers.ContinuationToken); } + + [TestMethod] + public void MaxItemCountNotModifiedInOriginalQueryRequestOptions_SimpleCopyTest() + { + // This test verifies that when QueryRequestOptions properties are copied, + // the original object is not modified. This reproduces the issue described in GitHub issue #5225. + + QueryRequestOptions originalOptions = new QueryRequestOptions + { + MaxItemCount = -1, + MaxConcurrency = 10, + EnableScanInQuery = true, + SessionToken = "test-session-token" + }; + + int originalMaxItemCount = originalOptions.MaxItemCount.Value; + int? originalMaxConcurrency = originalOptions.MaxConcurrency; + bool? originalEnableScanInQuery = originalOptions.EnableScanInQuery; + string originalSessionToken = originalOptions.SessionToken; + + // Simulate the copy logic that would happen in CosmosQueryClientCore.ExecuteItemQueryAsync + QueryRequestOptions requestOptionsCopy = new QueryRequestOptions + { + ResponseContinuationTokenLimitInKb = originalOptions.ResponseContinuationTokenLimitInKb, + EnableScanInQuery = originalOptions.EnableScanInQuery, + EnableLowPrecisionOrderBy = originalOptions.EnableLowPrecisionOrderBy, + EnableOptimisticDirectExecution = originalOptions.EnableOptimisticDirectExecution, + MaxBufferedItemCount = originalOptions.MaxBufferedItemCount, + MaxItemCount = originalOptions.MaxItemCount, + MaxConcurrency = originalOptions.MaxConcurrency, + PartitionKey = originalOptions.PartitionKey, + PopulateIndexMetrics = originalOptions.PopulateIndexMetrics, + PopulateQueryAdvice = originalOptions.PopulateQueryAdvice, + ConsistencyLevel = originalOptions.ConsistencyLevel, + SessionToken = originalOptions.SessionToken, + DedicatedGatewayRequestOptions = originalOptions.DedicatedGatewayRequestOptions, + QueryTextMode = originalOptions.QueryTextMode, + // Base RequestOptions properties + IfMatchEtag = originalOptions.IfMatchEtag, + IfNoneMatchEtag = originalOptions.IfNoneMatchEtag, + Properties = originalOptions.Properties, + AddRequestHeaders = originalOptions.AddRequestHeaders, + PriorityLevel = originalOptions.PriorityLevel, + CosmosThresholdOptions = originalOptions.CosmosThresholdOptions, + ExcludeRegions = originalOptions.ExcludeRegions + }; + + // Simulate the modification that would happen in ExecuteItemQueryAsync + int pageSize = 5; + requestOptionsCopy.MaxItemCount = pageSize; + + // Assert: The original QueryRequestOptions should NOT be modified + Assert.AreEqual(originalMaxItemCount, originalOptions.MaxItemCount.Value, + "Original QueryRequestOptions.MaxItemCount should not be modified"); + Assert.AreEqual(originalMaxConcurrency, originalOptions.MaxConcurrency, + "Original QueryRequestOptions.MaxConcurrency should not be modified"); + Assert.AreEqual(originalEnableScanInQuery, originalOptions.EnableScanInQuery, + "Original QueryRequestOptions.EnableScanInQuery should not be modified"); + Assert.AreEqual(originalSessionToken, originalOptions.SessionToken, + "Original QueryRequestOptions.SessionToken should not be modified"); + + // Assert: The copy should have the new MaxItemCount + Assert.AreEqual(pageSize, requestOptionsCopy.MaxItemCount.Value, + "Copied QueryRequestOptions.MaxItemCount should be updated to pageSize"); + + // Assert: Other properties should be preserved in the copy + Assert.AreEqual(originalMaxConcurrency, requestOptionsCopy.MaxConcurrency, + "Copied QueryRequestOptions.MaxConcurrency should match original"); + Assert.AreEqual(originalEnableScanInQuery, requestOptionsCopy.EnableScanInQuery, + "Copied QueryRequestOptions.EnableScanInQuery should match original"); + Assert.AreEqual(originalSessionToken, requestOptionsCopy.SessionToken, + "Copied QueryRequestOptions.SessionToken should match original"); + } } } \ No newline at end of file