Skip to content

Commit 0f13fcf

Browse files
aavasthykirankumarkollikundadebdatta
authored
[Internal] Binary Encoding: Fixes serializer for non-point operations. (#5058)
# Pull Request Template ## Description In the current code, CosmosClient used a default version of CosmosJsonDotNetSerializer as a propertiesSerializer which used the ConfigurationManager.IsBinaryEncodingEnabled() to determine whether to use binary encoding during serialization and de-serialization. This propertiesSerializer is hooked into multiple serializers to use as internal serializers and caused unexpected behaviors for APIs like ReadManyItems, ChangeFeed and caused a regression. ## Type of change There are 3 parts of the change: - As part of this change we are introducing a binary serializer. The default serializer will not just use `propertySerializer` but it will check if the binary encoding flag is enabled and accordingly assign the serializer. - When a custom serializer is present, on the response path, no matter what we always return the stream as Text to the serializer. If the default serializer is used then we pass the binary stream in the serializer. - During some local testing, I realized that `CreateItemAsync` with `PreTriggers` or `PostTriggers` doesn't work when binary encoding is enabled. This is because when triggers are present in the item request options the backend will pass the stream to the javascript engine, which does not support binary encoded content at the moment. For long term, since trigger operations won't be supported in the backend, avoiding the binary encoding in such cases, will be the ideal approach. Therefore, needed to skip using binary in such situation. For example, the below code will fail today (before the fix in the PR) if it's used with binary encoding: ``` ItemRequestOptions requestOptions = new ItemRequestOptions() { PreTriggers = new List<string>() { triggerResponse.Resource.Id }, }; Comment comment = new Comment( Guid.NewGuid().ToString(), "pk", random.Next().ToString(), "[email protected]", "This document is intended for binary encoding."); ItemResponse<Comment> writeResponse = await container.CreateItemAsync<Comment>( item: comment, partitionKey: new Cosmos.PartitionKey(comment.pk), requestOptions: requestOptions ); ``` Please delete options that are not relevant. - [X] Bug fix (non-breaking change which fixes an issue) ## Closing issues To automatically close an issue: closes #5043 --------- Co-authored-by: Kiran Kumar Kolli <[email protected]> Co-authored-by: Debdatta Kunda <[email protected]> Co-authored-by: Debdatta Kunda <[email protected]>
1 parent 8696b8e commit 0f13fcf

File tree

6 files changed

+482
-101
lines changed

6 files changed

+482
-101
lines changed

Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public class CosmosClientOptions
7474
private IWebProxy webProxy;
7575
private Func<HttpClient> httpClientFactory;
7676
private string applicationName;
77-
private IFaultInjector faultInjector;
77+
private IFaultInjector faultInjector;
78+
private bool isCustomSerializerProvided;
7879

7980
/// <summary>
8081
/// Creates a new CosmosClientOptions
@@ -624,7 +625,8 @@ public CosmosSerializer Serializer
624625
throw new ArgumentException(
625626
$"{nameof(this.Serializer)} is not compatible with {nameof(this.SerializerOptions)} or {nameof(this.UseSystemTextJsonSerializerWithOptions)}. Only one can be set. ");
626627
}
627-
628+
629+
this.isCustomSerializerProvided = true;
628630
this.serializerInternal = value;
629631
}
630632
}
@@ -1084,6 +1086,11 @@ internal static CosmosClientOptions GetCosmosClientOptionsWithCertificateFlag(st
10841086

10851087
return clientOptions;
10861088
}
1089+
1090+
internal bool IsCustomSerializerProvided()
1091+
{
1092+
return this.isCustomSerializerProvided;
1093+
}
10871094

10881095
private static T GetValueFromConnectionString<T>(string connectionString, string keyName, T defaultValue)
10891096
{

Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,15 @@ public async Task<ItemResponse<T>> ReadItemAsync<T>(
112112
ITrace trace,
113113
ItemRequestOptions requestOptions = null,
114114
CancellationToken cancellationToken = default)
115-
{
115+
{
116116
ResponseMessage response = await this.ProcessItemStreamAsync(
117117
partitionKey: partitionKey,
118118
itemId: id,
119119
streamPayload: null,
120120
operationType: OperationType.Read,
121121
requestOptions: requestOptions,
122122
trace: trace,
123-
targetResponseSerializationFormat: default,
123+
targetResponseSerializationFormat: this.GetTargetResponseSerializationFormat(),
124124
cancellationToken: cancellationToken);
125125

126126
return this.ClientContext.ResponseFactory.CreateItemResponse<T>(response);
@@ -249,7 +249,7 @@ public async Task<ItemResponse<T>> DeleteItemAsync<T>(
249249
operationType: OperationType.Delete,
250250
requestOptions: requestOptions,
251251
trace: trace,
252-
targetResponseSerializationFormat: default,
252+
targetResponseSerializationFormat: this.GetTargetResponseSerializationFormat(),
253253
cancellationToken: cancellationToken);
254254

255255
return this.ClientContext.ResponseFactory.CreateItemResponse<T>(response);
@@ -847,8 +847,13 @@ private async Task<ResponseMessage> ExtractPartitionKeyAndProcessItemStreamAsync
847847

848848
Stream itemStream;
849849
using (trace.StartChild("ItemSerialize"))
850-
{
851-
itemStream = this.ClientContext.SerializerCore.ToStream<T>(item);
850+
{
851+
// Serializing the item to a binary stream should be avoided when triggers are present in the item request options.
852+
// This is because when triggers are present in the request options, the backend will pass the stream to the javascript
853+
// engine, which does not support binary encoded content at the moment. For long term, since trigger operations won't
854+
// be supported in the backend, avoiding the binary encoding in such cases, will be the ideal approach.
855+
bool canUseBinaryEncoding = !ContainerCore.IsTriggerPresentInRequestOptions(requestOptions);
856+
itemStream = this.ClientContext.SerializerCore.ToStream<T>(item, canUseBinaryEncodingForPointOperations: canUseBinaryEncoding);
852857
}
853858

854859
// User specified PK value, no need to extract it
@@ -861,7 +866,7 @@ private async Task<ResponseMessage> ExtractPartitionKeyAndProcessItemStreamAsync
861866
operationType,
862867
requestOptions,
863868
trace: trace,
864-
targetResponseSerializationFormat: default,
869+
targetResponseSerializationFormat: this.GetTargetResponseSerializationFormat(),
865870
cancellationToken: cancellationToken);
866871
}
867872

@@ -877,7 +882,7 @@ private async Task<ResponseMessage> ExtractPartitionKeyAndProcessItemStreamAsync
877882
operationType,
878883
requestOptions,
879884
trace: trace,
880-
targetResponseSerializationFormat: default,
885+
targetResponseSerializationFormat: this.GetTargetResponseSerializationFormat(),
881886
cancellationToken: cancellationToken);
882887

883888
if (responseMessage.IsSuccessStatusCode)
@@ -921,10 +926,15 @@ private async Task<ResponseMessage> ProcessItemStreamAsync(
921926
}
922927

923928
ContainerInternal.ValidatePartitionKey(partitionKey, requestOptions);
924-
string resourceUri = this.GetResourceUri(requestOptions, operationType, itemId);
929+
string resourceUri = this.GetResourceUri(requestOptions, operationType, itemId);
925930

926931
// Convert Text to Binary Stream.
927-
if (ConfigurationManager.IsBinaryEncodingEnabled())
932+
// Exception: Serializing a text stream to a binary stream should be avoided when triggers are present in the item request options.
933+
// This is because when triggers are present in the request options, the backend will pass the stream to the javascript
934+
// engine, which does not support binary encoded content at the moment. For long term, since trigger operations won't
935+
// be supported in the backend, avoiding the binary encoding in such cases, will be the ideal approach.
936+
if (ConfigurationManager.IsBinaryEncodingEnabled()
937+
&& !ContainerCore.IsTriggerPresentInRequestOptions(requestOptions))
928938
{
929939
streamPayload = CosmosSerializationUtil.TrySerializeStreamToTargetFormat(
930940
targetSerializationFormat: ContainerCore.GetTargetRequestSerializationFormat(),
@@ -1286,6 +1296,16 @@ private ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderPrivate(
12861296
applyBuilderConfiguration: changeFeedProcessor.ApplyBuildConfiguration).WithChangeFeedMode(mode);
12871297
}
12881298

1299+
private JsonSerializationFormat? GetTargetResponseSerializationFormat()
1300+
{
1301+
if (this.ClientContext.ClientOptions.IsCustomSerializerProvided())
1302+
{
1303+
return JsonSerializationFormat.Text;
1304+
}
1305+
1306+
return default;
1307+
}
1308+
12891309
private static JsonSerializationFormat GetTargetRequestSerializationFormat()
12901310
{
12911311
return ConfigurationManager.IsBinaryEncodingEnabled()
@@ -1607,6 +1627,17 @@ private static bool IsYMaxWithinX(
16071627
Documents.Routing.Range<string> y)
16081628
{
16091629
return x.Max == y.Max || x.Contains(y.Max);
1630+
}
1631+
1632+
/// <summary>
1633+
/// Checks if the request options contain any triggers (pre or post).
1634+
/// </summary>
1635+
/// <param name="requestOptions">An instance of <see cref="ItemRequestOptions"/>.</param>
1636+
/// <returns>A boolean flag indicating if triggers were present in the request options.</returns>
1637+
private static bool IsTriggerPresentInRequestOptions(
1638+
ItemRequestOptions requestOptions)
1639+
{
1640+
return requestOptions?.PreTriggers != null || requestOptions?.PostTriggers != null;
16101641
}
16111642
}
16121643
}

Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializerCore.cs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ namespace Microsoft.Azure.Cosmos
1717
/// </summary>
1818
internal class CosmosSerializerCore
1919
{
20+
private readonly bool isBinaryEncodingEnabled;
21+
private static readonly CosmosSerializer binarySerializer = new CosmosJsonSerializerWrapper(
22+
new CosmosJsonDotNetSerializer(binaryEncodingEnabled: true));
23+
2024
private static readonly CosmosSerializer propertiesSerializer = new CosmosJsonSerializerWrapper(
21-
new CosmosJsonDotNetSerializer(
22-
ConfigurationManager.IsBinaryEncodingEnabled()));
25+
new CosmosJsonDotNetSerializer());
2326

2427
private readonly CosmosSerializer customSerializer;
2528
private readonly CosmosSerializer sqlQuerySpecSerializer;
@@ -28,6 +31,7 @@ internal class CosmosSerializerCore
2831
internal CosmosSerializerCore(
2932
CosmosSerializer customSerializer = null)
3033
{
34+
this.isBinaryEncodingEnabled = ConfigurationManager.IsBinaryEncodingEnabled();
3135
if (customSerializer == null)
3236
{
3337
this.customSerializer = null;
@@ -81,9 +85,9 @@ internal T[] FromFeedStream<T>(Stream stream)
8185
return serializer.FromStream<T[]>(stream);
8286
}
8387

84-
internal Stream ToStream<T>(T input)
88+
internal Stream ToStream<T>(T input, bool canUseBinaryEncodingForPointOperations = false)
8589
{
86-
CosmosSerializer serializer = this.GetSerializer<T>();
90+
CosmosSerializer serializer = this.GetSerializer<T>(canUseBinaryEncodingForPointOperations);
8791
return serializer.ToStream<T>(input);
8892
}
8993

@@ -116,31 +120,29 @@ internal CosmosSerializer GetCustomOrDefaultSerializer()
116120
return this.customSerializer;
117121
}
118122

119-
return CosmosSerializerCore.propertiesSerializer;
123+
return this.GetDefaultSerializer();
120124
}
121125

122-
private CosmosSerializer GetSerializer<T>()
126+
private CosmosSerializer GetSerializer<T>(
127+
bool canUseBinaryEncodingForPointOperations = false)
123128
{
124129
Type inputType = typeof(T);
125130
if (inputType == typeof(PatchSpec))
126131
{
127-
if (this.patchOperationSerializer == null)
128-
{
129-
this.patchOperationSerializer = PatchOperationsJsonConverter.CreatePatchOperationsSerializer(
132+
this.patchOperationSerializer ??= PatchOperationsJsonConverter.CreatePatchOperationsSerializer(
130133
cosmosSerializer: this.customSerializer ?? new CosmosJsonDotNetSerializer(),
131134
propertiesSerializer: CosmosSerializerCore.propertiesSerializer);
132-
}
133135
return this.patchOperationSerializer;
134136
}
135137

136-
if (this.customSerializer == null)
138+
if (CosmosSerializerCore.IsInputTypeInternal(inputType))
137139
{
138140
return CosmosSerializerCore.propertiesSerializer;
139141
}
140142

141-
if (CosmosSerializerCore.IsInputTypeInternal(inputType))
143+
if (this.customSerializer == null)
142144
{
143-
return CosmosSerializerCore.propertiesSerializer;
145+
return this.GetDefaultSerializer(canUseBinaryEncodingForPointOperations);
144146
}
145147

146148
if (inputType == typeof(SqlQuerySpec))
@@ -165,6 +167,14 @@ private CosmosSerializer GetSerializer<T>()
165167
return this.customSerializer;
166168
}
167169

170+
private CosmosSerializer GetDefaultSerializer(
171+
bool canUseBinaryEncodingForPointOperations = false)
172+
{
173+
return this.isBinaryEncodingEnabled && canUseBinaryEncodingForPointOperations
174+
? CosmosSerializerCore.binarySerializer
175+
: CosmosSerializerCore.propertiesSerializer;
176+
}
177+
168178
internal static bool IsInputTypeInternal(
169179
Type inputType)
170180
{
@@ -184,4 +194,4 @@ internal static bool IsInputTypeInternal(
184194
|| inputType == typeof(ChangeFeedQuerySpec);
185195
}
186196
}
187-
}
197+
}

0 commit comments

Comments
 (0)