diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 476774e137..d15f67fece 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; + using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -712,6 +713,22 @@ public static SqlScalarExpression VisitConstant(ConstantExpression inputExpressi return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); } + if (context.linqSerializerOptions?.CustomCosmosSerializer != null) + { + StringWriter writer = new StringWriter(CultureInfo.InvariantCulture); + + // Use the user serializer for the parameter values so custom conversions are correctly handled + using (Stream stream = context.linqSerializerOptions.CustomCosmosSerializer.ToStream(inputExpression.Value)) + { + using (StreamReader streamReader = new StreamReader(stream)) + { + string propertyValue = streamReader.ReadToEnd(); + writer.Write(propertyValue); + return CosmosElement.Parse(writer.ToString()).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); + } + } + } + return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); } diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs index 0ef4fb05cd..60b8db6776 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs @@ -503,11 +503,14 @@ public override IOrderedQueryable GetItemLinqQueryable( { requestOptions ??= new QueryRequestOptions(); - if (linqSerializerOptions == null && this.ClientContext.ClientOptions.SerializerOptions != null) + if (linqSerializerOptions == null && this.ClientContext.ClientOptions != null) { linqSerializerOptions = new CosmosLinqSerializerOptions { - PropertyNamingPolicy = this.ClientContext.ClientOptions.SerializerOptions.PropertyNamingPolicy + PropertyNamingPolicy = this.ClientContext.ClientOptions.SerializerOptions != null + ? this.ClientContext.ClientOptions.SerializerOptions.PropertyNamingPolicy + : CosmosPropertyNamingPolicy.Default, + CustomCosmosSerializer = this.ClientContext.ClientOptions.Serializer }; } diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs index b7523b2c6a..0affdde976 100644 --- a/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs @@ -22,6 +22,15 @@ public CosmosLinqSerializerOptions() this.PropertyNamingPolicy = CosmosPropertyNamingPolicy.Default; } + /// + /// Gets or sets the user defined customer serializer. If no customer serializer was defined, + /// then the value is set to the default value + /// + /// + /// The default value is null + /// + internal CosmosSerializer CustomCosmosSerializer { get; set; } + /// /// Gets or sets whether the naming policy used to convert a string-based name to another format, /// such as a camel-casing format. diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestLiteralSerialization.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestLiteralSerialization.xml index c4c2fb38dd..b72807f399 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestLiteralSerialization.xml +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestLiteralSerialization.xml @@ -435,7 +435,7 @@ FROM root]]> diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializer.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializer.xml new file mode 100644 index 0000000000..8763ef6866 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializer.xml @@ -0,0 +1,48 @@ + + + + + (doc == new DataObject() {NumericField = 12, StringField = "12"}))]]> + + + + + + + + + new DataObject() {NumericField = 12, StringField = "12"})]]> + + + + + + + + + IIF((doc.NumericField > 12), new DataObject() {NumericField = 12, StringField = "12"}, new DataObject() {NumericField = 12, StringField = "12"}))]]> + + + 12) ? {"number": 12, "String_value": "12", "id": null, "Pk": null} : {"number": 12, "String_value": "12", "id": null, "Pk": null}) +FROM root]]> + + + + + + (doc == new DataObject() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTestsCommon.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTestsCommon.cs index 5907d861fc..2b8f5c2d4a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTestsCommon.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTestsCommon.cs @@ -455,9 +455,7 @@ Family createDataObj(Random random) return getQuery; } - public static Func> GenerateSimpleCosmosData( - Cosmos.Database cosmosDatabase - ) + public static Func> GenerateSimpleCosmosData(Cosmos.Database cosmosDatabase) { const int DocumentCount = 10; PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition { Paths = new System.Collections.ObjectModel.Collection(new[] { "/Pk" }), Kind = PartitionKind.Hash }; diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationBaselineTests.cs index 3f2866ae31..67a4aa9364 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationBaselineTests.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // //----------------------------------------------------------------------- diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs new file mode 100644 index 0000000000..be3412e05f --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//----------------------------------------------------------------------- +namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests +{ + using BaselineTest; + using Microsoft.Azure.Cosmos.Linq; + using Microsoft.Azure.Cosmos.Spatial; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Linq.Dynamic; + using System.Text; + using System.Text.Json; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Cosmos.SDK.EmulatorTests; + using System.Threading.Tasks; + using global::Azure.Core.Serialization; + using System.IO; + using System.Text.Json.Serialization; + + [Microsoft.Azure.Cosmos.SDK.EmulatorTests.TestClass] + public class LinqTranslationWithCustomSerializerBaseline : BaselineTests + { + private static CosmosClient cosmosClient; + private static Cosmos.Database testDb; + private static Container testContainer; + + [ClassInitialize] + public async static Task Initialize(TestContext textContext) + { + cosmosClient = TestCommon.CreateCosmosClient((cosmosClientBuilder) + => cosmosClientBuilder.WithCustomSerializer(new SystemTextJsonSerializer(new JsonSerializerOptions())).WithConnectionModeGateway()); + + string dbName = $"{nameof(LinqTranslationBaselineTests)}-{Guid.NewGuid().ToString("N")}"; + testDb = await cosmosClient.CreateDatabaseAsync(dbName); + } + + [ClassCleanup] + public async static Task CleanUp() + { + if (testDb != null) + { + await testDb.DeleteStreamAsync(); + } + } + + [TestInitialize] + public async Task TestInitialize() + { + testContainer = await testDb.CreateContainerAsync(new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: "/Pk")); + } + + [TestCleanup] + public async Task TestCleanUp() + { + await testContainer.DeleteContainerStreamAsync(); + } + + // Custom serializer that uses System.Text.Json.JsonSerializer instead of NewtonsoftJson.JsonSerializer + private class SystemTextJsonSerializer : CosmosSerializer + { + private readonly JsonObjectSerializer systemTextJsonSerializer; + + public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions) + { + this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions); + } + + public override T FromStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using (stream) + { + if (stream.CanSeek && stream.Length == 0) + { + return default; + } + + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + return (T)(object)stream; + } + + return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); + } + } + + public override Stream ToStream(T input) + { + MemoryStream streamPayload = new MemoryStream(); + this.systemTextJsonSerializer.Serialize(streamPayload, input, typeof(T), default); + streamPayload.Position = 0; + return streamPayload; + } + } + + internal class DataObject : LinqTestObject + { + [JsonPropertyName("number")] + public double NumericField { get; set; } + + [JsonPropertyName("String_value")] + public string StringField { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("Pk")] + public string Pk { get; set; } + } + + [TestMethod] + public void TestMemberInitializer() + { + const int Records = 100; + const int NumAbsMax = 500; + const int MaxStringLength = 100; + DataObject createDataObj(Random random) + { + DataObject obj = new DataObject + { + NumericField = random.Next(NumAbsMax * 2) - NumAbsMax, + StringField = LinqTestsCommon.RandomString(random, random.Next(MaxStringLength)), + Id = Guid.NewGuid().ToString(), + Pk = "Test" + }; + return obj; + } + Func> getQuery = LinqTestsCommon.GenerateTestCosmosData(createDataObj, Records, testContainer); + + List inputs = new List + { + new LinqTestInput("Filter w/ DataObject initializer with constant value", b => getQuery(b).Where(doc => doc == new DataObject() { NumericField = 12, StringField = "12" })), + new LinqTestInput("Select w/ DataObject initializer", b => getQuery(b).Select(doc => new DataObject() { NumericField = 12, StringField = "12" })), + new LinqTestInput("Deeper than top level reference", b => getQuery(b).Select(doc => doc.NumericField > 12 ? new DataObject() { NumericField = 12, StringField = "12" } : new DataObject() { NumericField = 12, StringField = "12" })), + + + // Negative test case: serializing only field name using custom serializer not currently supported + new LinqTestInput("Filter w/ DataObject initializer with member initialization", b => getQuery(b).Where(doc => doc == new DataObject() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A")) + }; + this.ExecuteTestSuite(inputs); + } + + + public override LinqTestOutput ExecuteTest(LinqTestInput input) + { + return LinqTestsCommon.ExecuteTest(input); + } + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj index f27d97d301..9849750f54 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj @@ -37,6 +37,7 @@ + @@ -246,6 +247,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest