Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,11 +503,14 @@ public override IOrderedQueryable<T> GetItemLinqQueryable<T>(
{
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
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,14 @@ public CosmosLinqSerializerOptions()
/// The default value is CosmosPropertyNamingPolicy.Default
/// </remarks>
public CosmosPropertyNamingPolicy PropertyNamingPolicy { get; set; }

/// <summary>
/// Gets or sets the user defined customer serializer. If no customer serializer was defined,
/// then the value is set to the default value
/// </summary>
/// <remarks>
/// The default value is null
/// </remarks>
public CosmosSerializer CustomCosmosSerializer { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ FROM root]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE {"value": null}
SELECT VALUE {}
FROM root]]></SqlQuery>
</Output>
</Result>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<Results>
<Result>
<Input>
<Description><![CDATA[Filter w/ DataObject initializer with constant value]]></Description>
<Expression><![CDATA[query.Where(doc => (doc == new DataObject() {NumericField = 12, StringField = "12"}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE (root = {"number": 12, "String_value": "12", "id": null, "Pk": null})]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[Select w/ DataObject initializer]]></Description>
<Expression><![CDATA[query.Select(doc => new DataObject() {NumericField = 12, StringField = "12"})]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE {"number": 12, "String_value": "12", "id": null, "Pk": null}
FROM root]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[Deeper than top level reference]]></Description>
<Expression><![CDATA[query.Select(doc => IIF((doc.NumericField > 12), new DataObject() {NumericField = 12, StringField = "12"}, new DataObject() {NumericField = 12, StringField = "12"}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE ((root["NumericField"] > 12) ? {"number": 12, "String_value": "12", "id": null, "Pk": null} : {"number": 12, "String_value": "12", "id": null, "Pk": null})
FROM root]]></SqlQuery>
</Output>
</Result>
<Result>
<Input>
<Description><![CDATA[Filter w/ DataObject initializer with member initialization]]></Description>
<Expression><![CDATA[query.Where(doc => (doc == new DataObject() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE "A"
FROM root
WHERE (root = {"NumericField": root["NumericField"], "StringField": root["StringField"]})]]></SqlQuery>
</Output>
</Result>
</Results>
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,7 @@ Family createDataObj(Random random)
return getQuery;
}

public static Func<bool, IQueryable<Data>> GenerateSimpleCosmosData(
Cosmos.Database cosmosDatabase
)
public static Func<bool, IQueryable<Data>> GenerateSimpleCosmosData(Cosmos.Database cosmosDatabase)
{
const int DocumentCount = 10;
PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition { Paths = new System.Collections.ObjectModel.Collection<string>(new[] { "/Pk" }), Kind = PartitionKind.Hash };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
// <copyright file="LinqAttributeContractTests.cs" company="Microsoft Corporation">
// <copyright file="LinqTranslationBaselineTests.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//-----------------------------------------------------------------------
// <copyright file="LinqTranslationWithCustomSerializerBaseline.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
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<LinqTestInput, LinqTestOutput>
{
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<T>(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>(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<bool, IQueryable<DataObject>> getQuery = LinqTestsCommon.GenerateTestCosmosData(createDataObj, Records, testContainer);

List<LinqTestInput> inputs = new List<LinqTestInput>
{
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<None Remove="BaselineTest\TestBaseline\IndexMetricsParserBaselineTest.IndexUtilizationParse.xml" />
<None Remove="BaselineTest\TestBaseline\LinqTranslationBaselineTests.TestDateTimeJsonConverterTimezones.xml" />
<None Remove="BaselineTest\TestBaseline\LinqTranslationBaselineTests.TestMemberAccessWithNullableTypes.xml" />
<None Remove="BaselineTest\TestBaseline\LinqTranslationWithCustomSerializerBaseline.TestMemberInitializer.xml" />
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -246,6 +247,9 @@
<Content Include="BaselineTest\TestBaseline\LinqTranslationBaselineTests.TestMemberAccess.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="BaselineTest\TestBaseline\LinqTranslationWithCustomSerializerBaseline.TestMemberInitializer.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Query\AggregateQueryTests.AggregateMixedTypes_baseline.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3240,11 +3240,30 @@
"Attributes": [],
"MethodInfo": "Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy PropertyNamingPolicy;CanRead:True;CanWrite:True;Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy get_PropertyNamingPolicy();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_PropertyNamingPolicy(Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Microsoft.Azure.Cosmos.CosmosSerializer CustomCosmosSerializer": {
"Type": "Property",
"Attributes": [],
"MethodInfo": "Microsoft.Azure.Cosmos.CosmosSerializer CustomCosmosSerializer;CanRead:True;CanWrite:True;Microsoft.Azure.Cosmos.CosmosSerializer get_CustomCosmosSerializer();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_CustomCosmosSerializer(Microsoft.Azure.Cosmos.CosmosSerializer);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Microsoft.Azure.Cosmos.CosmosSerializer get_CustomCosmosSerializer()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": {
"Type": "Method",
"Attributes": [
"CompilerGeneratedAttribute"
],
"MethodInfo": "Microsoft.Azure.Cosmos.CosmosSerializer get_CustomCosmosSerializer();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Void .ctor()": {
"Type": "Constructor",
"Attributes": [],
"MethodInfo": "[Void .ctor(), Void .ctor()]"
},
"Void set_CustomCosmosSerializer(Microsoft.Azure.Cosmos.CosmosSerializer)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": {
"Type": "Method",
"Attributes": [
"CompilerGeneratedAttribute"
],
"MethodInfo": "Void set_CustomCosmosSerializer(Microsoft.Azure.Cosmos.CosmosSerializer);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
},
"Void set_PropertyNamingPolicy(Microsoft.Azure.Cosmos.CosmosPropertyNamingPolicy)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": {
"Type": "Method",
"Attributes": [
Expand Down