Skip to content

Commit 6fd8f4b

Browse files
committed
CSHARP-4368: Support Convert to BsonValue in LINQ3.
1 parent 8f89593 commit 6fd8f4b

File tree

5 files changed

+244
-2
lines changed

5 files changed

+244
-2
lines changed

src/MongoDB.Bson/Serialization/PrimitiveSerializationProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Globalization;
1919
using System.Net;
2020
using System.Reflection;
21+
using System.Text.RegularExpressions;
2122
using MongoDB.Bson.Serialization.Serializers;
2223

2324
namespace MongoDB.Bson.Serialization
@@ -53,6 +54,7 @@ static PrimitiveSerializationProvider()
5354
{ typeof(Nullable<>), typeof(NullableSerializer<>) },
5455
{ typeof(Object), typeof(ObjectSerializer) },
5556
{ typeof(ObjectId), typeof(ObjectIdSerializer) },
57+
{ typeof(Regex), typeof(RegexSerializer) },
5658
{ typeof(SByte), typeof(SByteSerializer) },
5759
{ typeof(Single), typeof(SingleSerializer) },
5860
{ typeof(String), typeof(StringSerializer) },

src/MongoDB.Bson/Serialization/Serializers/RegexSerializer.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ public override Regex Deserialize(BsonDeserializationContext context, BsonDeseri
9090
var bsonType = reader.GetCurrentBsonType();
9191
switch (bsonType)
9292
{
93+
case BsonType.Null:
94+
reader.ReadNull();
95+
return null;
96+
9397
case BsonType.RegularExpression:
9498
return reader.ReadRegularExpression().ToRegex();
9599

@@ -111,6 +115,12 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati
111115
{
112116
var writer = context.Writer;
113117

118+
if (value == null)
119+
{
120+
writer.WriteNull();
121+
return;
122+
}
123+
114124
switch (_representation)
115125
{
116126
case BsonType.RegularExpression:

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ConvertExpressionToAggregationExpressionTranslator.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515

1616
using System;
1717
using System.Linq.Expressions;
18+
using System.Text.RegularExpressions;
19+
using System.Xml.Schema;
20+
using MongoDB.Bson;
1821
using MongoDB.Bson.Serialization;
1922
using MongoDB.Bson.Serialization.Serializers;
2023
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
21-
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2224

2325
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators
2426
{
@@ -28,10 +30,15 @@ public static AggregationExpression Translate(TranslationContext context, UnaryE
2830
{
2931
if (expression.NodeType == ExpressionType.Convert)
3032
{
33+
var expressionType = expression.Type;
34+
if (expressionType == typeof(BsonValue))
35+
{
36+
return TranslateConvertToBsonValue(context, expression, expression.Operand);
37+
}
38+
3139
var operandExpression = expression.Operand;
3240
var operandTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, operandExpression);
3341

34-
var expressionType = expression.Type;
3542
if (expressionType.IsConstructedGenericType && expressionType.GetGenericTypeDefinition() == typeof(Nullable<>))
3643
{
3744
var valueType = expressionType.GetGenericArguments()[0];
@@ -65,5 +72,20 @@ public static AggregationExpression Translate(TranslationContext context, UnaryE
6572

6673
throw new ExpressionNotSupportedException(expression);
6774
}
75+
76+
private static AggregationExpression TranslateConvertToBsonValue(TranslationContext context, UnaryExpression expression, Expression operand)
77+
{
78+
// handle double conversions like `(BsonValue)(object)x.Anything`
79+
if (operand is UnaryExpression unaryExpression &&
80+
unaryExpression.NodeType == ExpressionType.Convert &&
81+
unaryExpression.Type == typeof(object))
82+
{
83+
operand = unaryExpression.Operand;
84+
}
85+
86+
var operandTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, operand);
87+
88+
return new AggregationExpression(expression, operandTranslation.Ast, BsonValueSerializer.Instance);
89+
}
6890
}
6991
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq.Expressions;
19+
using System.Text.RegularExpressions;
20+
using FluentAssertions;
21+
using MongoDB.Bson;
22+
using MongoDB.Bson.IO;
23+
using MongoDB.Bson.Serialization;
24+
using MongoDB.Bson.Serialization.Serializers;
25+
using MongoDB.Bson.TestHelpers;
26+
using MongoDB.Driver.Linq;
27+
using MongoDB.Driver.Linq.Linq3Implementation.Serializers;
28+
using Xunit;
29+
30+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
31+
{
32+
public class CSharp4368Tests : Linq3IntegrationTest
33+
{
34+
private static readonly IBsonSerializer<Guid> __guidSerializerWithStandardRepresentation;
35+
private static readonly IBsonSerializer<Guid?> __nullableGuidSerializerWithStandardRepresentation;
36+
37+
static CSharp4368Tests()
38+
{
39+
__guidSerializerWithStandardRepresentation = new GuidSerializer(GuidRepresentation.Standard);
40+
__nullableGuidSerializerWithStandardRepresentation = new NullableSerializer<Guid>(__guidSerializerWithStandardRepresentation);
41+
42+
var guidClassMap = BsonClassMap.RegisterClassMap<Document<Guid>>(
43+
cm =>
44+
{
45+
cm.MapMember(x => x.V).SetSerializer(__guidSerializerWithStandardRepresentation);
46+
});
47+
48+
var nullableGuidClassMap = BsonClassMap.RegisterClassMap<Document<Guid?>>(
49+
cm =>
50+
{
51+
cm.MapMember(x => x.V).SetSerializer(__nullableGuidSerializerWithStandardRepresentation);
52+
});
53+
}
54+
55+
public class TestCase
56+
{
57+
public Type ValueType { get; set; }
58+
public string ValueAsJson { get; set; }
59+
public LambdaExpression Projection { get; set; }
60+
}
61+
62+
public static TestCase CreateTestCase<TValue>(
63+
string valueAsJson,
64+
Expression<Func<Document<TValue>, BsonValue>> projection)
65+
{
66+
return new TestCase { ValueType = typeof(TValue), ValueAsJson = valueAsJson, Projection = projection };
67+
}
68+
69+
public static TestCase[] __testCases = new TestCase[]
70+
{
71+
CreateTestCase<Anything>("{ X : 1 }", x => (BsonValue)(object)x.V),
72+
CreateTestCase<Anything>("null", x => (BsonValue)(object)x.V),
73+
CreateTestCase<byte[]>("BinData(0, 'AQID')'", x => (BsonValue)x.V),
74+
CreateTestCase<byte[]>("null", x => (BsonValue)x.V),
75+
CreateTestCase<bool>("true", x => (BsonValue)x.V),
76+
CreateTestCase<bool?>("true", x => (BsonValue)x.V),
77+
CreateTestCase<bool?>("null", x => (BsonValue)x.V),
78+
CreateTestCase<DateTime>("ISODate('2021-01-02T03:04:05.123')", x => (BsonValue)x.V),
79+
CreateTestCase<DateTime?>("ISODate('2021-01-02T03:04:05.123')", x => (BsonValue)x.V),
80+
CreateTestCase<DateTime?>("null", x => (BsonValue)x.V),
81+
CreateTestCase<decimal>("'1'", x => (BsonValue)x.V),
82+
CreateTestCase<decimal?>("'1'", x => (BsonValue)x.V),
83+
CreateTestCase<decimal?>("null", x => (BsonValue)x.V),
84+
CreateTestCase<Decimal128>("'1'", x => (BsonValue)x.V),
85+
CreateTestCase<Decimal128?>("'1'", x => (BsonValue)x.V),
86+
CreateTestCase<Decimal128?>("null", x => (BsonValue)x.V),
87+
CreateTestCase<double>("{ $numberDouble : '1.0' }", x => (BsonValue)x.V),
88+
CreateTestCase<double?>("{ $numberDouble : '1.0' }", x => (BsonValue)x.V),
89+
CreateTestCase<double?>("null", x => (BsonValue)x.V),
90+
#pragma warning disable CS0618 // Type or member is obsolete
91+
CreateTestCase<Guid>("UUID('01020304-0506-0708-090a-0b0c0d0e0f10')", x => (BsonValue)x.V),
92+
CreateTestCase<Guid?>("UUID('01020304-0506-0708-090a-0b0c0d0e0f10')", x => (BsonValue)x.V),
93+
CreateTestCase<Guid?>("null", x => (BsonValue)x.V),
94+
#pragma warning restore CS0618 // Type or member is obsolete
95+
CreateTestCase<Guid>("UUID('01020304-0506-0708-090a-0b0c0d0e0f10')", x => (BsonValue)(object)x.V),
96+
CreateTestCase<Guid?>("UUID('01020304-0506-0708-090a-0b0c0d0e0f10')", x => (BsonValue)(object)x.V),
97+
CreateTestCase<Guid?>("null", x => (BsonValue)(object)x.V),
98+
CreateTestCase<int>("1", x => (BsonValue)x.V),
99+
CreateTestCase<int?>("1", x => (BsonValue)x.V),
100+
CreateTestCase<int?>("null", x => (BsonValue)x.V),
101+
CreateTestCase<long>("{ $numberLong : '1' }", x => (BsonValue)x.V),
102+
CreateTestCase<long?>("{ $numberLong : '1' }", x => (BsonValue)x.V),
103+
CreateTestCase<long?>("null", x => (BsonValue)x.V),
104+
CreateTestCase<ObjectId>("ObjectId('0102030405060708090a0b0c')", x => (BsonValue)x.V),
105+
CreateTestCase<ObjectId?>("ObjectId('0102030405060708090a0b0c')", x => (BsonValue)x.V),
106+
CreateTestCase<ObjectId?>("null", x => (BsonValue)x.V),
107+
CreateTestCase<Regex>("/abc/i", x => (BsonValue)x.V),
108+
CreateTestCase<Regex>("null", x => (BsonValue)x.V),
109+
CreateTestCase<string>("'abc'", x => (BsonValue)x.V),
110+
CreateTestCase<string>("null", x => (BsonValue)x.V)
111+
};
112+
113+
public static IEnumerable<object[]> Convert_to_BsonValue_from_TValue_should_work_MemberData()
114+
{
115+
for (var i = 0; i < __testCases.Length; i++)
116+
{
117+
var valueType = __testCases[i].ValueType;
118+
var valueAsJson = __testCases[i].ValueAsJson;
119+
var projectionAsString = __testCases[i].Projection.ToString();
120+
yield return new object[] { valueType, i, valueAsJson, projectionAsString };
121+
}
122+
}
123+
124+
[Theory]
125+
[MemberData(nameof(Convert_to_BsonValue_from_TValue_should_work_MemberData))]
126+
[ResetGuidModeAfterTest]
127+
public void Convert_to_BsonValue_from_TValue_should_work_invoker(Type valueType, int i, string valueAsJson, string projectionAsString)
128+
{
129+
GuidMode.Set(GuidRepresentationMode.V3);
130+
131+
var testMethodInfo = this.GetType().GetMethod(nameof(Convert_to_BsonValue_from_TValue_should_work));
132+
var testMethod = testMethodInfo.MakeGenericMethod(valueType);
133+
testMethod.Invoke(this, new object[] { i, valueAsJson, projectionAsString });
134+
}
135+
136+
public void Convert_to_BsonValue_from_TValue_should_work<TValue>(int i, string valueAsJson, string projectionAsString)
137+
{
138+
var serializer = typeof(TValue) switch
139+
{
140+
var type when type == typeof(Guid) => (IBsonSerializer<TValue>)__guidSerializerWithStandardRepresentation,
141+
var type when type == typeof(Guid?) => (IBsonSerializer<TValue>)__nullableGuidSerializerWithStandardRepresentation,
142+
_ => BsonSerializer.LookupSerializer<TValue>()
143+
};
144+
var value = Deserialize(serializer, valueAsJson);
145+
var projection = (Expression<Func<Document<TValue>, BsonValue>>)__testCases[i].Projection;
146+
var expectedResult = BsonSerializer.Deserialize<BsonValue>(valueAsJson);
147+
148+
var collection = CreateCollection(value);
149+
150+
var queryable = collection
151+
.AsQueryable()
152+
.Select(projection);
153+
154+
var stages = Translate(collection, queryable, out var outputSerializer);
155+
AssertStages(
156+
stages,
157+
"{ $project : { _v : '$V', _id : 0 } }");
158+
159+
var wrappedValueSerializer = outputSerializer.Should().BeOfType<WrappedValueSerializer<BsonValue>>().Subject;
160+
wrappedValueSerializer.ValueSerializer.Should().Be(BsonValueSerializer.Instance);
161+
162+
var results = queryable.ToList();
163+
results.Should().Equal(expectedResult);
164+
}
165+
166+
private IMongoCollection<Document<TValue>> CreateCollection<TValue>(TValue value)
167+
{
168+
var collection = GetCollection<Document<TValue>>();
169+
170+
CreateCollection(
171+
collection,
172+
new Document<TValue> { V = value });
173+
174+
return collection;
175+
}
176+
177+
private TValue Deserialize<TValue>(IBsonSerializer<TValue> serializer, string json)
178+
{
179+
using (var reader = new JsonReader(json))
180+
{
181+
var context = BsonDeserializationContext.CreateRoot(reader);
182+
return serializer.Deserialize(context);
183+
}
184+
}
185+
186+
public class Document<TValue>
187+
{
188+
public TValue V { get; set; }
189+
}
190+
191+
public class Anything
192+
{
193+
public int X { get; set; }
194+
}
195+
}
196+
}

tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Linq3IntegrationTest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ protected List<BsonDocument> Translate<TDocument, TResult>(IMongoCollection<TDoc
9999
return Translate<TDocument, TResult>(queryable);
100100
}
101101

102+
// in this overload the collection argument is used only to infer the TDocument type
103+
protected List<BsonDocument> Translate<TDocument, TResult>(IMongoCollection<TDocument> collection, IQueryable<TResult> queryable, out IBsonSerializer<TResult> outputSerializer)
104+
{
105+
return Translate<TDocument, TResult>(queryable, out outputSerializer);
106+
}
107+
102108
protected static List<BsonDocument> Translate<TResult>(IMongoDatabase database, IAggregateFluent<TResult> aggregate)
103109
{
104110
var pipelineDefinition = ((AggregateFluent<NoPipelineInput, TResult>)aggregate).Pipeline;
@@ -107,10 +113,16 @@ protected static List<BsonDocument> Translate<TResult>(IMongoDatabase database,
107113
}
108114

109115
protected List<BsonDocument> Translate<TDocument, TResult>(IQueryable<TResult> queryable)
116+
{
117+
return Translate<TDocument, TResult>(queryable, out _);
118+
}
119+
120+
protected List<BsonDocument> Translate<TDocument, TResult>(IQueryable<TResult> queryable, out IBsonSerializer<TResult> outputSerializer)
110121
{
111122
var provider = (MongoQueryProvider<TDocument>)queryable.Provider;
112123
var executableQuery = ExpressionToExecutableQueryTranslator.Translate<TDocument, TResult>(provider, queryable.Expression);
113124
var stages = executableQuery.Pipeline.Stages;
125+
outputSerializer = (IBsonSerializer<TResult>)executableQuery.Pipeline.OutputSerializer;
114126
return stages.Select(s => s.Render().AsBsonDocument).ToList();
115127
}
116128

0 commit comments

Comments
 (0)