Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0094685
initial commit
Maya-Painter Oct 18, 2023
5ab7959
add more tests
Maya-Painter Oct 18, 2023
260a6fc
cleanup
Maya-Painter Oct 19, 2023
2583c8e
camel case fix
Maya-Painter Oct 19, 2023
3fe6006
fix precidence and add test
Maya-Painter Oct 19, 2023
6ebafd2
cleanup
Maya-Painter Oct 19, 2023
61082f5
cleanup
Maya-Painter Oct 26, 2023
28a08c1
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Oct 26, 2023
ce9b2a8
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Nov 8, 2023
3ad6110
Implement serializers
Maya-Painter Nov 8, 2023
a0da2c3
clenaup and bugfix
Maya-Painter Nov 8, 2023
656c278
cleanup constructors
Maya-Painter Nov 9, 2023
32ba80d
fix baseline
Maya-Painter Nov 9, 2023
74e2410
updates
Maya-Painter Nov 9, 2023
573b1e4
cleanup
Maya-Painter Nov 9, 2023
d6b6f9a
removed requirement to decorate enums
Maya-Painter Nov 10, 2023
0dec53d
nit PR comments
Maya-Painter Nov 13, 2023
6a2d26a
bug fix and api updates
Maya-Painter Nov 13, 2023
6821838
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Nov 13, 2023
ab6be11
Pr comments
Maya-Painter Nov 13, 2023
7e9f1ea
remove datacontract and newtonsoft serializer types
Maya-Painter Nov 14, 2023
739e914
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Nov 15, 2023
4e34878
Pr comments and adding tests
Maya-Painter Nov 15, 2023
36c5e67
updates
Maya-Painter Nov 16, 2023
238bcdd
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Nov 16, 2023
0c2bc46
Aggregate tests and new serializer member
Maya-Painter Nov 16, 2023
e45c2a2
More tests and cleanup
Maya-Painter Nov 17, 2023
be90c91
PR comments
Maya-Painter Nov 17, 2023
3186741
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Nov 28, 2023
22e05dd
PR comments
Maya-Painter Nov 29, 2023
4b52602
PR comments - internal options class
Maya-Painter Nov 30, 2023
a62eb58
Update Program.cs
Maya-Painter Nov 30, 2023
dead02e
comment fixes
Maya-Painter Nov 30, 2023
1f23678
PR comments
Maya-Painter Dec 6, 2023
436573e
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Dec 6, 2023
0a3a7fc
Add preview flag
Maya-Painter Dec 6, 2023
981b56c
API changes
Maya-Painter Dec 6, 2023
c1f2737
new serialization interface
Maya-Painter Dec 12, 2023
86c4515
Update API and serializer updates
Maya-Painter Dec 12, 2023
5593fd0
API fix
Maya-Painter Dec 13, 2023
ad38065
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Dec 14, 2023
125198d
cleanup
Maya-Painter Dec 14, 2023
dc0afd3
PR comments
Maya-Painter Dec 16, 2023
be43ed6
update interface
Maya-Painter Dec 18, 2023
ed82751
fix
Maya-Painter Dec 18, 2023
81e29e2
API
Maya-Painter Dec 19, 2023
6684fc8
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Jan 2, 2024
7eba850
Update Microsoft.Azure.Cosmos/src/Serializer/ICosmosLinqSerializer.cs
Maya-Painter Jan 2, 2024
10ed71e
Some PR comments
Maya-Painter Jan 2, 2024
0bd4a26
adding sample code
Maya-Painter Jan 3, 2024
4153a5e
Enum rename and interfact to abstract class
Maya-Painter Jan 3, 2024
7055cf5
Merge branch 'master' into users/mayapainter/LinqCustomSerialization
Maya-Painter Jan 3, 2024
ee3afe8
PR comments
Maya-Painter Jan 3, 2024
ad1a222
PR comments
Maya-Painter Jan 4, 2024
8efedf1
Remove CosmosLinqSerializerType
Maya-Painter Jan 4, 2024
fa402f9
last one (hopefully)
Maya-Painter Jan 5, 2024
7e2bbd5
Update Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs
Maya-Painter Jan 5, 2024
090642a
Update Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs
Maya-Painter Jan 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 67 additions & 12 deletions Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ namespace Microsoft.Azure.Cosmos.Linq
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.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;
using Newtonsoft.Json.Serialization;
using static Microsoft.Azure.Cosmos.Linq.FromParameterBindings;

// ReSharper disable UnusedParameter.Local
Expand Down Expand Up @@ -169,7 +173,7 @@ private static Collection TranslateInput(ConstantExpression inputExpression, Tra
}

/// <summary>
/// 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.
/// </summary>
Expand All @@ -189,7 +193,7 @@ private static string GetBindingParameterName(TranslationContext context)
}
}

if (parameterName == null) parameterName = ExpressionToSql.DefaultParameterName;
parameterName ??= ExpressionToSql.DefaultParameterName;

return parameterName;
}
Expand Down Expand Up @@ -524,10 +528,10 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit
// 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 = memberExpression.Member.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute));
converterAttribute ??= memberType.GetsCustomAttributes().FirstOrDefault(ca => ca.AttributeType == typeof(Newtonsoft.Json.JsonConverterAttribute));
converterAttribute ??= memberExpression.Member.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(System.Text.Json.Serialization.JsonConverterAttribute));

CustomAttributeData converterAttribute = memberAttribute ?? typeAttribute;
if (converterAttribute != null)
{
Debug.Assert(converterAttribute.ConstructorArguments.Count > 0);
Expand All @@ -538,14 +542,21 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit
// Enum
if (memberType.IsEnum())
{
Number64 number64 = ((SqlNumberLiteral)right.Literal).Value;
if (number64.IsDouble)
try
{
value = Enum.ToObject(memberType, Number64.ToDouble(number64));
Number64 number64 = ((SqlNumberLiteral)right.Literal).Value;
if (number64.IsDouble)
{
value = Enum.ToObject(memberType, Number64.ToDouble(number64));
}
else
{
value = Enum.ToObject(memberType, Number64.ToLong(number64));
}
}
else
catch
{
value = Enum.ToObject(memberType, Number64.ToLong(number64));
value = ((SqlStringLiteral)right.Literal).Value;
}

}
Expand All @@ -561,8 +572,19 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit
string serializedValue;

if (converterType.GetConstructor(Type.EmptyTypes) != null)
{
serializedValue = JsonConvert.SerializeObject(value, (JsonConverter)Activator.CreateInstance(converterType));
{
if (converterAttribute.AttributeType == typeof(System.Text.Json.Serialization.JsonConverterAttribute))
{
// DotNet serializer
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
serializedValue = System.Text.Json.JsonSerializer.Serialize(value, options);
}
else
{
// Newtonsoft serializer
serializedValue = JsonConvert.SerializeObject(value, (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(converterType));
}
}
else
{
Expand Down Expand Up @@ -770,6 +792,39 @@ public static SqlScalarExpression VisitConstant(ConstantExpression inputExpressi
return SqlArrayCreateScalarExpression.Create(arrayItems.ToImmutableArray());
}

// if ANY property is decorated with Newtonsoft JsonPropertyAttribute, we serialize all with this serializer
PropertyInfo[] propInfo = inputExpression.Value.GetType().GetProperties();
bool hasCustomAttributesNewtonsoft = propInfo.Any(property => property.GetCustomAttributes().Any(attribute => attribute.GetType() == typeof(JsonPropertyAttribute)));

bool hasCustomAttributesDataMember = inputExpression.Type.CustomAttributes != null && inputExpression.Type.CustomAttributes.Count() > 0;

if (hasCustomAttributesNewtonsoft || hasCustomAttributesDataMember )
{
JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
if (context.linqSerializerOptions != null && context.linqSerializerOptions.PropertyNamingPolicy == CosmosPropertyNamingPolicy.CamelCase)
{
serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}

return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value, serializerSettings)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton);
}

// Use DotNet custom serializer if provided
if (context.linqSerializerOptions?.CustomCosmosSerializer != null)
{
StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);

using (Stream stream = context.linqSerializerOptions.CustomCosmosSerializer.ToStream(inputExpression.Value))
{
using (StreamReader streamReader = new StreamReader(stream))
{
string propertyValue = streamReader.ReadToEnd();
writer.Write(propertyValue);
return CosmosElement.Parse(writer.ToString()).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton);
}
}
}

return CosmosElement.Parse(JsonConvert.SerializeObject(inputExpression.Value)).Accept(CosmosElementToSqlScalarExpressionVisitor.Singleton);
}

Expand Down
21 changes: 13 additions & 8 deletions Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Cosmos.Linq
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Microsoft.Azure.Cosmos.Serializer;
using System.Text.Json.Serialization;
using Microsoft.Azure.Documents;
using Newtonsoft.Json;

Expand All @@ -27,14 +27,13 @@ public static string GetMemberName(this MemberInfo memberInfo, CosmosLinqSeriali
string memberName = null;

// Check if Newtonsoft JsonExtensionDataAttribute is present on the member, if so, return empty member name.
JsonExtensionDataAttribute jsonExtensionDataAttribute = memberInfo.GetCustomAttribute<JsonExtensionDataAttribute>(true);
Newtonsoft.Json.JsonExtensionDataAttribute jsonExtensionDataAttribute = memberInfo.GetCustomAttribute<Newtonsoft.Json.JsonExtensionDataAttribute>(true);
if (jsonExtensionDataAttribute != null && jsonExtensionDataAttribute.ReadData)
{
return null;
}

// Json.Net honors JsonPropertyAttribute more than DataMemberAttribute
// So we check for JsonPropertyAttribute first.
// Precedence is (highest to lowest) : JsonPropertyAttribute, DataMemberAttribute, JsonPropertyNameAttribute
JsonPropertyAttribute jsonPropertyAttribute = memberInfo.GetCustomAttribute<JsonPropertyAttribute>(true);
if (jsonPropertyAttribute != null && !string.IsNullOrEmpty(jsonPropertyAttribute.PropertyName))
{
Expand All @@ -51,13 +50,19 @@ public static string GetMemberName(this MemberInfo memberInfo, CosmosLinqSeriali
memberName = dataMemberAttribute.Name;
}
}
else
{
JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute<JsonPropertyNameAttribute>(true);
if (jsonPropertyNameAttribute != null && !string.IsNullOrEmpty(jsonPropertyNameAttribute.Name))
{
memberName = jsonPropertyNameAttribute.Name;
}
}
}

if (memberName == null)
{
memberName = memberInfo.Name;
}
memberName ??= memberInfo.Name;

// Apply camel casing if specified
if (linqSerializerOptions != null)
{
memberName = CosmosSerializationUtil.GetStringWithPropertyNamingPolicy(linqSerializerOptions, memberName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,11 +502,14 @@ public override IOrderedQueryable<T> GetItemLinqQueryable<T>(
{
requestOptions ??= new QueryRequestOptions();

if (linqSerializerOptions == null && this.ClientContext.ClientOptions.SerializerOptions != null)
if (linqSerializerOptions == null && this.ClientContext.ClientOptions != null)
{
linqSerializerOptions = new CosmosLinqSerializerOptions
{
PropertyNamingPolicy = this.ClientContext.ClientOptions.SerializerOptions.PropertyNamingPolicy
PropertyNamingPolicy = this.ClientContext.ClientOptions.SerializerOptions != null
? this.ClientContext.ClientOptions.SerializerOptions.PropertyNamingPolicy
: CosmosPropertyNamingPolicy.Default,
CustomCosmosSerializer = this.ClientContext.ClientOptions.Serializer
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ public CosmosLinqSerializerOptions()
this.PropertyNamingPolicy = CosmosPropertyNamingPolicy.Default;
}

/// <summary>
/// Gets or sets the user defined customer serializer. If no customer serializer was defined,
/// then the value is set to the default value
/// </summary>
/// <remarks>
/// The default value is null
/// </remarks>
internal CosmosSerializer CustomCosmosSerializer { get; set; }

/// <summary>
/// Gets or sets whether the naming policy used to convert a string-based name to another format,
/// such as a camel-casing format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,17 @@

namespace Microsoft.Azure.Cosmos
{
using Microsoft.Azure.Cosmos.Serializer;
using Newtonsoft.Json.Serialization;

internal static class CosmosSerializationUtil
{
private static readonly CamelCaseNamingStrategy camelCaseNamingStrategy = new CamelCaseNamingStrategy();

internal static string ToCamelCase(string name)
{
return CosmosSerializationUtil.camelCaseNamingStrategy.GetPropertyName(name, false);
}

internal static string GetStringWithPropertyNamingPolicy(CosmosLinqSerializerOptions options, string name)
{
if (options != null && options.PropertyNamingPolicy == CosmosPropertyNamingPolicy.CamelCase)
{
return CosmosSerializationUtil.ToCamelCase(name);
return CosmosSerializationUtil.camelCaseNamingStrategy.GetPropertyName(name, false);
}

return name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ FROM root]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE {"dbl": 0, "str": null, "b": false, "dblArray": null, "inside": {"x": 0, "y": 0, "id": null, "pk": null}, "id": null, "pk": null, "NetExtensionData": {"OtherTest": 1.5}, "test": 1.5}
SELECT VALUE {"dbl": 0, "b": false, "inside": {"x": 0, "y": 0}, "NetExtensionData": {"OtherTest": 1.5}, "test": 1.5}
FROM root]]></SqlQuery>
</Output>
</Result>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ WHERE (root["numericFieldDataMember"] = 1)]]></SqlQuery>
<SqlQuery><![CDATA[
SELECT VALUE root
FROM root
WHERE (root = {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null})]]></SqlQuery>
WHERE (root = {"numericFieldDataMember": 1, "stringFieldDataMember": "1", "id": null, "pk": null})]]></SqlQuery>
<InputData><![CDATA[[
"{\"NumericField\": 0, \"StringField\": \"0\", \"id\": \"0-True\", \"Pk\": \"Test\"}",
"{\"NumericField\": 1, \"StringField\": \"1\", \"id\": \"1-True\", \"Pk\": \"Test\"}",
Expand All @@ -48,7 +48,7 @@ WHERE (root = {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id":
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null}
SELECT VALUE {"numericFieldDataMember": 1, "stringFieldDataMember": "1", "id": null, "pk": null}
FROM root]]></SqlQuery>
<InputData><![CDATA[[
"{\"NumericField\": 0, \"StringField\": \"0\", \"id\": \"0-True\", \"Pk\": \"Test\"}",
Expand All @@ -75,7 +75,7 @@ FROM root]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[
SELECT VALUE ((root["numericFieldDataMember"] > 1) ? {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null} : {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null})
SELECT VALUE ((root["numericFieldDataMember"] > 1) ? {"numericFieldDataMember": 1, "stringFieldDataMember": "1", "id": null, "pk": null} : {"numericFieldDataMember": 1, "stringFieldDataMember": "1", "id": null, "pk": null})
FROM root]]></SqlQuery>
<InputData><![CDATA[[
"{\"NumericField\": 0, \"StringField\": \"0\", \"id\": \"0-True\", \"Pk\": \"Test\"}",
Expand Down
Loading