Skip to content

Commit 2d5eac9

Browse files
committed
CSHARP-1585: Support for $elemMatch filters directly against the elements with no field name.
1 parent 9f52b9c commit 2d5eac9

File tree

3 files changed

+127
-30
lines changed

3 files changed

+127
-30
lines changed

src/MongoDB.Driver/FieldDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ public static void Resolve<TDocument>(string fieldName, IBsonSerializer<TDocumen
439439
}
440440
}
441441

442-
internal class UntypedFieldDefinitionAdapter<TDocument, TField> : FieldDefinition<TDocument>
442+
internal sealed class UntypedFieldDefinitionAdapter<TDocument, TField> : FieldDefinition<TDocument>
443443
{
444444
private readonly FieldDefinition<TDocument, TField> _adaptee;
445445

src/MongoDB.Driver/FilterDefinitionBuilder.cs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
using MongoDB.Bson.Serialization.Serializers;
2525
using MongoDB.Driver.Core.Misc;
2626
using MongoDB.Driver.GeoJsonObjectModel;
27-
using MongoDB.Driver.Linq;
28-
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
2927
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters;
3028
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers;
29+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
3130
using MongoDB.Driver.Linq.Linq3Implementation.Translators;
3231

3332
namespace MongoDB.Driver
@@ -473,6 +472,30 @@ public FilterDefinition<TDocument> BitsAnySet(Expression<Func<TDocument, object>
473472
return BitsAnySet(new ExpressionFieldDefinition<TDocument>(field), bitmask);
474473
}
475474

475+
/// <summary>
476+
/// Creates an element match filter for an array value.
477+
/// </summary>
478+
/// <remarks>TDocument must implement IEnumerable{TITem} when using this overload of ElemMatch.</remarks>
479+
/// <param name="impliedElementFilter">The implied element filter.</param>
480+
/// <returns>An element match filter.</returns>
481+
public FilterDefinition<TDocument> ElemMatch<TItem>(FilterDefinition<TItem> impliedElementFilter)
482+
// where TDocument : IEnumerable<TItem> (can only be checked at runtime)
483+
{
484+
return new ElementMatchFilterDefinition<TDocument, TItem>(impliedElementFilter);
485+
}
486+
487+
/// <summary>
488+
/// Creates an element match filter for an array value.
489+
/// </summary>
490+
/// <remarks>TDocument must implement IEnumerable{TITem} when using this overload of ElemMatch.</remarks>
491+
/// <param name="impliedElementFilter">The implied element filter.</param>
492+
/// <returns>An element match filter.</returns>
493+
public FilterDefinition<TDocument> ElemMatch<TItem>(Expression<Func<TItem, bool>> impliedElementFilter)
494+
// where TDocument : IEnumerable<TItem> (can only be checked at runtime)
495+
{
496+
return ElemMatch(new ExpressionFilterDefinition<TItem>(impliedElementFilter));
497+
}
498+
476499
/// <summary>
477500
/// Creates an element match filter for an array field.
478501
/// </summary>
@@ -481,6 +504,7 @@ public FilterDefinition<TDocument> BitsAnySet(Expression<Func<TDocument, object>
481504
/// <param name="filter">The filter.</param>
482505
/// <returns>An element match filter.</returns>
483506
public FilterDefinition<TDocument> ElemMatch<TItem>(FieldDefinition<TDocument> field, FilterDefinition<TItem> filter)
507+
// where TField : IEnumerable<TItem> (can only be checked at runtime)
484508
{
485509
return new ElementMatchFilterDefinition<TDocument, TItem>(field, filter);
486510
}
@@ -493,6 +517,7 @@ public FilterDefinition<TDocument> ElemMatch<TItem>(FieldDefinition<TDocument> f
493517
/// <param name="filter">The filter.</param>
494518
/// <returns>An element match filter.</returns>
495519
public FilterDefinition<TDocument> ElemMatch<TItem>(Expression<Func<TDocument, IEnumerable<TItem>>> field, FilterDefinition<TItem> filter)
520+
// where TField : IEnumerable<TItem> (can only be checked at runtime)
496521
{
497522
return ElemMatch(new ExpressionFieldDefinition<TDocument>(field), filter);
498523
}
@@ -1852,24 +1877,49 @@ internal sealed class ElementMatchFilterDefinition<TDocument, TItem> : FilterDef
18521877
private readonly FieldDefinition<TDocument> _field;
18531878
private readonly FilterDefinition<TItem> _filter;
18541879

1880+
public ElementMatchFilterDefinition(FilterDefinition<TItem> filter)
1881+
// where TDocument : IEnumerable<TItem> (can only be checked at runtime)
1882+
{
1883+
_filter = filter;
1884+
1885+
// TODO: CSHARP-5517 validate type constraint
1886+
}
1887+
18551888
public ElementMatchFilterDefinition(FieldDefinition<TDocument> field, FilterDefinition<TItem> filter)
1889+
// where TField : IEnumerable<TItem> (checked in Render)
18561890
{
18571891
_field = Ensure.IsNotNull(field, nameof(field));
18581892
_filter = filter;
1893+
1894+
// TODO: CSHARP-5517 validate type constraint
18591895
}
18601896

18611897
public override BsonDocument Render(RenderArgs<TDocument> args)
18621898
{
1863-
var renderedField = _field.Render(args);
1899+
string fieldName = null;
1900+
IBsonSerializer enumerableSerializer;
1901+
1902+
if (_field == null)
1903+
{
1904+
enumerableSerializer = args.DocumentSerializer; // note that TDocument : IEnumerable<TItem>
1905+
}
1906+
else
1907+
{
1908+
var renderedField = _field.Render(args);
1909+
fieldName = renderedField.FieldName;
1910+
enumerableSerializer = renderedField.FieldSerializer; // note that TField : IEnumerable<TItem>
1911+
}
18641912

18651913
IBsonSerializer<TItem> itemSerializer;
1866-
if (renderedField.FieldSerializer != null)
1914+
if (enumerableSerializer != null)
18671915
{
1868-
var arraySerializer = renderedField.FieldSerializer as IBsonArraySerializer;
1916+
var arraySerializer = enumerableSerializer as IBsonArraySerializer;
18691917
BsonSerializationInfo itemSerializationInfo;
18701918
if (arraySerializer == null || !arraySerializer.TryGetItemSerializationInfo(out itemSerializationInfo))
18711919
{
1872-
var message = string.Format("The serializer for field '{0}' must implement IBsonArraySerializer and provide item serialization info.", renderedField.FieldName);
1920+
var message = fieldName == null ?
1921+
$"The serializer '{enumerableSerializer.GetType()}' must implement IBsonArraySerializer and provide item serialization info." :
1922+
$"The serializer for field '{fieldName}' must implement IBsonArraySerializer and provide item serialization info.";
18731923
throw new InvalidOperationException(message);
18741924
}
18751925
itemSerializer = (IBsonSerializer<TItem>)itemSerializationInfo.Serializer;
@@ -1880,8 +1930,11 @@ public override BsonDocument Render(RenderArgs<TDocument> args)
18801930
}
18811931

18821932
var renderedFilter = _filter.Render(args.WithNewDocumentType(itemSerializer) with { RenderForElemMatch = true });
1933+
var renderedElemMatchOperation = new BsonDocument("$elemMatch", renderedFilter);
18831934

1884-
return new BsonDocument(renderedField.FieldName, new BsonDocument("$elemMatch", renderedFilter));
1935+
return fieldName == null ?
1936+
renderedElemMatchOperation :
1937+
new BsonDocument(fieldName, renderedElemMatchOperation);
18851938
}
18861939
}
18871940

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp1585Tests.cs

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,78 @@
1414
*/
1515

1616
using System;
17+
using System.Collections.Generic;
1718
using System.Linq;
18-
using System.Linq.Expressions;
1919
using System.Text.RegularExpressions;
2020
using FluentAssertions;
21-
using MongoDB.Bson.Serialization;
22-
using MongoDB.Bson.Serialization.Serializers;
23-
using MongoDB.Driver;
24-
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
21+
using MongoDB.Bson;
2522
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters;
2623
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers;
27-
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
28-
using MongoDB.Driver.Linq.Linq3Implementation.Translators;
29-
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators;
24+
using MongoDB.Driver.TestHelpers;
3025
using Xunit;
3126

3227
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
3328
{
34-
public class CSharp1585Tests
29+
public class CSharp1585Tests : LinqIntegrationTest<CSharp1585Tests.ClassFixture>
3530
{
31+
public CSharp1585Tests(ClassFixture fixture)
32+
: base(fixture)
33+
{
34+
}
35+
3636
[Fact]
37-
public void Nested_Any_should_translate_correctly()
37+
public void Filter_Builder_Where_should_translate_correctly()
3838
{
39-
var expression = (Expression<Func<Document, bool>>)(document => document.Details.A.Any(x => x.Any(y => Regex.IsMatch(y.DeviceName, @".Name0."))));
40-
var parameter = expression.Parameters[0];
41-
var serializerRegistry = BsonSerializer.SerializerRegistry;
42-
var documentSerializer = serializerRegistry.GetSerializer<Document>();
43-
var context = TranslationContext.Create(translationOptions: null);
44-
var symbol = context.CreateSymbol(parameter, documentSerializer, isCurrent: true);
45-
context = context.WithSymbol(symbol);
46-
var filter = ExpressionToFilterTranslator.Translate(context, expression.Body, exprOk: false);
47-
var simplifiedFilter = AstSimplifier.Simplify(filter);
48-
49-
var rendered = simplifiedFilter.Render();
39+
var collection = Fixture.Collection;
40+
var filter = Builders<Document>.Filter.Where(
41+
document => document.Details.A.Any(x => x.Any(y => Regex.IsMatch(y.DeviceName, @".Name0."))));
5042

51-
rendered.Should().Be("{ 'Details.A' : { $elemMatch : { $elemMatch : { DeviceName : /.Name0./ } } } }");
43+
var find = collection.Find(filter);
44+
45+
var translatedFilter = TranslateFindFilter(collection, find);
46+
translatedFilter.Should().Be("{ 'Details.A' : { $elemMatch : { $elemMatch : { DeviceName : /.Name0./ } } } }");
47+
}
48+
49+
[Fact]
50+
public void Filter_Builder_ElemMatch_ElemMatch_should_translate_correctly()
51+
{
52+
var collection = Fixture.Collection;
53+
var deviceFilter = Builders<Device>.Filter.Regex(x => x.DeviceName, new BsonRegularExpression(".Name0."));
54+
var deviceArrayFilter = Builders<Device[]>.Filter.ElemMatch(deviceFilter);
55+
var filter = Builders<Document>.Filter.ElemMatch(x => x.Details.A, deviceArrayFilter);
56+
57+
var find = collection.Find(filter);
58+
59+
var translatedFilter = TranslateFindFilter(collection, find);
60+
translatedFilter.Should().Be("{ 'Details.A' : { $elemMatch : { $elemMatch : { DeviceName : /.Name0./ } } } }");
61+
}
62+
63+
[Fact]
64+
public void Filter_Builder_ElemMatch_ElemMatch_untyped_should_translate_correctly()
65+
{
66+
var collection = Fixture.BsonDocumentCollection;
67+
var deviceFilter = Builders<BsonValue>.Filter.Regex("DeviceName", new BsonRegularExpression(".Name0."));
68+
var deviceArrayFilter = Builders<BsonValue>.Filter.ElemMatch(deviceFilter);
69+
var filter = Builders<BsonDocument>.Filter.ElemMatch("Details.A", deviceArrayFilter);
70+
71+
var find = collection.Find(filter);
72+
73+
var translatedFilter = TranslateFindFilter(collection, find);
74+
translatedFilter.Should().Be("{ 'Details.A' : { $elemMatch : { $elemMatch : { DeviceName : /.Name0./ } } } }");
75+
}
76+
77+
[Fact]
78+
public void Filter_Builder_ElemMatch_ElemMatch_semityped_should_translate_correctly()
79+
{
80+
var collection = Fixture.BsonDocumentCollection;
81+
var deviceFilter = Builders<BsonDocument>.Filter.Regex((FieldDefinition<BsonDocument, BsonValue>)"DeviceName", new BsonRegularExpression(".Name0."));
82+
var deviceArrayFilter = Builders<BsonDocument[]>.Filter.ElemMatch(deviceFilter);
83+
var filter = Builders<BsonDocument>.Filter.ElemMatch((FieldDefinition<BsonDocument, BsonDocument[][]>)"Details.A", deviceArrayFilter);
84+
85+
var find = collection.Find(filter);
86+
87+
var translatedFilter = TranslateFindFilter(collection, find);
88+
translatedFilter.Should().Be("{ 'Details.A' : { $elemMatch : { $elemMatch : { DeviceName : /.Name0./ } } } }");
5289
}
5390

5491
[Fact]
@@ -82,5 +119,12 @@ public class Device
82119
{
83120
public string DeviceName { get; set; }
84121
}
122+
123+
public sealed class ClassFixture : MongoCollectionFixture<Document>
124+
{
125+
public IMongoCollection<BsonDocument> BsonDocumentCollection => Collection.Database.GetCollection<BsonDocument>(Collection.CollectionNamespace.CollectionName);
126+
127+
protected override IEnumerable<Document> InitialData => null;
128+
}
85129
}
86130
}

0 commit comments

Comments
 (0)