From 0490463d7c593ab958bb2b4faac8c9228367c40b Mon Sep 17 00:00:00 2001 From: Anuj Toshniwal Date: Fri, 16 Apr 2021 12:52:13 +0530 Subject: [PATCH 1/2] Expose the TrySerializeValueParameter method for PatchOperation so that it can be consumed in Encryption package (and others). --- .../src/Patch/PatchOperation.cs | 12 ++++++++-- .../src/Patch/PatchOperationCore{T}.cs | 14 ++++-------- .../src/Patch/PatchOperationsJsonConverter.cs | 12 +++++++++- .../Contracts/DotNetPreviewSDKAPI.json | 5 +++++ .../PatchOperationTests.cs | 22 ++++++++++++++----- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Patch/PatchOperation.cs b/Microsoft.Azure.Cosmos/src/Patch/PatchOperation.cs index 78c9adb872..cba61fb4a5 100644 --- a/Microsoft.Azure.Cosmos/src/Patch/PatchOperation.cs +++ b/Microsoft.Azure.Cosmos/src/Patch/PatchOperation.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.Cosmos { using System; + using System.IO; using Newtonsoft.Json; /// @@ -29,9 +30,16 @@ abstract class PatchOperation [JsonProperty(PropertyName = PatchConstants.PropertyNames.Path)] public abstract string Path { get; } - internal virtual bool TrySerializeValueParameter( + /// + /// Serializes the value parameter, if specified for the PatchOperation. + /// + /// Serializer to be used. + /// Outputs the serialized stream if value parameter is specified, null otherwise. + /// True if value is serialized, false otherwise. + /// Output stream should be disposed after use. + public virtual bool TrySerializeValueParameter( CosmosSerializer cosmosSerializer, - out string valueParam) + out Stream valueParam) { valueParam = null; return false; diff --git a/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs b/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs index 3c3e9f5f67..157c092bd1 100644 --- a/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs +++ b/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs @@ -33,20 +33,14 @@ public PatchOperationCore( public override string Path { get; } - internal override bool TrySerializeValueParameter( + public override bool TrySerializeValueParameter( CosmosSerializer cosmosSerializer, - out string valueParam) + out Stream valueParam) { // Use the user serializer so custom conversions are correctly handled - using (Stream stream = cosmosSerializer.ToStream(this.Value)) - { - using (StreamReader streamReader = new StreamReader(stream)) - { - valueParam = streamReader.ReadToEnd(); - } - } + valueParam = cosmosSerializer.ToStream(this.Value); return true; } } -} +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Patch/PatchOperationsJsonConverter.cs b/Microsoft.Azure.Cosmos/src/Patch/PatchOperationsJsonConverter.cs index be07b3922d..1e943409e4 100644 --- a/Microsoft.Azure.Cosmos/src/Patch/PatchOperationsJsonConverter.cs +++ b/Microsoft.Azure.Cosmos/src/Patch/PatchOperationsJsonConverter.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos { using System; using System.Collections.Generic; + using System.IO; using Newtonsoft.Json; /// @@ -87,8 +88,17 @@ public override void WriteJson( writer.WritePropertyName(PatchConstants.PropertyNames.Path); writer.WriteValue(operation.Path); - if (operation.TrySerializeValueParameter(this.userSerializer, out string valueParam)) + if (operation.TrySerializeValueParameter(this.userSerializer, out Stream valueStream)) { + string valueParam; + using (valueStream) + { + using (StreamReader streamReader = new StreamReader(valueStream)) + { + valueParam = streamReader.ReadToEnd(); + } + } + writer.WritePropertyName(PatchConstants.PropertyNames.Value); writer.WriteRawValue(valueParam); } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index 40d30a5f92..14802ea1dd 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -1340,6 +1340,11 @@ } }, "Members": { + "Boolean TrySerializeValueParameter(Microsoft.Azure.Cosmos.CosmosSerializer, System.IO.Stream ByRef)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Boolean TrySerializeValueParameter(Microsoft.Azure.Cosmos.CosmosSerializer, System.IO.Stream ByRef);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Microsoft.Azure.Cosmos.PatchOperation Add[T](System.String, T)": { "Type": "Method", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/PatchOperationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/PatchOperationTests.cs index 0736ad490a..913c8c86e7 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/PatchOperationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/PatchOperationTests.cs @@ -72,14 +72,26 @@ private static void ValidateOperations(PatchOperation patchOperation, PatchOp { string expected; CosmosSerializer cosmosSerializer = new CosmosJsonDotNetSerializer(); - Stream stream = cosmosSerializer.ToStream(value); - using (StreamReader streamReader = new StreamReader(stream)) + using (Stream stream = cosmosSerializer.ToStream(value)) { - expected = streamReader.ReadToEnd(); + using (StreamReader streamReader = new StreamReader(stream)) + { + expected = streamReader.ReadToEnd(); + } } - Assert.IsTrue(patchOperation.TrySerializeValueParameter(new CustomSerializer(), out string valueParam)); - Assert.AreEqual(expected, valueParam); + Assert.IsTrue(patchOperation.TrySerializeValueParameter(new CustomSerializer(), out Stream valueParam)); + + string actual; + using (valueParam) + { + using (StreamReader streamReader = new StreamReader(valueParam)) + { + actual = streamReader.ReadToEnd(); + } + } + + Assert.AreEqual(expected, actual); } } From a29d9ee5a0593eebbb11050eb20943202e5f1153 Mon Sep 17 00:00:00 2001 From: Anuj Toshniwal Date: Mon, 26 Apr 2021 13:44:47 +0530 Subject: [PATCH 2/2] Ensure stream input is not serialized again. --- .../src/Patch/PatchOperationCore{T}.cs | 12 ++- .../src/Patch/PatchOperation{T}.cs | 7 +- .../CosmosItemTests.cs | 74 ++++++++++++++++++- .../Contracts/DotNetPreviewSDKAPI.json | 39 +--------- .../CosmosSerializerCoreTests.cs | 7 ++ 5 files changed, 90 insertions(+), 49 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs b/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs index 157c092bd1..91f7ae953b 100644 --- a/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs +++ b/Microsoft.Azure.Cosmos/src/Patch/PatchOperationCore{T}.cs @@ -37,8 +37,16 @@ public override bool TrySerializeValueParameter( CosmosSerializer cosmosSerializer, out Stream valueParam) { - // Use the user serializer so custom conversions are correctly handled - valueParam = cosmosSerializer.ToStream(this.Value); + // If value is of type Stream, do not serialize + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + valueParam = (Stream)(object)this.Value; + } + else + { + // Use the user serializer so custom conversions are correctly handled + valueParam = cosmosSerializer.ToStream(this.Value); + } return true; } diff --git a/Microsoft.Azure.Cosmos/src/Patch/PatchOperation{T}.cs b/Microsoft.Azure.Cosmos/src/Patch/PatchOperation{T}.cs index 3ce1eb521d..f716e7b5df 100644 --- a/Microsoft.Azure.Cosmos/src/Patch/PatchOperation{T}.cs +++ b/Microsoft.Azure.Cosmos/src/Patch/PatchOperation{T}.cs @@ -10,12 +10,7 @@ namespace Microsoft.Azure.Cosmos /// Defines PatchOperation with a value parameter. /// /// Data type of value provided for PatchOperation. -#if PREVIEW - public -#else - internal -#endif - abstract class PatchOperation : PatchOperation + internal abstract class PatchOperation : PatchOperation { /// /// Value parameter. diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs index 2113a8207a..ab2456ace0 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -2091,7 +2091,7 @@ public async Task ItemPatchViaGatewayTest() } [TestMethod] - public async Task ItemPatchCustomSerilizerTest() + public async Task ItemPatchCustomSerializerTest() { CosmosClientOptions clientOptions = new CosmosClientOptions() { @@ -2114,9 +2114,23 @@ public async Task ItemPatchCustomSerilizerTest() }; DateTime patchDate = new DateTime(2020, 07, 01, 01, 02, 03); - List patchOperations = new List() + Stream patchDateStreamInput = new CosmosJsonDotNetSerializer().ToStream(patchDate); + string streamDateJson; + using (Stream stream = new MemoryStream()) + { + patchDateStreamInput.CopyTo(stream); + stream.Position = 0; + patchDateStreamInput.Position = 0; + using (StreamReader streamReader = new StreamReader(stream)) + { + streamDateJson = streamReader.ReadToEnd(); + } + } + + List patchOperations = new List() { - PatchOperation.Add("/date", patchDate) + PatchOperation.Add("/date", patchDate), + PatchOperation.Add("/dateStream", patchDateStreamInput) }; ItemResponse response = await containerInternal.PatchItemAsync( @@ -2139,6 +2153,60 @@ public async Task ItemPatchCustomSerilizerTest() Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); Assert.IsNotNull(response.Resource); Assert.IsTrue(dateJson.Contains(response.Resource["date"].ToString())); + Assert.AreEqual(patchDate.ToString(), response.Resource["dateStream"].ToString()); + Assert.AreNotEqual(response.Resource["date"], response.Resource["dateStream"]); + } + + [TestMethod] + public async Task ItemPatchStreamInputTest() + { + dynamic testItem = new + { + id = "test", + cost = (double?)null, + totalCost = 98.2789, + pk = "MyCustomStatus", + taskNum = 4909, + itemIds = new int[] { 1, 5, 10 }, + itemCode = new byte?[5] { 0x16, (byte)'\0', 0x3, null, (byte)'}' }, + }; + + // Create item + await this.Container.CreateItemAsync(item: testItem); + ContainerInternal containerInternal = (ContainerInternal)this.Container; + + dynamic testItemUpdated = new + { + cost = 100, + totalCost = 198.2789, + taskNum = 4910, + itemCode = new byte?[3] { 0x14, (byte)'\0', (byte)'{' } + }; + + CosmosJsonDotNetSerializer cosmosJsonDotNetSerializer = new CosmosJsonDotNetSerializer(); + + List patchOperations = new List() + { + PatchOperation.Replace("/cost", cosmosJsonDotNetSerializer.ToStream(testItemUpdated.cost)), + PatchOperation.Replace("/totalCost", cosmosJsonDotNetSerializer.ToStream(testItemUpdated.totalCost)), + PatchOperation.Replace("/taskNum", cosmosJsonDotNetSerializer.ToStream(testItemUpdated.taskNum)), + PatchOperation.Replace("/itemCode", cosmosJsonDotNetSerializer.ToStream(testItemUpdated.itemCode)), + }; + + ItemResponse response = await containerInternal.PatchItemAsync( + id: testItem.id, + partitionKey: new Cosmos.PartitionKey(testItem.pk), + patchOperations: patchOperations); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.IsNotNull(response.Resource); + + Assert.AreEqual(testItemUpdated.cost.ToString(), response.Resource.cost.ToString()); + Assert.AreEqual(testItemUpdated.totalCost.ToString(), response.Resource.totalCost.ToString()); + Assert.AreEqual(testItemUpdated.taskNum.ToString(), response.Resource.taskNum.ToString()); + Assert.AreEqual(testItemUpdated.itemCode[0].ToString(), response.Resource.itemCode[0].ToString()); + Assert.AreEqual(testItemUpdated.itemCode[1].ToString(), response.Resource.itemCode[1].ToString()); + Assert.AreEqual(testItemUpdated.itemCode[2].ToString(), response.Resource.itemCode[2].ToString()); } // Read write non partition Container item. diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index 88d0d5ce67..84f0a2627a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -1369,26 +1369,7 @@ "NestedTypes": {} }, "Microsoft.Azure.Cosmos.PatchOperation;System.Object;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { - "Subclasses": { - "Microsoft.Azure.Cosmos.PatchOperation`1;Microsoft.Azure.Cosmos.PatchOperation;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:True;IsSerializable:False": { - "Subclasses": {}, - "Members": { - "T get_Value()": { - "Type": "Method", - "Attributes": [], - "MethodInfo": "T get_Value();IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" - }, - "T Value[Newtonsoft.Json.JsonPropertyAttribute(PropertyName = \"value\")]": { - "Type": "Property", - "Attributes": [ - "JsonPropertyAttribute" - ], - "MethodInfo": "T Value;CanRead:True;CanWrite:False;T get_Value();IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" - } - }, - "NestedTypes": {} - } - }, + "Subclasses": {}, "Members": { "Boolean TrySerializeValueParameter(Microsoft.Azure.Cosmos.CosmosSerializer, System.IO.Stream ByRef)": { "Type": "Method", @@ -1452,24 +1433,6 @@ }, "NestedTypes": {} }, - "Microsoft.Azure.Cosmos.PatchOperation`1;Microsoft.Azure.Cosmos.PatchOperation;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:True;IsSerializable:False": { - "Subclasses": {}, - "Members": { - "T get_Value()": { - "Type": "Method", - "Attributes": [], - "MethodInfo": "T get_Value();IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" - }, - "T Value[Newtonsoft.Json.JsonPropertyAttribute(PropertyName = \"value\")]": { - "Type": "Property", - "Attributes": [ - "JsonPropertyAttribute" - ], - "MethodInfo": "T Value;CanRead:True;CanWrite:False;T get_Value();IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" - } - }, - "NestedTypes": {} - }, "Microsoft.Azure.Cosmos.PatchOperationType;System.Enum;IsAbstract:False;IsSealed:True;IsInterface:False;IsEnum:True;IsClass:False;IsValueType:True;IsNested:False;IsGenericType:False;IsSerializable:True": { "Subclasses": {}, "Members": { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosSerializerCoreTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosSerializerCoreTests.cs index e5caf0e635..1144d4371f 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosSerializerCoreTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosSerializerCoreTests.cs @@ -118,6 +118,13 @@ public void ValidatePatchOperationSerialization() // custom serializer is used since there is Add operation type also using (Stream stream = serializerCore.ToStream(new PatchSpec(patch, patchRequestOptions))) { } Assert.AreEqual(1, toCount); + + patch.Clear(); + toCount = 0; + patch.Add(PatchOperation.Add("/addPath", new CosmosJsonDotNetSerializer().ToStream("addValue"))); + // custom serializer is not used since the input value is of type stream + using (Stream stream = serializerCore.ToStream(new PatchSpec(patch, patchRequestOptions))) { } + Assert.AreEqual(0, toCount); } [TestMethod]