diff --git a/src/MongoDB.Driver/Core/Misc/Feature.cs b/src/MongoDB.Driver/Core/Misc/Feature.cs index a201eec9cbf..666b0fa46e6 100644 --- a/src/MongoDB.Driver/Core/Misc/Feature.cs +++ b/src/MongoDB.Driver/Core/Misc/Feature.cs @@ -59,6 +59,7 @@ public class Feature private static readonly Feature __dateFromStringFormatArgument = new Feature("DateFromStringFormatArgument", WireVersion.Server40); private static readonly Feature __dateOperatorsNewIn50 = new Feature("DateOperatorsNewIn50", WireVersion.Server50); private static readonly Feature __densifyStage = new Feature("DensifyStage", WireVersion.Server51); + private static readonly Feature __deserializeEJsonOperator = new Feature("DeserializeEJsonOperator", WireVersion.Server83); private static readonly Feature __documentsStage = new Feature("DocumentsStage", WireVersion.Server51); private static readonly Feature __directConnectionSetting = new Feature("DirectConnectionSetting", WireVersion.Server44); private static readonly Feature __electionIdPriorityInSDAM = new Feature("ElectionIdPriorityInSDAM ", WireVersion.Server60); @@ -95,6 +96,7 @@ public class Feature private static readonly Feature __scramSha256Authentication = new Feature("ScramSha256Authentication", WireVersion.Server40); private static readonly Feature __serverReturnsResumableChangeStreamErrorLabel = new Feature("ServerReturnsResumableChangeStreamErrorLabel", WireVersion.Server44); private static readonly Feature __serverReturnsRetryableWriteErrorLabel = new Feature("ServerReturnsRetryableWriteErrorLabel", WireVersion.Server44); + private static readonly Feature __serializeEJsonOperator = new Feature("SerializeEJsonOperator", WireVersion.Server83); private static readonly Feature __setStage = new Feature("SetStage", WireVersion.Server42); private static readonly Feature __setWindowFields = new Feature("SetWindowFields", WireVersion.Server50); private static readonly Feature __setWindowFieldsLocf = new Feature("SetWindowFieldsLocf", WireVersion.Server52); @@ -280,6 +282,11 @@ public class Feature /// public static Feature DensifyStage => __densifyStage; + /// + /// Gets the $deserializeEJSON operator feature. + /// + public static Feature DeserializeEJsonOperator => __deserializeEJsonOperator; + /// /// Gets the documents stage feature. /// @@ -468,6 +475,11 @@ public class Feature /// public static Feature ServerReturnsRetryableWriteErrorLabel => __serverReturnsRetryableWriteErrorLabel; + /// + /// Gets the $serializeEJSON operator feature. + /// + public static Feature SerializeEJsonOperator => __serializeEJsonOperator; + /// /// Gets the $set stage feature. /// diff --git a/src/MongoDB.Driver/DeserializeEJsonOptions.cs b/src/MongoDB.Driver/DeserializeEJsonOptions.cs new file mode 100644 index 00000000000..e989d4afce4 --- /dev/null +++ b/src/MongoDB.Driver/DeserializeEJsonOptions.cs @@ -0,0 +1,55 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver +{ + /// + /// Represents the options parameter for . + /// + public abstract class DeserializeEJsonOptions + { + internal abstract bool OnErrorWasSet(out object onError); + } + + /// + /// Represents the options parameter for . + /// This class allows setting 'onError'. + /// + /// The type of 'onError'. + public class DeserializeEJsonOptions : DeserializeEJsonOptions + { + private TOutput _onError; + private bool _onErrorWasSet; + + /// + /// The onError parameter. + /// + public TOutput OnError + { + get => _onError; + set + { + _onError = value; + _onErrorWasSet = true; + } + } + + internal override bool OnErrorWasSet(out object onError) + { + onError = _onError; + return _onErrorWasSet; + } + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs index ace59a917e0..0e2cc6e6d55 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs @@ -52,6 +52,7 @@ internal enum AstNodeType DateTruncExpression, DensifyStage, DerivativeOrIntegralWindowExpression, + DeserializeEJsonExpression, DocumentsStage, ElemMatchFilterOperation, ExistsFilterOperation, @@ -130,6 +131,7 @@ internal enum AstNodeType ReplaceWithStage, RTrimExpression, SampleStage, + SerializeEJsonExpression, SetStage, SetWindowFieldsStage, ShiftWindowExpression, diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstDeserializeEJsonExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstDeserializeEJsonExpression.cs new file mode 100644 index 00000000000..deef39e418d --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstDeserializeEJsonExpression.cs @@ -0,0 +1,66 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; + +internal sealed class AstDeserializeEJsonExpression : AstExpression +{ + private readonly AstExpression _input; + private readonly AstExpression _onError; + + public AstDeserializeEJsonExpression( + AstExpression input, + AstExpression onError = null) + { + _input = Ensure.IsNotNull(input, nameof(input)); + _onError = onError; + } + + public AstExpression Input => _input; + public override AstNodeType NodeType => AstNodeType.DeserializeEJsonExpression; + public AstExpression OnError => _onError; + + public override AstNode Accept(AstNodeVisitor visitor) => + visitor.VisitDeserializeEJsonExpression(this); + + public override BsonValue Render() + { + return new BsonDocument + { + { "$deserializeEJSON", new BsonDocument + { + { "input", _input.Render() }, + { "onError", () => _onError.Render(), _onError != null } + } + } + }; + } + + public AstDeserializeEJsonExpression Update( + AstExpression input, + AstExpression onError) + { + if (input == _input && onError == _onError) + { + return this; + } + + return new AstDeserializeEJsonExpression(input, onError); + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs index 54462a0aa4d..7291d46d57a 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs @@ -379,6 +379,13 @@ public static AstExpression DerivativeOrIntegralWindowExpression(AstDerivativeOr return new AstDerivativeOrIntegralWindowExpression(@operator, arg, unit, window); } + public static AstExpression DeserializeEJson( + AstExpression input, + AstExpression onError = null) + { + return new AstDeserializeEJsonExpression(input, onError); + } + public static AstExpression Divide(AstExpression arg1, AstExpression arg2) { if (arg1.IsConstant(out var constant1) && arg2.IsConstant(out var constant2)) @@ -743,6 +750,14 @@ public static AstExpression RTrim(AstExpression input, AstExpression chars = nul return new AstRTrimExpression(input, chars); } + public static AstExpression SerializeEJson( + AstExpression input, + AstExpression relaxed = null, + AstExpression onError = null) + { + return new AstSerializeEJsonExpression(input, relaxed, onError); + } + public static AstExpression SetDifference(AstExpression arg1, AstExpression arg2) { return new AstBinaryExpression(AstBinaryOperator.SetDifference, arg1, arg2); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstSerializeEJsonExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstSerializeEJsonExpression.cs new file mode 100644 index 00000000000..c058ae11f8e --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstSerializeEJsonExpression.cs @@ -0,0 +1,72 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; + +internal sealed class AstSerializeEJsonExpression : AstExpression +{ + private readonly AstExpression _input; + private readonly AstExpression _onError; + private readonly AstExpression _relaxed; + + public AstSerializeEJsonExpression( + AstExpression input, + AstExpression relaxed = null, + AstExpression onError = null) + { + _input = Ensure.IsNotNull(input, nameof(input)); + _relaxed = relaxed; + _onError = onError; + } + + public AstExpression Input => _input; + public override AstNodeType NodeType => AstNodeType.SerializeEJsonExpression; + public AstExpression OnError => _onError; + public AstExpression Relaxed => _relaxed; + + public override AstNode Accept(AstNodeVisitor visitor) => + visitor.VisitSerializeEJsonExpression(this); + + public override BsonValue Render() + { + return new BsonDocument + { + { "$serializeEJSON", new BsonDocument + { + { "input", _input.Render() }, + { "relaxed", () => _relaxed.Render(), _relaxed != null }, + { "onError", () => _onError.Render(), _onError != null } + } + } + }; + } + + public AstSerializeEJsonExpression Update( + AstExpression input, + AstExpression relaxed, + AstExpression onError) + { + if (input == _input && relaxed == _relaxed && onError == _onError) + { + return this; + } + + return new AstSerializeEJsonExpression(input, relaxed, onError); + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs index 733cdb8c7bd..768acb2c9f5 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs @@ -304,6 +304,11 @@ public virtual AstNode VisitDerivativeOrIntegralWindowExpression(AstDerivativeOr return node.Update(node.Operator, VisitAndConvert(node.Arg), node.Unit, node.Window); } + public virtual AstNode VisitDeserializeEJsonExpression(AstDeserializeEJsonExpression node) + { + return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.OnError)); + } + public virtual AstNode VisitDocumentsStage(AstDocumentsStage node) { return node.Update(VisitAndConvert(node.Documents)); @@ -689,6 +694,11 @@ public virtual AstNode VisitSampleStage(AstSampleStage node) return node; } + public virtual AstNode VisitSerializeEJsonExpression(AstSerializeEJsonExpression node) + { + return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.Relaxed), VisitAndConvert(node.OnError)); + } + public virtual AstNode VisitSetStage(AstSetStage node) { return node.Update(VisitAndConvert(node.Fields)); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs index 3a935cd8908..2e691b6ea34 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs @@ -31,10 +31,12 @@ internal static class MqlMethod private static readonly MethodInfo __dateFromStringWithFormat; private static readonly MethodInfo __dateFromStringWithFormatAndTimezone; private static readonly MethodInfo __dateFromStringWithFormatAndTimezoneAndOnErrorAndOnNull; + private static readonly MethodInfo __deserializeEJson; private static readonly MethodInfo __exists; private static readonly MethodInfo __field; private static readonly MethodInfo __isMissing; private static readonly MethodInfo __isNullOrMissing; + private static readonly MethodInfo __serializeEJson; private static readonly MethodInfo __sigmoid; private static readonly MethodInfo __subtype; @@ -55,10 +57,12 @@ static MqlMethod() __dateFromStringWithFormat = ReflectionInfo.Method((string dateString, string format) => Mql.DateFromString(dateString, format)); __dateFromStringWithFormatAndTimezone = ReflectionInfo.Method((string dateString, string format, string timezone) => Mql.DateFromString(dateString, format, timezone)); __dateFromStringWithFormatAndTimezoneAndOnErrorAndOnNull = ReflectionInfo.Method((string dateString, string format, string timezone, DateTime? onError, DateTime? onNull) => Mql.DateFromString(dateString, format, timezone, onError, onNull)); + __deserializeEJson = ReflectionInfo.Method((object value, DeserializeEJsonOptions options) => Mql.DeserializeEJson(value, options)); __exists = ReflectionInfo.Method((object field) => Mql.Exists(field)); __field = ReflectionInfo.Method((object container, string fieldName, IBsonSerializer serializer) => Mql.Field(container, fieldName, serializer)); __isMissing = ReflectionInfo.Method((object field) => Mql.IsMissing(field)); __isNullOrMissing = ReflectionInfo.Method((object field) => Mql.IsNullOrMissing(field)); + __serializeEJson = ReflectionInfo.Method((object value, SerializeEJsonOptions options) => Mql.SerializeEJson(value, options)); __sigmoid = ReflectionInfo.Method((double value) => Mql.Sigmoid(value)); __subtype = ReflectionInfo.Method((object value) => Mql.Subtype(value)); @@ -110,10 +114,12 @@ static MqlMethod() public static MethodInfo DateFromStringWithFormat => __dateFromStringWithFormat; public static MethodInfo DateFromStringWithFormatAndTimezone => __dateFromStringWithFormatAndTimezone; public static MethodInfo DateFromStringWithFormatAndTimezoneAndOnErrorAndOnNull => __dateFromStringWithFormatAndTimezoneAndOnErrorAndOnNull; + public static MethodInfo DeserializeEJson => __deserializeEJson; public static MethodInfo Exists => __exists; public static MethodInfo Field => __field; public static MethodInfo IsMissing => __isMissing; public static MethodInfo IsNullOrMissing => __isNullOrMissing; + public static MethodInfo SerializeEJson => __serializeEJson; public static MethodInfo Sigmoid => __sigmoid; public static MethodInfo Subtype => __subtype; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs index d7afec2bf46..2877deb902d 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs @@ -104,6 +104,7 @@ void DeduceMethodCallSerializers() case "DefaultIfEmpty": DeduceDefaultIfEmptyMethodSerializers(); break; case "DegreesToRadians": DeduceDegreesToRadiansMethodSerializers(); break; case "Densify": DeduceDensifyMethodSerializers(); break; + case "DeserializeEJson": DeduceDeserializeEJsonMethodSerializers(); break; case "Distinct": DeduceDistinctMethodSerializers(); break; case "DocumentNumber": DeduceDocumentNumberMethodSerializers(); break; case "Documents": DeduceDocumentsMethodSerializers(); break; @@ -137,6 +138,7 @@ void DeduceMethodCallSerializers() case "Select": DeduceSelectMethodSerializers(); break; case "SelectMany": DeduceSelectManySerializers(); break; case "SequenceEqual": DeduceSequenceEqualMethodSerializers(); break; + case "SerializeEJson": DeduceSerializeEJsonMethodSerializers(); break; case "SetEquals": DeduceSetEqualsMethodSerializers(); break; case "SetWindowFields": DeduceSetWindowFieldsMethodSerializers(); break; case "Shift": DeduceShiftMethodSerializers(); break; @@ -1165,6 +1167,23 @@ void DeduceDensifyMethodSerializers() } } + void DeduceDeserializeEJsonMethodSerializers() + { + if (method.Is(MqlMethod.DeserializeEJson)) + { + if (IsNotKnown(node)) + { + var outputType = method.GetGenericArguments()[1]; + var outputSerializer = BsonSerializer.LookupSerializer(outputType); + AddNodeSerializer(node, outputSerializer); + } + } + else + { + DeduceUnknownMethodSerializer(); + } + } + void DeduceDistinctMethodSerializers() { if (method.IsOneOf(EnumerableMethod.Distinct, QueryableMethod.Distinct)) @@ -2386,6 +2405,23 @@ void DeduceSequenceEqualMethodSerializers() } } + void DeduceSerializeEJsonMethodSerializers() + { + if (method.Is(MqlMethod.SerializeEJson)) + { + if (IsNotKnown(node)) + { + var outputType = method.GetGenericArguments()[1]; + var outputSerializer = BsonSerializer.LookupSerializer(outputType); + AddNodeSerializer(node, outputSerializer); + } + } + else + { + DeduceUnknownMethodSerializer(); + } + } + void DeduceSetEqualsMethodSerializers() { if (ISetMethod.IsSetEqualsMethod(method)) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs index 7dd42095e6b..c53b746dc6f 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs @@ -46,6 +46,7 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC case "DefaultIfEmpty": return DefaultIfEmptyMethodToAggregationExpressionTranslator.Translate(context, expression); case "DenseRank": return DenseRankMethodToAggregationExpressionTranslator.Translate(context, expression); case "Derivative": return DerivativeMethodToAggregationExpressionTranslator.Translate(context, expression); + case "DeserializeEJson": return DeserializeEJsonMethodToAggregationExpressionTranslator.Translate(context, expression); case "Distinct": return DistinctMethodToAggregationExpressionTranslator.Translate(context, expression); case "DocumentNumber": return DocumentNumberMethodToAggregationExpressionTranslator.Translate(context, expression); case "Equals": return EqualsMethodToAggregationExpressionTranslator.Translate(context, expression); @@ -77,6 +78,7 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC case "Select": return SelectMethodToAggregationExpressionTranslator.Translate(context, expression); case "SelectMany": return SelectManyMethodToAggregationExpressionTranslator.Translate(context, expression); case "SequenceEqual": return SequenceEqualMethodToAggregationExpressionTranslator.Translate(context, expression); + case "SerializeEJson": return SerializeEJsonMethodToAggregationExpressionTranslator.Translate(context, expression); case "SetEquals": return SetEqualsMethodToAggregationExpressionTranslator.Translate(context, expression); case "Shift": return ShiftMethodToAggregationExpressionTranslator.Translate(context, expression); case "Sigmoid": return SigmoidMethodToAggregationExpressionTranslator.Translate(context, expression); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslator.cs new file mode 100644 index 00000000000..cb35f8b2a1f --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslator.cs @@ -0,0 +1,113 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq.Expressions; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + internal class DeserializeEJsonMethodToAggregationExpressionTranslator + { + public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + if (!method.Is(MqlMethod.DeserializeEJson)) + { + throw new ExpressionNotSupportedException(expression); + } + + var valueExpression = arguments[0]; + var optionsExpression = arguments[1]; + + var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression); + var outputSerializer = context.GetSerializer(expression); + var onErrorAst = TranslateOptions(context, expression, optionsExpression, outputSerializer); + + var ast = AstExpression.DeserializeEJson(valueTranslation.Ast, onErrorAst); + return new TranslatedExpression(expression, ast, outputSerializer); + } + + private static AstExpression TranslateOptions( + TranslationContext context, + Expression expression, + Expression optionsExpression, + IBsonSerializer outputSerializer) + { + return optionsExpression switch + { + ConstantExpression constantExpression => TranslateOptions(constantExpression, outputSerializer), + MemberInitExpression memberInitExpression => TranslateOptions(context, expression, memberInitExpression, outputSerializer), + _ => throw new ExpressionNotSupportedException(optionsExpression, containingExpression: expression, because: "the options argument must be either a constant or a member initialization expression.") + }; + } + + private static AstExpression TranslateOptions( + ConstantExpression optionsExpression, + IBsonSerializer outputSerializer) + { + var options = (DeserializeEJsonOptions)optionsExpression.Value; + + AstExpression onErrorAst = null; + if (options != null) + { + if (options.OnErrorWasSet(out var onErrorValue)) + { + var serializedOnErrorValue = SerializationHelper.SerializeValue(outputSerializer, onErrorValue); + onErrorAst = AstExpression.Constant(serializedOnErrorValue); + } + } + + return onErrorAst; + } + + private static AstExpression TranslateOptions( + TranslationContext context, + Expression expression, + MemberInitExpression optionsExpression, + IBsonSerializer outputSerializer) + { + TranslatedExpression onErrorTranslation = null; + + foreach (var binding in optionsExpression.Bindings) + { + if (binding is not MemberAssignment memberAssignment) + { + throw new ExpressionNotSupportedException(optionsExpression, containingExpression: expression, because: "only member assignment is supported"); + } + + var memberName = memberAssignment.Member.Name; + var memberExpression = memberAssignment.Expression; + + switch (memberName) + { + case nameof(DeserializeEJsonOptions.OnError): + onErrorTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, memberExpression); + SerializationHelper.EnsureSerializerIsCompatible(memberExpression, containingExpression: expression, onErrorTranslation.Serializer, expectedSerializer: outputSerializer); + break; + default: + throw new ExpressionNotSupportedException(memberExpression, because: $"memberName {memberName} is invalid"); + } + } + + return onErrorTranslation?.Ast; + } + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslator.cs new file mode 100644 index 00000000000..6ba1d0fd235 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslator.cs @@ -0,0 +1,131 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq.Expressions; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + internal class SerializeEJsonMethodToAggregationExpressionTranslator + { + public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + if (!method.Is(MqlMethod.SerializeEJson)) + { + throw new ExpressionNotSupportedException(expression); + } + + var valueExpression = arguments[0]; + var optionsExpression = arguments[1]; + + var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression); + var outputSerializer = context.GetSerializer(expression); + var (relaxedAst, onErrorAst) = TranslateOptions(context, expression, optionsExpression, outputSerializer); + + var ast = AstExpression.SerializeEJson(valueTranslation.Ast, relaxedAst, onErrorAst); + return new TranslatedExpression(expression, ast, outputSerializer); + } + + private static (AstExpression relaxedAst, AstExpression onErrorAst) + TranslateOptions( + TranslationContext context, + Expression expression, + Expression optionsExpression, + IBsonSerializer outputSerializer) + { + return optionsExpression switch + { + ConstantExpression constantExpression => TranslateOptions(constantExpression, outputSerializer), + MemberInitExpression memberInitExpression => TranslateOptions(context, expression, memberInitExpression, outputSerializer), + _ => throw new ExpressionNotSupportedException(optionsExpression, containingExpression: expression, because: "the options argument must be either a constant or a member initialization expression.") + }; + } + + private static (AstExpression relaxedAst, AstExpression onErrorAst) + TranslateOptions( + ConstantExpression optionsExpression, + IBsonSerializer outputSerializer) + { + var options = (SerializeEJsonOptions)optionsExpression.Value; + + AstExpression relaxedAst = null; + AstExpression onErrorAst = null; + if (options != null) + { + if (options.Relaxed.HasValue) + { + relaxedAst = AstExpression.Constant(options.Relaxed.Value); + } + + if (options.OnErrorWasSet(out var onErrorValue)) + { + var serializedOnErrorValue = SerializationHelper.SerializeValue(outputSerializer, onErrorValue); + onErrorAst = AstExpression.Constant(serializedOnErrorValue); + } + } + + return (relaxedAst, onErrorAst); + } + + private static (AstExpression relaxedAst, AstExpression onErrorAst) + TranslateOptions( + TranslationContext context, + Expression expression, + MemberInitExpression optionsExpression, + IBsonSerializer outputSerializer) + { + AstExpression relaxedAst = null; + TranslatedExpression onErrorTranslation = null; + + foreach (var binding in optionsExpression.Bindings) + { + if (binding is not MemberAssignment memberAssignment) + { + throw new ExpressionNotSupportedException(optionsExpression, containingExpression: expression, because: "only member assignment is supported"); + } + + var memberName = memberAssignment.Member.Name; + var memberExpression = memberAssignment.Expression; + + switch (memberName) + { + case nameof(SerializeEJsonOptions.Relaxed): + var relaxedExpression = memberExpression.IsConvert(out var unwrapped) ? unwrapped : memberExpression; + var relaxedValue = relaxedExpression.GetConstantValue(expression); + if (relaxedValue.HasValue) + { + relaxedAst = AstExpression.Constant(relaxedValue.Value); + } + break; + case nameof(SerializeEJsonOptions.OnError): + onErrorTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, memberExpression); + SerializationHelper.EnsureSerializerIsCompatible(memberExpression, containingExpression: expression, onErrorTranslation.Serializer, expectedSerializer: outputSerializer); + break; + default: + throw new ExpressionNotSupportedException(memberExpression, because: $"memberName {memberName} is invalid"); + } + } + + return (relaxedAst, onErrorTranslation?.Ast); + } + } +} diff --git a/src/MongoDB.Driver/Mql.cs b/src/MongoDB.Driver/Mql.cs index af56a058295..1b5f77ced16 100644 --- a/src/MongoDB.Driver/Mql.cs +++ b/src/MongoDB.Driver/Mql.cs @@ -121,6 +121,19 @@ public static DateTime DateFromString( throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); } + /// + /// Deserializes Extended JSON values back to native BSON types using the $deserializeEJSON aggregation operator. + /// + /// The type of the input value. + /// The type of the output value. + /// The value to deserialize. + /// The deserialization options. + /// The deserialized value. + public static TOutput DeserializeEJson(TInput value, DeserializeEJsonOptions options = null) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + /// /// Tests whether a field exists. /// @@ -168,6 +181,19 @@ public static bool IsNullOrMissing(TField field) throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); } + /// + /// Serializes BSON values to their Extended JSON v2 representation using the $serializeEJSON aggregation operator. + /// + /// The type of the input value. + /// The type of the output value. + /// The value to serialize. + /// The serialization options. + /// The serialized value. + public static TOutput SerializeEJson(TInput value, SerializeEJsonOptions options = null) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + /// /// Transforms a real-valued input into a value between 0 and 1 using the $sigmoid operator. /// diff --git a/src/MongoDB.Driver/SerializeEJsonOptions.cs b/src/MongoDB.Driver/SerializeEJsonOptions.cs new file mode 100644 index 00000000000..734d4000681 --- /dev/null +++ b/src/MongoDB.Driver/SerializeEJsonOptions.cs @@ -0,0 +1,66 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver +{ + /// + /// Represents the options parameter for . + /// + public abstract class SerializeEJsonOptions + { + private bool? _relaxed; + + /// + /// The relaxed parameter. When true, produces relaxed Extended JSON format. When false, produces canonical format. The server defaults to true when this is not specified. + /// + public bool? Relaxed + { + get => _relaxed; + set => _relaxed = value; + } + + internal abstract bool OnErrorWasSet(out object onError); + } + + /// + /// Represents the options parameter for . + /// This class allows setting 'onError'. + /// + /// The type of 'onError'. + public class SerializeEJsonOptions : SerializeEJsonOptions + { + private TOutput _onError; + private bool _onErrorWasSet; + + /// + /// The onError parameter. + /// + public TOutput OnError + { + get => _onError; + set + { + _onError = value; + _onErrorWasSet = true; + } + } + + internal override bool OnErrorWasSet(out object onError) + { + onError = _onError; + return _onErrorWasSet; + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Integration/MqlDeserializeEJsonTests.cs b/tests/MongoDB.Driver.Tests/Linq/Integration/MqlDeserializeEJsonTests.cs new file mode 100644 index 00000000000..d022ca47d2f --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Integration/MqlDeserializeEJsonTests.cs @@ -0,0 +1,167 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Integration; + +public class MqlDeserializeEJsonTests : LinqIntegrationTest +{ + public MqlDeserializeEJsonTests(ClassFixture fixture) + : base(fixture, server => server.Supports(Feature.DeserializeEJsonOperator)) + { + } + + [Fact] + public void DeserializeEJson_should_convert_numberLong_wrapper_to_native_long() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 1) + .Select(d => Mql.DeserializeEJson(d.EJsonValue, null)); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 1 } }", + "{ '$project' : { '_v' : { '$deserializeEJSON' : { 'input' : '$EJsonValue' } }, '_id' : 0 } }"); + + // { "$numberLong": "123" } should become NumberLong(123) + var result = queryable.Single(); + result.Should().Be(BsonValue.Create(123L)); + } + + [Fact] + public void DeserializeEJson_should_convert_numberInt_wrapper_to_native_int() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 2) + .Select(d => Mql.DeserializeEJson(d.Value, null)); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 2 } }", + "{ '$project' : { '_v' : { '$deserializeEJSON' : { 'input' : '$Value' } }, '_id' : 0 } }"); + + // { "$numberInt": "42" } should become Int32(42) + var result = queryable.Single(); + result.Should().Be(BsonValue.Create(42)); + } + + [Fact] + public void DeserializeEJson_should_pass_through_plain_values() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 3) + .Select(d => Mql.DeserializeEJson(d.Value, null)); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 3 } }", + "{ '$project' : { '_v' : { '$deserializeEJSON' : { 'input' : '$Value' } }, '_id' : 0 } }"); + + // A plain string without EJSON wrappers should pass through unchanged + var result = queryable.Single(); + result.Should().Be(BsonValue.Create("hello")); + } + + [Fact] + public void DeserializeEJson_should_convert_document_with_wrapped_fields() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 4) + .Select(d => Mql.DeserializeEJson(d.EJsonValue, null)); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 4 } }", + "{ '$project' : { '_v' : { '$deserializeEJSON' : { 'input' : '$EJsonValue' } }, '_id' : 0 } }"); + + // { "a": { "$numberInt": "1" }, "b": { "$numberLong": "2" } } + // should become { a: 1, b: NumberLong(2) } + var result = queryable.Single(); + result["a"].Should().Be(BsonValue.Create(1)); + result["b"].Should().Be(BsonValue.Create(2L)); + } + + [Fact] + public void DeserializeEJson_with_string_output_should_return_native_string() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 3) + .Select(d => Mql.DeserializeEJson(d.Value, null)); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 3 } }", + "{ '$project' : { '_v' : { '$deserializeEJSON' : { 'input' : '$Value' } }, '_id' : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("hello"); + } + + [Fact] + public void DeserializeEJson_with_onError_should_return_fallback_on_invalid_input() + { + var collection = Fixture.Collection; + // Id == 5 has invalid EJSON: { "$numberLong": "not_a_number" } + var queryable = collection.AsQueryable() + .Where(d => d.Id == 5) + .Select(d => Mql.DeserializeEJson(d.EJsonValue, new DeserializeEJsonOptions { OnError = "fallback" })); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 5 } }", + "{ '$project' : { '_v' : { '$deserializeEJSON' : { 'input' : '$EJsonValue', 'onError' : 'fallback' } }, '_id' : 0 } }"); + + // Invalid EJSON should trigger onError and return "fallback" + var result = queryable.Single(); + result.Should().Be(BsonValue.Create("fallback")); + } + + public class C + { + public int Id { get; set; } + public BsonDocument EJsonValue { get; set; } + public BsonValue Value { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new() { Id = 1, EJsonValue = new BsonDocument("$numberLong", "123") }, + new() { Id = 2, Value = new BsonDocument("$numberInt", "42") }, + new() { Id = 3, Value = BsonValue.Create("hello") }, + new() { Id = 4, EJsonValue = new BsonDocument { { "a", new BsonDocument("$numberInt", "1") }, { "b", new BsonDocument("$numberLong", "2") } } }, + new() { Id = 5, EJsonValue = new BsonDocument("$numberLong", "not_a_number") }, + ]; + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Integration/MqlSerializeEJsonTests.cs b/tests/MongoDB.Driver.Tests/Linq/Integration/MqlSerializeEJsonTests.cs new file mode 100644 index 00000000000..b8049be3a31 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Integration/MqlSerializeEJsonTests.cs @@ -0,0 +1,164 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Integration; + +public class MqlSerializeEJsonTests : LinqIntegrationTest +{ + public MqlSerializeEJsonTests(ClassFixture fixture) + : base(fixture, server => server.Supports(Feature.SerializeEJsonOperator)) + { + } + + [Fact] + public void SerializeEJson_with_no_options_should_use_relaxed_format() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 1) + .Select(d => Mql.SerializeEJson(d.IntValue, null)); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 1 } }", + "{ '$project' : { '_v' : { '$serializeEJSON' : { 'input' : '$IntValue' } }, '_id' : 0 } }"); + + // Default is relaxed: int stays as int (no wrapper) + var result = queryable.Single(); + result.Should().Be(BsonValue.Create(42)); + } + + [Fact] + public void SerializeEJson_with_relaxed_false_should_produce_canonical_format() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 1) + .Select(d => Mql.SerializeEJson(d.IntValue, new SerializeEJsonOptions { Relaxed = false })); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 1 } }", + "{ '$project' : { '_v' : { '$serializeEJSON' : { 'input' : '$IntValue', 'relaxed' : false } }, '_id' : 0 } }"); + + // Canonical: int gets wrapped as { "$numberInt": "42" } + var result = queryable.Single(); + result.Should().Be(new BsonDocument("$numberInt", "42")); + } + + [Fact] + public void SerializeEJson_with_relaxed_true() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 1) + .Select(d => Mql.SerializeEJson(d.IntValue, new SerializeEJsonOptions { Relaxed = true })); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 1 } }", + "{ '$project' : { '_v' : { '$serializeEJSON' : { 'input' : '$IntValue', 'relaxed' : true } }, '_id' : 0 } }"); + + // Relaxed: int stays as int + var result = queryable.Single(); + result.Should().Be(BsonValue.Create(42)); + } + + [Fact] + public void SerializeEJson_with_document_input_canonical() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 2) + .Select(d => Mql.SerializeEJson(d.Document, new SerializeEJsonOptions { Relaxed = false })); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 2 } }", + "{ '$project' : { '_v' : { '$serializeEJSON' : { 'input' : '$Document', 'relaxed' : false } }, '_id' : 0 } }"); + + // Document { a: 1 } in canonical should become { a: { "$numberInt": "1" } } + var result = queryable.Single(); + result.Should().Be(new BsonDocument("a", new BsonDocument("$numberInt", "1"))); + } + + [Fact] + public void SerializeEJson_with_long_value_canonical() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 3) + .Select(d => Mql.SerializeEJson(d.LongValue, new SerializeEJsonOptions { Relaxed = false })); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 3 } }", + "{ '$project' : { '_v' : { '$serializeEJSON' : { 'input' : '$LongValue', 'relaxed' : false } }, '_id' : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(new BsonDocument("$numberLong", "100")); + } + + // $serializeEJSON only errors on BSON depth/size limit violations, which are impractical to + // trigger in a test. This test verifies the server accepts the onError option without errors. + [Fact] + public void SerializeEJson_with_onError_should_return_serialized_value_when_no_error() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(d => d.Id == 1) + .Select(d => Mql.SerializeEJson(d.IntValue, new SerializeEJsonOptions { Relaxed = false, OnError = "fallback" })); + + var renderedStages = Translate(collection, queryable); + AssertStages( + renderedStages, + "{ '$match' : { '_id' : 1 } }", + "{ '$project' : { '_v' : { '$serializeEJSON' : { 'input' : '$IntValue', 'relaxed' : false, 'onError' : 'fallback' } }, '_id' : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(new BsonDocument("$numberInt", "42")); + } + + public class C + { + public int Id { get; set; } + public int IntValue { get; set; } + public long LongValue { get; set; } + public BsonDocument Document { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new() { Id = 1, IntValue = 42 }, + new() { Id = 2, Document = new BsonDocument("a", 1) }, + new() { Id = 3, LongValue = 100L }, + ]; + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/SerializerFinders/MqlEJsonTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/SerializerFinders/MqlEJsonTests.cs new file mode 100644 index 00000000000..6d8167abf7b --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/SerializerFinders/MqlEJsonTests.cs @@ -0,0 +1,125 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq.Expressions; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq.Linq3Implementation.SerializerFinders; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.SerializerFinders; + +public class MqlEJsonTests +{ + [Fact] + public void SerializerFinder_should_resolve_mql_deserialize_ejson() + { + var expression = TestHelpers.MakeLambda(model => Mql.DeserializeEJson(model.StringField, null)); + var serializerMap = TestHelpers.CreateSerializerMap(expression); + + SerializerFinder.FindSerializers(expression.Body, null, serializerMap); + + serializerMap.IsKnown(expression.Body, out _).Should().BeTrue(); + serializerMap.GetSerializer(expression.Body).Should().BeOfType(); + } + + [Fact] + public void SerializerFinder_should_resolve_mql_serialize_ejson() + { + var expression = TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.IntField, null)); + var serializerMap = TestHelpers.CreateSerializerMap(expression); + + SerializerFinder.FindSerializers(expression.Body, null, serializerMap); + + serializerMap.IsKnown(expression.Body, out _).Should().BeTrue(); + serializerMap.GetSerializer(expression.Body).Should().BeOfType(); + } + + [Fact] + public void SerializerFinder_should_use_custom_serializer_from_parent_for_mql_deserialize_ejson() + { + var expression = TestHelpers.MakeLambda( + model => new OutputModel { Nested = Mql.DeserializeEJson(model.StringField, null) }); + var serializerMap = TestHelpers.CreateSerializerMap(expression); + + // Pre-assign a custom serializer to the MemberInit node (simulating parent context) + var customNestedSerializer = new CustomNestedModelSerializer(); + var outputSerializer = CreateOutputModelSerializer(customNestedSerializer); + serializerMap.AddSerializer(expression.Body, outputSerializer); + + SerializerFinder.FindSerializers(expression.Body, null, serializerMap); + + var memberInit = (MemberInitExpression)expression.Body; + var memberAssignment = (MemberAssignment)memberInit.Bindings[0]; + var deserializeExpr = memberAssignment.Expression; + + serializerMap.GetSerializer(deserializeExpr).Should().BeSameAs(customNestedSerializer); + } + + [Fact] + public void SerializerFinder_should_use_custom_serializer_from_parent_for_mql_serialize_ejson() + { + var expression = TestHelpers.MakeLambda( + model => new OutputModel { Nested = Mql.SerializeEJson(model.IntField, null) }); + var serializerMap = TestHelpers.CreateSerializerMap(expression); + + // Pre-assign a custom serializer to the MemberInit node (simulating parent context) + var customNestedSerializer = new CustomNestedModelSerializer(); + var outputSerializer = CreateOutputModelSerializer(customNestedSerializer); + serializerMap.AddSerializer(expression.Body, outputSerializer); + + SerializerFinder.FindSerializers(expression.Body, null, serializerMap); + + var memberInit = (MemberInitExpression)expression.Body; + var memberAssignment = (MemberAssignment)memberInit.Bindings[0]; + var serializeExpr = memberAssignment.Expression; + + serializerMap.GetSerializer(serializeExpr).Should().BeSameAs(customNestedSerializer); + } + + private static IBsonSerializer CreateOutputModelSerializer(IBsonSerializer nestedMemberSerializer) + { + var classMap = new BsonClassMap(); + classMap.AutoMap(); + classMap.GetMemberMap(nameof(OutputModel.Nested)).SetSerializer(nestedMemberSerializer); + classMap.Freeze(); + return new BsonClassMapSerializer(classMap); + } + + private class MyModel + { + public string StringField { get; set; } + public int IntField { get; set; } + } + + private class OutputModel + { + public NestedModel Nested { get; set; } + } + + private class NestedModel + { + public string Value { get; set; } + } + + private class CustomNestedModelSerializer : SerializerBase + { + public override NestedModel Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) => null; + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, NestedModel value) { } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslatorTests.cs new file mode 100644 index 00000000000..b01be9542b8 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslatorTests.cs @@ -0,0 +1,62 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Linq.Expressions; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Linq; +using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators; + +public class DeserializeEJsonMethodToAggregationExpressionTranslatorTests +{ + [Theory] + [MemberData(nameof(SupportedTestCases))] + public void Translate_should_produce_proper_ast(LambdaExpression expression, string expectedAst) + { + var translationContext = TestHelpers.CreateTranslationContext(expression); + var translation = DeserializeEJsonMethodToAggregationExpressionTranslator.Translate(translationContext, (MethodCallExpression)expression.Body); + + translation.Ast.Render().Should().Be(BsonDocument.Parse(expectedAst)); + } + + public static IEnumerable SupportedTestCases = + [ + // No options (defaults) + [ + TestHelpers.MakeLambda(model => Mql.DeserializeEJson(model.Document, null)), + "{ $deserializeEJSON : { input : { $getField : { field : 'Document', input : '$$ROOT' } } } }" + ], + // With BsonValue input + [ + TestHelpers.MakeLambda(model => Mql.DeserializeEJson(model.Value, null)), + "{ $deserializeEJSON : { input : { $getField : { field : 'Value', input : '$$ROOT' } } } }" + ], + // With onError + [ + TestHelpers.MakeLambda(model => Mql.DeserializeEJson(model.Document, new DeserializeEJsonOptions { OnError = "fallback" })), + "{ $deserializeEJSON : { input : { $getField : { field : 'Document', input : '$$ROOT' } }, onError : 'fallback' } }" + ], + ]; + + public class MyModel + { + public BsonDocument Document { get; set; } + public BsonValue Value { get; set; } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslatorTests.cs new file mode 100644 index 00000000000..705c94cf0b6 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslatorTests.cs @@ -0,0 +1,83 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Linq.Expressions; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Linq; +using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators; + +public class SerializeEJsonMethodToAggregationExpressionTranslatorTests +{ + [Theory] + [MemberData(nameof(SupportedTestCases))] + public void Translate_should_produce_proper_ast(LambdaExpression expression, string expectedAst) + { + var translationContext = TestHelpers.CreateTranslationContext(expression); + var translation = SerializeEJsonMethodToAggregationExpressionTranslator.Translate(translationContext, (MethodCallExpression)expression.Body); + + translation.Ast.Render().Should().Be(BsonDocument.Parse(expectedAst)); + } + + public static IEnumerable SupportedTestCases = + [ + // No options (defaults) + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.IntValue, null)), + "{ $serializeEJSON : { input : { $getField : { field : 'IntValue', input : '$$ROOT' } } } }" + ], + // With relaxed = false (canonical) + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.IntValue, new SerializeEJsonOptions { Relaxed = false })), + "{ $serializeEJSON : { input : { $getField : { field : 'IntValue', input : '$$ROOT' } }, relaxed : false } }" + ], + // With relaxed = true + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.IntValue, new SerializeEJsonOptions { Relaxed = true })), + "{ $serializeEJSON : { input : { $getField : { field : 'IntValue', input : '$$ROOT' } }, relaxed : true } }" + ], + // With string input + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.StringValue, null)), + "{ $serializeEJSON : { input : { $getField : { field : 'StringValue', input : '$$ROOT' } } } }" + ], + // With BsonDocument input + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.Document, null)), + "{ $serializeEJSON : { input : { $getField : { field : 'Document', input : '$$ROOT' } } } }" + ], + // With relaxed = null (not specified, should not emit relaxed field) + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.IntValue, new SerializeEJsonOptions { Relaxed = null })), + "{ $serializeEJSON : { input : { $getField : { field : 'IntValue', input : '$$ROOT' } } } }" + ], + // With relaxed and onError + [ + TestHelpers.MakeLambda(model => Mql.SerializeEJson(model.IntValue, new SerializeEJsonOptions { Relaxed = false, OnError = "error" })), + "{ $serializeEJSON : { input : { $getField : { field : 'IntValue', input : '$$ROOT' } }, relaxed : false, onError : 'error' } }" + ], + ]; + + public class MyModel + { + public int IntValue { get; set; } + public string StringValue { get; set; } + public BsonDocument Document { get; set; } + } +}