diff --git a/src/MongoDB.Bson/Serialization/BsonSerializationInfo.cs b/src/MongoDB.Bson/Serialization/BsonSerializationInfo.cs
index 3b9dcf977cd..ac0e28842b7 100644
--- a/src/MongoDB.Bson/Serialization/BsonSerializationInfo.cs
+++ b/src/MongoDB.Bson/Serialization/BsonSerializationInfo.cs
@@ -15,6 +15,8 @@
using System;
using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
using MongoDB.Bson.IO;
namespace MongoDB.Bson.Serialization
@@ -24,10 +26,25 @@ namespace MongoDB.Bson.Serialization
///
public class BsonSerializationInfo
{
+ #region static
+ ///
+ /// Creates anew instance of the BsonSerializationinfo class with an element path instead of an element name.
+ ///
+ /// The element path.
+ /// The serializer.
+ /// The nominal type.
+ /// A BsonSerializationInfo.
+ public static BsonSerializationInfo CreateWithPath(IEnumerable elementPath, IBsonSerializer serializer, Type nominalType)
+ {
+ return new BsonSerializationInfo(elementPath.ToList(), serializer, nominalType);
+ }
+ #endregion
+
// private fields
- private string _elementName;
- private IBsonSerializer _serializer;
- private Type _nominalType;
+ private readonly string _elementName;
+ private readonly IReadOnlyList _elementPath;
+ private readonly IBsonSerializer _serializer;
+ private readonly Type _nominalType;
// constructors
///
@@ -43,13 +60,35 @@ public BsonSerializationInfo(string elementName, IBsonSerializer serializer, Typ
_nominalType = nominalType;
}
+ private BsonSerializationInfo(IReadOnlyList elementPath, IBsonSerializer serializer, Type nominalType)
+ {
+ _elementPath = elementPath;
+ _serializer = serializer;
+ _nominalType = nominalType;
+ }
+
// public properties
///
- /// Gets or sets the dotted element name.
+ /// Gets the element name.
///
public string ElementName
{
- get { return _elementName; }
+ get
+ {
+ if (_elementPath != null)
+ {
+ throw new InvalidOperationException("When ElementPath is not null you must use it instead.");
+ }
+ return _elementName;
+ }
+ }
+
+ ///
+ /// Gets element path.
+ ///
+ public IReadOnlyList ElementPath
+ {
+ get { return _elementPath; }
}
///
@@ -92,20 +131,21 @@ public object DeserializeValue(BsonValue value)
///
/// The new info.
/// A new BsonSerializationInfo.
+ [Obsolete("This method is no longer relevant because field names are now allowed to contain dots.")]
public BsonSerializationInfo Merge(BsonSerializationInfo newSerializationInfo)
{
string elementName = null;
- if (_elementName != null && newSerializationInfo._elementName != null)
+ if (ElementName != null && newSerializationInfo.ElementName != null)
{
- elementName = _elementName + "." + newSerializationInfo._elementName;
+ elementName = ElementName + "." + newSerializationInfo.ElementName;
}
- else if (_elementName != null)
+ else if (ElementName != null)
{
- elementName = _elementName;
+ elementName = ElementName;
}
- else if (newSerializationInfo._elementName != null)
+ else if (newSerializationInfo.ElementName != null)
{
- elementName = newSerializationInfo._elementName;
+ elementName = newSerializationInfo.ElementName;
}
return new BsonSerializationInfo(
diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/DocumentSerializerHelper.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/DocumentSerializerHelper.cs
index 37692e69e8c..329a8b477e5 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/DocumentSerializerHelper.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/DocumentSerializerHelper.cs
@@ -32,7 +32,14 @@ public static MemberSerializationInfo GetMemberSerializationInfo(IBsonSerializer
throw new InvalidOperationException($"Serializer for {serializer.ValueType} does not have a member named {memberName}.");
}
- return new MemberSerializationInfo(serializationInfo.ElementName, serializationInfo.Serializer);
+ if (serializationInfo.ElementPath == null)
+ {
+ return new MemberSerializationInfo(serializationInfo.ElementName, serializationInfo.Serializer);
+ }
+ else
+ {
+ return new MemberSerializationInfo(serializationInfo.ElementPath, serializationInfo.Serializer);
+ }
}
public static bool HasMemberSerializationInfo(IBsonSerializer serializer, string memberName)
diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/MemberSerializationInfo.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/MemberSerializationInfo.cs
index 637a7b4e0ab..4fa1fea88ac 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/MemberSerializationInfo.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/MemberSerializationInfo.cs
@@ -13,6 +13,8 @@
* limitations under the License.
*/
+using System;
+using System.Collections.Generic;
using MongoDB.Bson.Serialization;
using MongoDB.Driver.Core.Misc;
@@ -22,6 +24,7 @@ internal class MemberSerializationInfo
{
// private fields
private readonly string _elementName;
+ private readonly IReadOnlyList _elementPath;
private readonly IBsonSerializer _serializer;
// constructors
@@ -31,8 +34,27 @@ public MemberSerializationInfo(string elementName, IBsonSerializer serializer)
_serializer = Ensure.IsNotNull(serializer, nameof(serializer));
}
+ public MemberSerializationInfo(IReadOnlyList elementPath, IBsonSerializer serializer)
+ {
+ _elementPath = Ensure.IsNotNull(elementPath, nameof(elementPath));
+ _serializer = Ensure.IsNotNull(serializer, nameof(serializer));
+ }
+
// public properties
- public string ElementName => _elementName;
+ public string ElementName
+ {
+ get
+ {
+ if (_elementPath != null)
+ {
+ throw new InvalidOperationException("When ElementPath is not null you must use it instead.");
+ }
+ return _elementName;
+ }
+ }
+
+ public IReadOnlyList ElementPath => _elementPath;
+
public IBsonSerializer Serializer => _serializer;
}
}
diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs
index 9bf0390dd1f..db47c8bb5a5 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs
@@ -77,7 +77,19 @@ public static AggregationExpression Translate(TranslationContext context, Member
}
var serializationInfo = DocumentSerializerHelper.GetMemberSerializationInfo(containerTranslation.Serializer, member.Name);
- var ast = AstExpression.GetField(containerTranslation.Ast, serializationInfo.ElementName);
+ AstExpression ast;
+ if (serializationInfo.ElementPath == null)
+ {
+ ast = AstExpression.GetField(containerTranslation.Ast, serializationInfo.ElementName);
+ }
+ else
+ {
+ ast = containerTranslation.Ast;
+ foreach (var subFieldName in serializationInfo.ElementPath)
+ {
+ ast = AstExpression.GetField(ast, subFieldName);
+ }
+ }
return new AggregationExpression(expression, ast, serializationInfo.Serializer);
}
diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs
index 31e5d2d73a3..29cd8dc9030 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs
@@ -89,9 +89,21 @@ public static AstFilterField Translate(TranslationContext context, MemberExpress
if (fieldSerializer is IBsonDocumentSerializer documentSerializer &&
documentSerializer.TryGetMemberSerializationInfo(memberExpression.Member.Name, out BsonSerializationInfo memberSerializationInfo))
{
- var subFieldName = memberSerializationInfo.ElementName;
var subFieldSerializer = memberSerializationInfo.Serializer;
- return field.SubField(subFieldName, subFieldSerializer);
+ if (memberSerializationInfo.ElementPath == null)
+ {
+ var subFieldName = memberSerializationInfo.ElementName;
+ return field.SubField(subFieldName, subFieldSerializer);
+ }
+ else
+ {
+ var subField = field;
+ foreach (var subFieldName in memberSerializationInfo.ElementPath)
+ {
+ subField = subField.SubField(subFieldName, subFieldSerializer);
+ }
+ return subField;
+ }
}
if (memberExpression.Expression.Type.IsConstructedGenericType &&
diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharpxxxxTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharpxxxxTests.cs
new file mode 100644
index 00000000000..15357b022bd
--- /dev/null
+++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharpxxxxTests.cs
@@ -0,0 +1,241 @@
+/* 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;
+using FluentAssertions;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Attributes;
+using MongoDB.Bson.Serialization.Serializers;
+using MongoDB.Driver.Linq;
+using MongoDB.TestHelpers.XunitExtensions;
+using Xunit;
+
+namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
+{
+ public class CSharpxxxTests : Linq3IntegrationTest
+ {
+ [Fact]
+ public void Deserialize_should_ungroup_members()
+ {
+ var serializedOrderItem = "{ _id : { OrderId : 1, ProductId : 2 }, Quantity : 3 }";
+
+ var orderItem = BsonSerializer.Deserialize(serializedOrderItem);
+
+ orderItem.OrderId.Should().Be(1);
+ orderItem.ProductId.Should().Be(2);
+ orderItem.Quantity.Should().Be(3);
+ }
+
+ [Fact]
+ public void Serialize_should_group_members()
+ {
+ var orderItem = new OrderItem { OrderId = 1, ProductId = 2, Quantity = 3 };
+
+ var serializedOrderItem = orderItem.ToBsonDocument();
+
+ serializedOrderItem.Should().Be("{ _id : { OrderId : 1, ProductId : 2 }, Quantity : 3 }");
+ }
+
+ [Theory]
+ [ParameterAttributeData]
+ public void Select_with_grouped_member_should_work(
+ [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
+ {
+ var collection = GetCollection(linqProvider);
+
+ var queryable = collection.AsQueryable()
+ .Select(x => x.OrderId);
+
+ if (linqProvider == LinqProvider.V2)
+ {
+ var exception = Record.Exception(() => Translate(collection, queryable));
+ exception.Should().BeOfType(); // LINQ2 doesn't understand grouped members
+ }
+ else
+ {
+ var stages = Translate(collection, queryable);
+ AssertStages(stages, "{ $project : { _v : '$_id.OrderId', _id : 0 } }");
+
+ var results = queryable.ToList();
+ results.Should().Equal(1, 4);
+ }
+ }
+
+ [Theory]
+ [ParameterAttributeData]
+ public void Select_with_ungrouped_member_should_work(
+ [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
+ {
+ var collection = GetCollection(linqProvider);
+
+ var queryable = collection.AsQueryable()
+ .Select(x => x.Quantity);
+
+ var stages = Translate(collection, queryable);
+ if (linqProvider == LinqProvider.V2)
+ {
+ AssertStages(stages, "{ $project : { Quantity : '$Quantity', _id : 0 } }");
+ }
+ else
+ {
+ AssertStages(stages, "{ $project : { _v : '$Quantity', _id : 0 } }");
+ }
+
+ var results = queryable.ToList();
+ results.Should().Equal(3, 6);
+ }
+
+ [Theory]
+ [ParameterAttributeData]
+ public void Where_with_grouped_member_should_work(
+ [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
+ {
+ var collection = GetCollection(linqProvider);
+
+ var queryable = collection.AsQueryable()
+ .Where(x => x.OrderId == 1);
+
+ if (linqProvider == LinqProvider.V2)
+ {
+ var exception = Record.Exception(() => Translate(collection, queryable));
+ exception.Should().BeOfType(); // LINQ2 doesn't understand grouped members
+ }
+ else
+ {
+ var stages = Translate(collection, queryable);
+ AssertStages(stages, "{ $match: { '_id.OrderId' : 1 } }");
+
+ var result = queryable.Single();
+ result.OrderId.Should().Be(1);
+ }
+ }
+
+ [Theory]
+ [ParameterAttributeData]
+ public void Where_with_ungrouped_member_should_work(
+ [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
+ {
+ var collection = GetCollection(linqProvider);
+
+ var queryable = collection.AsQueryable()
+ .Where(x => x.Quantity == 3);
+
+ var stages = Translate(collection, queryable);
+ AssertStages(stages, "{ $match: { Quantity : 3 } }");
+
+ var result = queryable.Single();
+ result.OrderId.Should().Be(1);
+ }
+
+ private IMongoCollection GetCollection(LinqProvider linqProvider)
+ {
+ var collection = GetCollection("test", linqProvider);
+ CreateCollection(
+ collection,
+ new OrderItem { OrderId = 1, ProductId = 2, Quantity = 3 },
+ new OrderItem { OrderId = 4, ProductId = 5, Quantity = 6 });
+ return collection;
+ }
+
+ [BsonSerializer(typeof(OrderItemsSerializer))]
+ private class OrderItem
+ {
+ public int OrderId { get; set; }
+ public int ProductId { get; set; }
+ public int Quantity { get; set; }
+ }
+
+ private class OrderItemsSerializer : ClassSerializerBase, IBsonDocumentSerializer
+ {
+ // public methods
+ public bool IsGroupedMember(string name, out string groupElementName)
+ {
+ switch (name)
+ {
+ case "OrderId":
+ groupElementName = "_id";
+ return true;
+
+ case "ProductId":
+ groupElementName = "_id";
+ return true;
+
+ default:
+ groupElementName = null;
+ return false;
+ }
+ }
+
+ public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo serializationInfo)
+ {
+ switch (memberName)
+ {
+ case "OrderId":
+ serializationInfo = BsonSerializationInfo.CreateWithPath(new[] { "_id", "OrderId" }, Int32Serializer.Instance, typeof(int));
+ return true;
+
+ case "ProductId":
+ serializationInfo = BsonSerializationInfo.CreateWithPath(new[] { "_id", "ProductId" }, Int32Serializer.Instance, typeof(int));
+ return true;
+
+ case "Quantity":
+ serializationInfo = new BsonSerializationInfo("Quantity", Int32Serializer.Instance, typeof(int));
+ return true;
+
+ default:
+ serializationInfo = null;
+ return false;
+ }
+ }
+
+ // protected methods
+ protected override OrderItem DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ var reader = context.Reader;
+ reader.ReadStartDocument();
+ reader.ReadName("_id");
+ reader.ReadStartDocument();
+ reader.ReadName("OrderId");
+ var orderId = reader.ReadInt32();
+ reader.ReadName("ProductId");
+ var productId = reader.ReadInt32();
+ reader.ReadEndDocument();
+ reader.ReadName("Quantity");
+ var quantity = reader.ReadInt32();
+ reader.ReadEndDocument();
+
+ return new OrderItem { OrderId = orderId, ProductId = productId, Quantity = quantity };
+ }
+
+ protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, OrderItem value)
+ {
+ var writer = context.Writer;
+ writer.WriteStartDocument();
+ writer.WriteName("_id");
+ writer.WriteStartDocument();
+ writer.WriteName("OrderId");
+ writer.WriteInt32(value.OrderId);
+ writer.WriteName("ProductId");
+ writer.WriteInt32(value.ProductId);
+ writer.WriteEndDocument();
+ writer.WriteName("Quantity");
+ writer.WriteInt32(value.Quantity);
+ writer.WriteEndDocument();
+ }
+ }
+ }
+}