Skip to content

Commit 269d224

Browse files
rstamJamesKovacs
authored andcommitted
CSHARP-3922: LINQ3: support calls to constructors in Select.
1 parent 07e8388 commit 269d224

File tree

3 files changed

+155
-19
lines changed

3 files changed

+155
-19
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KnownSerializers/KnownSerializersRegistry.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,18 @@ public void AddKnownSerializer(Expression expression, IBsonSerializer knownSeria
5959

6060
public IBsonSerializer GetSerializer(Expression expression, IBsonSerializer defaultSerializer = null)
6161
{
62-
var type = expression is LambdaExpression lambdaExpression ? lambdaExpression.ReturnType : expression.Type;
63-
return GetSerializer(expression, type, defaultSerializer);
62+
var expressionType = expression is LambdaExpression lambdaExpression ? lambdaExpression.ReturnType : expression.Type;
63+
return GetSerializer(expression, expressionType, defaultSerializer);
6464
}
6565

66-
private IBsonSerializer GetSerializer(Expression expression, Type type, IBsonSerializer defaultSerializer = null)
66+
public IBsonSerializer GetSerializer(Expression expression, Type type, IBsonSerializer defaultSerializer = null)
6767
{
6868
var possibleSerializers = _registry.TryGetValue(expression, out var knownSerializers) ? knownSerializers.GetPossibleSerializers(type) : new HashSet<IBsonSerializer>();
6969
return possibleSerializers.Count switch
7070
{
7171
0 => defaultSerializer ?? LookupSerializer(expression, type), // sometimes there is no known serializer from the context (e.g. CSHARP-4062)
7272
1 => possibleSerializers.First(),
73-
_ => throw new InvalidOperationException($"More than one possible serializer found for {expression}.")
73+
_ => throw new InvalidOperationException($"More than one possible serializer found for {type} in {expression}.")
7474
};
7575
}
7676

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

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,37 +27,62 @@ internal static class NewExpressionToAggregationExpressionTranslator
2727
{
2828
public static AggregationExpression Translate(TranslationContext context, NewExpression expression)
2929
{
30-
if (expression.Type == typeof(DateTime))
30+
var expressionType = expression.Type;
31+
var constructorInfo = expression.Constructor;
32+
var arguments = expression.Arguments.ToArray();
33+
var members = expression.Members;
34+
35+
if (expressionType == typeof(DateTime))
3136
{
3237
return NewDateTimeExpressionToAggregationExpressionTranslator.Translate(context, expression);
3338
}
34-
if (expression.Type.IsConstructedGenericType && expression.Type.GetGenericTypeDefinition() == typeof(HashSet<>))
39+
if (expressionType.IsConstructedGenericType && expressionType.GetGenericTypeDefinition() == typeof(HashSet<>))
3540
{
3641
return NewHashSetExpressionToAggregationExpressionTranslator.Translate(context, expression);
3742
}
38-
if (expression.Type.IsConstructedGenericType && expression.Type.GetGenericTypeDefinition() == typeof(List<>))
43+
if (expressionType.IsConstructedGenericType && expressionType.GetGenericTypeDefinition() == typeof(List<>))
3944
{
4045
return NewListExpressionToAggregationExpressionTranslator.Translate(context, expression);
4146
}
4247

43-
var classMapType = typeof(BsonClassMap<>).MakeGenericType(expression.Type);
48+
var classMapType = typeof(BsonClassMap<>).MakeGenericType(expressionType);
4449
var classMap = (BsonClassMap)Activator.CreateInstance(classMapType);
4550
var computedFields = new List<AstComputedField>();
4651

47-
for (var i = 0; i < expression.Members.Count; i++)
52+
string[] propertyNames;
53+
if (members != null)
54+
{
55+
// if Members is not null then trust Members more than the constructor parameter names (which are compiler generated for anonymous types)
56+
propertyNames = members.Select(member => member.Name).ToArray();
57+
}
58+
else
4859
{
49-
var member = expression.Members[i];
50-
var fieldExpression = expression.Arguments[i];
51-
var fieldTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, fieldExpression);
52-
var memberSerializer = fieldTranslation.Serializer ?? BsonSerializer.LookupSerializer(fieldExpression.Type);
53-
var defaultValue = GetDefaultValue(memberSerializer.ValueType);
54-
classMap.MapProperty(member.Name).SetSerializer(memberSerializer).SetDefaultValue(defaultValue);
55-
computedFields.Add(AstExpression.ComputedField(member.Name, fieldTranslation.Ast));
60+
propertyNames = constructorInfo.GetParameters().Select(p => GetMatchingPropertyName(expression, p.Name)).ToArray();
5661
}
5762

58-
var constructorInfo = expression.Constructor;
59-
var constructorArgumentNames = expression.Members.Select(m => m.Name).ToArray();
60-
classMap.MapConstructor(constructorInfo, constructorArgumentNames);
63+
for (var i = 0; i < arguments.Length; i++)
64+
{
65+
var propertyName = propertyNames[i];
66+
var valueExpression = arguments[i];
67+
var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression);
68+
var valueSerializer = valueTranslation.Serializer ?? BsonSerializer.LookupSerializer(valueExpression.Type);
69+
var defaultValue = GetDefaultValue(valueSerializer.ValueType);
70+
classMap.MapProperty(propertyName).SetSerializer(valueSerializer).SetDefaultValue(defaultValue);
71+
computedFields.Add(AstExpression.ComputedField(propertyName, valueTranslation.Ast));
72+
}
73+
74+
// map any properties that didn't match a constructor argument
75+
foreach (var property in expressionType.GetProperties())
76+
{
77+
if (!propertyNames.Contains(property.Name))
78+
{
79+
var valueSerializer = context.KnownSerializersRegistry.GetSerializer(expression, property.PropertyType);
80+
var defaultValue = GetDefaultValue(valueSerializer.ValueType);
81+
classMap.MapProperty(property.Name).SetSerializer(valueSerializer).SetDefaultValue(defaultValue);
82+
}
83+
}
84+
85+
classMap.MapConstructor(constructorInfo, propertyNames);
6186
classMap.Freeze();
6287

6388
var ast = AstExpression.ComputedDocument(computedFields);
@@ -82,5 +107,18 @@ private static object GetDefaultValue(Type type)
82107
return null;
83108
}
84109
}
110+
111+
private static string GetMatchingPropertyName(NewExpression expression, string constructorParameterName)
112+
{
113+
foreach (var property in expression.Type.GetProperties())
114+
{
115+
if (property.Name.Equals(constructorParameterName, StringComparison.OrdinalIgnoreCase))
116+
{
117+
return property.Name;
118+
}
119+
}
120+
121+
throw new ExpressionNotSupportedException(expression, because: $"constructor parameter {constructorParameterName} does not match any property");
122+
}
85123
}
86124
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.Linq;
17+
using Xunit;
18+
19+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
20+
{
21+
public class CSharp3922Tests : Linq3IntegrationTest
22+
{
23+
[Fact]
24+
public void Select_with_anonymous_class_should_work()
25+
{
26+
var collection = GetCollection<C>();
27+
28+
var queryable = collection.AsQueryable()
29+
.Select(c => new { R = c.X })
30+
.Select(a => new { S = a.R });
31+
32+
var stages = Translate(collection, queryable);
33+
AssertStages(
34+
stages,
35+
"{ $project : { R : '$X', _id : 0 } }",
36+
"{ $project : { S : '$R', _id : 0 } }");
37+
}
38+
39+
[Fact]
40+
public void Select_with_constructor_call_should_work()
41+
{
42+
var collection = GetCollection<C>();
43+
44+
var queryable = collection.AsQueryable()
45+
.Select(c => new D(c.X))
46+
.Select(d => new { R = d.X, S = d.Y })
47+
.Select(a => new { T = a.R, U = a.S });
48+
49+
var stages = Translate(collection, queryable);
50+
AssertStages(
51+
stages,
52+
"{ $project : { X : '$X', _id : 0 } }",
53+
"{ $project : { R : '$X', S : '$Y', _id : 0 } }",
54+
"{ $project : { T : '$R', U : '$S', _id : 0 } }");
55+
}
56+
57+
[Fact]
58+
public void Select_with_constructor_call_and_property_set_should_work()
59+
{
60+
var collection = GetCollection<C>();
61+
62+
var queryable = collection.AsQueryable()
63+
.Select(c => new D(c.X) { Y = 123 })
64+
.Select(d => new { R = d.X, S = d.Y })
65+
.Select(a => new { T = a.R, U = a.S });
66+
67+
var stages = Translate(collection, queryable);
68+
AssertStages(
69+
stages,
70+
"{ $project : { X : '$X', Y : { $literal : 123 }, _id : 0 } }",
71+
"{ $project : { R : '$X', S : '$Y', _id : 0 } }",
72+
"{ $project : { T : '$R', U : '$S', _id : 0 } }");
73+
}
74+
75+
private class C
76+
{
77+
public int Id { get; set; }
78+
public int X { get; set; }
79+
}
80+
81+
private class D
82+
{
83+
public D(int x)
84+
{
85+
X = x;
86+
}
87+
88+
public D(int x, int y)
89+
{
90+
X = x;
91+
Y = y;
92+
}
93+
94+
public int X { get; set; }
95+
public int Y { get; set; }
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)