From a285da247d2776005573ada765bf47b6a1a89147 Mon Sep 17 00:00:00 2001
From: Ferdinando Papale <4850119+papafe@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:22:04 +0200
Subject: [PATCH 01/10] CSHARP-5656: Investigate changes in SERVER-107499:
Support (de)serialization between BSON and EJSON
---
src/MongoDB.Driver/Core/Misc/Feature.cs | 12 ++
src/MongoDB.Driver/DeserializeEJsonOptions.cs | 55 ++++++
.../Linq3Implementation/Ast/AstNodeType.cs | 2 +
.../AstDeserializeEJsonExpression.cs | 69 ++++++++
.../Ast/Expressions/AstExpression.cs | 15 ++
.../AstSerializeEJsonExpression.cs | 75 ++++++++
.../Ast/Visitors/AstNodeVisitor.cs | 10 ++
.../Reflection/MqlMethod.cs | 6 +
.../SerializerFinderVisitMethodCall.cs | 36 ++++
...essionToAggregationExpressionTranslator.cs | 2 +
...MethodToAggregationExpressionTranslator.cs | 113 ++++++++++++
...MethodToAggregationExpressionTranslator.cs | 128 ++++++++++++++
src/MongoDB.Driver/Mql.cs | 26 +++
src/MongoDB.Driver/SerializeEJsonOptions.cs | 66 +++++++
.../Integration/MqlDeserializeEJsonTests.cs | 149 ++++++++++++++++
.../Integration/MqlSerializeEJsonTests.cs | 167 ++++++++++++++++++
...dToAggregationExpressionTranslatorTests.cs | 62 +++++++
...dToAggregationExpressionTranslatorTests.cs | 78 ++++++++
18 files changed, 1071 insertions(+)
create mode 100644 src/MongoDB.Driver/DeserializeEJsonOptions.cs
create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstDeserializeEJsonExpression.cs
create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstSerializeEJsonExpression.cs
create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslator.cs
create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslator.cs
create mode 100644 src/MongoDB.Driver/SerializeEJsonOptions.cs
create mode 100644 tests/MongoDB.Driver.Tests/Linq/Integration/MqlDeserializeEJsonTests.cs
create mode 100644 tests/MongoDB.Driver.Tests/Linq/Integration/MqlSerializeEJsonTests.cs
create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DeserializeEJsonMethodToAggregationExpressionTranslatorTests.cs
create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SerializeEJsonMethodToAggregationExpressionTranslatorTests.cs
diff --git a/src/MongoDB.Driver/Core/Misc/Feature.cs b/src/MongoDB.Driver/Core/Misc/Feature.cs
index a201eec9cbf..7b5719602ed 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);
@@ -99,6 +100,7 @@ public class Feature
private static readonly Feature __setWindowFields = new Feature("SetWindowFields", WireVersion.Server50);
private static readonly Feature __setWindowFieldsLocf = new Feature("SetWindowFieldsLocf", WireVersion.Server52);
private static readonly Feature __shardedTransactions = new Feature("ShardedTransactions", WireVersion.Server42);
+ private static readonly Feature __serializeEJsonOperator = new Feature("SerializeEJsonOperator", WireVersion.Server83);
private static readonly Feature __sigmoidOperator = new Feature("SigmoidOperator", WireVersion.Server81);
private static readonly Feature __similarityFunctions = new Feature("SimilarityFunctions", WireVersion.Server82);
private static readonly Feature __snapshotReads = new Feature("SnapshotReads", WireVersion.Server50, notSupportedMessage: "Snapshot reads require MongoDB 5.0 or later");
@@ -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..dc23ffeba92
--- /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 to set '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..0984441ebdd 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs
@@ -40,6 +40,7 @@ internal enum AstNodeType
CountStage,
CurrentOpStage,
CustomAccumulatorExpression,
+ DeserializeEJsonExpression,
DateAddExpression,
DateDiffExpression,
DateFromIsoWeekPartsExpression,
@@ -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..508bc76e7cb
--- /dev/null
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstDeserializeEJsonExpression.cs
@@ -0,0 +1,69 @@
+/* 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)
+ {
+ return 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..5406c92bbec 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs
@@ -303,6 +303,13 @@ public static AstExpression Convert(
return new AstConvertExpression(input, to, subType, byteOrder, format, onError, onNull);
}
+ public static AstExpression DeserializeEJson(
+ AstExpression input,
+ AstExpression onError = null)
+ {
+ return new AstDeserializeEJsonExpression(input, onError);
+ }
+
public static AstExpression DateAdd(
AstExpression startDate,
AstExpression unit,
@@ -778,6 +785,14 @@ public static AstExpression SetUnion(params AstExpression[] args)
return new AstNaryExpression(AstNaryOperator.SetUnion, args);
}
+ public static AstExpression SerializeEJson(
+ AstExpression input,
+ AstExpression relaxed = null,
+ AstExpression onError = null)
+ {
+ return new AstSerializeEJsonExpression(input, relaxed, onError);
+ }
+
public static AstExpression ShiftWindowExpression(AstExpression arg, int by, AstExpression defaultValue)
{
return new AstShiftWindowExpression(arg, by, defaultValue);
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..146e8857cb5
--- /dev/null
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstSerializeEJsonExpression.cs
@@ -0,0 +1,75 @@
+/* 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)
+ {
+ return 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..444f97c7195 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs
@@ -234,6 +234,11 @@ public virtual AstNode VisitCountStage(AstCountStage node)
return node;
}
+ public virtual AstNode VisitDeserializeEJsonExpression(AstDeserializeEJsonExpression node)
+ {
+ return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.OnError));
+ }
+
public virtual AstNode VisitCurrentOpStage(AstCurrentOpStage node)
{
return node;
@@ -699,6 +704,11 @@ public virtual AstNode VisitSetWindowFieldsStage(AstSetWindowFieldsStage node)
return node.Update(VisitAndConvert(node.PartitionBy), node.SortBy, VisitAndConvert(node.Output));
}
+ public virtual AstNode VisitSerializeEJsonExpression(AstSerializeEJsonExpression node)
+ {
+ return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.Relaxed), VisitAndConvert(node.OnError));
+ }
+
public virtual AstNode VisitShiftWindowExpression(AstShiftWindowExpression node)
{
return node.Update(VisitAndConvert(node.Arg), node.By, VisitAndConvert(node.DefaultValue));
diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs
index 3a935cd8908..bf21d38a6f3 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs
@@ -28,6 +28,7 @@ internal static class MqlMethod
private static readonly MethodInfo __constantWithSerializer;
private static readonly MethodInfo __convert;
private static readonly MethodInfo __dateFromString;
+ private static readonly MethodInfo __deserializeEJson;
private static readonly MethodInfo __dateFromStringWithFormat;
private static readonly MethodInfo __dateFromStringWithFormatAndTimezone;
private static readonly MethodInfo __dateFromStringWithFormatAndTimezoneAndOnErrorAndOnNull;
@@ -35,6 +36,7 @@ internal static class MqlMethod
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;
@@ -52,6 +54,7 @@ static MqlMethod()
__constantWithSerializer = ReflectionInfo.Method((object value, IBsonSerializer