From f17a85bfa8d194263988be77031693779e62fe86 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 1 Nov 2023 09:01:19 -0700 Subject: [PATCH 01/14] intial commit --- .../BuiltinFunctions/ArrayBuiltinFunctions.cs | 2 +- .../src/Linq/CosmosLinqSerializer.cs | 305 ++++++++++++++++++ .../src/Linq/ExpressionToSQL.cs | 252 +-------------- Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs | 47 --- .../LinqAttributeContractBaselineTests.cs | 8 +- 5 files changed, 316 insertions(+), 298 deletions(-) create mode 100644 Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs diff --git a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs index d96ac56a22..c414af1058 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs @@ -80,7 +80,7 @@ private SqlScalarExpression VisitIN(Expression expression, ConstantExpression co List items = new List(); foreach (object item in (IEnumerable)constantExpressionList.Value) { - items.Add(ExpressionToSql.VisitConstant(Expression.Constant(item), context)); + items.Add(CosmosLinqSerializer.VisitConstant(Expression.Constant(item), context)); } // if the items list empty, then just return false expression diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs new file mode 100644 index 0000000000..1574659bc3 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs @@ -0,0 +1,305 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.Linq +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using System.Runtime.Serialization; + using Microsoft.Azure.Cosmos.CosmosElements; + using Microsoft.Azure.Cosmos.CosmosElements.Numbers; + using Microsoft.Azure.Cosmos.Spatial; + using Microsoft.Azure.Cosmos.SqlObjects; + using Microsoft.Azure.Documents; + using Newtonsoft.Json; + + internal static class CosmosLinqSerializer + { + public static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right) + { + MemberExpression memberExpression; + if (left is UnaryExpression unaryExpression) + { + memberExpression = unaryExpression.Operand as MemberExpression; + } + else + { + memberExpression = left as MemberExpression; + } + + if (memberExpression != null) + { + Type memberType = memberExpression.Type; + if (memberType.IsNullable()) + { + memberType = memberType.NullableUnderlyingType(); + } + + // There are two ways to specify a custom attribute + // 1- by specifying the JsonConverterAttribute on a Class/Enum + // [JsonConverter(typeof(StringEnumConverter))] + // Enum MyEnum + // { + // ... + // } + // + // 2- by specifying the JsonConverterAttribute on a property + // class MyClass + // { + // [JsonConverter(typeof(StringEnumConverter))] + // public MyEnum MyEnum; + // } + // + // Newtonsoft gives high precedence to the attribute specified + // on a property over on a type (class/enum) + // so we check both attributes and apply the same precedence rules + // JsonConverterAttribute doesn't allow duplicates so it's safe to + // use FirstOrDefault() + CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); + CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); + + CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; + if (converterAttribute != null) + { + Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); + + Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; + + object value = default(object); + // Enum + if (memberType.IsEnum()) + { + Number64 number64 = ((SqlNumberLiteral)right.Literal).Value; + if (number64.IsDouble) + { + value = Enum.ToObject(memberType, Number64.ToDouble(number64)); + } + else + { + value = Enum.ToObject(memberType, Number64.ToLong(number64)); + } + + } + // DateTime + else if (memberType == typeof(DateTime)) + { + SqlStringLiteral serializedDateTime = (SqlStringLiteral)right.Literal; + value = DateTime.Parse(serializedDateTime.Value, provider: null, DateTimeStyles.RoundtripKind); + } + + if (value != default(object)) + { + string serializedValue; + + if (converterType.GetConstructor(Type.EmptyTypes) != null) + { + serializedValue = JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType)); + } + else + { + serializedValue = JsonConvert.SerializeObject(value); + } + + return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); + } + } + } + + return right; + } + + public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) + { + if (inputExpression.Value == null) + { + return SqlLiteralScalarExpression.SqlNullLiteralScalarExpression; + } + + if (inputExpression.Type.IsNullable()) + { + return CosmosLinqSerializer.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), context); + } + + if (context.parameters != null && context.parameters.TryGetValue(inputExpression.Value, out string paramName)) + { + SqlParameter sqlParameter = SqlParameter.Create(paramName); + return SqlParameterRefScalarExpression.Create(sqlParameter); + } + + Type constantType = inputExpression.Value.GetType(); + if (constantType.IsValueType()) + { + if (inputExpression.Value is bool boolValue) + { + SqlBooleanLiteral literal = SqlBooleanLiteral.Create(boolValue); + return SqlLiteralScalarExpression.Create(literal); + } + + if (ExpressionToSql.TryGetSqlNumberLiteral(inputExpression.Value, out SqlNumberLiteral numberLiteral)) + { + return SqlLiteralScalarExpression.Create(numberLiteral); + } + + if (inputExpression.Value is Guid guidValue) + { + SqlStringLiteral literal = SqlStringLiteral.Create(guidValue.ToString()); + return SqlLiteralScalarExpression.Create(literal); + } + } + + if (inputExpression.Value is string stringValue) + { + SqlStringLiteral literal = SqlStringLiteral.Create(stringValue); + return SqlLiteralScalarExpression.Create(literal); + } + + if (typeof(Geometry).IsAssignableFrom(constantType)) + { + return GeometrySqlExpressionFactory.Construct(inputExpression); + } + + if (inputExpression.Value is IEnumerable enumerable) + { + List arrayItems = new List(); + + foreach (object item in enumerable) + { + arrayItems.Add(CosmosLinqSerializer.VisitConstant(Expression.Constant(item), context)); + } + + return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); + } + + return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); + } + + public static string GetMemberName(this MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) + { + string memberName = null; + + // Check if Newtonsoft JsonExtensionDataAttribute is present on the member, if so, return empty member name. + Newtonsoft.Json.JsonExtensionDataAttribute jsonExtensionDataAttribute = memberInfo.GetCustomAttribute(true); + if (jsonExtensionDataAttribute != null && jsonExtensionDataAttribute.ReadData) + { + return null; + } + + // Json.Net honors JsonPropertyAttribute more than DataMemberAttribute + // So we check for JsonPropertyAttribute first. + JsonPropertyAttribute jsonPropertyAttribute = memberInfo.GetCustomAttribute(true); + if (jsonPropertyAttribute != null && !string.IsNullOrEmpty(jsonPropertyAttribute.PropertyName)) + { + memberName = jsonPropertyAttribute.PropertyName; + } + else + { + DataContractAttribute dataContractAttribute = memberInfo.DeclaringType.GetCustomAttribute(true); + if (dataContractAttribute != null) + { + DataMemberAttribute dataMemberAttribute = memberInfo.GetCustomAttribute(true); + if (dataMemberAttribute != null && !string.IsNullOrEmpty(dataMemberAttribute.Name)) + { + memberName = dataMemberAttribute.Name; + } + } + } + + if (memberName == null) + { + memberName = memberInfo.Name; + } + + if (linqSerializerOptions != null) + { + memberName = CosmosSerializationUtil.GetStringWithPropertyNamingPolicy(linqSerializerOptions, memberName); + } + + return memberName; + } + + private sealed class CosmosElementToSqlScalarExpressionVisitor : ICosmosElementVisitor + { + public static readonly CosmosElementToSqlScalarExpressionVisitor Singleton = new CosmosElementToSqlScalarExpressionVisitor(); + + private CosmosElementToSqlScalarExpressionVisitor() + { + // Private constructor, since this class is a singleton. + } + + public SqlScalarExpression Visit(CosmosArray cosmosArray) + { + List items = new List(); + foreach (CosmosElement item in cosmosArray) + { + items.Add(item.Accept(this)); + } + + return SqlArrayCreateScalarExpression.Create(items.ToImmutableArray()); + } + + public SqlScalarExpression Visit(CosmosBinary cosmosBinary) + { + // Can not convert binary to scalar expression without knowing the API type. + throw new NotImplementedException(); + } + + public SqlScalarExpression Visit(CosmosBoolean cosmosBoolean) + { + return SqlLiteralScalarExpression.Create(SqlBooleanLiteral.Create(cosmosBoolean.Value)); + } + + public SqlScalarExpression Visit(CosmosGuid cosmosGuid) + { + // Can not convert guid to scalar expression without knowing the API type. + throw new NotImplementedException(); + } + + public SqlScalarExpression Visit(CosmosNull cosmosNull) + { + return SqlLiteralScalarExpression.Create(SqlNullLiteral.Create()); + } + + public SqlScalarExpression Visit(CosmosNumber cosmosNumber) + { + if (!(cosmosNumber is CosmosNumber64 cosmosNumber64)) + { + throw new ArgumentException($"Unknown {nameof(CosmosNumber)} type: {cosmosNumber.GetType()}."); + } + + return SqlLiteralScalarExpression.Create(SqlNumberLiteral.Create(cosmosNumber64.GetValue())); + } + + public SqlScalarExpression Visit(CosmosObject cosmosObject) + { + List properties = new List(); + foreach (KeyValuePair prop in cosmosObject) + { + SqlPropertyName name = SqlPropertyName.Create(prop.Key); + CosmosElement value = prop.Value; + SqlScalarExpression expression = value.Accept(this); + SqlObjectProperty property = SqlObjectProperty.Create(name, expression); + properties.Add(property); + } + + return SqlObjectCreateScalarExpression.Create(properties.ToImmutableArray()); + } + + public SqlScalarExpression Visit(CosmosString cosmosString) + { + return SqlLiteralScalarExpression.Create(SqlStringLiteral.Create(cosmosString.Value)); + } + + public SqlScalarExpression Visit(CosmosUndefined cosmosUndefined) + { + return SqlLiteralScalarExpression.Create(SqlUndefinedLiteral.Create()); + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index fef136d8d2..45c372e378 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -4,7 +4,6 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; - using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; @@ -13,12 +12,8 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Linq; using System.Linq.Expressions; using System.Reflection; - using Microsoft.Azure.Cosmos.CosmosElements; - using Microsoft.Azure.Cosmos.CosmosElements.Numbers; using Microsoft.Azure.Cosmos.Spatial; using Microsoft.Azure.Cosmos.SqlObjects; - using Microsoft.Azure.Documents; - using Newtonsoft.Json; using static Microsoft.Azure.Cosmos.Linq.FromParameterBindings; // ReSharper disable UnusedParameter.Local @@ -169,7 +164,7 @@ private static Collection TranslateInput(ConstantExpression inputExpression, Tra } /// - /// Get a paramter name to be binded to the a collection from the next lambda. + /// Get a parameter name to be binded to the collection from the next lambda. /// It's merely for readability purpose. If that is not possible, use a default /// parameter name. /// @@ -189,7 +184,7 @@ private static string GetBindingParameterName(TranslationContext context) } } - if (parameterName == null) parameterName = ExpressionToSql.DefaultParameterName; + parameterName ??= ExpressionToSql.DefaultParameterName; return parameterName; } @@ -249,7 +244,7 @@ internal static SqlScalarExpression VisitNonSubqueryScalarExpression(Expression case ExpressionType.Conditional: return ExpressionToSql.VisitConditional((ConditionalExpression)inputExpression, context); case ExpressionType.Constant: - return ExpressionToSql.VisitConstant((ConstantExpression)inputExpression, context); + return CosmosLinqSerializer.VisitConstant((ConstantExpression)inputExpression, context); case ExpressionType.Parameter: return ExpressionToSql.VisitParameter((ParameterExpression)inputExpression, context); case ExpressionType.MemberAccess: @@ -305,7 +300,7 @@ private static SqlScalarExpression VisitMethodCallScalar(MethodCallExpression me object[] argumentsExpressions = (object[])((ConstantExpression)methodCallExpression.Arguments[1]).Value; foreach (object argument in argumentsExpressions) { - arguments.Add(ExpressionToSql.VisitConstant(Expression.Constant(argument), context)); + arguments.Add(CosmosLinqSerializer.VisitConstant(Expression.Constant(argument), context)); } } else @@ -474,109 +469,16 @@ private static SqlScalarExpression VisitBinary(BinaryExpression inputExpression, if (left is SqlMemberIndexerScalarExpression && right is SqlLiteralScalarExpression literalScalarExpression) { - right = ExpressionToSql.ApplyCustomConverters(inputExpression.Left, literalScalarExpression); + right = CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Left, literalScalarExpression); } else if (right is SqlMemberIndexerScalarExpression && left is SqlLiteralScalarExpression sqlLiteralScalarExpression) { - left = ExpressionToSql.ApplyCustomConverters(inputExpression.Right, sqlLiteralScalarExpression); + left = CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Right, sqlLiteralScalarExpression); } return SqlBinaryScalarExpression.Create(op, left, right); } - private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right) - { - MemberExpression memberExpression; - if (left is UnaryExpression unaryExpression) - { - memberExpression = unaryExpression.Operand as MemberExpression; - } - else - { - memberExpression = left as MemberExpression; - } - - if (memberExpression != null) - { - Type memberType = memberExpression.Type; - if (memberType.IsNullable()) - { - memberType = memberType.NullableUnderlyingType(); - } - - // There are two ways to specify a custom attribute - // 1- by specifying the JsonConverterAttribute on a Class/Enum - // [JsonConverter(typeof(StringEnumConverter))] - // Enum MyEnum - // { - // ... - // } - // - // 2- by specifying the JsonConverterAttribute on a property - // class MyClass - // { - // [JsonConverter(typeof(StringEnumConverter))] - // public MyEnum MyEnum; - // } - // - // Newtonsoft gives high precedence to the attribute specified - // on a property over on a type (class/enum) - // so we check both attributes and apply the same precedence rules - // JsonConverterAttribute doesn't allow duplicates so it's safe to - // use FirstOrDefault() - CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.Where(ca => ca.AttributeType == typeof(JsonConverterAttribute)).FirstOrDefault(); - CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().Where(ca => ca.AttributeType == typeof(JsonConverterAttribute)).FirstOrDefault(); - - CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; - if (converterAttribute != null) - { - Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); - - Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; - - object value = default(object); - // Enum - if (memberType.IsEnum()) - { - Number64 number64 = ((SqlNumberLiteral)right.Literal).Value; - if (number64.IsDouble) - { - value = Enum.ToObject(memberType, Number64.ToDouble(number64)); - } - else - { - value = Enum.ToObject(memberType, Number64.ToLong(number64)); - } - - } - // DateTime - else if (memberType == typeof(DateTime)) - { - SqlStringLiteral serializedDateTime = (SqlStringLiteral)right.Literal; - value = DateTime.Parse(serializedDateTime.Value, provider: null, DateTimeStyles.RoundtripKind); - } - - if (value != default(object)) - { - string serializedValue; - - if (converterType.GetConstructor(Type.EmptyTypes) != null) - { - serializedValue = JsonConvert.SerializeObject(value, (JsonConverter)Activator.CreateInstance(converterType)); - } - else - { - serializedValue = JsonConvert.SerializeObject(value); - } - - return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); - } - } - } - - return right; - } - private static bool TryMatchStringCompareTo(MethodCallExpression left, ConstantExpression right, ExpressionType compareOperator) { if (left.Method.Equals(typeof(string).GetMethod("CompareTo", new Type[] { typeof(string) })) && left.Arguments.Count == 1) @@ -708,71 +610,6 @@ private static SqlScalarExpression VisitTypeIs(TypeBinaryExpression inputExpress throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, inputExpression.NodeType)); } - public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) - { - if (inputExpression.Value == null) - { - return SqlLiteralScalarExpression.SqlNullLiteralScalarExpression; - } - - if (inputExpression.Type.IsNullable()) - { - return ExpressionToSql.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), context); - } - - if (context.parameters != null && context.parameters.TryGetValue(inputExpression.Value, out string paramName)) - { - SqlParameter sqlParameter = SqlParameter.Create(paramName); - return SqlParameterRefScalarExpression.Create(sqlParameter); - } - - Type constantType = inputExpression.Value.GetType(); - if (constantType.IsValueType()) - { - if (inputExpression.Value is bool boolValue) - { - SqlBooleanLiteral literal = SqlBooleanLiteral.Create(boolValue); - return SqlLiteralScalarExpression.Create(literal); - } - - if (ExpressionToSql.TryGetSqlNumberLiteral(inputExpression.Value, out SqlNumberLiteral numberLiteral)) - { - return SqlLiteralScalarExpression.Create(numberLiteral); - } - - if (inputExpression.Value is Guid guidValue) - { - SqlStringLiteral literal = SqlStringLiteral.Create(guidValue.ToString()); - return SqlLiteralScalarExpression.Create(literal); - } - } - - if (inputExpression.Value is string stringValue) - { - SqlStringLiteral literal = SqlStringLiteral.Create(stringValue); - return SqlLiteralScalarExpression.Create(literal); - } - - if (typeof(Geometry).IsAssignableFrom(constantType)) - { - return GeometrySqlExpressionFactory.Construct(inputExpression); - } - - if (inputExpression.Value is IEnumerable enumerable) - { - List arrayItems = new List(); - - foreach (object item in enumerable) - { - arrayItems.Add(ExpressionToSql.VisitConstant(Expression.Constant(item), context)); - } - - return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); - } - - return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); - } - private static SqlScalarExpression VisitConditional(ConditionalExpression inputExpression, TranslationContext context) { SqlScalarExpression conditionExpression = ExpressionToSql.VisitScalarExpression(inputExpression.Test, context); @@ -2004,83 +1841,6 @@ private static SqlInputPathCollection ConvertMemberIndexerToPath(SqlMemberIndexe #endregion LINQ Specific Visitors - private sealed class CosmosElementToSqlScalarExpressionVisitor : ICosmosElementVisitor - { - public static readonly CosmosElementToSqlScalarExpressionVisitor Singleton = new CosmosElementToSqlScalarExpressionVisitor(); - - private CosmosElementToSqlScalarExpressionVisitor() - { - // Private constructor, since this class is a singleton. - } - - public SqlScalarExpression Visit(CosmosArray cosmosArray) - { - List items = new List(); - foreach (CosmosElement item in cosmosArray) - { - items.Add(item.Accept(this)); - } - - return SqlArrayCreateScalarExpression.Create(items.ToImmutableArray()); - } - - public SqlScalarExpression Visit(CosmosBinary cosmosBinary) - { - // Can not convert binary to scalar expression without knowing the API type. - throw new NotImplementedException(); - } - - public SqlScalarExpression Visit(CosmosBoolean cosmosBoolean) - { - return SqlLiteralScalarExpression.Create(SqlBooleanLiteral.Create(cosmosBoolean.Value)); - } - - public SqlScalarExpression Visit(CosmosGuid cosmosGuid) - { - // Can not convert guid to scalar expression without knowing the API type. - throw new NotImplementedException(); - } - - public SqlScalarExpression Visit(CosmosNull cosmosNull) - { - return SqlLiteralScalarExpression.Create(SqlNullLiteral.Create()); - } - - public SqlScalarExpression Visit(CosmosNumber cosmosNumber) - { - if (!(cosmosNumber is CosmosNumber64 cosmosNumber64)) - { - throw new ArgumentException($"Unknown {nameof(CosmosNumber)} type: {cosmosNumber.GetType()}."); - } - - return SqlLiteralScalarExpression.Create(SqlNumberLiteral.Create(cosmosNumber64.GetValue())); - } - - public SqlScalarExpression Visit(CosmosObject cosmosObject) - { - List properties = new List(); - foreach (KeyValuePair prop in cosmosObject) - { - SqlPropertyName name = SqlPropertyName.Create(prop.Key); - CosmosElement value = prop.Value; - SqlScalarExpression expression = value.Accept(this); - SqlObjectProperty property = SqlObjectProperty.Create(name, expression); - properties.Add(property); - } - - return SqlObjectCreateScalarExpression.Create(properties.ToImmutableArray()); - } - - public SqlScalarExpression Visit(CosmosString cosmosString) - { - return SqlLiteralScalarExpression.Create(SqlStringLiteral.Create(cosmosString.Value)); - } - - public SqlScalarExpression Visit(CosmosUndefined cosmosUndefined) - { - return SqlLiteralScalarExpression.Create(SqlUndefinedLiteral.Create()); - } - } private enum SubqueryKind { ArrayScalarExpression, diff --git a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs index 7c8e62d69c..ededa0394b 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs @@ -10,10 +10,7 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; - using System.Runtime.Serialization; - using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Documents; - using Newtonsoft.Json; internal static class TypeSystem { @@ -22,50 +19,6 @@ public static Type GetElementType(Type type) return GetElementType(type, new HashSet()); } - public static string GetMemberName(this MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) - { - string memberName = null; - - // Check if Newtonsoft JsonExtensionDataAttribute is present on the member, if so, return empty member name. - JsonExtensionDataAttribute jsonExtensionDataAttribute = memberInfo.GetCustomAttribute(true); - if (jsonExtensionDataAttribute != null && jsonExtensionDataAttribute.ReadData) - { - return null; - } - - // Json.Net honors JsonPropertyAttribute more than DataMemberAttribute - // So we check for JsonPropertyAttribute first. - JsonPropertyAttribute jsonPropertyAttribute = memberInfo.GetCustomAttribute(true); - if (jsonPropertyAttribute != null && !string.IsNullOrEmpty(jsonPropertyAttribute.PropertyName)) - { - memberName = jsonPropertyAttribute.PropertyName; - } - else - { - DataContractAttribute dataContractAttribute = memberInfo.DeclaringType.GetCustomAttribute(true); - if (dataContractAttribute != null) - { - DataMemberAttribute dataMemberAttribute = memberInfo.GetCustomAttribute(true); - if (dataMemberAttribute != null && !string.IsNullOrEmpty(dataMemberAttribute.Name)) - { - memberName = dataMemberAttribute.Name; - } - } - } - - if (memberName == null) - { - memberName = memberInfo.Name; - } - - if (linqSerializerOptions != null) - { - memberName = CosmosSerializationUtil.GetStringWithPropertyNamingPolicy(linqSerializerOptions, memberName); - } - - return memberName; - } - private static Type GetElementType(Type type, HashSet visitedSet) { Debug.Assert(type != null); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs index 7ffc36bdbd..108e7bc0cf 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs @@ -173,10 +173,10 @@ public Datum2(string jsonProperty, string dataMember, string defaultMember, stri [TestMethod] public void TestAttributePriority() { - Assert.AreEqual("jsonProperty", TypeSystem.GetMemberName(typeof(Datum).GetMember("JsonProperty").First())); - Assert.AreEqual("dataMember", TypeSystem.GetMemberName(typeof(Datum).GetMember("DataMember").First())); - Assert.AreEqual("Default", TypeSystem.GetMemberName(typeof(Datum).GetMember("Default").First())); - Assert.AreEqual("jsonPropertyHasHigherPriority", TypeSystem.GetMemberName(typeof(Datum).GetMember("JsonPropertyAndDataMember").First())); + Assert.AreEqual("jsonProperty", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonProperty").First())); + Assert.AreEqual("dataMember", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("DataMember").First())); + Assert.AreEqual("Default", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("Default").First())); + Assert.AreEqual("jsonPropertyHasHigherPriority", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonPropertyAndDataMember").First())); } /// From 9a1f18cf5b718d5d35017d50f588be3003e89a17 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 1 Nov 2023 09:37:30 -0700 Subject: [PATCH 02/14] add interface --- .../BuiltinFunctions/ArrayBuiltinFunctions.cs | 2 +- .../src/Linq/CosmosLinqSerializer.cs | 14 ++-- .../src/Linq/ExpressionToSQL.cs | 80 +++++++++---------- .../src/Linq/ICosmosLinqSerializer.cs | 18 +++++ .../src/Linq/QueryUnderConstruction.cs | 8 +- .../src/Linq/TranslationContext.cs | 35 +++++--- Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs | 5 ++ .../LinqAttributeContractBaselineTests.cs | 9 ++- 8 files changed, 102 insertions(+), 69 deletions(-) create mode 100644 Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs diff --git a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs index c414af1058..5d212ab929 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs @@ -80,7 +80,7 @@ private SqlScalarExpression VisitIN(Expression expression, ConstantExpression co List items = new List(); foreach (object item in (IEnumerable)constantExpressionList.Value) { - items.Add(CosmosLinqSerializer.VisitConstant(Expression.Constant(item), context)); + items.Add(context.CosmosLinqSerializer.VisitConstant(Expression.Constant(item), context)); } // if the items list empty, then just return false expression diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs index 1574659bc3..49c27f715f 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs @@ -20,9 +20,9 @@ namespace Microsoft.Azure.Cosmos.Linq using Microsoft.Azure.Documents; using Newtonsoft.Json; - internal static class CosmosLinqSerializer + internal class CosmosLinqSerializer : ICosmosLinqSerializer { - public static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right) + public SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right) { MemberExpression memberExpression; if (left is UnaryExpression unaryExpression) @@ -115,7 +115,7 @@ public static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLite return right; } - public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) + public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) { if (inputExpression.Value == null) { @@ -124,10 +124,10 @@ public static SqlScalarExpression VisitConstant(ConstantExpression inputExpressi if (inputExpression.Type.IsNullable()) { - return CosmosLinqSerializer.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), context); + return this.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), context); } - if (context.parameters != null && context.parameters.TryGetValue(inputExpression.Value, out string paramName)) + if (context.Parameters != null && context.Parameters.TryGetValue(inputExpression.Value, out string paramName)) { SqlParameter sqlParameter = SqlParameter.Create(paramName); return SqlParameterRefScalarExpression.Create(sqlParameter); @@ -171,7 +171,7 @@ public static SqlScalarExpression VisitConstant(ConstantExpression inputExpressi foreach (object item in enumerable) { - arrayItems.Add(CosmosLinqSerializer.VisitConstant(Expression.Constant(item), context)); + arrayItems.Add(this.VisitConstant(Expression.Constant(item), context)); } return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); @@ -180,7 +180,7 @@ public static SqlScalarExpression VisitConstant(ConstantExpression inputExpressi return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); } - public static string GetMemberName(this MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) + public string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) { string memberName = null; diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 45c372e378..60e33ea568 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -89,7 +89,7 @@ public static SqlQuery TranslateQuery( TranslationContext context = new TranslationContext(linqSerializerOptions, parameters); ExpressionToSql.Translate(inputExpression, context); // ignore result here - QueryUnderConstruction query = context.currentQuery; + QueryUnderConstruction query = context.CurrentQuery; query = query.FlattenAsPossible(); SqlQuery result = query.GetSqlQuery(); @@ -154,7 +154,7 @@ private static Collection TranslateInput(ConstantExpression inputExpression, Tra throw new DocumentQueryException(ClientResources.InputIsNotIDocumentQuery); } - context.currentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc()); + context.CurrentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc()); Type elemType = TypeSystem.GetElementType(inputExpression.Type); context.SetInputParameter(elemType, ParameterSubstitution.InputParameterName); // ignore result @@ -244,7 +244,7 @@ internal static SqlScalarExpression VisitNonSubqueryScalarExpression(Expression case ExpressionType.Conditional: return ExpressionToSql.VisitConditional((ConditionalExpression)inputExpression, context); case ExpressionType.Constant: - return CosmosLinqSerializer.VisitConstant((ConstantExpression)inputExpression, context); + return context.CosmosLinqSerializer.VisitConstant((ConstantExpression)inputExpression, context); case ExpressionType.Parameter: return ExpressionToSql.VisitParameter((ParameterExpression)inputExpression, context); case ExpressionType.MemberAccess: @@ -300,7 +300,7 @@ private static SqlScalarExpression VisitMethodCallScalar(MethodCallExpression me object[] argumentsExpressions = (object[])((ConstantExpression)methodCallExpression.Arguments[1]).Value; foreach (object argument in argumentsExpressions) { - arguments.Add(CosmosLinqSerializer.VisitConstant(Expression.Constant(argument), context)); + arguments.Add(context.CosmosLinqSerializer.VisitConstant(Expression.Constant(argument), context)); } } else @@ -469,11 +469,11 @@ private static SqlScalarExpression VisitBinary(BinaryExpression inputExpression, if (left is SqlMemberIndexerScalarExpression && right is SqlLiteralScalarExpression literalScalarExpression) { - right = CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Left, literalScalarExpression); + right = context.CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Left, literalScalarExpression); } else if (right is SqlMemberIndexerScalarExpression && left is SqlLiteralScalarExpression sqlLiteralScalarExpression) { - left = CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Right, sqlLiteralScalarExpression); + left = context.CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Right, sqlLiteralScalarExpression); } return SqlBinaryScalarExpression.Create(op, left, right); @@ -635,7 +635,7 @@ private static SqlScalarExpression VisitParameter(ParameterExpression inputExpre private static SqlScalarExpression VisitMemberAccess(MemberExpression inputExpression, TranslationContext context) { SqlScalarExpression memberExpression = ExpressionToSql.VisitScalarExpression(inputExpression.Expression, context); - string memberName = inputExpression.Member.GetMemberName(context.linqSerializerOptions); + string memberName = inputExpression.Member.GetMemberName(context.CosmosLinqSerializer, context.LinqSerializerOptions); // If the resulting memberName is null, then the indexer should be on the root of the object. if (memberName == null) @@ -690,7 +690,7 @@ private static SqlScalarExpression[] VisitExpressionList(ReadOnlyCollectionThe scalar Any collection private static Collection ConvertToScalarAnyCollection(TranslationContext context) { - SqlQuery query = context.currentQuery.FlattenAsPossible().GetSqlQuery(); + SqlQuery query = context.CurrentQuery.FlattenAsPossible().GetSqlQuery(); SqlCollection subqueryCollection = SqlSubqueryCollection.Create(query); ParameterExpression parameterExpression = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName); Binding binding = new Binding(parameterExpression, subqueryCollection, isInCollection: false, isInputParameter: true); - context.currentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc()); - context.currentQuery.AddBinding(binding); + context.CurrentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc()); + context.CurrentQuery.AddBinding(binding); SqlSelectSpec selectSpec = SqlSelectValueSpec.Create( SqlBinaryScalarExpression.Create( @@ -869,7 +869,7 @@ private static Collection ConvertToScalarAnyCollection(TranslationContext contex SqlPropertyRefScalarExpression.Create(null, SqlIdentifier.Create(parameterExpression.Name))), SqlLiteralScalarExpression.Create(SqlNumberLiteral.Create(0)))); SqlSelectClause selectClause = SqlSelectClause.Create(selectSpec); - context.currentQuery.AddSelectClause(selectClause); + context.CurrentQuery.AddSelectClause(selectClause); return new Collection(LinqMethods.Any); } @@ -1010,106 +1010,106 @@ private static Collection VisitMethodCall(MethodCallExpression inputExpression, context.PushCollection(collection); Collection result = new Collection(inputExpression.Method.Name); - bool shouldBeOnNewQuery = context.currentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count); + bool shouldBeOnNewQuery = context.CurrentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count); context.PushSubqueryBinding(shouldBeOnNewQuery); switch (inputExpression.Method.Name) { case LinqMethods.Select: { SqlSelectClause select = ExpressionToSql.VisitSelect(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Where: { SqlWhereClause where = ExpressionToSql.VisitWhere(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddWhereClause(where, context); + context.CurrentQuery = context.CurrentQuery.AddWhereClause(where, context); break; } case LinqMethods.SelectMany: { - context.currentQuery = context.PackageCurrentQueryIfNeccessary(); + context.CurrentQuery = context.PackageCurrentQueryIfNeccessary(); result = ExpressionToSql.VisitSelectMany(inputExpression.Arguments, context); break; } case LinqMethods.OrderBy: { SqlOrderByClause orderBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, false, context); - context.currentQuery = context.currentQuery.AddOrderByClause(orderBy, context); + context.CurrentQuery = context.CurrentQuery.AddOrderByClause(orderBy, context); break; } case LinqMethods.OrderByDescending: { SqlOrderByClause orderBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, true, context); - context.currentQuery = context.currentQuery.AddOrderByClause(orderBy, context); + context.CurrentQuery = context.CurrentQuery.AddOrderByClause(orderBy, context); break; } case LinqMethods.ThenBy: { SqlOrderByClause thenBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, false, context); - context.currentQuery = context.currentQuery.UpdateOrderByClause(thenBy, context); + context.CurrentQuery = context.CurrentQuery.UpdateOrderByClause(thenBy, context); break; } case LinqMethods.ThenByDescending: { SqlOrderByClause thenBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, true, context); - context.currentQuery = context.currentQuery.UpdateOrderByClause(thenBy, context); + context.CurrentQuery = context.CurrentQuery.UpdateOrderByClause(thenBy, context); break; } case LinqMethods.Skip: { SqlOffsetSpec offsetSpec = ExpressionToSql.VisitSkip(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddOffsetSpec(offsetSpec, context); + context.CurrentQuery = context.CurrentQuery.AddOffsetSpec(offsetSpec, context); break; } case LinqMethods.Take: { - if (context.currentQuery.HasOffsetSpec()) + if (context.CurrentQuery.HasOffsetSpec()) { SqlLimitSpec limitSpec = ExpressionToSql.VisitTakeLimit(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddLimitSpec(limitSpec, context); + context.CurrentQuery = context.CurrentQuery.AddLimitSpec(limitSpec, context); } else { SqlTopSpec topSpec = ExpressionToSql.VisitTakeTop(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddTopSpec(topSpec); + context.CurrentQuery = context.CurrentQuery.AddTopSpec(topSpec); } break; } case LinqMethods.Distinct: { SqlSelectClause select = ExpressionToSql.VisitDistinct(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Max: { SqlSelectClause select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Max); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Min: { SqlSelectClause select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Min); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Average: { SqlSelectClause select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Avg); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Count: { SqlSelectClause select = ExpressionToSql.VisitCount(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Sum: { SqlSelectClause select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Sum); - context.currentQuery = context.currentQuery.AddSelectClause(select, context); + context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context); break; } case LinqMethods.Any: @@ -1119,7 +1119,7 @@ private static Collection VisitMethodCall(MethodCallExpression inputExpression, { // Any is translated to an SELECT VALUE EXISTS() where Any operation itself is treated as a Where. SqlWhereClause where = ExpressionToSql.VisitWhere(inputExpression.Arguments, context); - context.currentQuery = context.currentQuery.AddWhereClause(where, context); + context.CurrentQuery = context.CurrentQuery.AddWhereClause(where, context); } break; } @@ -1420,18 +1420,18 @@ private static SqlQuery CreateSubquery(Expression expression, ReadOnlyCollection { bool shouldBeOnNewQuery = context.CurrentSubqueryBinding.ShouldBeOnNewQuery; - QueryUnderConstruction queryBeforeVisit = context.currentQuery; - QueryUnderConstruction packagedQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc(), context.currentQuery); - packagedQuery.fromParameters.SetInputParameter(typeof(object), context.currentQuery.GetInputParameterInContext(shouldBeOnNewQuery).Name, context.InScope); - context.currentQuery = packagedQuery; + QueryUnderConstruction queryBeforeVisit = context.CurrentQuery; + QueryUnderConstruction packagedQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc(), context.CurrentQuery); + packagedQuery.fromParameters.SetInputParameter(typeof(object), context.CurrentQuery.GetInputParameterInContext(shouldBeOnNewQuery).Name, context.InScope); + context.CurrentQuery = packagedQuery; if (shouldBeOnNewQuery) context.CurrentSubqueryBinding.ShouldBeOnNewQuery = false; Collection collection = ExpressionToSql.VisitCollectionExpression(expression, parameters, context); - QueryUnderConstruction subquery = context.currentQuery.GetSubquery(queryBeforeVisit); + QueryUnderConstruction subquery = context.CurrentQuery.GetSubquery(queryBeforeVisit); context.CurrentSubqueryBinding.ShouldBeOnNewQuery = shouldBeOnNewQuery; - context.currentQuery = queryBeforeVisit; + context.CurrentQuery = queryBeforeVisit; SqlQuery sqlSubquery = subquery.FlattenAsPossible().GetSqlQuery(); return sqlSubquery; @@ -1502,7 +1502,7 @@ private static Collection VisitSelectMany(ReadOnlyCollection argumen SqlCollection subqueryCollection = SqlSubqueryCollection.Create(query); ParameterExpression parameterExpression = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName); binding = new Binding(parameterExpression, subqueryCollection, isInCollection: false, isInputParameter: true); - context.currentQuery.fromParameters.Add(binding); + context.CurrentQuery.fromParameters.Add(binding); } return collection; @@ -1739,7 +1739,7 @@ private static SqlSelectClause VisitCount( if (arguments.Count == 2) { SqlWhereClause whereClause = ExpressionToSql.VisitWhere(arguments, context); - context.currentQuery = context.currentQuery.AddWhereClause(whereClause, context); + context.CurrentQuery = context.CurrentQuery.AddWhereClause(whereClause, context); } else if (arguments.Count != 1) { diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs new file mode 100644 index 0000000000..d21d5c2fdd --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -0,0 +1,18 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.Linq +{ + using System.Linq.Expressions; + using System.Reflection; + using Microsoft.Azure.Cosmos.SqlObjects; + + internal interface ICosmosLinqSerializer + { + SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right); + + string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); + + SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context); + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Linq/QueryUnderConstruction.cs b/Microsoft.Azure.Cosmos/src/Linq/QueryUnderConstruction.cs index c5c0573a14..d2e19046e1 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/QueryUnderConstruction.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/QueryUnderConstruction.cs @@ -583,13 +583,13 @@ public QueryUnderConstruction AddOrderByClause(SqlOrderByClause orderBy, Transla public QueryUnderConstruction UpdateOrderByClause(SqlOrderByClause thenBy, TranslationContext context) { - List items = new List(context.currentQuery.orderByClause.OrderByItems); + List items = new List(context.CurrentQuery.orderByClause.OrderByItems); items.AddRange(thenBy.OrderByItems); - context.currentQuery.orderByClause = SqlOrderByClause.Create(items.ToImmutableArray()); + context.CurrentQuery.orderByClause = SqlOrderByClause.Create(items.ToImmutableArray()); - foreach (Binding binding in context.CurrentSubqueryBinding.TakeBindings()) context.currentQuery.AddBinding(binding); + foreach (Binding binding in context.CurrentSubqueryBinding.TakeBindings()) context.CurrentQuery.AddBinding(binding); - return context.currentQuery; + return context.CurrentQuery; } public QueryUnderConstruction AddOffsetSpec(SqlOffsetSpec offsetSpec, TranslationContext context) diff --git a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs index 8fc95d8701..24ecc117bf 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs @@ -7,7 +7,6 @@ namespace Microsoft.Azure.Cosmos.Linq using System; using System.Collections.Generic; using System.Linq.Expressions; - using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.SqlObjects; using static Microsoft.Azure.Cosmos.Linq.ExpressionToSql; using static Microsoft.Azure.Cosmos.Linq.FromParameterBindings; @@ -29,30 +28,39 @@ internal sealed class TranslationContext /// /// Query that is being assembled. /// - public QueryUnderConstruction currentQuery; + public QueryUnderConstruction CurrentQuery; /// /// Dictionary for parameter name and value /// - public IDictionary parameters; + public IDictionary Parameters; + + /// + /// The LINQ serializer + /// + public ICosmosLinqSerializer CosmosLinqSerializer; /// /// If the FROM clause uses a parameter name, it will be substituted for the parameter used in /// the lambda expressions for the WHERE and SELECT clauses. /// private ParameterSubstitution substitutions; + /// /// We are currently visiting these methods. /// private List methodStack; + /// /// Stack of parameters from lambdas currently in scope. /// private List lambdaParametersStack; + /// /// Stack of collection-valued inputs. /// private List collectionStack; + /// /// The stack of subquery binding information. /// @@ -65,14 +73,15 @@ public TranslationContext(CosmosLinqSerializerOptions linqSerializerOptions, IDi this.methodStack = new List(); this.lambdaParametersStack = new List(); this.collectionStack = new List(); - this.currentQuery = new QueryUnderConstruction(this.GetGenFreshParameterFunc()); + this.CurrentQuery = new QueryUnderConstruction(this.GetGenFreshParameterFunc()); this.subqueryBindingStack = new Stack(); - this.linqSerializerOptions = linqSerializerOptions; - this.parameters = parameters; + this.LinqSerializerOptions = linqSerializerOptions; + this.Parameters = parameters; this.memberNames = new MemberNames(linqSerializerOptions); + this.CosmosLinqSerializer = new CosmosLinqSerializer(); } - public CosmosLinqSerializerOptions linqSerializerOptions; + public CosmosLinqSerializerOptions LinqSerializerOptions; public Expression LookupSubstitution(ParameterExpression parameter) { @@ -103,12 +112,12 @@ public void PushParameter(ParameterExpression parameter, bool shouldBeOnNewQuery if (last.isOuter) { // substitute - ParameterExpression inputParam = this.currentQuery.GetInputParameterInContext(shouldBeOnNewQuery); + ParameterExpression inputParam = this.CurrentQuery.GetInputParameterInContext(shouldBeOnNewQuery); this.substitutions.AddSubstitution(parameter, inputParam); } else { - this.currentQuery.Bind(parameter, last.inner); + this.CurrentQuery.Bind(parameter, last.inner); } } @@ -182,7 +191,7 @@ public void PopCollection() /// Suggested name for the input parameter. public ParameterExpression SetInputParameter(Type type, string name) { - return this.currentQuery.fromParameters.SetInputParameter(type, name, this.InScope); + return this.CurrentQuery.fromParameters.SetInputParameter(type, name, this.InScope); } /// @@ -193,7 +202,7 @@ public ParameterExpression SetInputParameter(Type type, string name) public void SetFromParameter(ParameterExpression parameter, SqlCollection collection) { Binding binding = new Binding(parameter, collection, isInCollection: true); - this.currentQuery.fromParameters.Add(binding); + this.CurrentQuery.fromParameters.Add(binding); } /// @@ -258,11 +267,11 @@ public QueryUnderConstruction PackageCurrentQueryIfNeccessary() { if (this.CurrentSubqueryBinding.ShouldBeOnNewQuery) { - this.currentQuery = this.currentQuery.PackageQuery(this.InScope); + this.CurrentQuery = this.CurrentQuery.PackageQuery(this.InScope); this.CurrentSubqueryBinding.ShouldBeOnNewQuery = false; } - return this.currentQuery; + return this.CurrentQuery; } public class SubqueryBinding diff --git a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs index ededa0394b..01d99b5792 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs @@ -19,6 +19,11 @@ public static Type GetElementType(Type type) return GetElementType(type, new HashSet()); } + public static string GetMemberName(this MemberInfo memberInfo, ICosmosLinqSerializer serializer, CosmosLinqSerializerOptions linqSerializerOptions = null) + { + return serializer.GetMemberName(memberInfo, linqSerializerOptions); + } + private static Type GetElementType(Type type, HashSet visitedSet) { Debug.Assert(type != null); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs index 108e7bc0cf..64662bac36 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs @@ -173,10 +173,11 @@ public Datum2(string jsonProperty, string dataMember, string defaultMember, stri [TestMethod] public void TestAttributePriority() { - Assert.AreEqual("jsonProperty", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonProperty").First())); - Assert.AreEqual("dataMember", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("DataMember").First())); - Assert.AreEqual("Default", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("Default").First())); - Assert.AreEqual("jsonPropertyHasHigherPriority", CosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonPropertyAndDataMember").First())); + ICosmosLinqSerializer cosmosLinqSerializer = new CosmosLinqSerializer(); + Assert.AreEqual("jsonProperty", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonProperty").First())); + Assert.AreEqual("dataMember", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("DataMember").First())); + Assert.AreEqual("Default", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("Default").First())); + Assert.AreEqual("jsonPropertyHasHigherPriority", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonPropertyAndDataMember").First())); } /// From 085734f6f649f25b67389cdb3eec3727c6ff8d68 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 1 Nov 2023 11:47:12 -0700 Subject: [PATCH 03/14] PR comments and TranslationContext cleanup --- .../BuiltinFunctions/ArrayBuiltinFunctions.cs | 2 +- .../src/Linq/ExpressionToSQL.cs | 20 ++++++++++++++----- .../src/Linq/SQLTranslator.cs | 1 - .../src/Linq/TranslationContext.cs | 16 +++++++-------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs index 5d212ab929..d96ac56a22 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs @@ -80,7 +80,7 @@ private SqlScalarExpression VisitIN(Expression expression, ConstantExpression co List items = new List(); foreach (object item in (IEnumerable)constantExpressionList.Value) { - items.Add(context.CosmosLinqSerializer.VisitConstant(Expression.Constant(item), context)); + items.Add(ExpressionToSql.VisitConstant(Expression.Constant(item), context)); } // if the items list empty, then just return false expression diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 60e33ea568..b9a5d4acd6 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -244,7 +244,7 @@ internal static SqlScalarExpression VisitNonSubqueryScalarExpression(Expression case ExpressionType.Conditional: return ExpressionToSql.VisitConditional((ConditionalExpression)inputExpression, context); case ExpressionType.Constant: - return context.CosmosLinqSerializer.VisitConstant((ConstantExpression)inputExpression, context); + return ExpressionToSql.VisitConstant((ConstantExpression)inputExpression, context); case ExpressionType.Parameter: return ExpressionToSql.VisitParameter((ParameterExpression)inputExpression, context); case ExpressionType.MemberAccess: @@ -300,7 +300,7 @@ private static SqlScalarExpression VisitMethodCallScalar(MethodCallExpression me object[] argumentsExpressions = (object[])((ConstantExpression)methodCallExpression.Arguments[1]).Value; foreach (object argument in argumentsExpressions) { - arguments.Add(context.CosmosLinqSerializer.VisitConstant(Expression.Constant(argument), context)); + arguments.Add(ExpressionToSql.VisitConstant(Expression.Constant(argument), context)); } } else @@ -469,16 +469,21 @@ private static SqlScalarExpression VisitBinary(BinaryExpression inputExpression, if (left is SqlMemberIndexerScalarExpression && right is SqlLiteralScalarExpression literalScalarExpression) { - right = context.CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Left, literalScalarExpression); + right = ExpressionToSql.ApplyCustomConverters(inputExpression.Left, literalScalarExpression, context); } else if (right is SqlMemberIndexerScalarExpression && left is SqlLiteralScalarExpression sqlLiteralScalarExpression) { - left = context.CosmosLinqSerializer.ApplyCustomConverters(inputExpression.Right, sqlLiteralScalarExpression); + left = ExpressionToSql.ApplyCustomConverters(inputExpression.Right, sqlLiteralScalarExpression, context); } return SqlBinaryScalarExpression.Create(op, left, right); } + private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right, TranslationContext context) + { + return context.CosmosLinqSerializer.ApplyCustomConverters(left, right); + } + private static bool TryMatchStringCompareTo(MethodCallExpression left, ConstantExpression right, ExpressionType compareOperator) { if (left.Method.Equals(typeof(string).GetMethod("CompareTo", new Type[] { typeof(string) })) && left.Arguments.Count == 1) @@ -610,6 +615,11 @@ private static SqlScalarExpression VisitTypeIs(TypeBinaryExpression inputExpress throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, inputExpression.NodeType)); } + public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) + { + return context.CosmosLinqSerializer.VisitConstant(inputExpression, context); + } + private static SqlScalarExpression VisitConditional(ConditionalExpression inputExpression, TranslationContext context) { SqlScalarExpression conditionExpression = ExpressionToSql.VisitScalarExpression(inputExpression.Test, context); @@ -646,7 +656,7 @@ private static SqlScalarExpression VisitMemberAccess(MemberExpression inputExpre // if expression is nullable if (inputExpression.Expression.Type.IsNullable()) { - MemberNames memberNames = context.memberNames; + MemberNames memberNames = context.MemberNames; // ignore .Value if (memberName == memberNames.Value) diff --git a/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs b/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs index 8648019219..c0bd6d38a0 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs @@ -6,7 +6,6 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Collections.Generic; using System.Linq.Expressions; using Microsoft.Azure.Cosmos.Query.Core; - using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.SqlObjects; /// diff --git a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs index 24ecc117bf..5289e67cf7 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs @@ -19,7 +19,12 @@ internal sealed class TranslationContext /// /// Member names for special mapping cases /// - internal readonly MemberNames memberNames; + public readonly MemberNames MemberNames; + + /// + /// The LINQ serializer + /// + public readonly ICosmosLinqSerializer CosmosLinqSerializer; /// /// Set of parameters in scope at any point; used to generate fresh parameter names if necessary. @@ -35,10 +40,7 @@ internal sealed class TranslationContext /// public IDictionary Parameters; - /// - /// The LINQ serializer - /// - public ICosmosLinqSerializer CosmosLinqSerializer; + public CosmosLinqSerializerOptions LinqSerializerOptions; /// /// If the FROM clause uses a parameter name, it will be substituted for the parameter used in @@ -77,12 +79,10 @@ public TranslationContext(CosmosLinqSerializerOptions linqSerializerOptions, IDi this.subqueryBindingStack = new Stack(); this.LinqSerializerOptions = linqSerializerOptions; this.Parameters = parameters; - this.memberNames = new MemberNames(linqSerializerOptions); + this.MemberNames = new MemberNames(linqSerializerOptions); this.CosmosLinqSerializer = new CosmosLinqSerializer(); } - public CosmosLinqSerializerOptions LinqSerializerOptions; - public Expression LookupSubstitution(ParameterExpression parameter) { return this.substitutions.Lookup(parameter); From ff1c07768dab0177a2be50ee9ce6119e16e55500 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 1 Nov 2023 11:54:20 -0700 Subject: [PATCH 04/14] update params --- Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs | 6 +++--- Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index b9a5d4acd6..0794d87805 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -645,7 +645,7 @@ private static SqlScalarExpression VisitParameter(ParameterExpression inputExpre private static SqlScalarExpression VisitMemberAccess(MemberExpression inputExpression, TranslationContext context) { SqlScalarExpression memberExpression = ExpressionToSql.VisitScalarExpression(inputExpression.Expression, context); - string memberName = inputExpression.Member.GetMemberName(context.CosmosLinqSerializer, context.LinqSerializerOptions); + string memberName = inputExpression.Member.GetMemberName(context); // If the resulting memberName is null, then the indexer should be on the root of the object. if (memberName == null) @@ -700,7 +700,7 @@ private static SqlScalarExpression[] VisitExpressionList(ReadOnlyCollection()); } - public static string GetMemberName(this MemberInfo memberInfo, ICosmosLinqSerializer serializer, CosmosLinqSerializerOptions linqSerializerOptions = null) + public static string GetMemberName(this MemberInfo memberInfo, TranslationContext context) { - return serializer.GetMemberName(memberInfo, linqSerializerOptions); + return context.CosmosLinqSerializer.GetMemberName(memberInfo, context.LinqSerializerOptions); } private static Type GetElementType(Type type, HashSet visitedSet) From 9e2ba6321c012c2a12302b6e52fb07ed192cfd00 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 1 Nov 2023 13:34:29 -0700 Subject: [PATCH 05/14] fix parameters --- Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs | 8 ++++---- Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs | 2 +- Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs index 49c27f715f..7ea9a6f802 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs @@ -115,7 +115,7 @@ public SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScal return right; } - public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) + public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, IDictionary parameters) { if (inputExpression.Value == null) { @@ -124,10 +124,10 @@ public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, Tra if (inputExpression.Type.IsNullable()) { - return this.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), context); + return this.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), parameters); } - if (context.Parameters != null && context.Parameters.TryGetValue(inputExpression.Value, out string paramName)) + if (parameters != null && parameters.TryGetValue(inputExpression.Value, out string paramName)) { SqlParameter sqlParameter = SqlParameter.Create(paramName); return SqlParameterRefScalarExpression.Create(sqlParameter); @@ -171,7 +171,7 @@ public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, Tra foreach (object item in enumerable) { - arrayItems.Add(this.VisitConstant(Expression.Constant(item), context)); + arrayItems.Add(this.VisitConstant(Expression.Constant(item), parameters)); } return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 0794d87805..ed4642a284 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -617,7 +617,7 @@ private static SqlScalarExpression VisitTypeIs(TypeBinaryExpression inputExpress public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) { - return context.CosmosLinqSerializer.VisitConstant(inputExpression, context); + return context.CosmosLinqSerializer.VisitConstant(inputExpression, context.Parameters); } private static SqlScalarExpression VisitConditional(ConditionalExpression inputExpression, TranslationContext context) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index d21d5c2fdd..bd4995b11f 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -3,6 +3,7 @@ //------------------------------------------------------------ namespace Microsoft.Azure.Cosmos.Linq { + using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using Microsoft.Azure.Cosmos.SqlObjects; @@ -13,6 +14,6 @@ internal interface ICosmosLinqSerializer string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); - SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context); + SqlScalarExpression VisitConstant(ConstantExpression inputExpression, IDictionary parameters); } } \ No newline at end of file From 2d13dfca8e4153759d14158a41118289ec3f5a46 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 1 Nov 2023 15:28:40 -0700 Subject: [PATCH 06/14] PR comments --- ...smosElementToSqlScalarExpressionVisitor.cs | 90 +++++++++++++++++++ ...izer.cs => DefaultCosmosLinqSerializer.cs} | 87 +----------------- .../src/Linq/ExpressionToSQL.cs | 2 +- .../src/Linq/ICosmosLinqSerializer.cs | 13 ++- .../src/Linq/TranslationContext.cs | 10 ++- .../LinqAttributeContractBaselineTests.cs | 9 +- 6 files changed, 117 insertions(+), 94 deletions(-) create mode 100644 Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs rename Microsoft.Azure.Cosmos/src/Linq/{CosmosLinqSerializer.cs => DefaultCosmosLinqSerializer.cs} (71%) diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs new file mode 100644 index 0000000000..71a82d08db --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.Linq +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using Microsoft.Azure.Cosmos.CosmosElements; + using Microsoft.Azure.Cosmos.CosmosElements.Numbers; + using Microsoft.Azure.Cosmos.SqlObjects; + + internal sealed class CosmosElementToSqlScalarExpressionVisitor : ICosmosElementVisitor + { + public static readonly CosmosElementToSqlScalarExpressionVisitor Singleton = new CosmosElementToSqlScalarExpressionVisitor(); + + private CosmosElementToSqlScalarExpressionVisitor() + { + // Private constructor, since this class is a singleton. + } + + public SqlScalarExpression Visit(CosmosArray cosmosArray) + { + List items = new List(); + foreach (CosmosElement item in cosmosArray) + { + items.Add(item.Accept(this)); + } + + return SqlArrayCreateScalarExpression.Create(items.ToImmutableArray()); + } + + public SqlScalarExpression Visit(CosmosBinary cosmosBinary) + { + // Can not convert binary to scalar expression without knowing the API type. + throw new NotImplementedException(); + } + + public SqlScalarExpression Visit(CosmosBoolean cosmosBoolean) + { + return SqlLiteralScalarExpression.Create(SqlBooleanLiteral.Create(cosmosBoolean.Value)); + } + + public SqlScalarExpression Visit(CosmosGuid cosmosGuid) + { + // Can not convert guid to scalar expression without knowing the API type. + throw new NotImplementedException(); + } + + public SqlScalarExpression Visit(CosmosNull cosmosNull) + { + return SqlLiteralScalarExpression.Create(SqlNullLiteral.Create()); + } + + public SqlScalarExpression Visit(CosmosNumber cosmosNumber) + { + if (!(cosmosNumber is CosmosNumber64 cosmosNumber64)) + { + throw new ArgumentException($"Unknown {nameof(CosmosNumber)} type: {cosmosNumber.GetType()}."); + } + + return SqlLiteralScalarExpression.Create(SqlNumberLiteral.Create(cosmosNumber64.GetValue())); + } + + public SqlScalarExpression Visit(CosmosObject cosmosObject) + { + List properties = new List(); + foreach (KeyValuePair prop in cosmosObject) + { + SqlPropertyName name = SqlPropertyName.Create(prop.Key); + CosmosElement value = prop.Value; + SqlScalarExpression expression = value.Accept(this); + SqlObjectProperty property = SqlObjectProperty.Create(name, expression); + properties.Add(property); + } + + return SqlObjectCreateScalarExpression.Create(properties.ToImmutableArray()); + } + + public SqlScalarExpression Visit(CosmosString cosmosString) + { + return SqlLiteralScalarExpression.Create(SqlStringLiteral.Create(cosmosString.Value)); + } + + public SqlScalarExpression Visit(CosmosUndefined cosmosUndefined) + { + return SqlLiteralScalarExpression.Create(SqlUndefinedLiteral.Create()); + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs similarity index 71% rename from Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs rename to Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index 7ea9a6f802..be03376a98 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -14,13 +14,12 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Reflection; using System.Runtime.Serialization; using Microsoft.Azure.Cosmos.CosmosElements; - using Microsoft.Azure.Cosmos.CosmosElements.Numbers; using Microsoft.Azure.Cosmos.Spatial; using Microsoft.Azure.Cosmos.SqlObjects; using Microsoft.Azure.Documents; using Newtonsoft.Json; - internal class CosmosLinqSerializer : ICosmosLinqSerializer + internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializer { public SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right) { @@ -115,7 +114,7 @@ public SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScal return right; } - public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, IDictionary parameters) + public SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters) { if (inputExpression.Value == null) { @@ -124,7 +123,7 @@ public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, IDi if (inputExpression.Type.IsNullable()) { - return this.VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), parameters); + return this.ConvertToSqlScalarExpression(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), parameters); } if (parameters != null && parameters.TryGetValue(inputExpression.Value, out string paramName)) @@ -171,7 +170,7 @@ public SqlScalarExpression VisitConstant(ConstantExpression inputExpression, IDi foreach (object item in enumerable) { - arrayItems.Add(this.VisitConstant(Expression.Constant(item), parameters)); + arrayItems.Add(this.ConvertToSqlScalarExpression(Expression.Constant(item), parameters)); } return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); @@ -223,83 +222,5 @@ public string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions l return memberName; } - - private sealed class CosmosElementToSqlScalarExpressionVisitor : ICosmosElementVisitor - { - public static readonly CosmosElementToSqlScalarExpressionVisitor Singleton = new CosmosElementToSqlScalarExpressionVisitor(); - - private CosmosElementToSqlScalarExpressionVisitor() - { - // Private constructor, since this class is a singleton. - } - - public SqlScalarExpression Visit(CosmosArray cosmosArray) - { - List items = new List(); - foreach (CosmosElement item in cosmosArray) - { - items.Add(item.Accept(this)); - } - - return SqlArrayCreateScalarExpression.Create(items.ToImmutableArray()); - } - - public SqlScalarExpression Visit(CosmosBinary cosmosBinary) - { - // Can not convert binary to scalar expression without knowing the API type. - throw new NotImplementedException(); - } - - public SqlScalarExpression Visit(CosmosBoolean cosmosBoolean) - { - return SqlLiteralScalarExpression.Create(SqlBooleanLiteral.Create(cosmosBoolean.Value)); - } - - public SqlScalarExpression Visit(CosmosGuid cosmosGuid) - { - // Can not convert guid to scalar expression without knowing the API type. - throw new NotImplementedException(); - } - - public SqlScalarExpression Visit(CosmosNull cosmosNull) - { - return SqlLiteralScalarExpression.Create(SqlNullLiteral.Create()); - } - - public SqlScalarExpression Visit(CosmosNumber cosmosNumber) - { - if (!(cosmosNumber is CosmosNumber64 cosmosNumber64)) - { - throw new ArgumentException($"Unknown {nameof(CosmosNumber)} type: {cosmosNumber.GetType()}."); - } - - return SqlLiteralScalarExpression.Create(SqlNumberLiteral.Create(cosmosNumber64.GetValue())); - } - - public SqlScalarExpression Visit(CosmosObject cosmosObject) - { - List properties = new List(); - foreach (KeyValuePair prop in cosmosObject) - { - SqlPropertyName name = SqlPropertyName.Create(prop.Key); - CosmosElement value = prop.Value; - SqlScalarExpression expression = value.Accept(this); - SqlObjectProperty property = SqlObjectProperty.Create(name, expression); - properties.Add(property); - } - - return SqlObjectCreateScalarExpression.Create(properties.ToImmutableArray()); - } - - public SqlScalarExpression Visit(CosmosString cosmosString) - { - return SqlLiteralScalarExpression.Create(SqlStringLiteral.Create(cosmosString.Value)); - } - - public SqlScalarExpression Visit(CosmosUndefined cosmosUndefined) - { - return SqlLiteralScalarExpression.Create(SqlUndefinedLiteral.Create()); - } - } } } diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index ed4642a284..d9c73ac2e8 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -617,7 +617,7 @@ private static SqlScalarExpression VisitTypeIs(TypeBinaryExpression inputExpress public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) { - return context.CosmosLinqSerializer.VisitConstant(inputExpression, context.Parameters); + return context.CosmosLinqSerializer.ConvertToSqlScalarExpression(inputExpression, context.Parameters); } private static SqlScalarExpression VisitConditional(ConditionalExpression inputExpression, TranslationContext context) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index bd4995b11f..64ca31f71f 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -10,10 +10,19 @@ namespace Microsoft.Azure.Cosmos.Linq internal interface ICosmosLinqSerializer { + /// + /// Applies specified custom converters to an expression. + /// SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right); - string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); + /// + /// Serializes a ConstantExpression as a SqlScalarExpression. + /// + SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters); - SqlScalarExpression VisitConstant(ConstantExpression inputExpression, IDictionary parameters); + /// + /// Gets a member name with any LINQ serializer options applied. + /// + string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs index 5289e67cf7..f5d53bd1e7 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs @@ -26,10 +26,16 @@ internal sealed class TranslationContext /// public readonly ICosmosLinqSerializer CosmosLinqSerializer; + /// + /// User-provided LINQ serializer options + /// + public CosmosLinqSerializerOptions LinqSerializerOptions; + /// /// Set of parameters in scope at any point; used to generate fresh parameter names if necessary. /// public HashSet InScope; + /// /// Query that is being assembled. /// @@ -40,8 +46,6 @@ internal sealed class TranslationContext /// public IDictionary Parameters; - public CosmosLinqSerializerOptions LinqSerializerOptions; - /// /// If the FROM clause uses a parameter name, it will be substituted for the parameter used in /// the lambda expressions for the WHERE and SELECT clauses. @@ -80,7 +84,7 @@ public TranslationContext(CosmosLinqSerializerOptions linqSerializerOptions, IDi this.LinqSerializerOptions = linqSerializerOptions; this.Parameters = parameters; this.MemberNames = new MemberNames(linqSerializerOptions); - this.CosmosLinqSerializer = new CosmosLinqSerializer(); + this.CosmosLinqSerializer = new DefaultCosmosLinqSerializer(); } public Expression LookupSubstitution(ParameterExpression parameter) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs index 64662bac36..9a94e45a60 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs @@ -9,14 +9,13 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using VisualStudio.TestTools.UnitTesting; + using System.Threading.Tasks; using BaselineTest; using Microsoft.Azure.Cosmos.Linq; using Microsoft.Azure.Cosmos.SDK.EmulatorTests; using Microsoft.Azure.Documents; - using System.Threading.Tasks; + using Newtonsoft.Json; + using VisualStudio.TestTools.UnitTesting; /// /// Class that tests to see that we honor the attributes for members in a class / struct when we create LINQ queries. @@ -173,7 +172,7 @@ public Datum2(string jsonProperty, string dataMember, string defaultMember, stri [TestMethod] public void TestAttributePriority() { - ICosmosLinqSerializer cosmosLinqSerializer = new CosmosLinqSerializer(); + ICosmosLinqSerializer cosmosLinqSerializer = new DefaultCosmosLinqSerializer(); Assert.AreEqual("jsonProperty", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonProperty").First())); Assert.AreEqual("dataMember", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("DataMember").First())); Assert.AreEqual("Default", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("Default").First())); From 0e15171610b57d353d56fc076ca82ca6afe9fb48 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Thu, 2 Nov 2023 13:39:26 -0700 Subject: [PATCH 07/14] PR comments --- .../src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs | 7 +++++-- Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs | 5 ++--- Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs index 71a82d08db..68232330de 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosElementToSqlScalarExpressionVisitor.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Linq using System; using System.Collections.Generic; using System.Collections.Immutable; + using System.Diagnostics; using Microsoft.Azure.Cosmos.CosmosElements; using Microsoft.Azure.Cosmos.CosmosElements.Numbers; using Microsoft.Azure.Cosmos.SqlObjects; @@ -33,7 +34,8 @@ public SqlScalarExpression Visit(CosmosArray cosmosArray) public SqlScalarExpression Visit(CosmosBinary cosmosBinary) { // Can not convert binary to scalar expression without knowing the API type. - throw new NotImplementedException(); + Debug.Fail("CosmosElementToSqlScalarExpressionVisitor Assert", "Unreachable"); + throw new InvalidOperationException(); } public SqlScalarExpression Visit(CosmosBoolean cosmosBoolean) @@ -44,7 +46,8 @@ public SqlScalarExpression Visit(CosmosBoolean cosmosBoolean) public SqlScalarExpression Visit(CosmosGuid cosmosGuid) { // Can not convert guid to scalar expression without knowing the API type. - throw new NotImplementedException(); + Debug.Fail("CosmosElementToSqlScalarExpressionVisitor Assert", "Unreachable"); + throw new InvalidOperationException(); } public SqlScalarExpression Visit(CosmosNull cosmosNull) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index d9c73ac2e8..79ead1949f 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -1356,7 +1356,7 @@ private static SqlScalarExpression VisitScalarExpression(Expression expression, ParameterExpression parameterExpression = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName); SqlCollection subqueryCollection = ExpressionToSql.CreateSubquerySqlCollection( - query, context, + query, isMinMaxAvgMethod ? SubqueryKind.ArrayScalarExpression : expressionObjKind.Value); Binding newBinding = new Binding(parameterExpression, subqueryCollection, @@ -1383,9 +1383,8 @@ private static SqlScalarExpression VisitScalarExpression(Expression expression, /// Create a subquery SQL collection object for a SQL query /// /// The SQL query object - /// The translation context /// The subquery type - private static SqlCollection CreateSubquerySqlCollection(SqlQuery query, TranslationContext context, SubqueryKind subqueryType) + private static SqlCollection CreateSubquerySqlCollection(SqlQuery query, SubqueryKind subqueryType) { SqlCollection subqueryCollection; switch (subqueryType) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index 64ca31f71f..e360644a50 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -21,7 +21,7 @@ internal interface ICosmosLinqSerializer SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters); /// - /// Gets a member name with any LINQ serializer options applied. + /// Gets a member name with LINQ serializer options applied. /// string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); } From 1a5692c1e6e144b1d064738d8a8c664a534a081b Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Thu, 2 Nov 2023 14:36:15 -0700 Subject: [PATCH 08/14] PR comments --- .../src/Linq/DefaultCosmosLinqSerializer.cs | 194 +++++------------- .../src/Linq/ExpressionToSQL.cs | 84 +++++++- .../src/Linq/ICosmosLinqSerializer.cs | 3 +- 3 files changed, 141 insertions(+), 140 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index be03376a98..b101ae9104 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -4,9 +4,7 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; - using System.Collections; using System.Collections.Generic; - using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -14,168 +12,88 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Reflection; using System.Runtime.Serialization; using Microsoft.Azure.Cosmos.CosmosElements; - using Microsoft.Azure.Cosmos.Spatial; using Microsoft.Azure.Cosmos.SqlObjects; using Microsoft.Azure.Documents; using Newtonsoft.Json; internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializer { - public SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right) + public SqlScalarExpression ApplyCustomConverters(MemberExpression memberExpression, Type memberType, SqlLiteralScalarExpression sqlLiteralScalarExpression) { - MemberExpression memberExpression; - if (left is UnaryExpression unaryExpression) + // There are two ways to specify a custom attribute + // 1- by specifying the JsonConverterAttribute on a Class/Enum + // [JsonConverter(typeof(StringEnumConverter))] + // Enum MyEnum + // { + // ... + // } + // + // 2- by specifying the JsonConverterAttribute on a property + // class MyClass + // { + // [JsonConverter(typeof(StringEnumConverter))] + // public MyEnum MyEnum; + // } + // + // Newtonsoft gives high precedence to the attribute specified + // on a property over on a type (class/enum) + // so we check both attributes and apply the same precedence rules + // JsonConverterAttribute doesn't allow duplicates so it's safe to + // use FirstOrDefault() + CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); + CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); + + CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; + if (converterAttribute != null) { - memberExpression = unaryExpression.Operand as MemberExpression; - } - else - { - memberExpression = left as MemberExpression; - } + Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); - if (memberExpression != null) - { - Type memberType = memberExpression.Type; - if (memberType.IsNullable()) - { - memberType = memberType.NullableUnderlyingType(); - } + Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; - // There are two ways to specify a custom attribute - // 1- by specifying the JsonConverterAttribute on a Class/Enum - // [JsonConverter(typeof(StringEnumConverter))] - // Enum MyEnum - // { - // ... - // } - // - // 2- by specifying the JsonConverterAttribute on a property - // class MyClass - // { - // [JsonConverter(typeof(StringEnumConverter))] - // public MyEnum MyEnum; - // } - // - // Newtonsoft gives high precedence to the attribute specified - // on a property over on a type (class/enum) - // so we check both attributes and apply the same precedence rules - // JsonConverterAttribute doesn't allow duplicates so it's safe to - // use FirstOrDefault() - CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); - CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); - - CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; - if (converterAttribute != null) + object value = default(object); + // Enum + if (memberType.IsEnum()) { - Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); - - Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; - - object value = default(object); - // Enum - if (memberType.IsEnum()) - { - Number64 number64 = ((SqlNumberLiteral)right.Literal).Value; - if (number64.IsDouble) - { - value = Enum.ToObject(memberType, Number64.ToDouble(number64)); - } - else - { - value = Enum.ToObject(memberType, Number64.ToLong(number64)); - } - - } - // DateTime - else if (memberType == typeof(DateTime)) + Number64 number64 = ((SqlNumberLiteral)sqlLiteralScalarExpression.Literal).Value; + if (number64.IsDouble) { - SqlStringLiteral serializedDateTime = (SqlStringLiteral)right.Literal; - value = DateTime.Parse(serializedDateTime.Value, provider: null, DateTimeStyles.RoundtripKind); + value = Enum.ToObject(memberType, Number64.ToDouble(number64)); } - - if (value != default(object)) + else { - string serializedValue; - - if (converterType.GetConstructor(Type.EmptyTypes) != null) - { - serializedValue = JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType)); - } - else - { - serializedValue = JsonConvert.SerializeObject(value); - } - - return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); + value = Enum.ToObject(memberType, Number64.ToLong(number64)); } - } - } - - return right; - } - - public SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters) - { - if (inputExpression.Value == null) - { - return SqlLiteralScalarExpression.SqlNullLiteralScalarExpression; - } - - if (inputExpression.Type.IsNullable()) - { - return this.ConvertToSqlScalarExpression(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), parameters); - } - if (parameters != null && parameters.TryGetValue(inputExpression.Value, out string paramName)) - { - SqlParameter sqlParameter = SqlParameter.Create(paramName); - return SqlParameterRefScalarExpression.Create(sqlParameter); - } - - Type constantType = inputExpression.Value.GetType(); - if (constantType.IsValueType()) - { - if (inputExpression.Value is bool boolValue) - { - SqlBooleanLiteral literal = SqlBooleanLiteral.Create(boolValue); - return SqlLiteralScalarExpression.Create(literal); } - - if (ExpressionToSql.TryGetSqlNumberLiteral(inputExpression.Value, out SqlNumberLiteral numberLiteral)) + // DateTime + else if (memberType == typeof(DateTime)) { - return SqlLiteralScalarExpression.Create(numberLiteral); + SqlStringLiteral serializedDateTime = (SqlStringLiteral)sqlLiteralScalarExpression.Literal; + value = DateTime.Parse(serializedDateTime.Value, provider: null, DateTimeStyles.RoundtripKind); } - if (inputExpression.Value is Guid guidValue) + if (value != default(object)) { - SqlStringLiteral literal = SqlStringLiteral.Create(guidValue.ToString()); - return SqlLiteralScalarExpression.Create(literal); - } - } - - if (inputExpression.Value is string stringValue) - { - SqlStringLiteral literal = SqlStringLiteral.Create(stringValue); - return SqlLiteralScalarExpression.Create(literal); - } + string serializedValue; - if (typeof(Geometry).IsAssignableFrom(constantType)) - { - return GeometrySqlExpressionFactory.Construct(inputExpression); - } - - if (inputExpression.Value is IEnumerable enumerable) - { - List arrayItems = new List(); + if (converterType.GetConstructor(Type.EmptyTypes) != null) + { + serializedValue = JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType)); + } + else + { + serializedValue = JsonConvert.SerializeObject(value); + } - foreach (object item in enumerable) - { - arrayItems.Add(this.ConvertToSqlScalarExpression(Expression.Constant(item), parameters)); + return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); } - - return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); } + return sqlLiteralScalarExpression; + } + + public SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters) + { return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); } diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 79ead1949f..3946748608 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -4,6 +4,7 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; + using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; @@ -481,7 +482,28 @@ private static SqlScalarExpression VisitBinary(BinaryExpression inputExpression, private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right, TranslationContext context) { - return context.CosmosLinqSerializer.ApplyCustomConverters(left, right); + MemberExpression memberExpression; + if (left is UnaryExpression unaryExpression) + { + memberExpression = unaryExpression.Operand as MemberExpression; + } + else + { + memberExpression = left as MemberExpression; + } + + Type memberType = memberExpression.Type; + if (memberType.IsNullable()) + { + memberType = memberType.NullableUnderlyingType(); + } + + if (memberExpression != null) + { + return context.CosmosLinqSerializer.ApplyCustomConverters(memberExpression, memberType, right); + } + + return right; } private static bool TryMatchStringCompareTo(MethodCallExpression left, ConstantExpression right, ExpressionType compareOperator) @@ -617,6 +639,66 @@ private static SqlScalarExpression VisitTypeIs(TypeBinaryExpression inputExpress public static SqlScalarExpression VisitConstant(ConstantExpression inputExpression, TranslationContext context) { + if (inputExpression.Value == null) + { + return SqlLiteralScalarExpression.SqlNullLiteralScalarExpression; + } + + if (inputExpression.Type.IsNullable()) + { + return VisitConstant(Expression.Constant(inputExpression.Value, Nullable.GetUnderlyingType(inputExpression.Type)), context); + } + + if (context.Parameters != null && context.Parameters.TryGetValue(inputExpression.Value, out string paramName)) + { + SqlParameter sqlParameter = SqlParameter.Create(paramName); + return SqlParameterRefScalarExpression.Create(sqlParameter); + } + + Type constantType = inputExpression.Value.GetType(); + if (constantType.IsValueType) + { + if (inputExpression.Value is bool boolValue) + { + SqlBooleanLiteral literal = SqlBooleanLiteral.Create(boolValue); + return SqlLiteralScalarExpression.Create(literal); + } + + if (ExpressionToSql.TryGetSqlNumberLiteral(inputExpression.Value, out SqlNumberLiteral numberLiteral)) + { + return SqlLiteralScalarExpression.Create(numberLiteral); + } + + if (inputExpression.Value is Guid guidValue) + { + SqlStringLiteral literal = SqlStringLiteral.Create(guidValue.ToString()); + return SqlLiteralScalarExpression.Create(literal); + } + } + + if (inputExpression.Value is string stringValue) + { + SqlStringLiteral literal = SqlStringLiteral.Create(stringValue); + return SqlLiteralScalarExpression.Create(literal); + } + + if (typeof(Geometry).IsAssignableFrom(constantType)) + { + return GeometrySqlExpressionFactory.Construct(inputExpression); + } + + if (inputExpression.Value is IEnumerable enumerable) + { + List arrayItems = new List(); + + foreach (object item in enumerable) + { + arrayItems.Add(VisitConstant(Expression.Constant(item), context)); + } + + return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); + } + return context.CosmosLinqSerializer.ConvertToSqlScalarExpression(inputExpression, context.Parameters); } diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index e360644a50..45e1ebd16b 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -3,6 +3,7 @@ //------------------------------------------------------------ namespace Microsoft.Azure.Cosmos.Linq { + using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -13,7 +14,7 @@ internal interface ICosmosLinqSerializer /// /// Applies specified custom converters to an expression. /// - SqlScalarExpression ApplyCustomConverters(Expression left, SqlLiteralScalarExpression right); + SqlScalarExpression ApplyCustomConverters(MemberExpression memberExpression, Type memberType, SqlLiteralScalarExpression sqlLiteralScalarExpression); /// /// Serializes a ConstantExpression as a SqlScalarExpression. From ab89f8167360c0e673ff5cc824c9eceea496ef24 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Thu, 2 Nov 2023 15:30:39 -0700 Subject: [PATCH 09/14] simplifying serializer class --- .../src/Linq/DefaultCosmosLinqSerializer.cs | 62 ++++--------------- .../src/Linq/ExpressionToSQL.cs | 58 ++++++++++++++--- .../src/Linq/ICosmosLinqSerializer.cs | 9 ++- 3 files changed, 69 insertions(+), 60 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index b101ae9104..e827951ca4 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -5,8 +5,6 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; using System.Collections.Generic; - using System.Diagnostics; - using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -18,7 +16,7 @@ namespace Microsoft.Azure.Cosmos.Linq internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializer { - public SqlScalarExpression ApplyCustomConverters(MemberExpression memberExpression, Type memberType, SqlLiteralScalarExpression sqlLiteralScalarExpression) + public CustomAttributeData GetConverterAttribute(MemberExpression memberExpression, Type memberType) { // There are two ways to specify a custom attribute // 1- by specifying the JsonConverterAttribute on a Class/Enum @@ -40,56 +38,18 @@ public SqlScalarExpression ApplyCustomConverters(MemberExpression memberExpressi // so we check both attributes and apply the same precedence rules // JsonConverterAttribute doesn't allow duplicates so it's safe to // use FirstOrDefault() - CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); - CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().Where(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)).FirstOrDefault(); - - CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; - if (converterAttribute != null) - { - Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); - - Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; - - object value = default(object); - // Enum - if (memberType.IsEnum()) - { - Number64 number64 = ((SqlNumberLiteral)sqlLiteralScalarExpression.Literal).Value; - if (number64.IsDouble) - { - value = Enum.ToObject(memberType, Number64.ToDouble(number64)); - } - else - { - value = Enum.ToObject(memberType, Number64.ToLong(number64)); - } - - } - // DateTime - else if (memberType == typeof(DateTime)) - { - SqlStringLiteral serializedDateTime = (SqlStringLiteral)sqlLiteralScalarExpression.Literal; - value = DateTime.Parse(serializedDateTime.Value, provider: null, DateTimeStyles.RoundtripKind); - } - - if (value != default(object)) - { - string serializedValue; - - if (converterType.GetConstructor(Type.EmptyTypes) != null) - { - serializedValue = JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType)); - } - else - { - serializedValue = JsonConvert.SerializeObject(value); - } + CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); + CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); + return memberAttribute ?? typeAttribute; + } - return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); - } - } + public string SerializeWithConverter(object value, Type converterType) + { + string serializedValue = converterType.GetConstructor(Type.EmptyTypes) != null + ? JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType)) + : JsonConvert.SerializeObject(value); - return sqlLiteralScalarExpression; + return serializedValue; } public SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 3946748608..c9aafa48a4 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -13,8 +13,11 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Linq; using System.Linq.Expressions; using System.Reflection; + using Microsoft.Azure.Cosmos.CosmosElements; using Microsoft.Azure.Cosmos.Spatial; using Microsoft.Azure.Cosmos.SqlObjects; + using Microsoft.Azure.Documents; + using Newtonsoft.Json; using static Microsoft.Azure.Cosmos.Linq.FromParameterBindings; // ReSharper disable UnusedParameter.Local @@ -492,15 +495,56 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit memberExpression = left as MemberExpression; } - Type memberType = memberExpression.Type; - if (memberType.IsNullable()) - { - memberType = memberType.NullableUnderlyingType(); - } - if (memberExpression != null) { - return context.CosmosLinqSerializer.ApplyCustomConverters(memberExpression, memberType, right); + Type memberType = memberExpression.Type; + if (memberType.IsNullable()) + { + memberType = memberType.NullableUnderlyingType(); + } + + CustomAttributeData converterAttribute = context.CosmosLinqSerializer.GetConverterAttribute(memberExpression, memberType); + if (converterAttribute != null) + { + Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); + + Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; + + object value = default(object); + // Enum + if (memberType.IsEnum()) + { + try + { + Number64 number64 = ((SqlNumberLiteral)right.Literal).Value; + if (number64.IsDouble) + { + value = Enum.ToObject(memberType, Number64.ToDouble(number64)); + } + else + { + value = Enum.ToObject(memberType, Number64.ToLong(number64)); + } + } + catch + { + value = ((SqlStringLiteral)right.Literal).Value; + } + + } + // DateTime + else if (memberType == typeof(DateTime)) + { + SqlStringLiteral serializedDateTime = (SqlStringLiteral)right.Literal; + value = DateTime.Parse(serializedDateTime.Value, provider: null, DateTimeStyles.RoundtripKind); + } + + if (value != default(object)) + { + string serializedValue = context.CosmosLinqSerializer.SerializeWithConverter(value, converterType); + return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); + } + } } return right; diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index 45e1ebd16b..c942fc04aa 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -12,9 +12,14 @@ namespace Microsoft.Azure.Cosmos.Linq internal interface ICosmosLinqSerializer { /// - /// Applies specified custom converters to an expression. + /// Gets custom attributes on a member expression. Returns null if none exist. /// - SqlScalarExpression ApplyCustomConverters(MemberExpression memberExpression, Type memberType, SqlLiteralScalarExpression sqlLiteralScalarExpression); + CustomAttributeData GetConverterAttribute(MemberExpression memberExpression, Type memberType); + + /// + /// Applies specified custom converter to an object. + /// + string SerializeWithConverter(object value, Type converterType); /// /// Serializes a ConstantExpression as a SqlScalarExpression. From c887719e00bc362c267fa90b2894cfa8ab8336e0 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Fri, 3 Nov 2023 08:38:22 -0700 Subject: [PATCH 10/14] interface updates --- .../src/Linq/DefaultCosmosLinqSerializer.cs | 4 ++-- Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs | 4 +++- Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index e827951ca4..4a735aef29 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -52,9 +52,9 @@ public string SerializeWithConverter(object value, Type converterType) return serializedValue; } - public SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters) + public string SerializeScalarExpression(ConstantExpression inputExpression) { - return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); + return JsonConvert.SerializeObject(inputExpression.Value); } public string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index c9aafa48a4..ee784407cb 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -743,7 +743,9 @@ public static SqlScalarExpression VisitConstant(ConstantExpression inputExpressi return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray()); } - return context.CosmosLinqSerializer.ConvertToSqlScalarExpression(inputExpression, context.Parameters); + string serializedConstant = context.CosmosLinqSerializer.SerializeScalarExpression(inputExpression); + + return CosmosElement.Parse(serializedConstant).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); } private static SqlScalarExpression VisitConditional(ConditionalExpression inputExpression, TranslationContext context) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index c942fc04aa..a8f66355dc 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -24,7 +24,7 @@ internal interface ICosmosLinqSerializer /// /// Serializes a ConstantExpression as a SqlScalarExpression. /// - SqlScalarExpression ConvertToSqlScalarExpression(ConstantExpression inputExpression, IDictionary parameters); + string SerializeScalarExpression(ConstantExpression inputExpression); /// /// Gets a member name with LINQ serializer options applied. From a7d81cfdf89b5992f22b3a29cfb96e0160919cdf Mon Sep 17 00:00:00 2001 From: Maya Painter <130110800+Maya-Painter@users.noreply.github.com> Date: Fri, 3 Nov 2023 08:40:41 -0700 Subject: [PATCH 11/14] Update docs --- Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index a8f66355dc..d0021a5c17 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -17,12 +17,12 @@ internal interface ICosmosLinqSerializer CustomAttributeData GetConverterAttribute(MemberExpression memberExpression, Type memberType); /// - /// Applies specified custom converter to an object. + /// Serializes object with provided custom converter. /// string SerializeWithConverter(object value, Type converterType); /// - /// Serializes a ConstantExpression as a SqlScalarExpression. + /// Serializes a ConstantExpression. /// string SerializeScalarExpression(ConstantExpression inputExpression); @@ -31,4 +31,4 @@ internal interface ICosmosLinqSerializer /// string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); } -} \ No newline at end of file +} From 090d94992849b72c3baf43f0fc7866afb08ce6a5 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Tue, 7 Nov 2023 10:47:06 -0800 Subject: [PATCH 12/14] PR comments --- .../src/Linq/DefaultCosmosLinqSerializer.cs | 18 ++++++++++++------ .../src/Linq/ExpressionToSQL.cs | 10 +++------- .../src/Linq/ICosmosLinqSerializer.cs | 10 ++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index 4a735aef29..f54e5255c2 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -4,19 +4,17 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; - using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.Serialization; - using Microsoft.Azure.Cosmos.CosmosElements; - using Microsoft.Azure.Cosmos.SqlObjects; using Microsoft.Azure.Documents; using Newtonsoft.Json; internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializer { - public CustomAttributeData GetConverterAttribute(MemberExpression memberExpression, Type memberType) + public bool HasCustomAttribute(MemberExpression memberExpression, Type memberType) { // There are two ways to specify a custom attribute // 1- by specifying the JsonConverterAttribute on a Class/Enum @@ -40,11 +38,19 @@ public CustomAttributeData GetConverterAttribute(MemberExpression memberExpressi // use FirstOrDefault() CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); - return memberAttribute ?? typeAttribute; + + return memberAttribute != null || typeAttribute != null; } - public string SerializeWithConverter(object value, Type converterType) + public string Serialize(object value, MemberExpression memberExpression, Type memberType) { + CustomAttributeData memberAttribute = memberExpression.Member.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); + CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); + CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; + + Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); + Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; + string serializedValue = converterType.GetConstructor(Type.EmptyTypes) != null ? JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType)) : JsonConvert.SerializeObject(value); diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index ee784407cb..a186b625a1 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -503,13 +503,9 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit memberType = memberType.NullableUnderlyingType(); } - CustomAttributeData converterAttribute = context.CosmosLinqSerializer.GetConverterAttribute(memberExpression, memberType); - if (converterAttribute != null) + bool hasConverterAttribute = context.CosmosLinqSerializer.HasCustomAttribute(memberExpression, memberType); + if (hasConverterAttribute) { - Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); - - Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; - object value = default(object); // Enum if (memberType.IsEnum()) @@ -541,7 +537,7 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit if (value != default(object)) { - string serializedValue = context.CosmosLinqSerializer.SerializeWithConverter(value, converterType); + string serializedValue = context.CosmosLinqSerializer.Serialize(value, memberExpression, memberType); return CosmosElement.Parse(serializedValue).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton); } } diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index d0021a5c17..df69bc208c 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -4,22 +4,20 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; - using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; - using Microsoft.Azure.Cosmos.SqlObjects; internal interface ICosmosLinqSerializer { /// - /// Gets custom attributes on a member expression. Returns null if none exist. + /// Returns true if there are custom attributes on a member expression. /// - CustomAttributeData GetConverterAttribute(MemberExpression memberExpression, Type memberType); + bool HasCustomAttribute(MemberExpression memberExpression, Type memberType); /// - /// Serializes object with provided custom converter. + /// Serializes object. /// - string SerializeWithConverter(object value, Type converterType); + string Serialize(object value, MemberExpression memberExpression, Type memberType); /// /// Serializes a ConstantExpression. From ad0881afd6ba162f19dfb9c18b27d250213ac249 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Tue, 7 Nov 2023 11:47:55 -0800 Subject: [PATCH 13/14] PR comments --- .../src/Linq/DefaultCosmosLinqSerializer.cs | 2 +- Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs | 4 ++-- Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index f54e5255c2..01f041116b 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -14,7 +14,7 @@ namespace Microsoft.Azure.Cosmos.Linq internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializer { - public bool HasCustomAttribute(MemberExpression memberExpression, Type memberType) + public bool RequiresCustomSerialization(MemberExpression memberExpression, Type memberType) { // There are two ways to specify a custom attribute // 1- by specifying the JsonConverterAttribute on a Class/Enum diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index a186b625a1..5153f29115 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -503,8 +503,8 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit memberType = memberType.NullableUnderlyingType(); } - bool hasConverterAttribute = context.CosmosLinqSerializer.HasCustomAttribute(memberExpression, memberType); - if (hasConverterAttribute) + bool requiresCustomSerializatior = context.CosmosLinqSerializer.RequiresCustomSerialization(memberExpression, memberType); + if (requiresCustomSerializatior) { object value = default(object); // Enum diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index df69bc208c..278fbe38ee 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -12,8 +12,9 @@ internal interface ICosmosLinqSerializer /// /// Returns true if there are custom attributes on a member expression. /// - bool HasCustomAttribute(MemberExpression memberExpression, Type memberType); + bool RequiresCustomSerialization(MemberExpression memberExpression, Type memberType); + // TODO : Clean up this interface member for better generalizability /// /// Serializes object. /// From 5dac2192b676d47e46c3653fe7166119a16c55f7 Mon Sep 17 00:00:00 2001 From: Maya-Painter Date: Wed, 8 Nov 2023 09:38:40 -0800 Subject: [PATCH 14/14] PR comments - rename and fix assert --- .../src/Linq/DefaultCosmosLinqSerializer.cs | 4 ++-- Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs | 4 ++-- Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs | 2 +- .../Linq/LinqAttributeContractBaselineTests.cs | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index 01f041116b..024c33fab0 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -48,7 +48,7 @@ public string Serialize(object value, MemberExpression memberExpression, Type me CustomAttributeData typeAttribute = memberType.GetsCustomAttributes().FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute)); CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute; - Debug.Assert(converterAttribute.ConstructorArguments.Count > 0); + Debug.Assert(converterAttribute.ConstructorArguments.Count > 0, $"{nameof(DefaultCosmosLinqSerializer)} Assert!", "At least one constructor argument exists."); Type converterType = (Type)converterAttribute.ConstructorArguments[0].Value; string serializedValue = converterType.GetConstructor(Type.EmptyTypes) != null @@ -63,7 +63,7 @@ public string SerializeScalarExpression(ConstantExpression inputExpression) return JsonConvert.SerializeObject(inputExpression.Value); } - public string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) + public string SerializeMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) { string memberName = null; diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs index 278fbe38ee..f31490832d 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs @@ -26,8 +26,8 @@ internal interface ICosmosLinqSerializer string SerializeScalarExpression(ConstantExpression inputExpression); /// - /// Gets a member name with LINQ serializer options applied. + /// Serializes a member name with LINQ serializer options applied. /// - string GetMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); + string SerializeMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); } } diff --git a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs index 4d9e8d1639..e11c145c71 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs @@ -21,7 +21,7 @@ public static Type GetElementType(Type type) public static string GetMemberName(this MemberInfo memberInfo, TranslationContext context) { - return context.CosmosLinqSerializer.GetMemberName(memberInfo, context.LinqSerializerOptions); + return context.CosmosLinqSerializer.SerializeMemberName(memberInfo, context.LinqSerializerOptions); } private static Type GetElementType(Type type, HashSet visitedSet) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs index 9a94e45a60..5ea03bf0b6 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqAttributeContractBaselineTests.cs @@ -173,10 +173,10 @@ public Datum2(string jsonProperty, string dataMember, string defaultMember, stri public void TestAttributePriority() { ICosmosLinqSerializer cosmosLinqSerializer = new DefaultCosmosLinqSerializer(); - Assert.AreEqual("jsonProperty", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonProperty").First())); - Assert.AreEqual("dataMember", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("DataMember").First())); - Assert.AreEqual("Default", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("Default").First())); - Assert.AreEqual("jsonPropertyHasHigherPriority", cosmosLinqSerializer.GetMemberName(typeof(Datum).GetMember("JsonPropertyAndDataMember").First())); + Assert.AreEqual("jsonProperty", cosmosLinqSerializer.SerializeMemberName(typeof(Datum).GetMember("JsonProperty").First())); + Assert.AreEqual("dataMember", cosmosLinqSerializer.SerializeMemberName(typeof(Datum).GetMember("DataMember").First())); + Assert.AreEqual("Default", cosmosLinqSerializer.SerializeMemberName(typeof(Datum).GetMember("Default").First())); + Assert.AreEqual("jsonPropertyHasHigherPriority", cosmosLinqSerializer.SerializeMemberName(typeof(Datum).GetMember("JsonPropertyAndDataMember").First())); } ///