Skip to content

Commit 4284dde

Browse files
rstamDmitryLukyanov
authored andcommitted
CSHARP-4557: Add support for ContainsKey in LINQ3.
1 parent 4643231 commit 4284dde

File tree

7 files changed

+276
-0
lines changed

7 files changed

+276
-0
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,11 @@ public static AstExpression Trunc(AstExpression arg)
847847
return new AstUnaryExpression(AstUnaryOperator.Trunc, arg);
848848
}
849849

850+
public static AstExpression Type(AstExpression arg)
851+
{
852+
return new AstUnaryExpression(AstUnaryOperator.Type, arg);
853+
}
854+
850855
public static AstExpression Unary(AstUnaryOperator @operator, AstExpression arg)
851856
{
852857
return new AstUnaryExpression(@operator, arg);

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstFilter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ public static AstFieldOperationFilter Eq(AstFilterField field, BsonValue value)
100100
return new AstFieldOperationFilter(field, new AstComparisonFilterOperation(AstComparisonFilterOperator.Eq, value));
101101
}
102102

103+
public static AstFieldOperationFilter Exists(AstFilterField field)
104+
{
105+
return new AstFieldOperationFilter(field, new AstExistsFilterOperation(exists: true));
106+
}
107+
103108
public static AstFilter Expr(AstExpression expression)
104109
{
105110
return new AstExprFilter(expression);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
3535
case "CompareTo": return CompareToMethodToAggregationExpressionTranslator.Translate(context, expression);
3636
case "Concat": return ConcatMethodToAggregationExpressionTranslator.Translate(context, expression);
3737
case "Contains": return ContainsMethodToAggregationExpressionTranslator.Translate(context, expression);
38+
case "ContainsKey": return ContainsKeyMethodToAggregationExpressionTranslator.Translate(context, expression);
3839
case "CovariancePopulation": return CovariancePopulationMethodToAggregationExpressionTranslator.Translate(context, expression);
3940
case "CovarianceSample": return CovarianceSampleMethodToAggregationExpressionTranslator.Translate(context, expression);
4041
case "DefaultIfEmpty": return DefaultIfEmptyMethodToAggregationExpressionTranslator.Translate(context, expression);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.Expressions;
17+
using System.Reflection;
18+
using MongoDB.Bson.Serialization;
19+
using MongoDB.Bson.Serialization.Options;
20+
using MongoDB.Bson.Serialization.Serializers;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
24+
{
25+
internal static class ContainsKeyMethodToAggregationExpressionTranslator
26+
{
27+
// public methods
28+
public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression)
29+
{
30+
var method = expression.Method;
31+
var arguments = expression.Arguments;
32+
33+
if (IsContainsKeyMethod(method))
34+
{
35+
var dictionaryExpression = expression.Object;
36+
var keyExpression = arguments[0];
37+
38+
var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression);
39+
var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation);
40+
var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation;
41+
42+
var keyTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, keyExpression);
43+
44+
AstExpression ast;
45+
switch (dictionaryRepresentation)
46+
{
47+
case DictionaryRepresentation.Document:
48+
if (keyExpression.Type != typeof(string))
49+
{
50+
throw new ExpressionNotSupportedException(expression, because: "ContainsKey requires key to be of type string when DictionaryRepresentation is: Document");
51+
}
52+
ast = AstExpression.Ne(AstExpression.Type(AstExpression.GetField(dictionaryTranslation.Ast, keyTranslation.Ast)), "missing");
53+
break;
54+
55+
default:
56+
throw new ExpressionNotSupportedException(expression, because: $"ContainsKey is not supported when DictionaryRepresentation is: {dictionaryRepresentation}");
57+
}
58+
59+
return new AggregationExpression(expression, ast, BooleanSerializer.Instance);
60+
}
61+
62+
throw new ExpressionNotSupportedException(expression);
63+
}
64+
65+
private static IBsonDictionarySerializer GetDictionarySerializer(Expression expression, AggregationExpression dictionaryTranslation)
66+
{
67+
if (dictionaryTranslation.Serializer is IBsonDictionarySerializer dictionarySerializer)
68+
{
69+
return dictionarySerializer;
70+
}
71+
72+
throw new ExpressionNotSupportedException(expression, because: $"class {dictionaryTranslation.Serializer.GetType().FullName} does not implement the IBsonDictionarySerializer interface");
73+
}
74+
75+
private static bool IsContainsKeyMethod(MethodInfo method)
76+
{
77+
return
78+
!method.IsStatic &&
79+
method.IsPublic &&
80+
method.ReturnType == typeof(bool) &&
81+
method.Name == "ContainsKey" &&
82+
method.GetParameters() is var parameters &&
83+
parameters.Length == 1;
84+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.Expressions;
17+
using System.Reflection;
18+
using MongoDB.Bson.Serialization;
19+
using MongoDB.Bson.Serialization.Options;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ToFilterFieldTranslators;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.MethodTranslators
24+
{
25+
internal static class ContainsKeyMethodToFilterTranslator
26+
{
27+
public static AstFilter Translate(TranslationContext context, MethodCallExpression expression)
28+
{
29+
var method = expression.Method;
30+
var arguments = expression.Arguments;
31+
32+
if (IsContainsKeyMethod(method))
33+
{
34+
var dictionaryExpression = expression.Object;
35+
var keyExpression = arguments[0];
36+
37+
var dictionaryField = ExpressionToFilterFieldTranslator.Translate(context, dictionaryExpression);
38+
var dictionarySerializer = GetDictionarySerializer(expression, dictionaryField);
39+
var valueSerializer = dictionarySerializer.ValueSerializer;
40+
var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation;
41+
42+
switch (dictionaryRepresentation)
43+
{
44+
case DictionaryRepresentation.Document:
45+
var key = GetKeyStringConstant(expression, keyExpression);
46+
var keyField = dictionaryField.SubField(key, valueSerializer);
47+
return AstFilter.Exists(keyField);
48+
49+
default:
50+
throw new ExpressionNotSupportedException(expression, because: $"ContainsKey is not supported when DictionaryRepresentation is: {dictionaryRepresentation}");
51+
}
52+
}
53+
54+
throw new ExpressionNotSupportedException(expression);
55+
}
56+
57+
private static IBsonDictionarySerializer GetDictionarySerializer(Expression expression, AstFilterField field)
58+
{
59+
if (field.Serializer is IBsonDictionarySerializer dictionarySerializer)
60+
{
61+
return dictionarySerializer;
62+
}
63+
64+
throw new ExpressionNotSupportedException(expression, because: $"class {field.Serializer.GetType().FullName} does not implement the IBsonDictionarySerializer interface");
65+
}
66+
67+
private static string GetKeyStringConstant(Expression expression, Expression keyExpression)
68+
{
69+
if (keyExpression is ConstantExpression keyConstantExpression && keyExpression.Type == typeof(string))
70+
{
71+
return (string)keyConstantExpression.Value;
72+
}
73+
74+
throw new ExpressionNotSupportedException(expression, because: "key must be a string constant");
75+
}
76+
77+
private static bool IsContainsKeyMethod(MethodInfo method)
78+
{
79+
return
80+
!method.IsStatic &&
81+
method.IsPublic &&
82+
method.ReturnType == typeof(bool) &&
83+
method.Name == "ContainsKey" &&
84+
method.GetParameters() is var parameters &&
85+
parameters.Length == 1;
86+
}
87+
}
88+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/MethodCallExpressionToFilterTranslator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi
2525
switch (expression.Method.Name)
2626
{
2727
case "Contains": return ContainsMethodToFilterTranslator.Translate(context, expression);
28+
case "ContainsKey": return ContainsKeyMethodToFilterTranslator.Translate(context, expression);
2829
case "EndsWith": return EndsWithMethodToFilterTranslator.Translate(context, expression);
2930
case "Equals": return EqualsMethodToFilterTranslator.Translate(context, expression);
3031
case "HasFlag": return HasFlagMethodToFilterTranslator.Translate(context, expression);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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;
19+
using FluentAssertions;
20+
using MongoDB.Driver.Linq;
21+
using MongoDB.TestHelpers.XunitExtensions;
22+
using Xunit;
23+
24+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
25+
{
26+
public class CSharp4557Tests : Linq3IntegrationTest
27+
{
28+
[Theory]
29+
[ParameterAttributeData]
30+
public void Where_with_ContainsKey_should_work(
31+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
32+
{
33+
var collection = CreateCollection(linqProvider);
34+
35+
var queryable = collection
36+
.AsQueryable()
37+
.Where(x => x.Foo.ContainsKey("bar"));
38+
39+
var stages = Translate(collection, queryable);
40+
AssertStages(stages, "{ $match : { 'Foo.bar' : { $exists : true } } }");
41+
42+
var results = queryable.ToList();
43+
results.Select(x => x.Id).Should().Equal(2);
44+
}
45+
46+
[Theory]
47+
[ParameterAttributeData]
48+
public void Select_with_ContainsKey_should_work(
49+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
50+
{
51+
var collection = CreateCollection(linqProvider);
52+
53+
var queryable = collection
54+
.AsQueryable()
55+
.Select(x => x.Foo.ContainsKey("bar"));
56+
57+
if (linqProvider == LinqProvider.V2)
58+
{
59+
var exception = Record.Exception(() => Translate(collection, queryable));
60+
exception.Should().BeOfType<NotSupportedException>();
61+
}
62+
else
63+
{
64+
var stages = Translate(collection, queryable);
65+
AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$Foo.bar' }, 'missing'] }, _id : 0 } }");
66+
67+
var results = queryable.ToList();
68+
results.Should().Equal(false, true);
69+
}
70+
}
71+
72+
private IMongoCollection<C> CreateCollection(LinqProvider linqProvider)
73+
{
74+
var collection = GetCollection<C>("C", linqProvider);
75+
76+
CreateCollection(
77+
collection,
78+
new C { Id = 1, Foo = new Dictionary<string, int> { { "foo", 100 } } },
79+
new C { Id = 2, Foo = new Dictionary<string, int> { { "bar", 100 } } });
80+
81+
return collection;
82+
}
83+
84+
private class C
85+
{
86+
public int Id { get; set; }
87+
public Dictionary<string, int> Foo { get; set; }
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)