Skip to content

Commit 3720217

Browse files
committed
CSHARP-4550: Make MemberInitExpression work with struct also as long as suitable constructor exists.
1 parent 1ef7dd3 commit 3720217

File tree

2 files changed

+131
-39
lines changed

2 files changed

+131
-39
lines changed

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

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,41 @@ internal static class MemberInitExpressionToAggregationExpressionTranslator
2929
public static AggregationExpression Translate(TranslationContext context, MemberInitExpression expression)
3030
{
3131
var newExpression = expression.NewExpression;
32-
var constructorInfo = newExpression.Constructor;
32+
var constructorInfo = newExpression.Constructor; // note: can be null when using the default constructor with a struct
3333
var constructorArguments = newExpression.Arguments;
34+
var computedFields = new List<AstComputedField>();
3435

3536
var classMap = CreateClassMap(expression.Type, constructorInfo, out var creatorMap);
36-
var creatorMapParameters = creatorMap.Arguments?.ToArray();
37-
if (constructorInfo.GetParameters().Length > 0 && creatorMapParameters == null )
37+
if (constructorInfo != null && creatorMap != null)
3838
{
39-
throw new ExpressionNotSupportedException(expression, because: $"couldn't find matching properties for constructor parameters.");
40-
}
39+
var creatorMapParameters = creatorMap.Arguments?.ToArray();
40+
if (constructorInfo.GetParameters().Length > 0 && creatorMapParameters == null)
41+
{
42+
throw new ExpressionNotSupportedException(expression, because: $"couldn't find matching properties for constructor parameters.");
43+
}
4144

42-
var computedFields = new List<AstComputedField>();
43-
for (var i = 0; i < creatorMapParameters.Length; i++)
44-
{
45-
var creatorMapParameter = creatorMapParameters[i];
46-
var constructorArgumentExpression = constructorArguments[i];
47-
var constructorArgumentTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, constructorArgumentExpression);
48-
var constructorArgumentType = constructorArgumentExpression.Type;
49-
var constructorArgumentSerializer = constructorArgumentTranslation.Serializer ?? BsonSerializer.LookupSerializer(constructorArgumentType);
50-
var memberMap = EnsureMemberMap(expression, classMap, creatorMapParameter);
51-
memberMap.SetSerializer(constructorArgumentSerializer);
52-
computedFields.Add(AstExpression.ComputedField(memberMap.ElementName, constructorArgumentTranslation.Ast));
45+
for (var i = 0; i < creatorMapParameters.Length; i++)
46+
{
47+
var creatorMapParameter = creatorMapParameters[i];
48+
var constructorArgumentExpression = constructorArguments[i];
49+
var constructorArgumentTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, constructorArgumentExpression);
50+
var constructorArgumentType = constructorArgumentExpression.Type;
51+
var constructorArgumentSerializer = constructorArgumentTranslation.Serializer ?? BsonSerializer.LookupSerializer(constructorArgumentType);
52+
var memberMap = EnsureMemberMap(expression, classMap, creatorMapParameter);
53+
memberMap.SetSerializer(constructorArgumentSerializer);
54+
computedFields.Add(AstExpression.ComputedField(memberMap.ElementName, constructorArgumentTranslation.Ast));
55+
}
5356
}
5457

5558
foreach (var binding in expression.Bindings)
5659
{
5760
var memberAssignment = (MemberAssignment)binding;
5861
var member = memberAssignment.Member;
5962
var memberMap = FindMemberMap(expression, classMap, member.Name);
60-
6163
var valueExpression = memberAssignment.Expression;
6264
var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression);
63-
computedFields.Add(AstExpression.ComputedField(memberMap.ElementName, valueTranslation.Ast));
64-
6565
memberMap.SetSerializer(valueTranslation.Serializer);
66+
computedFields.Add(AstExpression.ComputedField(memberMap.ElementName, valueTranslation.Ast));
6667
}
6768

6869
var ast = AstExpression.ComputedDocument(computedFields);

tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Translators/ExpressionToAggregationExpressionTranslators/MemberInitExpressionToAggregationExpressionTranslatorTests.cs

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,51 @@
1515

1616
using System;
1717
using FluentAssertions;
18+
using MongoDB.Bson.Serialization.Attributes;
1819
using MongoDB.Driver.Linq;
1920
using Xunit;
2021

2122
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Translators.ExpressionToAggregationExpressionTranslators
2223
{
2324
public class MemberInitExpressionToAggregationExpressionTranslatorTests : Linq3IntegrationTest
2425
{
25-
private readonly IMongoCollection<MyData> _collection;
26-
27-
public MemberInitExpressionToAggregationExpressionTranslatorTests()
26+
[Fact]
27+
public void Should_project_class_via_parameterless_constructor()
2828
{
29-
_collection = CreateCollection(LinqProvider.V3);
29+
var collection = CreateCollection();
30+
31+
var queryable = collection.AsQueryable()
32+
.Select(x => new SpawnDataClassParameterless
33+
{
34+
Identifier = x.Id,
35+
SpawnDate = x.Date,
36+
SpawnText = x.Text
37+
});
38+
39+
var stages = Translate(collection, queryable);
40+
AssertStages(stages, "{ $project : { Identifier : '$_id', SpawnDate : '$Date', SpawnText : '$Text', _id : 0 } }");
41+
42+
var results = queryable.Single();
43+
44+
results.SpawnDate.Should().Be(new DateTime(2023, 1, 2, 3, 4, 5, DateTimeKind.Utc));
45+
results.SpawnText.Should().Be("data text");
46+
results.Identifier.Should().Be(1);
3047
}
3148

3249
[Fact]
33-
public void Should_project_via_parameterless_constructor()
50+
public void Should_project_struct_via_parameterless_constructor()
3451
{
35-
var queryable = _collection.AsQueryable()
36-
.Select(x => new SpawnDataParameterless
52+
var collection = CreateCollection();
53+
54+
var queryable = collection.AsQueryable()
55+
.Select(x => new SpawnDataStructParameterless
3756
{
3857
Identifier = x.Id,
3958
SpawnDate = x.Date,
4059
SpawnText = x.Text
4160
});
4261

43-
var stages = Translate(_collection, queryable);
62+
var stages = Translate(collection, queryable);
4463
AssertStages(stages, "{ $project : { Identifier : '$_id', SpawnDate : '$Date', SpawnText : '$Text', _id : 0 } }");
4564

4665
var results = queryable.Single();
@@ -51,15 +70,38 @@ public void Should_project_via_parameterless_constructor()
5170
}
5271

5372
[Fact]
54-
public void Should_project_via_constructor()
73+
public void Should_project_class_via_constructor()
5574
{
56-
var queryable = _collection.AsQueryable()
57-
.Select(x => new SpawnData(x.Id, x.Date)
75+
var collection = CreateCollection();
76+
77+
var queryable = collection.AsQueryable()
78+
.Select(x => new SpawnDataClass(x.Id, x.Date)
79+
{
80+
SpawnText = x.Text
81+
});
82+
83+
var stages = Translate(collection, queryable);
84+
AssertStages(stages, "{ $project : { Identifier : '$_id', SpawnDate : '$Date', SpawnText : '$Text', _id : 0 } }");
85+
86+
var results = queryable.Single();
87+
88+
results.SpawnDate.Should().Be(new DateTime(2023, 1, 2, 3, 4, 5, DateTimeKind.Utc));
89+
results.SpawnText.Should().Be("data text");
90+
results.Identifier.Should().Be(1);
91+
}
92+
93+
[Fact]
94+
public void Should_project_struct_via_constructor()
95+
{
96+
var collection = CreateCollection();
97+
98+
var queryable = collection.AsQueryable()
99+
.Select(x => new SpawnDataStruct(x.Id, x.Date)
58100
{
59101
SpawnText = x.Text
60102
});
61103

62-
var stages = Translate(_collection, queryable);
104+
var stages = Translate(collection, queryable);
63105
AssertStages(stages, "{ $project : { Identifier : '$_id', SpawnDate : '$Date', SpawnText : '$Text', _id : 0 } }");
64106

65107
var results = queryable.Single();
@@ -72,13 +114,15 @@ public void Should_project_via_constructor()
72114
[Fact]
73115
public void Should_project_via_constructor_with_inheritance()
74116
{
75-
var queryable = _collection.AsQueryable()
117+
var collection = CreateCollection();
118+
119+
var queryable = collection.AsQueryable()
76120
.Select(x => new InheritedSpawnData(x.Id, x.Date)
77121
{
78122
SpawnText = x.Text
79123
});
80124

81-
var stages = Translate(_collection, queryable);
125+
var stages = Translate(collection, queryable);
82126
AssertStages(stages, "{ $project : { Identifier : '$_id', SpawnDate : '$Date', SpawnText : '$Text', _id : 0 } }");
83127

84128
var results = queryable.Single();
@@ -88,9 +132,9 @@ public void Should_project_via_constructor_with_inheritance()
88132
results.Identifier.Should().Be(1);
89133
}
90134

91-
private IMongoCollection<MyData> CreateCollection(LinqProvider linqProvider)
135+
private IMongoCollection<MyData> CreateCollection()
92136
{
93-
var collection = GetCollection<MyData>("data", linqProvider);
137+
var collection = GetCollection<MyData>("data");
94138

95139
CreateCollection(
96140
collection,
@@ -106,23 +150,70 @@ public class MyData
106150
public string Text;
107151
}
108152

109-
public class SpawnDataParameterless
153+
public class SpawnDataClassParameterless
110154
{
111155
public int Identifier;
112156
public DateTime SpawnDate;
113157
public string SpawnText;
114158
}
115159

116-
public class SpawnData
160+
public struct SpawnDataStructParameterless
161+
{
162+
public int Identifier;
163+
public DateTime SpawnDate;
164+
public string SpawnText;
165+
166+
// this constructor is required to be able to deserialize instances of this struct
167+
[BsonConstructor]
168+
public SpawnDataStructParameterless(int identifier, DateTime spawnDate, string spawnText)
169+
{
170+
Identifier = identifier;
171+
SpawnDate = spawnDate;
172+
SpawnText = spawnText;
173+
}
174+
}
175+
176+
public class SpawnDataClass
177+
{
178+
public readonly int Identifier;
179+
public DateTime SpawnDate;
180+
private string spawnText;
181+
182+
public SpawnDataClass(int identifier, DateTime spawnDate)
183+
{
184+
Identifier = identifier;
185+
SpawnDate = spawnDate;
186+
}
187+
188+
public string SpawnText
189+
{
190+
get => spawnText;
191+
set => spawnText = value;
192+
}
193+
}
194+
195+
public struct SpawnDataStruct
117196
{
197+
[BsonElement]
118198
public readonly int Identifier;
119199
public DateTime SpawnDate;
120200
private string spawnText;
121201

122-
public SpawnData(int identifier, DateTime spawnDate)
202+
// this constructor is required for the test to compile
203+
public SpawnDataStruct(int identifier, DateTime spawnDate)
204+
{
205+
Identifier = identifier;
206+
SpawnDate = spawnDate;
207+
spawnText = default;
208+
}
209+
210+
// this constructor is required to be able to deserialize instances of this struct
211+
[BsonConstructor]
212+
public SpawnDataStruct(int identifier, DateTime spawnDate, string spawnText)
123213
{
124214
Identifier = identifier;
125215
SpawnDate = spawnDate;
216+
this.spawnText = spawnText;
126217
}
127218

128219
public string SpawnText
@@ -132,7 +223,7 @@ public string SpawnText
132223
}
133224
}
134225

135-
public class InheritedSpawnData : SpawnData
226+
public class InheritedSpawnData : SpawnDataClass
136227
{
137228
public InheritedSpawnData(int identifier, DateTime spawnDate)
138229
: base(identifier, spawnDate)

0 commit comments

Comments
 (0)