diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQuery.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQuery.cs index f8400229b2..a4c4c5e938 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQuery.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQuery.cs @@ -12,8 +12,8 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Diagnostics; - using Microsoft.Azure.Cosmos.Query; using Microsoft.Azure.Cosmos.Query.Core; + using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.Tracing; using Newtonsoft.Json; @@ -32,7 +32,7 @@ internal sealed class CosmosLinqQuery : IDocumentQuery, IOrderedQueryable< private readonly QueryRequestOptions cosmosQueryRequestOptions; private readonly bool allowSynchronousQueryExecution = false; private readonly string continuationToken; - private readonly CosmosLinqSerializerOptions linqSerializationOptions; + private readonly CosmosLinqSerializerOptionsInternal linqSerializationOptions; public CosmosLinqQuery( ContainerInternal container, @@ -42,7 +42,7 @@ public CosmosLinqQuery( QueryRequestOptions cosmosQueryRequestOptions, Expression expression, bool allowSynchronousQueryExecution, - CosmosLinqSerializerOptions linqSerializationOptions = null) + CosmosLinqSerializerOptionsInternal linqSerializationOptions = null) { this.container = container ?? throw new ArgumentNullException(nameof(container)); this.responseFactory = responseFactory ?? throw new ArgumentNullException(nameof(responseFactory)); @@ -72,7 +72,7 @@ public CosmosLinqQuery( string continuationToken, QueryRequestOptions cosmosQueryRequestOptions, bool allowSynchronousQueryExecution, - CosmosLinqSerializerOptions linqSerializerOptions = null) + CosmosLinqSerializerOptionsInternal linqSerializerOptions = null) : this( container, responseFactory, diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQueryProvider.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQueryProvider.cs index 8ba71d7b98..2c695c6477 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQueryProvider.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqQueryProvider.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.Cosmos.Linq { using System; - using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -24,7 +23,7 @@ internal sealed class CosmosLinqQueryProvider : IQueryProvider private readonly bool allowSynchronousQueryExecution; private readonly Action onExecuteScalarQueryCallback; private readonly string continuationToken; - private readonly CosmosLinqSerializerOptions linqSerializerOptions; + private readonly CosmosLinqSerializerOptionsInternal linqSerializerOptions; public CosmosLinqQueryProvider( ContainerInternal container, @@ -34,7 +33,7 @@ public CosmosLinqQueryProvider( QueryRequestOptions cosmosQueryRequestOptions, bool allowSynchronousQueryExecution, Action onExecuteScalarQueryCallback = null, - CosmosLinqSerializerOptions linqSerializerOptions = null) + CosmosLinqSerializerOptionsInternal linqSerializerOptions = null) { this.container = container; this.responseFactory = responseFactory; diff --git a/Microsoft.Azure.Cosmos/src/Linq/CustomCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/CustomCosmosLinqSerializer.cs new file mode 100644 index 0000000000..03f7c9cff6 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Linq/CustomCosmosLinqSerializer.cs @@ -0,0 +1,56 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.Linq +{ + using System; + using System.Globalization; + using System.IO; + using System.Linq.Expressions; + using System.Reflection; + + internal class CustomCosmosLinqSerializer : ICosmosLinqSerializerInternal + { + private readonly CosmosLinqSerializer CustomCosmosSerializer; + + public CustomCosmosLinqSerializer(CosmosLinqSerializer customCosmosLinqSerializer) + { + this.CustomCosmosSerializer = customCosmosLinqSerializer; + } + + public bool RequiresCustomSerialization(MemberExpression memberExpression, Type memberType) + { + return true; + } + + public string Serialize(object value, MemberExpression memberExpression, Type memberType) + { + return this.SerializeWithCustomSerializer(value); + } + + public string SerializeScalarExpression(ConstantExpression inputExpression) + { + return this.SerializeWithCustomSerializer(inputExpression.Value); + } + + public string SerializeMemberName(MemberInfo memberInfo) + { + return this.CustomCosmosSerializer.SerializeMemberName(memberInfo); + } + + private string SerializeWithCustomSerializer(object value) + { + StringWriter writer = new StringWriter(CultureInfo.InvariantCulture); + + using (Stream stream = this.CustomCosmosSerializer.ToStream(value)) + { + using (StreamReader streamReader = new StreamReader(stream)) + { + string propertyValue = streamReader.ReadToEnd(); + writer.Write(propertyValue); + return writer.ToString(); + } + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs index 024c33fab0..31f20d5059 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DefaultCosmosLinqSerializer.cs @@ -12,8 +12,15 @@ namespace Microsoft.Azure.Cosmos.Linq using Microsoft.Azure.Documents; using Newtonsoft.Json; - internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializer + internal class DefaultCosmosLinqSerializer : ICosmosLinqSerializerInternal { + private readonly CosmosPropertyNamingPolicy PropertyNamingPolicy; + + public DefaultCosmosLinqSerializer(CosmosPropertyNamingPolicy propertyNamingPolicy) + { + this.PropertyNamingPolicy = propertyNamingPolicy; + } + public bool RequiresCustomSerialization(MemberExpression memberExpression, Type memberType) { // There are two ways to specify a custom attribute @@ -63,9 +70,9 @@ public string SerializeScalarExpression(ConstantExpression inputExpression) return JsonConvert.SerializeObject(inputExpression.Value); } - public string SerializeMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null) + public string SerializeMemberName(MemberInfo memberInfo) { - string memberName = null; + string memberName = memberInfo.Name; // Check if Newtonsoft JsonExtensionDataAttribute is present on the member, if so, return empty member name. Newtonsoft.Json.JsonExtensionDataAttribute jsonExtensionDataAttribute = memberInfo.GetCustomAttribute(true); @@ -94,15 +101,7 @@ public string SerializeMemberName(MemberInfo memberInfo, CosmosLinqSerializerOpt } } - if (memberName == null) - { - memberName = memberInfo.Name; - } - - if (linqSerializerOptions != null) - { - memberName = CosmosSerializationUtil.GetStringWithPropertyNamingPolicy(linqSerializerOptions, memberName); - } + memberName = CosmosSerializationUtil.GetStringWithPropertyNamingPolicy(this.PropertyNamingPolicy, memberName); return memberName; } diff --git a/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs b/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs index c81f5a9645..da25188bc2 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs @@ -17,7 +17,7 @@ internal static class DocumentQueryEvaluator public static SqlQuerySpec Evaluate( Expression expression, - CosmosLinqSerializerOptions linqSerializerOptions = null, + CosmosLinqSerializerOptionsInternal linqSerializerOptions = null, IDictionary parameters = null) { switch (expression.NodeType) @@ -76,7 +76,7 @@ private static SqlQuerySpec HandleEmptyQuery(ConstantExpression expression) private static SqlQuerySpec HandleMethodCallExpression( MethodCallExpression expression, IDictionary parameters, - CosmosLinqSerializerOptions linqSerializerOptions = null) + CosmosLinqSerializerOptionsInternal linqSerializerOptions = null) { if (DocumentQueryEvaluator.IsTransformExpression(expression)) { diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 5153f29115..6be1e82e9c 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -14,10 +14,10 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Linq.Expressions; using System.Reflection; using Microsoft.Azure.Cosmos.CosmosElements; + using Microsoft.Azure.Cosmos.Serializer; 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 @@ -88,7 +88,7 @@ public static class LinqMethods public static SqlQuery TranslateQuery( Expression inputExpression, IDictionary parameters, - CosmosLinqSerializerOptions linqSerializerOptions) + CosmosLinqSerializerOptionsInternal linqSerializerOptions) { TranslationContext context = new TranslationContext(linqSerializerOptions, parameters); ExpressionToSql.Translate(inputExpression, context); // ignore result here @@ -503,8 +503,8 @@ private static SqlScalarExpression ApplyCustomConverters(Expression left, SqlLit memberType = memberType.NullableUnderlyingType(); } - bool requiresCustomSerializatior = context.CosmosLinqSerializer.RequiresCustomSerialization(memberExpression, memberType); - if (requiresCustomSerializatior) + bool requiresCustomSerialization = context.CosmosLinqSerializer.RequiresCustomSerialization(memberExpression, memberType); + if (requiresCustomSerialization) { object value = default(object); // Enum diff --git a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializerInternal.cs similarity index 86% rename from Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs rename to Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializerInternal.cs index f31490832d..7a3054e3b6 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ICosmosLinqSerializerInternal.cs @@ -7,7 +7,7 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Linq.Expressions; using System.Reflection; - internal interface ICosmosLinqSerializer + internal interface ICosmosLinqSerializerInternal { /// /// Returns true if there are custom attributes on a member expression. @@ -28,6 +28,6 @@ internal interface ICosmosLinqSerializer /// /// Serializes a member name with LINQ serializer options applied. /// - string SerializeMemberName(MemberInfo memberInfo, CosmosLinqSerializerOptions linqSerializerOptions = null); + string SerializeMemberName(MemberInfo memberInfo); } } diff --git a/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs b/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs index c0bd6d38a0..8491d9cf0c 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/SQLTranslator.cs @@ -6,6 +6,7 @@ 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; /// @@ -21,7 +22,7 @@ internal static class SqlTranslator /// A string describing the expression translation. internal static string TranslateExpression( Expression inputExpression, - CosmosLinqSerializerOptions linqSerializerOptions = null) + CosmosLinqSerializerOptionsInternal linqSerializerOptions = null) { TranslationContext context = new TranslationContext(linqSerializerOptions); @@ -32,7 +33,7 @@ internal static string TranslateExpression( internal static string TranslateExpressionOld( Expression inputExpression, - CosmosLinqSerializerOptions linqSerializerOptions = null) + CosmosLinqSerializerOptionsInternal linqSerializerOptions = null) { TranslationContext context = new TranslationContext(linqSerializerOptions); @@ -43,7 +44,7 @@ internal static string TranslateExpressionOld( internal static SqlQuerySpec TranslateQuery( Expression inputExpression, - CosmosLinqSerializerOptions linqSerializerOptions, + CosmosLinqSerializerOptionsInternal linqSerializerOptions, IDictionary parameters) { inputExpression = ConstantEvaluator.PartialEval(inputExpression); diff --git a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs index f5d53bd1e7..3b1d9cd20b 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs @@ -7,6 +7,7 @@ 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; @@ -24,12 +25,7 @@ internal sealed class TranslationContext /// /// The LINQ serializer /// - public readonly ICosmosLinqSerializer CosmosLinqSerializer; - - /// - /// User-provided LINQ serializer options - /// - public CosmosLinqSerializerOptions LinqSerializerOptions; + public readonly ICosmosLinqSerializerInternal CosmosLinqSerializer; /// /// Set of parameters in scope at any point; used to generate fresh parameter names if necessary. @@ -72,7 +68,11 @@ internal sealed class TranslationContext /// private Stack subqueryBindingStack; - public TranslationContext(CosmosLinqSerializerOptions linqSerializerOptions, IDictionary parameters = null) + private static readonly ICosmosLinqSerializerInternal DefaultLinqSerializer = new DefaultCosmosLinqSerializer(new CosmosLinqSerializerOptions().PropertyNamingPolicy); + + private static readonly MemberNames DefaultMemberNames = new MemberNames(new CosmosLinqSerializerOptions()); + + public TranslationContext(CosmosLinqSerializerOptionsInternal linqSerializerOptionsInternal, IDictionary parameters = null) { this.InScope = new HashSet(); this.substitutions = new ParameterSubstitution(); @@ -81,10 +81,25 @@ public TranslationContext(CosmosLinqSerializerOptions linqSerializerOptions, IDi this.collectionStack = new List(); this.CurrentQuery = new QueryUnderConstruction(this.GetGenFreshParameterFunc()); this.subqueryBindingStack = new Stack(); - this.LinqSerializerOptions = linqSerializerOptions; this.Parameters = parameters; - this.MemberNames = new MemberNames(linqSerializerOptions); - this.CosmosLinqSerializer = new DefaultCosmosLinqSerializer(); + + if (linqSerializerOptionsInternal?.CustomCosmosLinqSerializer != null) + { + this.CosmosLinqSerializer = new CustomCosmosLinqSerializer(linqSerializerOptionsInternal.CustomCosmosLinqSerializer); + this.MemberNames = new MemberNames(new CosmosLinqSerializerOptions()); + } + else if (linqSerializerOptionsInternal?.CosmosLinqSerializerOptions != null) + { + CosmosLinqSerializerOptions linqSerializerOptions = linqSerializerOptionsInternal.CosmosLinqSerializerOptions; + + this.CosmosLinqSerializer = new DefaultCosmosLinqSerializer(linqSerializerOptions.PropertyNamingPolicy); + this.MemberNames = new MemberNames(linqSerializerOptions); + } + else + { + this.CosmosLinqSerializer = TranslationContext.DefaultLinqSerializer; + this.MemberNames = TranslationContext.DefaultMemberNames; + } } public Expression LookupSubstitution(ParameterExpression parameter) diff --git a/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs b/Microsoft.Azure.Cosmos/src/Linq/TypeSystem.cs index e11c145c71..dbe6809017 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.SerializeMemberName(memberInfo, context.LinqSerializerOptions); + return context.CosmosLinqSerializer.SerializeMemberName(memberInfo); } private static Type GetElementType(Type type, HashSet visitedSet) diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs index 13e5425d7d..2397ab437a 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs @@ -24,9 +24,9 @@ namespace Microsoft.Azure.Cosmos using Microsoft.Azure.Cosmos.Query.Core; using Microsoft.Azure.Cosmos.Query.Core.Monads; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; - using Microsoft.Azure.Cosmos.Query.Core.QueryPlan; using Microsoft.Azure.Cosmos.ReadFeed; using Microsoft.Azure.Cosmos.ReadFeed.Pagination; + using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.Tracing; using Microsoft.Azure.Documents; @@ -387,14 +387,16 @@ public override IOrderedQueryable GetItemLinqQueryable( { requestOptions ??= new QueryRequestOptions(); - if (linqSerializerOptions == null && this.ClientContext.ClientOptions.SerializerOptions != null) + if (this.ClientContext.ClientOptions != null) { - linqSerializerOptions = new CosmosLinqSerializerOptions + linqSerializerOptions ??= new CosmosLinqSerializerOptions { - PropertyNamingPolicy = this.ClientContext.ClientOptions.SerializerOptions.PropertyNamingPolicy + PropertyNamingPolicy = this.ClientContext.ClientOptions.SerializerOptions?.PropertyNamingPolicy ?? CosmosPropertyNamingPolicy.Default }; } + CosmosLinqSerializerOptionsInternal linqSerializerOptionsInternal = CosmosLinqSerializerOptionsInternal.Create(linqSerializerOptions, this.ClientContext.ClientOptions.Serializer); + return new CosmosLinqQuery( this, this.ClientContext.ResponseFactory, @@ -402,7 +404,7 @@ public override IOrderedQueryable GetItemLinqQueryable( continuationToken, requestOptions, allowSynchronousQueryExecution, - linqSerializerOptions); + linqSerializerOptionsInternal); } public override FeedIterator GetItemQueryIterator( diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializer.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializer.cs new file mode 100644 index 0000000000..e58abfcebd --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializer.cs @@ -0,0 +1,84 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos +{ + using System.Reflection; + + /// + /// This abstract class can be implemented to allow a custom serializer (Non [Json.NET serializer](https://www.newtonsoft.com/json/help/html/Introduction.htm)'s) + /// to be used by the CosmosClient for LINQ queries. + /// + /// + /// This example implements the CosmosLinqSerializer contract. + /// This example custom serializer will honor System.Text.Json attributes. + /// + /// (Stream stream) + /// { + /// if (stream == null) + /// throw new ArgumentNullException(nameof(stream)); + /// + /// using (stream) + /// { + /// if (stream.CanSeek && stream.Length == 0) + /// { + /// return default; + /// } + /// + /// if (typeof(Stream).IsAssignableFrom(typeof(T))) + /// { + /// return (T)(object)stream; + /// } + /// + /// return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); + /// } + /// } + /// + /// public override Stream ToStream(T input) + /// { + /// MemoryStream streamPayload = new MemoryStream(); + /// this.systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default); + /// streamPayload.Position = 0; + /// return streamPayload; + /// } + /// + /// public override string SerializeMemberName(MemberInfo memberInfo) + /// { + /// JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute(true); + /// + /// string memberName = !string.IsNullOrEmpty(jsonPropertyNameAttribute?.Name) + /// ? jsonPropertyNameAttribute.Name + /// : memberInfo.Name; + /// + /// return memberName; + /// } + /// } + /// ]]> + /// + /// +#if PREVIEW + public +#else + internal +#endif + abstract class CosmosLinqSerializer : CosmosSerializer + { + /// + /// Convert a MemberInfo to a string for use in LINQ query translation. + /// This must be implemented when using a custom serializer for LINQ queries. + /// + /// Any MemberInfo used in the query. + /// A serialized representation of the member. + public abstract string SerializeMemberName(MemberInfo memberInfo); + } +} diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptionsInternal.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptionsInternal.cs new file mode 100644 index 0000000000..638c05c47b --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptionsInternal.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Serializer +{ + using System; + using Microsoft.Azure.Cosmos; + + /// + /// This class stores user-provided LINQ Serialization Properties. + /// + internal sealed class CosmosLinqSerializerOptionsInternal + { + /// + /// Creates an instance of CosmosSerializationOptionsInternal. + /// + public static CosmosLinqSerializerOptionsInternal Create( + CosmosLinqSerializerOptions cosmosLinqSerializerOptions, + CosmosSerializer customCosmosSerializer) + { + if (customCosmosSerializer is CosmosLinqSerializer customQueryCosmosSerializer) + { + if (cosmosLinqSerializerOptions.PropertyNamingPolicy != CosmosPropertyNamingPolicy.Default) + { + throw new InvalidOperationException($"CosmosPropertyNamingPolicy must be CosmosPropertyNamingPolicy.Default if using custom serializer for LINQ translations. See https://aka.ms/CosmosDB/dotnetlinq for more information."); + } + + return new CosmosLinqSerializerOptionsInternal(cosmosLinqSerializerOptions, customQueryCosmosSerializer); + } + else + { + return new CosmosLinqSerializerOptionsInternal(cosmosLinqSerializerOptions, null); + } + } + + private CosmosLinqSerializerOptionsInternal( + CosmosLinqSerializerOptions cosmosLinqSerializerOptions, + CosmosLinqSerializer customCosmosLinqSerializer) + { + this.CosmosLinqSerializerOptions = cosmosLinqSerializerOptions; + this.CustomCosmosLinqSerializer = customCosmosLinqSerializer; + } + + /// + /// User-provided CosmosLinqSerializerOptions. + /// + public CosmosLinqSerializerOptions CosmosLinqSerializerOptions { get; } + + /// + /// User defined customer serializer, if one exists. + /// Otherwise set to null. + /// + public CosmosLinqSerializer CustomCosmosLinqSerializer { get; } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationUtil.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationUtil.cs index b2e0838248..8946d9ac7a 100644 --- a/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationUtil.cs +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializationUtil.cs @@ -4,26 +4,31 @@ namespace Microsoft.Azure.Cosmos { - using Microsoft.Azure.Cosmos.Serializer; + using System; 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) + if (options == null) { - return CosmosSerializationUtil.ToCamelCase(name); + return name; } - return name; + return GetStringWithPropertyNamingPolicy(options.PropertyNamingPolicy, name); + } + + internal static string GetStringWithPropertyNamingPolicy(CosmosPropertyNamingPolicy namingPolicy, string name) + { + return namingPolicy switch + { + CosmosPropertyNamingPolicy.CamelCase => CosmosSerializationUtil.camelCaseNamingStrategy.GetPropertyName(name, false), + CosmosPropertyNamingPolicy.Default => name, + _ => throw new NotImplementedException("Unsupported CosmosPropertyNamingPolicy value"), + }; } } } diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializer.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializer.cs index 3632bd6740..e6bd34d1f5 100644 --- a/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializer.cs +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosSerializer.cs @@ -7,8 +7,11 @@ namespace Microsoft.Azure.Cosmos using System.IO; /// - /// This is an interface to allow a custom serializer to be used by the CosmosClient + /// This abstract class can be implemented to allow a custom serializer to be used by the CosmosClient. /// + /// + /// To use the custom serializer for LINQ queries, must be implemented. + /// public abstract class CosmosSerializer { /// diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqAggregateCustomSerializationBaseline.TestAggregateQueriesWithCustomSerializer.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqAggregateCustomSerializationBaseline.TestAggregateQueriesWithCustomSerializer.xml new file mode 100644 index 0000000000..1a7337ba81 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqAggregateCustomSerializationBaseline.TestAggregateQueriesWithCustomSerializer.xml @@ -0,0 +1,126 @@ + + + + + doc.NumericField), Object)]]> + + + + + + + + + doc.NumericField), Object)]]> + + + + + + + + Filter -> Select -> Average, Custom serializer: True]]> + doc.ArrayField.Where(m => ((m % 3) == 0)).Select(m => m)).Average(), Object)]]> + + + + + + + + Skip -> Count, Custom serializer: True]]> + f.NumericField).Skip(2).Count(), Object)]]> + + + + + + + + + Min w/ mapping]]> + doc.NumericField).Min(num => num), Object)]]> + + + + + + + + + doc.NumericField), Object)]]> + + + + + + + + + doc.NumericField), Object)]]> + + + + + + + + Filter -> Select -> Average, Custom serializer: False]]> + doc.ArrayField.Where(m => ((m % 3) == 0)).Select(m => m)).Average(), Object)]]> + + + + + + + + Skip -> Count, Custom serializer: False]]> + f.NumericField).Skip(2).Count(), Object)]]> + + + + + + + + + Min w/ mapping]]> + doc.NumericField).Min(num => num), Object)]]> + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqAggregateCustomSerializationBaseline.TestAggregatesWithCustomSerializer.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqAggregateCustomSerializationBaseline.TestAggregatesWithCustomSerializer.xml new file mode 100644 index 0000000000..752edecde8 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqAggregateCustomSerializationBaseline.TestAggregatesWithCustomSerializer.xml @@ -0,0 +1,58 @@ + + + + + doc.NumericField), Object)]]> + + + + . Actual:<0>. ]]> + + + + + + doc.NumericField), Object)]]> + + + + . Actual:<0>. ]]> + + + + + + + + + 0) +FROM ( + SELECT VALUE root + FROM root) AS v0 +]]> + + + + + + Filter -> Select -> Any]]> + doc.ArrayField.Where(m => ((m % 3) == 0)).Select(m => m)).Any(), Object)]]> + + + 0) +FROM ( + SELECT VALUE m0 + FROM root + JOIN m0 IN root["ArrayField"] + WHERE ((m0 % 3) = 0)) AS v0 +]]> + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNetCustomSerializer.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNetCustomSerializer.xml new file mode 100644 index 0000000000..a9c63ebf41 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNetCustomSerializer.xml @@ -0,0 +1,143 @@ + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectDotNet() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectDotNet() {NumericField = 1, StringField = "1"}, new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueDotNet": 1, "StringValueDotNet": "1", "id": null, "Pk": null} : {"NumberValueDotNet": 1, "StringValueDotNet": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + + + + x).OrderBy(x => x.NumericField).Take(5)]]> + + + + + + + + + + + IIF((c.NumericField > 1), "true", "false"))]]> + + + 1) ? "true" : "false") +FROM root]]> + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNet.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNetDataMember.xml similarity index 84% rename from Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNet.xml rename to Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNetDataMember.xml index b381e2b651..df12581f67 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNet.xml +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNetDataMember.xml @@ -8,7 +8,7 @@ +WHERE (root["numericFieldDataMember"] = 1)]]> - (doc == new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + (doc == new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"}))]]> +WHERE (root = {"NumericFieldDataMember": 1, "StringFieldDataMember": "1"})]]> - new DataObjectDotNet() {NumericField = 1, StringField = "1"})]]> + new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"})]]> - IIF((doc.NumericField > 1), new DataObjectDotNet() {NumericField = 1, StringField = "1"}, new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + IIF((doc.NumericField > 1), new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"}, new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"}))]]> 1) ? {"NumericField": 1, "StringField": "1", "id": null, "Pk": null} : {"NumericField": 1, "StringField": "1", "id": null, "Pk": null}) +SELECT VALUE ((root["numericFieldDataMember"] > 1) ? {"NumericFieldDataMember": 1, "StringFieldDataMember": "1"} : {"NumericFieldDataMember": 1, "StringFieldDataMember": "1"}) FROM root]]> - (doc == new DataObjectDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + (doc == new DataObjectDotNetDataMember() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> +WHERE (root = {"numericFieldDataMember": root["numericFieldDataMember"], "stringFieldDataMember": root["stringFieldDataMember"]})]]> +WHERE (root["NumericFieldDataMember"] = 1)]]> - (doc == new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + (doc == new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"}))]]> +WHERE (root = {"NumericFieldDataMember": 1, "StringFieldDataMember": "1"})]]> - new DataObjectDotNet() {NumericField = 1, StringField = "1"})]]> + new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"})]]> - IIF((doc.NumericField > 1), new DataObjectDotNet() {NumericField = 1, StringField = "1"}, new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + IIF((doc.NumericField > 1), new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"}, new DataObjectDotNetDataMember() {NumericField = 1, StringField = "1"}))]]> 1) ? {"NumericField": 1, "StringField": "1", "id": null, "Pk": null} : {"NumericField": 1, "StringField": "1", "id": null, "Pk": null}) +SELECT VALUE ((root["NumericFieldDataMember"] > 1) ? {"NumericFieldDataMember": 1, "StringFieldDataMember": "1"} : {"NumericFieldDataMember": 1, "StringFieldDataMember": "1"}) FROM root]]> - (doc == new DataObjectDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + (doc == new DataObjectDotNetDataMember() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> +WHERE (root = {"NumericFieldDataMember": root["NumericFieldDataMember"], "StringFieldDataMember": root["StringFieldDataMember"]})]]> @@ -31,12 +31,12 @@ SELECT VALUE root FROM root WHERE (root = {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null})]]> @@ -51,12 +51,12 @@ WHERE (root = {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": n SELECT VALUE {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} FROM root]]> SELECT VALUE ((root["numberValueNewtonsoft"] > 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null}) FROM root]]> @@ -127,12 +127,12 @@ SELECT VALUE root FROM root WHERE (root["NumberValueNewtonsoft"] = 1)]]> @@ -148,12 +148,12 @@ SELECT VALUE root FROM root WHERE (root = {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null})]]> @@ -168,12 +168,12 @@ WHERE (root = {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": n SELECT VALUE {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} FROM root]]> SELECT VALUE ((root["NumberValueNewtonsoft"] > 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null}) FROM root]]> diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoftDataMember.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoftDataMember.xml new file mode 100644 index 0000000000..215f54921f --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoftDataMember.xml @@ -0,0 +1,236 @@ + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"}, new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1"} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1"}) +FROM root]]> + + + + + + + + (doc == new DataObjectNewtonsoftDataMember() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"}, new DataObjectNewtonsoftDataMember() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1"} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1"}) +FROM root]]> + + + + + + + + (doc == new DataObjectNewtonsoftDataMember() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerMultiSerializer.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoftDotNet.xml similarity index 92% rename from Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerMultiSerializer.xml rename to Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoftDotNet.xml index 704eb39807..be82c8dea7 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerMultiSerializer.xml +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoftDotNet.xml @@ -23,7 +23,7 @@ WHERE (root["numberValueNewtonsoft"] = 1)]]> - (doc == new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + (doc == new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"}))]]> - new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"})]]> + new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"})]]> - IIF((doc.NumericField > 1), new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}, new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + IIF((doc.NumericField > 1), new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"}, new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"}))]]> - (doc == new DataObjectMultiSerializer() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + (doc == new DataObjectNewtonsoftDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> - (doc == new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + (doc == new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"}))]]> - new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"})]]> + new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"})]]> - IIF((doc.NumericField > 1), new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}, new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + IIF((doc.NumericField > 1), new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"}, new DataObjectNewtonsoftDotNet() {NumericField = 1, StringField = "1"}))]]> - (doc == new DataObjectMultiSerializer() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + (doc == new DataObjectNewtonsoftDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + { + private static CosmosSerializer customCosmosLinqSerializer; + private static CosmosClient clientLinq; + private static Cosmos.Database testDbLinq; + private static Container testContainerLinq; + private static IQueryable lastExecutedScalarQuery; + + private static CosmosSerializer customCosmosSerializer; + private static CosmosClient client; + private static Cosmos.Database testDb; + private static Container testContainer; + + [ClassInitialize] + public async static Task Initialize(TestContext textContext) + { + customCosmosLinqSerializer = new SystemTextJsonLinqSerializer(new JsonSerializerOptions()); + clientLinq = TestCommon.CreateCosmosClient((cosmosClientBuilder) + => cosmosClientBuilder.WithCustomSerializer(customCosmosLinqSerializer)); + + // Set a callback to get the handle of the last executed query to do the verification + // This is neede because aggregate queries return type is a scalar so it can't be used + // to verify the translated LINQ directly as other queries type. + clientLinq.DocumentClient.OnExecuteScalarQueryCallback = q => lastExecutedScalarQuery = q; + + string dbName = $"{nameof(LinqAggregateCustomSerializationBaseline)}-{Guid.NewGuid().ToString("N")}"; + testDbLinq = await clientLinq.CreateDatabaseAsync(dbName); + testContainerLinq = testDbLinq.CreateContainerAsync(new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: "/Pk")).Result; + + customCosmosSerializer = new SystemTextJsonSerializer(new JsonSerializerOptions()); + + client = TestCommon.CreateCosmosClient((cosmosClientBuilder) + => cosmosClientBuilder.WithCustomSerializer(customCosmosSerializer)); + + // Set a callback to get the handle of the last executed query to do the verification + // This is neede because aggregate queries return type is a scalar so it can't be used + // to verify the translated LINQ directly as other queries type. + client.DocumentClient.OnExecuteScalarQueryCallback = q => lastExecutedScalarQuery = q; + + dbName = $"{nameof(LinqAggregateCustomSerializationBaseline)}-{Guid.NewGuid().ToString("N")}"; + testDb = await client.CreateDatabaseAsync(dbName); + testContainer = testDb.CreateContainerAsync(new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: "/Pk")).Result; + } + + [ClassCleanup] + public async static Task CleanUp() + { + if (testDbLinq != null) + { + await testDbLinq.DeleteStreamAsync(); + } + + clientLinq?.Dispose(); + + if (testDb != null) + { + await testDb.DeleteStreamAsync(); + } + + client?.Dispose(); + } + + [TestMethod] + [Owner("mayapainter")] + public void TestAggregateQueriesWithCustomSerializer() + { + static DataObjectDotNet createDataObj(int index, bool camelCase) + { + DataObjectDotNet obj = new DataObjectDotNet + { + NumericField = index, + StringField = index.ToString(), + ArrayField = new int[] { 1, 2, 3, 4, 5 }, + id = Guid.NewGuid().ToString(), + Pk = "Test" + }; + return obj; + } + + List>> getQueryList = new List>> + { + LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, 5, testContainerLinq, new CosmosLinqSerializerOptions()), + LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, 5, testContainer, new CosmosLinqSerializerOptions()) + }; + + List inputs = new List(); + + foreach (bool applyCustomSerializer in new List{ true, false }) + { + Func> getQuery = getQueryList[applyCustomSerializer ? 0 : 1]; + + inputs.Add(new LinqAggregateInput( + "Avg, Custom serializer: " + applyCustomSerializer, b => getQuery(b) + .Average(doc => doc.NumericField))); + + inputs.Add(new LinqAggregateInput( + "Sum, Custom serializer: " + applyCustomSerializer, b => getQuery(b) + .Sum(doc => doc.NumericField))); + + inputs.Add(new LinqAggregateInput( + "Select many -> Filter -> Select -> Average, Custom serializer: " + applyCustomSerializer, b => getQuery(b) + .SelectMany(doc => doc.ArrayField.Where(m => (m % 3) == 0).Select(m => m)).Average())); + + inputs.Add(new LinqAggregateInput( + "Select number -> Skip -> Count, Custom serializer: " + applyCustomSerializer, b => getQuery(b) + .Select(f => f.NumericField).Skip(2).Count())); + + inputs.Add(new LinqAggregateInput( + "Select number -> Min w/ mapping", b => getQuery(b) + .Select(doc => doc.NumericField).Min(num => num))); + } + + this.ExecuteTestSuite(inputs); + } + + public override LinqAggregateOutput ExecuteTest(LinqAggregateInput input) + { + lastExecutedScalarQuery = null; + Func compiledQuery = input.expression.Compile(); + + string errorMessage = null; + string query = string.Empty; + try + { + object queryResult; + try + { + queryResult = compiledQuery(true); + } + finally + { + Assert.IsNotNull(lastExecutedScalarQuery, "lastExecutedScalarQuery is not set"); + + query = JObject + .Parse(lastExecutedScalarQuery.ToString()) + .GetValue("query", StringComparison.Ordinal) + .ToString(); + } + } + catch (Exception e) + { + errorMessage = LinqTestsCommon.BuildExceptionMessageForTest(e); + } + + return new LinqAggregateOutput(query, errorMessage); + } + + private class DataObjectDotNet : LinqTestObject + { + [JsonPropertyName("NumberValueDotNet")] + public double NumericField { get; set; } + + [JsonPropertyName("StringValueDotNet")] + public string StringField { get; set; } + + [JsonPropertyName("ArrayValuesDotNet")] + public int[] ArrayField { get; set; } + + public string id { get; set; } + + public string Pk { get; set; } + + public DataObjectDotNet() { } + + public DataObjectDotNet(double numericField, string stringField, int[] arrayField, string id, string pk) + { + this.NumericField = numericField; + this.StringField = stringField; + this.ArrayField = arrayField; + this.id = id; + this.Pk = pk; + } + + public override string ToString() + { + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; + } + } + } +} 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 5ea03bf0b6..1f511a3c11 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 @@ -172,7 +172,7 @@ public Datum2(string jsonProperty, string dataMember, string defaultMember, stri [TestMethod] public void TestAttributePriority() { - ICosmosLinqSerializer cosmosLinqSerializer = new DefaultCosmosLinqSerializer(); + ICosmosLinqSerializerInternal cosmosLinqSerializer = new DefaultCosmosLinqSerializer(new CosmosPropertyNamingPolicy()); 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())); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs index c8513e41b8..8cc819014a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs @@ -10,18 +10,23 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; + using System.IO; using System.Linq; using System.Linq.Expressions; + using System.Reflection; using System.Runtime.CompilerServices; using System.Text; + using System.Text.Json.Serialization; + using System.Text.Json; using System.Text.RegularExpressions; using System.Xml; + using global::Azure.Core.Serialization; using Microsoft.Azure.Cosmos.Services.Management.Tests.BaselineTest; using Microsoft.Azure.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - + using Newtonsoft.Json.Linq; + internal class LinqTestsCommon { /// @@ -333,13 +338,13 @@ public static Func> GenerateTestCosmosData(Funcnumber of test data to be created /// the target container /// if theCosmosLinqSerializerOption of camelCaseSerialization should be applied - /// a lambda that takes a boolean which indicate where the query should run against CosmosDB or against original data, and return a query results as IQueryable. Also the serialized payload. - public static Func> GenerateSerializationTestCosmosData(Func func, int count, Container container, bool camelCaseSerialization = false) + /// a lambda that takes a boolean which indicate where the query should run against CosmosDB or against original data, and return a query results as IQueryable. + public static Func> GenerateSerializationTestCosmosData(Func func, int count, Container container, CosmosLinqSerializerOptions linqSerializerOptions) { List data = new List(); for (int i = 0; i < count; i++) { - data.Add(func(i, camelCaseSerialization)); + data.Add(func(i, linqSerializerOptions.PropertyNamingPolicy == CosmosPropertyNamingPolicy.CamelCase)); } foreach (T obj in data) @@ -355,7 +360,6 @@ public static Func> GenerateSerializationTestCosmosData(F #endif }; - CosmosLinqSerializerOptions linqSerializerOptions = new CosmosLinqSerializerOptions { PropertyNamingPolicy = camelCaseSerialization ? CosmosPropertyNamingPolicy.CamelCase : CosmosPropertyNamingPolicy.Default }; IOrderedQueryable query = container.GetItemLinqQueryable(allowSynchronousQueryExecution: true, requestOptions: requestOptions, linqSerializerOptions: linqSerializerOptions); IQueryable getQuery(bool useQuery) => useQuery ? query : data.AsQueryable(); @@ -836,4 +840,93 @@ public override void SerializeAsXml(XmlWriter xmlWriter) } } } + + class SystemTextJsonLinqSerializer : CosmosLinqSerializer + { + private readonly JsonObjectSerializer systemTextJsonSerializer; + + public SystemTextJsonLinqSerializer(JsonSerializerOptions jsonSerializerOptions) + { + this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions); + } + + public override T FromStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using (stream) + { + if (stream.CanSeek && stream.Length == 0) + { + return default; + } + + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + return (T)(object)stream; + } + + return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); + } + } + + public override Stream ToStream(T input) + { + MemoryStream streamPayload = new MemoryStream(); + this.systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default); + streamPayload.Position = 0; + return streamPayload; + } + + public override string SerializeMemberName(MemberInfo memberInfo) + { + JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute(true); + + string memberName = !string.IsNullOrEmpty(jsonPropertyNameAttribute?.Name) + ? jsonPropertyNameAttribute.Name + : memberInfo.Name; + + return memberName; + } + } + + class SystemTextJsonSerializer : CosmosSerializer + { + private readonly JsonObjectSerializer systemTextJsonSerializer; + + public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions) + { + this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions); + } + + public override T FromStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using (stream) + { + if (stream.CanSeek && stream.Length == 0) + { + return default; + } + + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + return (T)(object)stream; + } + + return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); + } + } + + public override Stream ToStream(T input) + { + MemoryStream streamPayload = new MemoryStream(); + this.systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default); + streamPayload.Position = 0; + return streamPayload; + } + } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs index 3266809926..817b030a60 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs @@ -15,7 +15,6 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests using System.Text.Json.Serialization; using System.Threading.Tasks; using BaselineTest; - using global::Azure.Core.Serialization; using Microsoft.Azure.Cosmos.SDK.EmulatorTests; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; @@ -24,6 +23,10 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests [SDK.EmulatorTests.TestClass] public class LinqTranslationWithCustomSerializerBaseline : BaselineTests { + private static CosmosClient CosmosLinqClient; + private static Database TestDbLinq; + private static Container TestLinqContainer; + private static CosmosClient CosmosClient; private static Database TestDb; private static Container TestContainer; @@ -36,8 +39,14 @@ public class LinqTranslationWithCustomSerializerBaseline : BaselineTests cosmosClientBuilder.WithCustomSerializer(new SystemTextJsonLinqSerializer(new JsonSerializerOptions()))); + + string dbNameLinq = $"{nameof(LinqTranslationBaselineTests)}-{Guid.NewGuid():N}"; + TestDbLinq = await CosmosLinqClient.CreateDatabaseAsync(dbNameLinq); + CosmosClient = TestCommon.CreateCosmosClient((cosmosClientBuilder) - => cosmosClientBuilder.WithCustomSerializer(new SystemTextJsonSerializer(new JsonSerializerOptions()))); + => cosmosClientBuilder.WithCustomSerializer(new SystemTextJsonSerializer(new JsonSerializerOptions()))); string dbName = $"{nameof(LinqTranslationBaselineTests)}-{Guid.NewGuid():N}"; TestDb = await CosmosClient.CreateDatabaseAsync(dbName); @@ -46,6 +55,11 @@ public async static Task Initialize(TestContext textContext) [ClassCleanup] public async static Task Cleanup() { + if (TestDbLinq != null) + { + await TestDbLinq.DeleteStreamAsync(); + } + if (TestDb != null) { await TestDb.DeleteStreamAsync(); @@ -55,12 +69,14 @@ public async static Task Cleanup() [TestInitialize] public async Task TestInitialize() { + TestLinqContainer = await TestDbLinq.CreateContainerAsync(new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: "/Pk")); TestContainer = await TestDb.CreateContainerAsync(new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: "/Pk")); } [TestCleanup] public async Task TestCleanup() { + await TestLinqContainer.DeleteContainerStreamAsync(); await TestContainer.DeleteContainerStreamAsync(); } @@ -70,33 +86,23 @@ public override LinqTestOutput ExecuteTest(LinqTestInput input) } [TestMethod] - public void TestMemberInitializerDotNet() + public void TestMemberInitializerDotNetCustomSerializer() { - Func> getQueryCamelCase; - Func> getQueryDefault; - (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + Func> getQuery; + (_, getQuery) = this.InsertDataAndGetQueryables(true, TestLinqContainer); - string insertedData = this.GetInsertedData().Result; + string insertedData = this.GetInsertedData(TestLinqContainer).Result; - List inputs = new List(); - foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + List inputs = new List { - Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; - - List camelCaseSettingInputs = new List - { - // TODO (10/13/23): extend this and other tests cases as more LINQ features are added (GROUP BY, etc.) - new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), - new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectDotNet() { NumericField = 1, StringField = "1" } : new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - - // Negative test case: serializing only field name using custom serializer not currently supported - new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDotNet() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) - }; - - inputs.AddRange(camelCaseSettingInputs); - } + new LinqTestInput("Filter w/ constant value", b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value", b => getQuery(b).Where(doc => doc == new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer", b => getQuery(b).Select(doc => new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference", b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectDotNet() { NumericField = 1, StringField = "1" } : new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with member initialization", b => getQuery(b).Where(doc => doc == new DataObjectDotNet() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData), + new LinqTestInput("OrderBy query", b => getQuery(b).Select(x => x).OrderBy(x => x.NumericField).Take(5), skipVerification : true, inputData: insertedData), + new LinqTestInput("Conditional", b => getQuery(b).Select(c => c.NumericField > 1 ? "true" : "false"), skipVerification : true, inputData: insertedData), + }; this.ExecuteTestSuite(inputs); } @@ -106,9 +112,9 @@ public void TestMemberInitializerNewtonsoft() { Func> getQueryCamelCase; Func> getQueryDefault; - (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(false, TestContainer); - string insertedData = this.GetInsertedData().Result; + string insertedData = this.GetInsertedData(TestContainer).Result; List inputs = new List(); foreach (bool useCamelCaseSerializer in new bool[] { true, false }) @@ -121,8 +127,6 @@ public void TestMemberInitializerNewtonsoft() new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" } : new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - - // Negative test case: serializing only field name using custom serializer not currently supported new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoft() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) }; @@ -137,9 +141,9 @@ public void TestMemberInitializerDataMember() { Func> getQueryCamelCase; Func> getQueryDefault; - (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(false, TestContainer); - string insertedData = this.GetInsertedData().Result; + string insertedData = this.GetInsertedData(TestContainer).Result; List inputs = new List(); foreach (bool useCamelCaseSerializer in new bool[] { true, false }) @@ -152,8 +156,6 @@ public void TestMemberInitializerDataMember() new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectDataMember() { NumericField = 1, StringField = "1" } : new DataObjectDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - - // Negative test case: serializing only field name using custom serializer not currently supported new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDataMember() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) }; @@ -164,28 +166,55 @@ public void TestMemberInitializerDataMember() } [TestMethod] - public void TestMemberInitializerMultiSerializer() + public void TestMemberInitializerNewtonsoftDotNet() { - Func> getQueryCamelCase; - Func> getQueryDefault; - (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(false, TestContainer); - string insertedData = this.GetInsertedData().Result; + string insertedData = this.GetInsertedData(TestContainer).Result; List inputs = new List(); foreach (bool useCamelCaseSerializer in new bool[] { true, false }) { - Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; List camelCaseSettingInputs = new List { new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), - new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), - new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" } : new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoftDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectNewtonsoftDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectNewtonsoftDotNet() { NumericField = 1, StringField = "1" } : new DataObjectNewtonsoftDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoftDotNet() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + }; + + inputs.AddRange(camelCaseSettingInputs); + } - // Negative test case: serializing only field name using custom serializer not currently supported - new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectMultiSerializer() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + this.ExecuteTestSuite(inputs); + } + + [TestMethod] + public void TestMemberInitializerNewtonsoftDataMember() + { + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(false, TestContainer); + + string insertedData = this.GetInsertedData(TestContainer).Result; + + List inputs = new List(); + foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + { + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + + List camelCaseSettingInputs = new List + { + new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoftDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectNewtonsoftDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectNewtonsoftDataMember() { NumericField = 1, StringField = "1" } : new DataObjectNewtonsoftDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoftDataMember() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) }; inputs.AddRange(camelCaseSettingInputs); @@ -194,7 +223,36 @@ public void TestMemberInitializerMultiSerializer() this.ExecuteTestSuite(inputs); } - private (Func>, Func>) InsertDataAndGetQueryables() where T : LinqTestObject + [TestMethod] + public void TestMemberInitializerDotNetDataMember() + { + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(false, TestContainer); + + string insertedData = this.GetInsertedData(TestContainer).Result; + + List inputs = new List(); + foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + { + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + + List camelCaseSettingInputs = new List + { + new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDotNetDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectDotNetDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectDotNetDataMember() { NumericField = 1, StringField = "1" } : new DataObjectDotNetDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDotNetDataMember() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + }; + + inputs.AddRange(camelCaseSettingInputs); + } + + this.ExecuteTestSuite(inputs); + } + + private (Func>, Func>) InsertDataAndGetQueryables(bool customSerializer, Container container) where T : LinqTestObject { static T createDataObj(int index, bool camelCase) { @@ -205,16 +263,28 @@ static T createDataObj(int index, bool camelCase) return obj; } - Func> getQueryCamelCase = LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, RecordCount, TestContainer, camelCaseSerialization: true); - Func> getQueryDefault = LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, RecordCount, TestContainer, camelCaseSerialization: false); + CosmosLinqSerializerOptions linqSerializerOptionsCamelCase = new CosmosLinqSerializerOptions + { + PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase, + }; + + CosmosLinqSerializerOptions linqSerializerOptionsDefault = new CosmosLinqSerializerOptions(); + + Func> getQueryCamelCase = null; + if (!customSerializer) + { + getQueryCamelCase = LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, RecordCount, container, linqSerializerOptionsCamelCase); + } + + Func> getQueryDefault = LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, RecordCount, container, linqSerializerOptionsDefault); return (getQueryCamelCase, getQueryDefault); } - private async Task GetInsertedData() + private async Task GetInsertedData(Container container) { List insertedDataList = new List(); - using (FeedIterator feedIterator = TestContainer.GetItemQueryStreamIterator("SELECT * FROM c")) + using (FeedIterator feedIterator = container.GetItemQueryStreamIterator("SELECT * FROM c")) { while (feedIterator.HasMoreResults) { @@ -244,64 +314,91 @@ private async Task GetInsertedData() } } - string insertedData = JsonConvert.SerializeObject(insertedDataList.Select(item => item), new JsonSerializerSettings { Formatting = Newtonsoft.Json.Formatting.Indented }); - return insertedData; + return JsonConvert.SerializeObject(insertedDataList.Select(item => item), new JsonSerializerSettings { Formatting = Newtonsoft.Json.Formatting.Indented }); } - private class SystemTextJsonSerializer : CosmosSerializer + private class DataObjectDotNet : LinqTestObject { - private readonly JsonObjectSerializer systemTextJsonSerializer; + [JsonPropertyName("NumberValueDotNet")] + public double NumericField { get; set; } + + [JsonPropertyName("StringValueDotNet")] + public string StringField { get; set; } + + [System.Text.Json.Serialization.JsonIgnore] + public string IgnoreField { get; set; } + + public string id { get; set; } + + public string Pk { get; set; } + + public DataObjectDotNet() { } - public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions) + public DataObjectDotNet(double numericField, string stringField, string id, string pk) { - this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions); + this.NumericField = numericField; + this.StringField = stringField; + this.IgnoreField = "Ignore"; + this.id = id; + this.Pk = pk; } - public override T FromStream(Stream stream) + public override string ToString() { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; + } + } - using (stream) - { - if (stream.CanSeek && stream.Length == 0) - { - return default; - } + private class DataObjectNewtonsoft : LinqTestObject + { + [Newtonsoft.Json.JsonProperty(PropertyName = "NumberValueNewtonsoft")] + public double NumericField { get; set; } - if (typeof(Stream).IsAssignableFrom(typeof(T))) - { - return (T)(object)stream; - } + [Newtonsoft.Json.JsonProperty(PropertyName = "StringValueNewtonsoft")] + public string StringField { get; set; } - return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); - } + [Newtonsoft.Json.JsonIgnore] + public string IgnoreField { get; set; } + + public string id { get; set; } + + public string Pk { get; set; } + + public DataObjectNewtonsoft() { } + + public DataObjectNewtonsoft(double numericField, string stringField, string id, string pk) + { + this.NumericField = numericField; + this.StringField = stringField; + this.IgnoreField = "ignore"; + this.id = id; + this.Pk = pk; } - public override Stream ToStream(T input) + public override string ToString() { - MemoryStream streamPayload = new MemoryStream(); - this.systemTextJsonSerializer.Serialize(streamPayload, input, typeof(T), default); - streamPayload.Position = 0; - return streamPayload; + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; } } - private class DataObjectDotNet : LinqTestObject + [DataContract] + private class DataObjectDataMember : LinqTestObject { - [JsonPropertyName("numberValueDotNet")] + [DataMember(Name = "NumericFieldDataMember")] public double NumericField { get; set; } - [JsonPropertyName("stringValueDotNet")] + [DataMember(Name = "StringFieldDataMember")] public string StringField { get; set; } + [DataMember(Name = "id")] public string id { get; set; } + [DataMember(Name = "Pk")] public string Pk { get; set; } - public DataObjectDotNet() { } + public DataObjectDataMember() { } - public DataObjectDotNet(double numericField, string stringField, string id, string pk) + public DataObjectDataMember(double numericField, string stringField, string id, string pk) { this.NumericField = numericField; this.StringField = stringField; @@ -315,21 +412,23 @@ public override string ToString() } } - private class DataObjectNewtonsoft : LinqTestObject + private class DataObjectNewtonsoftDotNet : LinqTestObject { [Newtonsoft.Json.JsonProperty(PropertyName = "NumberValueNewtonsoft")] + [JsonPropertyName("numberValueDotNet")] public double NumericField { get; set; } [Newtonsoft.Json.JsonProperty(PropertyName = "StringValueNewtonsoft")] + [JsonPropertyName("stringValueDotNet")] public string StringField { get; set; } public string id { get; set; } public string Pk { get; set; } - public DataObjectNewtonsoft() { } + public DataObjectNewtonsoftDotNet() { } - public DataObjectNewtonsoft(double numericField, string stringField, string id, string pk) + public DataObjectNewtonsoftDotNet(double numericField, string stringField, string id, string pk) { this.NumericField = numericField; this.StringField = stringField; @@ -344,23 +443,23 @@ public override string ToString() } [DataContract] - private class DataObjectDataMember : LinqTestObject + private class DataObjectNewtonsoftDataMember : LinqTestObject { + [Newtonsoft.Json.JsonProperty(PropertyName = "NumberValueNewtonsoft")] [DataMember(Name = "NumericFieldDataMember")] public double NumericField { get; set; } + [Newtonsoft.Json.JsonProperty(PropertyName = "StringValueNewtonsoft")] [DataMember(Name = "StringFieldDataMember")] public string StringField { get; set; } - [DataMember(Name = "id")] public string id { get; set; } - [DataMember(Name = "Pk")] public string Pk { get; set; } - public DataObjectDataMember() { } + public DataObjectNewtonsoftDataMember() { } - public DataObjectDataMember(double numericField, string stringField, string id, string pk) + public DataObjectNewtonsoftDataMember(double numericField, string stringField, string id, string pk) { this.NumericField = numericField; this.StringField = stringField; @@ -374,13 +473,14 @@ public override string ToString() } } - private class DataObjectMultiSerializer : LinqTestObject + [DataContract] + private class DataObjectDotNetDataMember : LinqTestObject { - [Newtonsoft.Json.JsonProperty(PropertyName = "NumberValueNewtonsoft")] + [DataMember(Name = "NumericFieldDataMember")] [JsonPropertyName("numberValueDotNet")] public double NumericField { get; set; } - [Newtonsoft.Json.JsonProperty(PropertyName = "StringValueNewtonsoft")] + [DataMember(Name = "StringFieldDataMember")] [JsonPropertyName("stringValueDotNet")] public string StringField { get; set; } @@ -388,9 +488,9 @@ private class DataObjectMultiSerializer : LinqTestObject public string Pk { get; set; } - public DataObjectMultiSerializer() { } + public DataObjectDotNetDataMember() { } - public DataObjectMultiSerializer(double numericField, string stringField, string id, string pk) + public DataObjectDotNetDataMember(double numericField, string stringField, string id, string pk) { this.NumericField = numericField; this.StringField = stringField; diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj index 7dd766d5d3..83de702ef3 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj @@ -40,9 +40,11 @@ - - + + + + @@ -123,6 +125,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -264,13 +269,19 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index dde2d208bc..94aae6155b 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -352,6 +352,34 @@ }, "NestedTypes": {} }, + "Microsoft.Azure.Cosmos.CosmosLinqSerializer;Microsoft.Azure.Cosmos.CosmosSerializer;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { + "Subclasses": {}, + "Members": { + "System.String SerializeMemberName(System.Reflection.MemberInfo)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.String SerializeMemberName(System.Reflection.MemberInfo);IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + } + }, + "NestedTypes": {} + }, + "Microsoft.Azure.Cosmos.CosmosSerializer;System.Object;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { + "Subclasses": { + "Microsoft.Azure.Cosmos.CosmosLinqSerializer;Microsoft.Azure.Cosmos.CosmosSerializer;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { + "Subclasses": {}, + "Members": { + "System.String SerializeMemberName(System.Reflection.MemberInfo)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.String SerializeMemberName(System.Reflection.MemberInfo);IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + } + }, + "NestedTypes": {} + } + }, + "Members": {}, + "NestedTypes": {} + }, "Microsoft.Azure.Cosmos.DedicatedGatewayRequestOptions;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/CosmosLinqJsonConverterTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/CosmosLinqJsonConverterTests.cs index 8e50a21dd0..0931271d92 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/CosmosLinqJsonConverterTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/CosmosLinqJsonConverterTests.cs @@ -10,7 +10,10 @@ namespace Microsoft.Azure.Cosmos.Linq using System.IO; using System.Linq; using System.Linq.Expressions; + using System.Reflection; + using System.Text.Json.Serialization; using global::Azure.Core.Serialization; + using Microsoft.Azure.Cosmos.Serializer; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -18,6 +21,8 @@ namespace Microsoft.Azure.Cosmos.Linq [TestClass] public class CosmosLinqJsonConverterTests { + private readonly CosmosLinqSerializerOptions defaultOptions = new(); + [TestMethod] public void DateTimeKindIsPreservedTest() { @@ -37,63 +42,59 @@ public void DateTimeKindIsPreservedTest() [TestMethod] public void EnumIsPreservedAsINTest() { - // Arrange - CosmosLinqSerializerOptions options = new() - { - //CustomCosmosSerializer = new TestCustomJsonSerializer() - }; + CosmosLinqSerializerOptionsInternal options = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonLinqSerializer()); + CosmosLinqSerializerOptionsInternal defaultOptions = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonSerializer()); - // Act TestEnum[] values = new[] { TestEnum.One, TestEnum.Two }; + Expression> expr = a => values.Contains(a.Value); - string sql = SqlTranslator.TranslateExpression(expr.Body, options); - // Assert - // Assert.AreEqual("(a[\"Value\"] IN (\"One\", \"Two\"))", sql); // <- TODO - Desired Behavior with CustomSerializer - Assert.AreEqual("(a[\"Value\"] IN (0, 1))", sql); // <- Actual behavior, with ability to set custom serializor reverted + Expression> exprNewtonsoft = a => values.Contains(a.Value); + string sqlDefault = SqlTranslator.TranslateExpression(exprNewtonsoft.Body, defaultOptions); + + Assert.AreEqual("(a[\"Value\"] IN (\"One\", \"Two\"))", sql); + Assert.AreEqual("(a[\"Value\"] IN (0, 1))", sqlDefault); } [TestMethod] public void EnumIsPreservedAsEQUALSTest() { - // Arrange - CosmosLinqSerializerOptions options = new() - { - // CustomCosmosSerializer = new TestCustomJsonSerializer() - }; + CosmosLinqSerializerOptionsInternal options = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonLinqSerializer()); + CosmosLinqSerializerOptionsInternal defaultOptions = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonSerializer()); - // Act TestEnum statusValue = TestEnum.One; - Expression> expr = a => a.Value == statusValue; + Expression> expr = a => a.Value == statusValue; string sql = SqlTranslator.TranslateExpression(expr.Body, options); - // Assert - // Assert.AreEqual("(a[\"Value\"] = \"One\")", sql); // <- THIS is the correct value, if we are able to use the custom serializer - Assert.AreEqual("(a[\"Value\"] = 0)", sql); // <- THIS is the current mis-behavior of the SDK + Expression> exprDefault = a => a.Value == statusValue; + string sqlNewtonsoft = SqlTranslator.TranslateExpression(exprDefault.Body, defaultOptions); + + Assert.AreEqual("(a[\"Value\"] = \"One\")", sql); + Assert.AreEqual("(a[\"Value\"] = \"One\")", sqlNewtonsoft); } [TestMethod] public void EnumIsPreservedAsEXPRESSIONTest() { - // Arrange - CosmosLinqSerializerOptions options = new() - { - // CustomCosmosSerializer = new TestCustomJsonSerializer() - }; - - // Act + CosmosLinqSerializerOptionsInternal options = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonLinqSerializer()); + CosmosLinqSerializerOptionsInternal defaultOptions = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonSerializer()); // Get status constant ConstantExpression status = Expression.Constant(TestEnum.One); // Get member access expression - ParameterExpression arg = Expression.Parameter(typeof(TestEnumNewtonsoftDocument), "a"); + ParameterExpression arg = Expression.Parameter(typeof(TestEnumDocument), "a"); + ParameterExpression argNewtonsoft = Expression.Parameter(typeof(TestEnumNewtonsoftDocument), "a"); // Access the value property MemberExpression docValueExpression = Expression.MakeMemberAccess( arg, + typeof(TestEnumDocument).GetProperty(nameof(TestEnumDocument.Value))! + ); + MemberExpression docValueExpressionDefault = Expression.MakeMemberAccess( + argNewtonsoft, typeof(TestEnumNewtonsoftDocument).GetProperty(nameof(TestEnumNewtonsoftDocument.Value))! ); @@ -102,15 +103,22 @@ public void EnumIsPreservedAsEXPRESSIONTest() docValueExpression, status ); + BinaryExpression expressionDefault = Expression.Equal( + docValueExpressionDefault, + status + ); // Create lambda expression - Expression> lambda = - Expression.Lambda>(expression, arg); - + Expression> lambda = + Expression.Lambda>(expression, arg); string sql = SqlTranslator.TranslateExpression(lambda.Body, options); - // Assert + Expression> lambdaNewtonsoft = + Expression.Lambda>(expressionDefault, argNewtonsoft); + string sqlDefault = SqlTranslator.TranslateExpression(lambdaNewtonsoft.Body, defaultOptions); + Assert.AreEqual("(a[\"Value\"] = \"One\")", sql); + Assert.AreEqual("(a[\"Value\"] = \"One\")", sqlDefault); } enum TestEnum @@ -122,19 +130,18 @@ enum TestEnum class TestEnumDocument { - [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] // TODO: Remove this once we have the ability to use custom serializer for LINQ queries public TestEnum Value { get; set; } } class TestEnumNewtonsoftDocument { - [JsonConverter(typeof(StringEnumConverter))] + [Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))] public TestEnum Value { get; set; } } class TestDocument { - [JsonConverter(typeof(DateJsonConverter))] + [Newtonsoft.Json.JsonConverter(typeof(DateJsonConverter))] public DateTime StartDate { get; set; } } @@ -156,8 +163,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s [TestMethod] public void TestNewtonsoftExtensionDataQuery() { + CosmosLinqSerializerOptionsInternal defaultOptions = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, null); + Expression> expr = a => (string)a.NewtonsoftExtensionData["foo"] == "bar"; - string sql = SqlTranslator.TranslateExpression(expr.Body); + string sql = SqlTranslator.TranslateExpression(expr.Body, defaultOptions); Assert.AreEqual("(a[\"foo\"] = \"bar\")", sql); } @@ -165,11 +174,11 @@ public void TestNewtonsoftExtensionDataQuery() [TestMethod] public void TestSystemTextJsonExtensionDataQuery() { + CosmosLinqSerializerOptionsInternal dotNetOptions = CosmosLinqSerializerOptionsInternal.Create(this.defaultOptions, new TestCustomJsonLinqSerializer()); + Expression> expr = a => ((object)a.NetExtensionData["foo"]) == "bar"; - string sql = SqlTranslator.TranslateExpression(expr.Body); + string sql = SqlTranslator.TranslateExpression(expr.Body, dotNetOptions); - // TODO: This is a limitation in the translator. It should be able to handle STJ extension data, if a custom - // JSON serializer is specified. Assert.AreEqual("(a[\"NetExtensionData\"][\"foo\"] = \"bar\")", sql); } @@ -182,6 +191,66 @@ class DocumentWithExtensionData public Dictionary NetExtensionData { get; set; } } + /// + // See: https://github.com/Azure/azure-cosmos-dotnet-v3/blob/master/Microsoft.Azure.Cosmos.Samples/Usage/SystemTextJson/CosmosSystemTextJsonSerializer.cs + /// + class TestCustomJsonLinqSerializer : CosmosLinqSerializer + { + private readonly JsonObjectSerializer systemTextJsonSerializer; + + public static readonly System.Text.Json.JsonSerializerOptions JsonOptions = new() + { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + Converters = { + new System.Text.Json.Serialization.JsonStringEnumConverter(), + } + }; + + public TestCustomJsonLinqSerializer() + { + this.systemTextJsonSerializer = new JsonObjectSerializer(JsonOptions); + } + + public override T FromStream(Stream stream) + { + using (stream) + { + if (stream.CanSeek && stream.Length == 0) + { + return default; + } + + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + return (T)(object)stream; + } + + return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); + } + } + + public override Stream ToStream(T input) + { + MemoryStream stream = new(); + + this.systemTextJsonSerializer.Serialize(stream, input, input.GetType(), default); + stream.Position = 0; + return stream; + } + + public override string SerializeMemberName(MemberInfo memberInfo) + { + JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute(true); + + string memberName = jsonPropertyNameAttribute != null && !string.IsNullOrEmpty(jsonPropertyNameAttribute.Name) + ? jsonPropertyNameAttribute.Name + : memberInfo.Name; + + return memberName; + } + } + /// // See: https://github.com/Azure/azure-cosmos-dotnet-v3/blob/master/Microsoft.Azure.Cosmos.Samples/Usage/SystemTextJson/CosmosSystemTextJsonSerializer.cs /// @@ -223,9 +292,9 @@ public override T FromStream(Stream stream) public override Stream ToStream(T input) { - MemoryStream stream = new (); + MemoryStream stream = new(); - this.systemTextJsonSerializer.Serialize(stream, input, typeof(T), default); + this.systemTextJsonSerializer.Serialize(stream, input, input.GetType(), default); stream.Position = 0; return stream; }